package speiger.src.collections.PACKAGE.sets; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.NoSuchElementException; #if TYPE_OBJECT import java.util.function.Consumer; #endif import speiger.src.collections.PACKAGE.collections.COLLECTION; import speiger.src.collections.PACKAGE.collections.ITERATOR; import speiger.src.collections.PACKAGE.lists.ARRAY_LIST; import speiger.src.collections.PACKAGE.lists.LIST; #if !TYPE_OBJECT import speiger.src.collections.PACKAGE.utils.ITERATORS; import speiger.src.collections.PACKAGE.functions.CONSUMER; #endif import speiger.src.collections.PACKAGE.utils.STRATEGY; import speiger.src.collections.utils.HashUtil; import speiger.src.collections.utils.ITrimmable; import speiger.src.collections.utils.SanityChecks; /** * A Type Specific HashSet that allows for custom HashControl. * For cases where Objects/primitive do not allow hashcoding this can be really useful and provide a lot of control. * @Type(T) */ public class CUSTOM_HASH_SET KEY_GENERIC_TYPE extends ABSTRACT_SET KEY_GENERIC_TYPE implements ITrimmable { /** The Backing keys array */ protected transient KEY_TYPE[] keys; /** If a null value is present */ protected transient boolean containsNull; /** Minimum array size the HashSet will be */ protected transient int minCapacity; /** Index of the Null Value */ protected transient int nullIndex; /** Maximum amount of Values that can be stored before the array gets expanded usually 75% */ protected transient int maxFill; /** Max Index that is allowed to be searched through nullIndex - 1 */ protected transient int mask; /** Amount of Elements stored in the HashSet */ protected int size; /** How full the Array is allowed to get before resize */ protected final float loadFactor; /** Strategy that allows to control the Hash Generation and equals comparason */ protected final STRATEGY KEY_SUPER_GENERIC_TYPE strategy; /** * Default Contstructor * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null */ public CUSTOM_HASH_SET(STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(HashUtil.DEFAULT_MIN_CAPACITY, HashUtil.DEFAULT_LOAD_FACTOR, strategy); } /** * Constructor that defines the minimum capacity * @param minCapacity the minimum capacity the HashSet is allowed to be. * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null * @throws IllegalStateException if the minimum capacity is negative */ public CUSTOM_HASH_SET(int minCapacity, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(minCapacity, HashUtil.DEFAULT_LOAD_FACTOR, strategy); } /** * Constructor that defines the minimum capacity and load factor * @param minCapacity the minimum capacity the HashSet is allowed to be. * @param loadFactor the percentage of how full the backing array can be before they resize * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null * @throws IllegalStateException if the minimum capacity is negative * @throws IllegalStateException if the loadfactor is either below/equal to 0 or above/equal to 1 */ public CUSTOM_HASH_SET(int minCapacity, float loadFactor, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { if(minCapacity < 0) throw new IllegalStateException("Minimum Capacity is negative. This is not allowed"); if(loadFactor <= 0 || loadFactor >= 1F) throw new IllegalStateException("Load Factor is not between 0 and 1"); this.loadFactor = loadFactor; this.minCapacity = nullIndex = HashUtil.arraySize(minCapacity, loadFactor); mask = nullIndex - 1; maxFill = Math.min((int)Math.ceil(nullIndex * loadFactor), nullIndex - 1); keys = NEW_KEY_ARRAY(nullIndex + 1); this.strategy = strategy; } /** * Helper constructor that allow to create a set from unboxed values * @param array the elements that should be put into the set * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null */ public CUSTOM_HASH_SET(KEY_TYPE[] array, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(array, 0, array.length, HashUtil.DEFAULT_LOAD_FACTOR, strategy); } /** * Helper constructor that allow to create a set from unboxed values * @param array the elements that should be put into the set * @param loadFactor the percentage of how full the backing array can be before they resize * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null * @throws IllegalStateException if the loadfactor is either below/equal to 0 or above/equal to 1 */ public CUSTOM_HASH_SET(KEY_TYPE[] array, float loadFactor, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(array, 0, array.length, loadFactor, strategy); } /** * Helper constructor that allow to create a set from unboxed values * @param array the elements that should be put into the set * @param offset the starting index within the array that should be used * @param length the amount of elements used from the array * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null * @throws IllegalStateException if offset and length causes to step outside of the arrays range */ public CUSTOM_HASH_SET(KEY_TYPE[] array, int offset, int length, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(array, offset, length, HashUtil.DEFAULT_LOAD_FACTOR, strategy); } /** * Helper constructor that allow to create a set from unboxed values * @param array the elements that should be put into the set * @param offset the starting index within the array that should be used * @param length the amount of elements used from the array * @param loadFactor the percentage of how full the backing array can be before they resize * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null * @throws IllegalStateException if the loadfactor is either below/equal to 0 or above/equal to 1 * @throws IllegalStateException if offset and length causes to step outside of the arrays range */ public CUSTOM_HASH_SET(KEY_TYPE[] array, int offset, int length, float loadFactor, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(length < 0 ? 0 : length, strategy); SanityChecks.checkArrayCapacity(array.length, offset, length); for(int i = 0;i collection, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(collection, HashUtil.DEFAULT_LOAD_FACTOR, strategy); } /** * A Helper constructor that allows to create a Set with exactly the same values as the provided collection. * @param collection the set the elements should be added to the Set * @param loadFactor the percentage of how full the backing array can be before they resize * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null * @throws IllegalStateException if the loadfactor is either below/equal to 0 or above/equal to 1 */ @Primitive public CUSTOM_HASH_SET(Collection collection, float loadFactor, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(collection.size(), loadFactor, strategy); addAll(collection); } /** * A Helper constructor that allows to create a Set with exactly the same values as the provided collection. * @param collection the set the elements should be added to the Set * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null */ public CUSTOM_HASH_SET(COLLECTION KEY_GENERIC_TYPE collection, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(collection, HashUtil.DEFAULT_LOAD_FACTOR, strategy); } /** * A Helper constructor that allows to create a Set with exactly the same values as the provided collection. * @param collection the set the elements should be added to the Set * @param loadFactor the percentage of how full the backing array can be before they resize * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null * @throws IllegalStateException if the loadfactor is either below/equal to 0 or above/equal to 1 */ public CUSTOM_HASH_SET(COLLECTION KEY_GENERIC_TYPE collection, float loadFactor, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(collection.size(), strategy); addAll(collection); } /** * A Helper constructor that allows to create a set from a iterator of an unknown size * @param iterator the elements that should be added to the set * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null */ public CUSTOM_HASH_SET(Iterator iterator, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(iterator, HashUtil.DEFAULT_LOAD_FACTOR, strategy); } /** * A Helper constructor that allows to create a set from a iterator of an unknown size * @param iterator the elements that should be added to the set * @param loadFactor the percentage of how full the backing array can be before they resize * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null * @throws IllegalStateException if the loadfactor is either below/equal to 0 or above/equal to 1 */ public CUSTOM_HASH_SET(Iterator iterator, float loadFactor, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { #if !TYPE_OBJECT this(ITERATORS.wrap(iterator), loadFactor, strategy); #else this(HashUtil.DEFAULT_MIN_CAPACITY, loadFactor, strategy); while(iterator.hasNext()) add(iterator.next()); #endif } #if !TYPE_OBJECT /** * A Helper constructor that allows to create a set from a iterator of an unknown size * @param iterator the elements that should be added to the set * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null */ public CUSTOM_HASH_SET(ITERATOR KEY_GENERIC_TYPE iterator, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(iterator, HashUtil.DEFAULT_LOAD_FACTOR, strategy); } /** * A Helper constructor that allows to create a set from a iterator of an unknown size * @param iterator the elements that should be added to the set * @param loadFactor the percentage of how full the backing array can be before they resize * @param strategy the strategy that allows hash control. * @throws NullPointerException if Strategy is null * @throws IllegalStateException if the loadfactor is either below/equal to 0 or above/equal to 1 */ public CUSTOM_HASH_SET(ITERATOR KEY_GENERIC_TYPE iterator, float loadFactor, STRATEGY KEY_SUPER_GENERIC_TYPE strategy) { this(HashUtil.DEFAULT_MIN_CAPACITY, loadFactor, strategy); while(iterator.hasNext()) add(iterator.NEXT()); } #endif /** * Helper getter function to get the current strategy * @return the current strategy */ public STRATEGY KEY_SUPER_GENERIC_TYPE getStrategy() { return strategy; } @Override public boolean add(KEY_TYPE o) { if(strategy.equals(o, EMPTY_KEY_VALUE)) { if(containsNull) return false; containsNull = true; onNodeAdded(nullIndex); } else { int pos = HashUtil.mix(strategy.hashCode(o)) & mask; KEY_TYPE current = keys[pos]; if(!strategy.equals(current, EMPTY_KEY_VALUE)) { if(strategy.equals(current, o)) return false; while(!strategy.equals((current = keys[pos = (++pos & mask)]), EMPTY_KEY_VALUE)) if(strategy.equals(current, o)) return false; } keys[pos] = o; onNodeAdded(pos); } if(size++ >= maxFill) rehash(HashUtil.arraySize(size+1, loadFactor)); return true; } @Override @Primitive public boolean addAll(Collection c) { if(loadFactor <= 0.5F) ensureCapacity(c.size()); else ensureCapacity(c.size() + size()); return super.addAll(c); } @Override public boolean addAll(COLLECTION KEY_GENERIC_TYPE c) { if(loadFactor <= 0.5F) ensureCapacity(c.size()); else ensureCapacity(c.size() + size()); return super.addAll(c); } #if TYPE_OBJECT @Override public boolean contains(Object o) { if(strategy.equals((KEY_TYPE)o, EMPTY_KEY_VALUE)) return containsNull; int pos = HashUtil.mix(strategy.hashCode((KEY_TYPE)o)) & mask; KEY_TYPE current = keys[pos]; if(strategy.equals(current, EMPTY_KEY_VALUE)) return false; if(strategy.equals(current, (KEY_TYPE)o)) return true; while(true) { if(strategy.equals((current = keys[pos = (++pos & mask)]), EMPTY_KEY_VALUE)) return false; else if(strategy.equals(current, (KEY_TYPE)o)) return true; } } @Override public boolean remove(Object o) { if(strategy.equals((KEY_TYPE)o, EMPTY_KEY_VALUE)) return (containsNull ? removeNullIndex() : false); int pos = HashUtil.mix(strategy.hashCode((KEY_TYPE)o)) & mask; KEY_TYPE current = keys[pos]; if(strategy.equals(current, EMPTY_KEY_VALUE)) return false; if(strategy.equals(current, (KEY_TYPE)o)) return removeIndex(pos); while(true) { if(strategy.equals((current = keys[pos = (++pos & mask)]), EMPTY_KEY_VALUE)) return false; else if(strategy.equals(current, (KEY_TYPE)o)) return removeIndex(pos); } } #else @Override public boolean contains(KEY_TYPE o) { if(strategy.equals(o, EMPTY_KEY_VALUE)) return containsNull; int pos = HashUtil.mix(strategy.hashCode(o)) & mask; KEY_TYPE current = keys[pos]; if(strategy.equals(current, EMPTY_KEY_VALUE)) return false; if(strategy.equals(current, o)) return true; while(true) { if(strategy.equals((current = keys[pos = (++pos & mask)]), EMPTY_KEY_VALUE)) return false; else if(strategy.equals(current, o)) return true; } } @Override public boolean remove(KEY_TYPE o) { if(strategy.equals(o, EMPTY_KEY_VALUE)) return (containsNull ? removeNullIndex() : false); int pos = HashUtil.mix(strategy.hashCode(o)) & mask; KEY_TYPE current = keys[pos]; if(strategy.equals(current, EMPTY_KEY_VALUE)) return false; if(strategy.equals(current, o)) return removeIndex(pos); while(true) { if(strategy.equals((current = keys[pos = (++pos & mask)]), EMPTY_KEY_VALUE)) return false; else if(strategy.equals(current, o)) return removeIndex(pos); } } #endif @Override public boolean trim(int size) { int newSize = HashUtil.nextPowerOfTwo((int)Math.ceil(size / loadFactor)); if(newSize >= nullIndex || size >= Math.min((int)Math.ceil(newSize * loadFactor), newSize - 1)) return false; try { rehash(newSize); } catch(OutOfMemoryError e) { return false; } return true; } @Override public void clearAndTrim(int size) { int request = Math.max(minCapacity, HashUtil.nextPowerOfTwo((int)Math.ceil(size / loadFactor))); if(size >= request) { clear(); return; } nullIndex = request; mask = request-1; maxFill = Math.min((int)Math.ceil(nullIndex * loadFactor), nullIndex - 1); keys = NEW_KEY_ARRAY(request + 1); this.size = 0; containsNull = false; } private void ensureCapacity(int newCapacity) { int size = HashUtil.arraySize(newCapacity, loadFactor); if(size > nullIndex) rehash(size); } protected boolean removeIndex(int pos) { if(pos == nullIndex) return containsNull ? removeNullIndex() : false; keys[pos] = EMPTY_KEY_VALUE; size--; onNodeRemoved(pos); shiftKeys(pos); if(nullIndex > minCapacity && size < maxFill / 4 && nullIndex > HashUtil.DEFAULT_MIN_CAPACITY) rehash(nullIndex / 2); return true; } protected boolean removeNullIndex() { containsNull = false; keys[nullIndex] = EMPTY_KEY_VALUE; size--; onNodeRemoved(nullIndex); if(nullIndex > minCapacity && size < maxFill / 4 && nullIndex > HashUtil.DEFAULT_MIN_CAPACITY) rehash(nullIndex / 2); return true; } protected void onNodeAdded(int pos) { } protected void onNodeRemoved(int pos) { } protected void onNodeMoved(int from, int to) { } protected void shiftKeys(int startPos) { int slot, last; KEY_TYPE current; while(true) { startPos = ((last = startPos) + 1) & mask; while(true){ if(strategy.equals((current = keys[startPos]), EMPTY_KEY_VALUE)) { keys[last] = EMPTY_KEY_VALUE; return; } slot = HashUtil.mix(strategy.hashCode(current)) & mask; if(last <= startPos ? (last >= slot || slot > startPos) : (last >= slot && slot > startPos)) break; startPos = ++startPos & mask; } keys[last] = current; onNodeMoved(startPos, last); } } protected void rehash(int newSize) { int newMask = newSize - 1; KEY_TYPE[] newKeys = NEW_KEY_ARRAY(newSize + 1); for(int i = nullIndex, pos = 0, j = (size - (containsNull ? 1 : 0));j-- != 0;) { while(strategy.equals(keys[--i], EMPTY_KEY_VALUE)); if(!strategy.equals(newKeys[pos = HashUtil.mix(KEY_TO_HASH(keys[i])) & newMask], EMPTY_KEY_VALUE)) while(!strategy.equals(newKeys[pos = (++pos & newMask)], EMPTY_KEY_VALUE)); newKeys[pos] = keys[i]; } nullIndex = newSize; mask = newMask; maxFill = Math.min((int)Math.ceil(nullIndex * loadFactor), nullIndex - 1); keys = newKeys; } @Override public ITERATOR KEY_GENERIC_TYPE iterator() { return new SetIterator(); } @Override public void clear() { if(size == 0) return; size = 0; containsNull = false; Arrays.fill(keys, EMPTY_KEY_VALUE); } @Override public int size() { return size; } @Override public void forEach(CONSUMER KEY_SUPER_GENERIC_TYPE action) { if(size() <= 0) return; if(containsNull) action.accept(keys[nullIndex]); for(int i = nullIndex-1;i>=0;i--) { if(KEY_EQUALS_NOT_NULL(keys[i])) action.accept(keys[i]); } } private class SetIterator implements ITERATOR KEY_GENERIC_TYPE { int pos = nullIndex; int lastReturned = -1; int nextIndex = Integer.MIN_VALUE; boolean returnNull = containsNull; LIST KEY_GENERIC_TYPE wrapped = null; @Override public boolean hasNext() { if(nextIndex == Integer.MIN_VALUE) { if(returnNull) { returnNull = false; nextIndex = nullIndex; } else { while(true) { if(--pos < 0) { if(wrapped == null || wrapped.size() <= -pos - 1) break; nextIndex = -pos - 1; break; } if(KEY_EQUALS_NOT_NULL(keys[pos])){ nextIndex = pos; break; } } } } return nextIndex != Integer.MIN_VALUE; } @Override public KEY_TYPE NEXT() { if(!hasNext()) throw new NoSuchElementException(); if(nextIndex < 0){ lastReturned = Integer.MAX_VALUE; KEY_TYPE value = wrapped.GET_KEY(nextIndex); nextIndex = Integer.MIN_VALUE; return value; } KEY_TYPE value = keys[(lastReturned = nextIndex)]; nextIndex = Integer.MIN_VALUE; return value; } @Override public void remove() { if(lastReturned == -1) throw new IllegalStateException(); if(lastReturned == nullIndex) { containsNull = false; keys[nullIndex] = EMPTY_KEY_VALUE; } else if(pos >= 0) shiftKeys(pos); else { CUSTOM_HASH_SET.this.remove(wrapped.GET_KEY(-pos - 1)); return; } size--; lastReturned = -1; } private void shiftKeys(int startPos) { int slot, last; KEY_TYPE current; while(true) { startPos = ((last = startPos) + 1) & mask; while(true){ if(strategy.equals((current = keys[startPos]), EMPTY_KEY_VALUE)) { keys[last] = EMPTY_KEY_VALUE; return; } slot = HashUtil.mix(strategy.hashCode(current)) & mask; if(last <= startPos ? (last >= slot || slot > startPos) : (last >= slot && slot > startPos)) break; startPos = ++startPos & mask; } if(startPos < last) { if(wrapped == null) wrapped = new ARRAY_LISTBRACES(2); wrapped.add(keys[startPos]); } keys[last] = current; } } } }