diff --git a/src/main/java/com/github/tommyettinger/ds/FilteredIterableOrderedSet.java b/src/main/java/com/github/tommyettinger/ds/FilteredIterableOrderedSet.java new file mode 100644 index 00000000..604f8c61 --- /dev/null +++ b/src/main/java/com/github/tommyettinger/ds/FilteredIterableOrderedSet.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2022-2023 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 com.github.tommyettinger.function.ObjPredicate; +import com.github.tommyettinger.function.ObjToSameFunction; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Objects; + +/** + * A customizable variant on ObjectOrderedSet that uses Iterable items made of T sub-items, and only considers a sub-item (for + * equality and hashing purposes) if that sub-item satisfies a predicate. This can also edit the sub-items that pass + * the filter, such as normalize their data during comparisons (and hashing). You will usually want to call + * {@link #setFilter(ObjPredicate)} and/or {@link #setEditor(ObjToSameFunction)} to change the behavior of hashing and + * equality before you enter any items, unless you have specified the filter and/or editor you want in the constructor. + * Calling {@link #setModifiers(ObjPredicate, ObjToSameFunction)} is recommended if you need to set both the filter and + * the editor; you could also set them in the constructor. + *
+ * This class is related to {@link FilteredStringOrderedSet}, which can be seen as using a String as an item and the characters + * of that String as its sub-items. That means this is also similar to {@link CaseInsensitiveOrderedSet}, which is essentially + * a specialized version of FilteredIterableOrderedSet (which can be useful for serialization). + */ +public class FilteredIterableOrderedSet> extends ObjectOrderedSet { + protected ObjPredicate filter = c -> true; + protected ObjToSameFunction editor = c -> c; + /** + * Creates a new set with an initial capacity of 51 and a load factor of {@link Utilities#getDefaultLoadFactor()}. + * This considers all sub-items in an Iterable item and does not edit any sub-items. + */ + public FilteredIterableOrderedSet () { + super(); + } + + /** + * Creates a new set with the specified initial capacity and a load factor of {@link Utilities#getDefaultLoadFactor()}. + * This set will hold initialCapacity items before growing the backing table. + * This considers all sub-items in an Iterable item and does not edit any sub-items. + * + * @param initialCapacity If not a power of two, it is increased to the next nearest power of two. + */ + public FilteredIterableOrderedSet (int initialCapacity) { + super(initialCapacity); + } + + /** + * Creates a new set with the specified initial capacity and load factor. This set will hold initialCapacity items before + * growing the backing table. + * This considers all sub-items in an Iterable item and does not edit any sub-items. + * + * @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 FilteredIterableOrderedSet (int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor); + } + + /** + * Creates a new set with an initial capacity of 51 and a load factor of {@link Utilities#getDefaultLoadFactor()}. + * This uses the specified filter and editor. + * + * @param filter a ObjPredicate that should return true iff a sub-item should be considered for equality/hashing + * @param editor a ObjToSameFunction that will be given a sub-item and may return a potentially different {@code T} item + */ + public FilteredIterableOrderedSet (ObjPredicate filter, ObjToSameFunction editor) { + super(); + this.filter = filter; + this.editor = editor; + } + + /** + * Creates a new set with the specified initial capacity and a load factor of {@link Utilities#getDefaultLoadFactor()}. + * This set will hold initialCapacity items before growing the backing table. + * This uses the specified filter and editor. + * + * @param filter a ObjPredicate that should return true iff a sub-item should be considered for equality/hashing + * @param editor a ObjToSameFunction that will be given a sub-item and may return a potentially different {@code T} item + * @param initialCapacity If not a power of two, it is increased to the next nearest power of two. + */ + public FilteredIterableOrderedSet (ObjPredicate filter, ObjToSameFunction editor, int initialCapacity) { + super(initialCapacity); + this.filter = filter; + this.editor = editor; + } + + /** + * Creates a new set with the specified initial capacity and load factor. This set will hold initialCapacity items before + * growing the backing table. + * This uses the specified filter and editor. + * + * @param filter a ObjPredicate that should return true iff a sub-item should be considered for equality/hashing + * @param editor a ObjToSameFunction that will be given a sub-item and may return a potentially different {@code T} item + * @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 FilteredIterableOrderedSet (ObjPredicate filter, ObjToSameFunction editor, int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor); + this.filter = filter; + this.editor = editor; + } + + /** + * Creates a new set identical to the specified set. + * + * @param set another FilteredIterableOrderedSet to copy + */ + public FilteredIterableOrderedSet (FilteredIterableOrderedSet set) { + super(set); + filter = set.filter; + editor = set.editor; + } + + /** + * Creates a new set that contains all distinct elements in {@code coll}. + * This uses the specified filter and editor, including while it enters the items in coll. + * + * @param filter a ObjPredicate that should return true iff a sub-item should be considered for equality/hashing + * @param editor a ObjToSameFunction that will be given a sub-item and may return a potentially different {@code T} item + * @param coll a Collection implementation to copy, such as an ObjectList or a Set that isn't a FilteredIterableOrderedSet + */ + public FilteredIterableOrderedSet (ObjPredicate filter, ObjToSameFunction editor, Collection coll) { + this(filter, editor, coll.size(), Utilities.getDefaultLoadFactor()); + addAll(coll); + } + + /** + * Creates a new set using {@code length} items from the given {@code array}, starting at {@code} offset (inclusive). + * This uses the specified filter and editor, including while it enters the items in array. + * + * @param filter a ObjPredicate that should return true iff a sub-item should be considered for equality/hashing + * @param editor a ObjToSameFunction that will be given a sub-item and may return a potentially different {@code T} item + * @param array an array to draw items from + * @param offset the first index in array to draw an item from + * @param length how many items to take from array; bounds-checking is the responsibility of the using code + */ + public FilteredIterableOrderedSet (ObjPredicate filter, ObjToSameFunction editor, I[] array, int offset, int length) { + this(filter, editor, length, Utilities.getDefaultLoadFactor()); + addAll(array, offset, length); + } + + /** + * Creates a new set containing all the items in the given array. + * This uses the specified filter and editor, including while it enters the items in array. + * + * @param filter a ObjPredicate that should return true iff a sub-item should be considered for equality/hashing + * @param editor a ObjToSameFunction that will be given a sub-item and may return a potentially different {@code T} item + * @param array an array that will be used in full, except for duplicate items + */ + public FilteredIterableOrderedSet (ObjPredicate filter, ObjToSameFunction editor, I[] array) { + this(filter, editor, array, 0, array.length); + } + + public ObjPredicate getFilter() { + return filter; + } + + /** + * Sets the filter that determines which sub-items in an Iterable are considered for equality and hashing, then + * returns this object, for chaining. ObjPredicate filters could be lambdas or method references that take a + * sub-item and return true if that sub-item will be used for hashing/equality, or return false to ignore it. + * The default filter always returns true. If the filter changes, that invalidates anything previously entered into + * this, so before changing the filter this clears the entire data structure, removing all existing items. + * @param filter a ObjPredicate that should return true iff a sub-item should be considered for equality/hashing + * @return this, for chaining + */ + public FilteredIterableOrderedSet setFilter(ObjPredicate filter) { + clear(); + this.filter = filter; + return this; + } + + public ObjToSameFunction getEditor() { + return editor; + } + + /** + * Sets the editor that can alter the sub-items in an Iterable item when they are being used for equality and + * hashing. This does not apply any changes to the items in this data structure; it only affects how they are + * hashed or compared. An editor could be a lambda or method reference; the only real requirement is that it + * takes a {@code T} item and returns a {@code T} item. + * The default filter returns the sub-item it is passed without changes. If the editor changes, that invalidates + * anything previously entered into this, so before changing the editor this clears the entire data + * structure, removing all existing items. + * @param editor a ObjToSameFunction that will be given a sub-item and may return a potentially different {@code T} item + * @return this, for chaining + */ + public FilteredIterableOrderedSet setEditor(ObjToSameFunction editor) { + clear(); + this.editor = editor; + return this; + } + + /** + * Equivalent to calling {@code mySet.setFilter(filter).setEditor(editor)}, but only clears the data structure once. + * @see #setFilter(ObjPredicate) + * @see #setEditor(ObjToSameFunction) + * @param filter a ObjPredicate that should return true iff a sub-item should be considered for equality/hashing + * @param editor a ObjToSameFunction that will be given a sub-item and may return a potentially different {@code T} item + * @return this, for chaining + */ + public FilteredIterableOrderedSet setModifiers(ObjPredicate filter, ObjToSameFunction editor) { + clear(); + this.filter = filter; + this.editor = editor; + return this; + } + + protected long hashHelper(I s) { + long hash = 0x9E3779B97F4A7C15L + hashMultiplier; // golden ratio + for (T c : s) { + if(filter.test(c)){ + hash = (hash + editor.apply(c).hashCode()) * hashMultiplier; + } + } + return hash; + } + + @Override + protected int place (Object item) { + if (item instanceof Iterable) { + return (int)(hashHelper((I) item) >>> shift); + } + return super.place(item); + } + + @Override + protected boolean equate (Object left, @Nullable Object right) { + if (left == right) + return true; + if(right == null) return false; + if ((left instanceof Iterable) && (right instanceof Iterable)) { + I l = (I)left, r = (I)right; + int countL = 0, countR = 0; + Iterator i = l.iterator(), j = r.iterator(); + T cl = null, cr = null; + while (i.hasNext() || j.hasNext()) { + if (!i.hasNext()) { + cl = null; + } else { + boolean found = false; + while (i.hasNext() && !(found = filter.test(cl = i.next()))) { + cl = null; + } + if(found) countL++; + } + if (!j.hasNext()) { + cr = null; + } else { + boolean found = false; + while (j.hasNext() && !(found = filter.test(cr = j.next()))) { + cr = null; + } + if(found) countR++; + } + if (!Objects.equals(cl, cr) && !Objects.equals((editor.apply(cl)), (editor.apply(cr)))) { + return false; + } + } + return countL == countR; + } + return false; + } + + @Override + public int hashCode () { + int h = size; + Object[] keyTable = this.keyTable; + for (int i = 0, n = keyTable.length; i < n; i++) { + Object key = keyTable[i]; + if (key != null) {h += hashHelper((I)key);} + } + return h; + } + + public static > FilteredIterableOrderedSet with (ObjPredicate filter, ObjToSameFunction editor, I item) { + FilteredIterableOrderedSet set = new FilteredIterableOrderedSet<>(filter, editor, 1, Utilities.getDefaultLoadFactor()); + set.add(item); + return set; + } + + @SafeVarargs + public static > FilteredIterableOrderedSet with (ObjPredicate filter, ObjToSameFunction editor, I... array) { + return new FilteredIterableOrderedSet<>(filter, editor, array); + } + +} diff --git a/src/main/java/com/github/tommyettinger/ds/FilteredIterableSet.java b/src/main/java/com/github/tommyettinger/ds/FilteredIterableSet.java index cda2c55c..fb6af5c5 100644 --- a/src/main/java/com/github/tommyettinger/ds/FilteredIterableSet.java +++ b/src/main/java/com/github/tommyettinger/ds/FilteredIterableSet.java @@ -35,7 +35,7 @@ *
* This class is related to {@link FilteredStringSet}, which can be seen as using a String as an item and the characters * of that String as its sub-items. That means this is also similar to {@link CaseInsensitiveSet}, which is essentially - * a specialized version of FilteredStringSet (which can be useful for serialization). + * a specialized version of FilteredIterableSet (which can be useful for serialization). */ public class FilteredIterableSet> extends ObjectSet { protected ObjPredicate filter = c -> true; @@ -118,7 +118,7 @@ public FilteredIterableSet (ObjPredicate filter, ObjToSameFunction editor, /** * Creates a new set identical to the specified set. * - * @param set another FilteredStringSet to copy + * @param set another FilteredIterableSet to copy */ public FilteredIterableSet (FilteredIterableSet set) { super(set); @@ -132,7 +132,7 @@ public FilteredIterableSet (FilteredIterableSet set) { * * @param filter a ObjPredicate that should return true iff a sub-item should be considered for equality/hashing * @param editor a ObjToSameFunction that will be given a sub-item and may return a potentially different {@code T} item - * @param coll a Collection implementation to copy, such as an ObjectList or a Set that isn't a FilteredStringSet + * @param coll a Collection implementation to copy, such as an ObjectList or a Set that isn't a FilteredIterableSet */ public FilteredIterableSet (ObjPredicate filter, ObjToSameFunction editor, Collection coll) { this(filter, editor, coll.size(), Utilities.getDefaultLoadFactor()); @@ -171,7 +171,7 @@ public ObjPredicate getFilter() { } /** - * Sets the filter that determines which sub-items in a String are considered for equality and hashing, then + * Sets the filter that determines which sub-items in an Iterable are considered for equality and hashing, then * returns this object, for chaining. ObjPredicate filters could be lambdas or method references that take a * sub-item and return true if that sub-item will be used for hashing/equality, or return false to ignore it. * The default filter always returns true. If the filter changes, that invalidates anything previously entered into