Passed
Push — master ( 9eb184...0e24c6 )
by Lewys
03:22
created

clear()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
/*
2
* Copyright (c) 2020 Lewys Davies
3
* 
4
* Permission is hereby granted, free of charge, to any person obtaining a copy
5
* of this software and associated documentation files (the "Software"), to deal
6
* in the Software without restriction, including without limitation the rights
7
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
* copies of the Software, and to permit persons to whom the Software is
9
* furnished to do so, subject to the following conditions:
10
*
11
* The above copyright notice and this permission notice shall be included in all
12
* copies or substantial portions of the Software.
13
* 
14
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
* SOFTWARE.
21
*/
22
package com.lewdev.probabilitylib;
23
24
import java.util.Comparator;
25
import java.util.Iterator;
26
import java.util.NavigableSet;
27
import java.util.Objects;
28
import java.util.SplittableRandom;
29
import java.util.TreeSet;
30
31
/**
32
 * ProbabilityCollection for retrieving random elements based on probability.
33
 * <br>
34
 * <br>
35
 * <b>Selection Algorithm Implementation</b>:
36
 * <p>
37
 * <ul>
38
 * <li>Elements have a "block" of space, sized based on their probability share
39
 * <li>"Blocks" start from index 1 and end at the total probability of all elements
40
 * <li>A random number is selected between 1 and the total probability
41
 * <li>Which "block" the random number falls in is the element that is selected
42
 * <li>Therefore "block"s with larger probability have a greater chance of being
43
 * selected than those with smaller probability.
44
 * </p>
45
 * </ul>
46
 * 
47
 * @author Lewys Davies
48
 * @version 0.7
49
 *
50
 * @param <E> Type of elements
51
 */
52
public class ProbabilityCollection<E> {
53
	
54
	private final NavigableSet<ProbabilitySetElement<E>> collection;
55
	private final SplittableRandom random = new SplittableRandom();
56
57
	private int totalProbability;
58
	
59
	/**
60
	 * Construct a new Probability Collection
61
	 */
62
	public ProbabilityCollection() {
63
		this.collection = new TreeSet<>(Comparator.comparingInt(ProbabilitySetElement::getIndex));
64
		this.totalProbability = 0;
65
	}
66
67
	/**
68
	 * @return Number of objects inside the collection
69
	 */
70
	public int size() {
71
		return this.collection.size();
72
	}
73
	
74
	/**
75
	 * @return True if collection contains no elements, else False
76
	 */
77
	public boolean isEmpty() {
78
		return this.collection.isEmpty();
79
	}
80
	
81
	/**
82
	 * @param <E> object
83
	 * @return True if collection contains the object, else False
84
	 * @throws IllegalArgumentException if object is null
85
	 */
86
	public boolean contains(E object) {
87
		if(object == null) {
88
			throw new IllegalArgumentException("Cannot check if null object is contained in this collection");
89
		}
90
		
91
		return this.collection.stream()
92
			.anyMatch(entry -> entry.getObject().equals(object));
93
	}
94
95
	/**
96
	 * @return Iterator over this collection
97
	 */
98
	public Iterator<ProbabilitySetElement<E>> iterator() {
99
		return this.collection.iterator();
100
	}
101
	
102
	/**
103
	 * Add an object to this collection
104
	 * 
105
	 * @param <E> object. Not null.
106
	 * @param probability share. Must be greater than 0.
107
	 * 
108
	 * @throws IllegalArgumentException if object is null
109
	 * @throws IllegalArgumentException if probability <= 0
110
	 */
111
	public void add(E object, int probability) {
112
		if(object == null) {
113
			throw new IllegalArgumentException("Cannot add null object");
114
		}
115
		
116
		if(probability <= 0) {
117
			throw new IllegalArgumentException("Probability must be greater than 0");
118
		}
119
		
120
		this.collection.add(new ProbabilitySetElement<E>(object, probability));
121
		this.totalProbability += probability;
122
		
123
		this.updateIndexes();
124
	}
125
126
	/**
127
	 * Remove a object from this collection
128
	 * 
129
	 * @param <E> object
130
	 * @return True if object was removed, else False.
131
	 * 
132
	 * @throws IllegalArgumentException if object is null
133
	 */
134
	public boolean remove(E object) {
135
		if(object == null) {
136
			throw new IllegalArgumentException("Cannot remove null object");
137
		}
138
		
139
		Iterator<ProbabilitySetElement<E>> it = this.iterator();
140
		boolean removed = it.hasNext();
141
		
142
		while(it.hasNext()) {
143
			ProbabilitySetElement<E> entry = it.next();
144
			if(entry.getObject().equals(object)) {
145
				this.totalProbability -= entry.getProbability();
146
				it.remove();
147
			}
148
		}
149
		
150
		this.updateIndexes();
151
		
152
		return removed;
153
	}
154
	
155
	/**
156
	 * Remove all objects from this collection
157
	 */
158
	public void clear() {
159
		this.collection.clear();
160
		this.totalProbability = 0;
161
	}
162
	
163
	/**
164
	 * Get a random object from this collection, based on probability.
165
	 * 
166
	 * @return <E> Random object
167
	 * 
168
	 * @throws IllegalStateException if this collection is empty
169
	 */
170
	public E get() {
171
		if(this.isEmpty()) {
172
			throw new IllegalStateException("Cannot get an object out of a empty collection");
173
		}
174
		
175
		ProbabilitySetElement<E> toFind = new ProbabilitySetElement<>(null, 0);
176
		toFind.setIndex(this.random.nextInt(1, this.totalProbability + 1));
177
		
178
		return Objects.requireNonNull(this.collection.floor(toFind).getObject());
179
	}
180
	
181
	/**
182
	 * @return Sum of all element's probability
183
	 */
184
	public final int getTotalProbability() {
185
		return this.totalProbability;
186
	}
187
	
188
	/*
189
	 * Calculate the size of all element's "block" of space: 
190
	 * i.e 1-5, 6-10, 11-14, 15, 16
191
	 * 
192
	 * We then only need to store the start index of each element,
193
	 * as we make use of the TreeSet#floor
194
	 */
195
	private final void updateIndexes() {
196
		int previousIndex = 0;
197
		
198
		for(ProbabilitySetElement<E> entry : this.collection) {
199
			previousIndex = entry.setIndex(previousIndex + 1) + (entry.getProbability() - 1);
200
		}
201
	}
202
	
203
	/**
204
	 * Used internally to store information about a object's
205
	 * state in a collection. Specifically, the probability 
206
	 * and index within the collection.
207
	 * 
208
	 * Indexes refer to the start position of this element's "block" of space.
209
	 * The space between element "block"s represents their probability of being selected
210
	 * 
211
	 * @author Lewys Davies
212
	 *
213
	 * @param <T> Type of element
214
	 */
215
	final static class ProbabilitySetElement<T> {
216
		private final T object;
217
		private final int probability;
218
		private int index;
219
		
220
		/**
221
		 * @param <T> object
222
		 * @param probability
223
		 */
224
		protected ProbabilitySetElement(T object, int probability) {
225
			this.object = object;
226
			this.probability = probability;
227
		}
228
229
		/**
230
		 * @return <T> The actual object
231
		 */
232
		public final T getObject() {
233
			return this.object;
234
		}
235
236
		/**
237
		 * @return Probability share in this collection
238
		 */
239
		public final int getProbability() {
240
			return this.probability;
241
		}
242
		
243
		// Used internally, see this class's documentation
244
		private final int getIndex() {
245
			return this.index;
246
		}
247
		
248
		// Used Internally, see this class's documentation
249
		private final int setIndex(int index) {
250
			this.index = index;
251
			return this.index;
252
		}
253
	}
254
}
255