com.github.tommyettinger.ds.HolderOrderedSet Maven / Gradle / Ivy
Show all versions of jdkgdxds Show documentation
/*
* Copyright (c) 2022-2025 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.tommyettinger.ds;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NoSuchElementException;
import com.github.tommyettinger.function.ObjToObjFunction;
import static com.github.tommyettinger.ds.Utilities.tableSize;
/**
* A {@link HolderSet} that also stores items in an {@link ObjectList} using the insertion order. This acts like
* HolderSet instead of like {@link ObjectSet}, with the constructors typically taking an extractor function,
* {@link #contains(Object)} and {@link #remove(Object)} accepting a K key instead of a T item, and
* {@link #get(Object)} used to get a T item from a K key. Neither null items nor null keys are allowed.
* No allocation is done except when growing the table size.
*
* {@link #iterator() Iteration} is ordered over items and faster than an unordered set. Items can also be accessed
* and the order changed using {@link #order()}. There is some additional overhead for put and remove.
*
* This class performs fast contains (typically O(1), worst case O(n) but that is rare in practice). Remove is somewhat slower due
* to {@link #order()}. Add may be slightly slower, depending on hash collisions. Hashcodes are rehashed to reduce
* collisions and the need to resize. Load factors greater than 0.91 greatly increase the chances to resize to the next higher POT
* size.
*
* Unordered sets and maps are not designed to provide especially fast iteration. Iteration is faster with {@link Ordered} types like
* HolderOrderedSet and ObjectObjectOrderedMap.
*
* You can customize most behavior of this set by extending it. {@link #place(Object)} can be overridden to change how hashCodes
* are calculated on K keys (which can be useful for types like {@link StringBuilder} that don't implement hashCode()), and
* {@link #locateKey(Object)} can be overridden to change how equality is calculated for K keys.
*
* This implementation uses linear probing with the backward shift algorithm for removal.
* It tries different hashes from a simple family, with the hash changing on resize.
* Linear probing continues to work even when all hashCodes collide, just more slowly.
*
* @author Nathan Sweet
* @author Tommy Ettinger
*/
public class HolderOrderedSet extends HolderSet implements Ordered {
protected final ObjectList items;
/**
* Creates a new set with an initial capacity of 51 and a load factor of {@link Utilities#getDefaultLoadFactor()}. This does not set the
* extractor, so the HolderSet will not be usable until {@link #setExtractor(ObjToObjFunction)} is called with
* a valid ObjToObjFunction that gets K keys from T items.
*/
public HolderOrderedSet () {
super();
items = new ObjectList<>();
}
/**
* Creates a new set with an initial capacity of 51 and a load factor of {@link Utilities#getDefaultLoadFactor()}.
*
* @param extractor a function that will be used to extract K keys from the T items put into this
*/
public HolderOrderedSet (ObjToObjFunction extractor) {
super(extractor);
items = new ObjectList<>();
}
/**
* Creates a new set with a load factor of {@link Utilities#getDefaultLoadFactor()}.
*
* @param extractor a function that will be used to extract K keys from the T items put into this
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two.
*/
public HolderOrderedSet (ObjToObjFunction extractor, int initialCapacity) {
super(extractor, initialCapacity);
items = new ObjectList<>(initialCapacity);
}
/**
* Creates a new set with the specified initial capacity and load factor. This set will hold initialCapacity items before
* growing the backing table.
*
* @param extractor a function that will be used to extract K keys from the T items put into this
* @param initialCapacity If not a power of two, it is increased to the next nearest power of two.
* @param loadFactor what fraction of the capacity can be filled before this has to resize; 0 < loadFactor <= 1
*/
public HolderOrderedSet (ObjToObjFunction extractor, int initialCapacity, float loadFactor) {
super(extractor, initialCapacity, loadFactor);
items = new ObjectList<>(initialCapacity);
}
/**
* Creates a new instance containing the items in the specified iterator.
*
* @param coll an iterator that will have its remaining contents added to this
*/
public HolderOrderedSet (@NonNull ObjToObjFunction extractor, Iterator extends T> coll) {
this(extractor);
addAll(coll);
}
/**
* Creates a new set identical to the specified set.
* This doesn't copy the extractor; instead it references the same ObjToObjFunction from the argument.
* This can have issues if the extractor causes side effects or is stateful.
*/
public HolderOrderedSet (HolderOrderedSet set) {
super(set);
items = new ObjectList<>(set.items);
}
/**
* Creates a new set that contains all distinct elements in {@code coll}, using {@code extractor} to get the keys that determine distinctness.
*
* @param extractor a function that will be used to extract K keys from the T items in coll
* @param coll a Collection of T items; depending on extractor, some different T items may not be added because their K key is equal
*/
public HolderOrderedSet (ObjToObjFunction extractor, Collection extends T> coll) {
this(extractor, coll.size());
addAll(coll);
}
/**
* Creates a new set that contains all distinct elements in {@code items}, using {@code extractor} to get the keys that determine distinctness.
*
* @param extractor a function that will be used to extract K keys from the T items in coll
* @param items an array of T items; depending on extractor, some different T items may not be added because their K key is equal
*/
public HolderOrderedSet (ObjToObjFunction extractor, T[] items) {
this(extractor, items.length);
addAll(items);
}
/**
* Creates a new set by copying {@code count} items from the given Ordered, starting at {@code offset} in that Ordered,
* into this, using {@code extractor} to get the keys that determine distinctness.
*
* @param extractor a function that will be used to extract K keys from the T items in coll
* @param other another Ordered of the same type
* @param offset the first index in other's ordering to draw an item from
* @param count how many items to copy from other
*/
public HolderOrderedSet (ObjToObjFunction extractor, Ordered other, int offset, int count) {
this(extractor, count);
addAll(0, other, offset, count);
}
@Override
public boolean add (T key) {
return super.add(key) && items.add(key);
}
/**
* Sets the key at the specified index. Returns true if the key was not already in the set. If this set already contains the
* key, the existing key's index is changed if needed and false is returned. Note, the order of the parameters matches the
* order in {@link ObjectList} and the rest of the JDK, not OrderedSet in libGDX.
*
* @param index where in the iteration order to add the given key, or to move it if already present
* @param key what T item to try to add, if not already present
* @return true if the key was added for the first time, or false if the key was already present (even if moved)
*/
public boolean add (int index, T key) {
if (!super.add(key)) {
int oldIndex = items.indexOf(key);
if (oldIndex != index) {items.add(index, items.remove(oldIndex));}
return false;
}
items.add(index, key);
return true;
}
public boolean addAll (HolderOrderedSet set) {
ensureCapacity(set.size);
ObjectList si = set.items;
int oldSize = size;
for (int i = 0, n = si.size(); i < n; i++) {add(si.get(i));}
return size != oldSize;
}
/**
* Adds up to {@code count} items, starting from {@code offset}, in the Ordered {@code other} to this set,
* inserting at the end of the iteration order.
*
* @param other a non-null {@link Ordered} of {@code T}
* @param offset the first index in {@code other} to use
* @param count how many indices in {@code other} to use
* @return true if this is modified by this call, as {@link #addAll(Collection)} does
*/
public boolean addAll (Ordered other, int offset, int count) {
return addAll(size, other, offset, count);
}
/**
* Adds up to {@code count} items, starting from {@code offset}, in the Ordered {@code other} to this set,
* inserting starting at {@code insertionIndex} in the iteration order.
*
* @param insertionIndex where to insert into the iteration order
* @param other a non-null {@link Ordered} of {@code T}
* @param offset the first index in {@code other} to use
* @param count how many indices in {@code other} to use
* @return true if this is modified by this call, as {@link #addAll(Collection)} does
*/
public boolean addAll (int insertionIndex, Ordered other, int offset, int count) {
boolean changed = false;
int end = Math.min(offset + count, other.size());
ensureCapacity(end - offset);
for (int i = offset; i < end; i++) {
add(insertionIndex++, other.order().get(i));
changed = true;
}
return changed;
}
/**
* Takes a K key to remove from this HolderOrderedSet, not a T item.
*
* @param key should be a K key, not a T item
* @return true if this was modified
*/
@Override
public boolean remove (Object key) {
return items.remove(super.get(key)) && super.remove(key);
}
/**
* Removes and returns the item at the given index in this set's order.
*
* @param index the index of the item to remove
* @return the removed item
*/
public T removeAt (int index) {
T item = items.removeAt(index);
assert extractor != null;
super.remove(extractor.apply(item));
return item;
}
/**
* Gets the first item in the order.
*
* @return the first item in this set's order
* @throws IllegalStateException if this is empty.
*/
@Override
public T first () {
if (size == 0)
throw new IllegalStateException("HolderOrderedSet is empty.");
return items.get(0);
}
/**
* Increases the size of the backing array to accommodate the specified number of additional items / loadFactor. Useful before
* adding many items to avoid multiple backing array resizes.
*
* @param additionalCapacity how many additional items this should be able to hold without resizing (probably)
*/
@Override
public void ensureCapacity (int additionalCapacity) {
int tableSize = tableSize(size + additionalCapacity, loadFactor);
if (keyTable.length < tableSize) {resize(tableSize);}
items.ensureCapacity(additionalCapacity);
}
/**
* Changes the item {@code before} to {@code after} without changing its position in the order. Returns true if {@code after}
* has been added to the ObjectOrderedSet and {@code before} has been removed; returns false if {@code after} is already present or
* {@code before} is not present. If you are iterating over an ObjectOrderedSet and have an index, you should prefer
* {@link #alterAt(int, Object)}, which doesn't need to search for an index like this does and so can be faster.
*
* @param before an item that must be present for this to succeed
* @param after an item that must not be in this set for this to succeed
* @return true if {@code before} was removed and {@code after} was added, false otherwise
*/
public boolean alter (T before, T after) {
if (contains(extractor.apply(after))) {return false;}
if (!super.remove(extractor.apply(before))) {return false;}
super.add(after);
items.set(items.indexOf(before), after);
return true;
}
/**
* Changes the item at the given {@code index} in the order to {@code after}, without changing the ordering of other items. If
* {@code after} is already present, this returns false; it will also return false if {@code index} is invalid for the size of
* this set. Otherwise, it returns true. Unlike {@link #alter(Object, Object)}, this operates in constant time.
*
* @param index the index in the order of the item to change; must be non-negative and less than {@link #size}
* @param after the item that will replace the contents at {@code index}; this item must not be present for this to succeed
* @return true if {@code after} successfully replaced the contents at {@code index}, false otherwise
*/
public boolean alterAt (int index, T after) {
if (index < 0 || index >= size || contains(extractor.apply(after))) {return false;}
super.remove(items.get(index));
super.add(after);
items.set(index, after);
return true;
}
/**
* Gets the T item at the given {@code index} in the insertion order. The index should be between 0 (inclusive) and
* {@link #size()} (exclusive).
*
* @param index an index in the insertion order, between 0 (inclusive) and {@link #size()} (exclusive)
* @return the item at the given index
*/
public T getAt (int index) {
return items.get(index);
}
@Override
public void clear (int maximumCapacity) {
items.clear();
super.clear(maximumCapacity);
}
@Override
public void clear () {
items.clear();
super.clear();
}
/**
* Gets the ObjectList of T items in the order this class will iterate through them.
* Returns a direct reference to the same ObjectList this uses, so changes to the returned list will
* also change the iteration order here.
*
* @return the ObjectList of T items, in iteration order (usually insertion-order), that this uses
*/
@Override
public ObjectList order () {
return items;
}
/**
* Sorts this ObjectOrderedSet in-place by the T items' natural ordering; {@code T} must implement {@link Comparable}.
*/
public void sort () {
items.sort(null);
}
/**
* Sorts this ObjectOrderedSet in-place by the given Comparator used on the T items. If {@code comp} is null, then this
* will sort by the natural ordering of the items, which requires {@code T} to {@link Comparable}.
*
* @param comp a Comparator that can compare two {@code T} items, or null to use the items' natural ordering
*/
public void sort (@Nullable Comparator super T> comp) {
items.sort(comp);
}
/**
* Removes the items between the specified start index, inclusive, and end index, exclusive.
* Note that this takes different arguments than some other range-related methods; this needs
* a start index and an end index, rather than a count of items. This matches the behavior in
* the JDK collections.
*
* @param start the first index to remove, inclusive
* @param end the last index (after what should be removed), exclusive
*/
@Override
public void removeRange (int start, int end) {
start = Math.max(0, start);
end = Math.min(items.size(), end);
for (int i = start; i < end; i++) {
super.remove(items.get(i));
}
items.removeRange(start, end);
}
/**
* Reduces the size of the set to the specified size. If the set is already smaller than the specified
* size, no action is taken.
*
* @param newSize the target size to try to reach by removing items, if smaller than the current size
*/
@Override
public void truncate (int newSize) {
if (size > newSize) {removeRange(newSize, size);}
}
/**
* Iterates through items in the same order as {@link #order()}.
* Reuses one of two iterators, and does not permit nested iteration;
* use {@link HolderOrderedSetIterator#HolderOrderedSetIterator(HolderOrderedSet)} to nest iterators.
*
* @return an {@link Iterator} over the T items in this, in order
*/
@Override
public @NonNull HolderSetIterator iterator () {
if (iterator1 == null || iterator2 == null) {
iterator1 = new HolderOrderedSetIterator<>(this);
iterator2 = new HolderOrderedSetIterator<>(this);
}
if (!iterator1.valid) {
iterator1.reset();
iterator1.valid = true;
iterator2.valid = false;
return iterator1;
}
iterator2.reset();
iterator2.valid = true;
iterator1.valid = false;
return iterator2;
}
@Override
public String toString (String separator) {
if (size == 0) {return "{}";}
ObjectList items = this.items;
StringBuilder buffer = new StringBuilder(32);
buffer.append('{');
buffer.append(items.get(0));
for (int i = 1; i < size; i++) {
buffer.append(separator);
buffer.append(items.get(i));
}
buffer.append('}');
return buffer.toString();
}
@Override
public String toString () {
return toString(", ");
}
public static class HolderOrderedSetIterator extends HolderSetIterator {
protected final ObjectList items;
public HolderOrderedSetIterator (HolderOrderedSet set) {
super(set);
items = set.items;
}
@Override
public void reset () {
nextIndex = 0;
hasNext = set.size > 0;
}
@Override
public T next () {
if (!hasNext) {throw new NoSuchElementException();}
if (!valid) {throw new RuntimeException("#iterator() cannot be used nested.");}
T key = items.get(nextIndex);
nextIndex++;
hasNext = nextIndex < set.size;
return key;
}
@Override
public void remove () {
if (nextIndex < 0) {throw new IllegalStateException("next must be called before remove.");}
nextIndex--;
assert set.extractor != null;
set.remove(set.extractor.apply(items.get(nextIndex)));
}
}
/**
* Constructs an empty set given only an extractor function.
* This is usually less useful than just using the constructor, but can be handy
* in some code-generation scenarios when you don't know how many arguments you will have.
*
* @param extractor a ObjToObjFunction that takes a T and gets a unique K from it; often a method reference
* @param the type of items; must be given explicitly
* @param the type of keys that extractor pulls from T items
* @return a new set containing nothing
*/
public static HolderOrderedSet with(ObjToObjFunction extractor) {
return new HolderOrderedSet<>(extractor, 0);
}
/**
* Creates a new HolderOrderedSet that holds only the given item, but can be resized.
* @param extractor a ObjToObjFunction that takes a T and gets a unique K from it; often a method reference
* @param item one T item
* @return a new HolderOrderedSet that holds the given item
* @param the type of item, typically inferred
* @param the type of keys that extractor pulls from T items
*/
public static HolderOrderedSet with(ObjToObjFunction extractor, T item) {
HolderOrderedSet set = new HolderOrderedSet<>(extractor, 1);
set.add(item);
return set;
}
/**
* Creates a new HolderOrderedSet that holds only the given items, but can be resized.
* @param extractor a ObjToObjFunction that takes a T and gets a unique K from it; often a method reference
* @param item0 a T item
* @param item1 a T item
* @return a new HolderOrderedSet that holds the given items
* @param the type of item, typically inferred
* @param the type of keys that extractor pulls from T items
*/
public static HolderOrderedSet with(ObjToObjFunction extractor, T item0, T item1) {
HolderOrderedSet set = new HolderOrderedSet<>(extractor, 2);
set.add(item0, item1);
return set;
}
/**
* Creates a new HolderOrderedSet that holds only the given items, but can be resized.
* @param extractor a ObjToObjFunction that takes a T and gets a unique K from it; often a method reference
* @param item0 a T item
* @param item1 a T item
* @param item2 a T item
* @return a new HolderOrderedSet that holds the given items
* @param the type of item, typically inferred
* @param the type of keys that extractor pulls from T items
*/
public static HolderOrderedSet with(ObjToObjFunction extractor, T item0, T item1, T item2) {
HolderOrderedSet set = new HolderOrderedSet<>(extractor, 3);
set.add(item0, item1, item2);
return set;
}
/**
* Creates a new HolderOrderedSet that holds only the given items, but can be resized.
* @param extractor a ObjToObjFunction that takes a T and gets a unique K from it; often a method reference
* @param item0 a T item
* @param item1 a T item
* @param item2 a T item
* @param item3 a T item
* @return a new HolderOrderedSet that holds the given items
* @param the type of item, typically inferred
* @param the type of keys that extractor pulls from T items
*/
public static HolderOrderedSet with(ObjToObjFunction extractor, T item0, T item1, T item2, T item3) {
HolderOrderedSet set = new HolderOrderedSet<>(extractor, 4);
set.add(item0, item1, item2, item3);
return set;
}
/**
* Creates a new HolderOrderedSet that holds only the given items, but can be resized.
* @param extractor a ObjToObjFunction that takes a T and gets a unique K from it; often a method reference
* @param item0 a T item
* @param item1 a T item
* @param item2 a T item
* @param item3 a T item
* @param item4 a T item
* @return a new HolderOrderedSet that holds the given items
* @param the type of item, typically inferred
* @param the type of keys that extractor pulls from T items
*/
public static HolderOrderedSet with(ObjToObjFunction extractor, T item0, T item1, T item2, T item3, T item4) {
HolderOrderedSet set = new HolderOrderedSet<>(extractor, 5);
set.add(item0, item1, item2, item3);
set.add(item4);
return set;
}
/**
* Creates a new HolderOrderedSet that holds only the given items, but can be resized.
* @param extractor a ObjToObjFunction that takes a T and gets a unique K from it; often a method reference
* @param item0 a T item
* @param item1 a T item
* @param item2 a T item
* @param item3 a T item
* @param item4 a T item
* @param item5 a T item
* @return a new HolderOrderedSet that holds the given items
* @param the type of item, typically inferred
* @param the type of keys that extractor pulls from T items
*/
public static HolderOrderedSet with(ObjToObjFunction extractor, T item0, T item1, T item2, T item3, T item4, T item5) {
HolderOrderedSet set = new HolderOrderedSet<>(extractor, 6);
set.add(item0, item1, item2, item3);
set.add(item4, item5);
return set;
}
/**
* Creates a new HolderOrderedSet that holds only the given items, but can be resized.
* @param extractor a ObjToObjFunction that takes a T and gets a unique K from it; often a method reference
* @param item0 a T item
* @param item1 a T item
* @param item2 a T item
* @param item3 a T item
* @param item4 a T item
* @param item5 a T item
* @param item6 a T item
* @return a new HolderOrderedSet that holds the given items
* @param the type of item, typically inferred
* @param the type of keys that extractor pulls from T items
*/
public static HolderOrderedSet with(ObjToObjFunction extractor, T item0, T item1, T item2, T item3, T item4, T item5, T item6) {
HolderOrderedSet set = new HolderOrderedSet<>(extractor, 7);
set.add(item0, item1, item2, item3);
set.add(item4, item5, item6);
return set;
}
/**
* Creates a new HolderOrderedSet that holds only the given items, but can be resized.
* @param extractor a ObjToObjFunction that takes a T and gets a unique K from it; often a method reference
* @param item0 a T item
* @param item1 a T item
* @param item2 a T item
* @param item3 a T item
* @param item4 a T item
* @param item5 a T item
* @param item6 a T item
* @return a new HolderOrderedSet that holds the given items
* @param the type of item, typically inferred
* @param the type of keys that extractor pulls from T items
*/
public static HolderOrderedSet with(ObjToObjFunction extractor, T item0, T item1, T item2, T item3, T item4, T item5, T item6, T item7) {
HolderOrderedSet set = new HolderOrderedSet<>(extractor, 8);
set.add(item0, item1, item2, item3);
set.add(item4, item5, item6, item7);
return set;
}
/**
* Creates a new HolderOrderedSet that holds only the given items, but can be resized.
* This overload will only be used when an array is supplied and the type of the
* items requested is the component type of the array, or if varargs are used and
* there are 9 or more arguments.
* @param extractor a ObjToObjFunction that takes a T and gets a unique K from it; often a method reference
* @param varargs a T varargs or T array; remember that varargs allocate
* @return a new HolderOrderedSet that holds the given items
* @param the type of item, typically inferred
* @param the type of keys that extractor pulls from T items
*/
@SafeVarargs
public static HolderOrderedSet with(ObjToObjFunction extractor, T... varargs) {
return new HolderOrderedSet<>(extractor, varargs);
}
}