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 extends I> 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 extends T> 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 extends I> 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