diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/Joiners.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/Joiners.java index 781dfc075c..9e476d370a 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/Joiners.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/Joiners.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.api.score.stream; +import java.util.Collection; import java.util.function.BiFunction; import java.util.function.BiPredicate; import java.util.function.Function; @@ -35,7 +36,6 @@ public final class Joiners { // TODO Support using non-natural comparators, such as lessThan(leftMapping, rightMapping, comparator). - // TODO Support collection-based joiners, such as containing(), intersecting() and disjoint(). // ************************************************************************ // BiJoiner @@ -78,6 +78,24 @@ public final class Joiners { return new DefaultBiJoiner<>(leftMapping, JoinerType.EQUAL, rightMapping); } + // TODO javadoc + public static @NonNull BiJoiner contain(Function> leftMapping, + Function rightMapping) { + return new DefaultBiJoiner<>(leftMapping, JoinerType.CONTAIN, rightMapping); + } + + // TODO javadoc + public static @NonNull BiJoiner containedIn(Function leftMapping, + Function> rightMapping) { + return new DefaultBiJoiner<>(leftMapping, JoinerType.CONTAINED_IN, rightMapping); + } + + // TODO javadoc + public static @NonNull BiJoiner containAny(Function> leftMapping, + Function> rightMapping) { + return new DefaultBiJoiner<>(leftMapping, JoinerType.CONTAIN_ANY, rightMapping); + } + /** * As defined by {@link #lessThan(Function, Function)} with both arguments using the same mapping. * diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/joiner/DefaultBiJoiner.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/joiner/DefaultBiJoiner.java index 9684f0c5ad..b959776c0e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/joiner/DefaultBiJoiner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/joiner/DefaultBiJoiner.java @@ -15,18 +15,16 @@ public final class DefaultBiJoiner extends AbstractJoiner implements Bi private static final DefaultBiJoiner NONE = new DefaultBiJoiner(new Function[0], new JoinerType[0], new Function[0]); - private final Function[] leftMappings; + private final Function[] leftMappings; - public DefaultBiJoiner(Function leftMapping, JoinerType joinerType, - Function rightMapping) { + public DefaultBiJoiner(Function leftMapping, JoinerType joinerType, Function rightMapping) { super(rightMapping, joinerType); this.leftMappings = new Function[] { leftMapping }; } - public DefaultBiJoiner(Function[] leftMappings, JoinerType[] joinerTypes, - Function[] rightMappings) { + public DefaultBiJoiner(Function[] leftMappings, JoinerType[] joinerTypes, Function[] rightMappings) { super(rightMappings, joinerTypes); - this.leftMappings = leftMappings; + this.leftMappings = (Function[]) Objects.requireNonNull(leftMappings); } public static DefaultBiJoiner merge(List> joinerList) { @@ -55,7 +53,7 @@ public static DefaultBiJoiner merge(List> joi } public Function getLeftMapping(int index) { - return (Function) leftMappings[index]; + return leftMappings[index]; } public boolean matches(A a, B b) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java index e2d03d889d..f284abdbd8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java @@ -25,31 +25,13 @@ final class ComparisonIndexer> private final NavigableMap> comparisonMap; /** - * Construct an {@link ComparisonIndexer} which immediately ends in the backend. - * This means {@code compositeKey} must be a single key. - * * @param comparisonJoinerType the type of comparison to use + * @param keyRetriever determines if it immediately goes to a {@link IndexerBackend} or if it uses a {@link CompositeKey}. * @param downstreamIndexerSupplier the supplier of the downstream indexer */ - public ComparisonIndexer(JoinerType comparisonJoinerType, Supplier> downstreamIndexerSupplier) { - this(comparisonJoinerType, new SingleKeyRetriever<>(), downstreamIndexerSupplier); - } - - /** - * Construct an {@link ComparisonIndexer} which does not immediately go to a {@link IndexerBackend}. - * This means {@code compositeKey} must be an instance of {@link CompositeKey}. - * - * @param comparisonJoinerType the type of comparison to use - * @param keyIndex the index of the key to use within {@link CompositeKey}. - * @param downstreamIndexerSupplier the supplier of the downstream indexer - */ - public ComparisonIndexer(JoinerType comparisonJoinerType, int keyIndex, Supplier> downstreamIndexerSupplier) { - this(comparisonJoinerType, new CompositeKeyRetriever<>(keyIndex), downstreamIndexerSupplier); - } - - private ComparisonIndexer(JoinerType comparisonJoinerType, KeyRetriever keyRetriever, + public ComparisonIndexer(JoinerType comparisonJoinerType, KeyRetriever keyRetriever, Supplier> downstreamIndexerSupplier) { - this.keyRetriever = Objects.requireNonNull(keyRetriever); + this.keyRetriever = Objects.requireNonNull((KeyRetriever) keyRetriever); this.downstreamIndexerSupplier = Objects.requireNonNull(downstreamIndexerSupplier); // For GT/GTE, the iteration order is reversed. // This allows us to iterate over the entire map from the start, stopping when the threshold is reached. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/CompositeKey.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/CompositeKey.java index d98930fef1..785e702e50 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/CompositeKey.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/CompositeKey.java @@ -20,7 +20,7 @@ * TriCompositeKey and higher are rare enough for {@link MegaCompositeKey} to suffice. */ public sealed interface CompositeKey - permits MegaCompositeKey, BiCompositeKey { + permits BiCompositeKey, MegaCompositeKey { static CompositeKey none() { return MegaCompositeKey.EMPTY; @@ -53,7 +53,7 @@ static CompositeKey ofMany(Object... keys) { * @param id Maps to a single {@link Indexer} instance in the indexer chain. * @return May be null if the key is null. * @param {@link ComparisonIndexer} will expect this to implement {@link Comparable}. - * {@link EqualsIndexer} will treat items as the same if they are equal. + * {@link EqualIndexer} will treat items as the same if they are equal. */ Key_ get(int id); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainAnyIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainAnyIndexer.java new file mode 100644 index 0000000000..b4a35d392c --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainAnyIndexer.java @@ -0,0 +1,169 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import ai.timefold.solver.core.impl.util.CompositeListEntry; +import ai.timefold.solver.core.impl.util.ListEntry; +import ai.timefold.solver.core.impl.util.Pair; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +final class ContainAnyIndexer> implements Indexer { + + private final KeyRetriever modifyKeyRetriever; + private final KeyRetriever queryKeyRetriever; + private final Supplier> downstreamIndexerSupplier; + private final Map> downstreamIndexerMap = new HashMap<>(); + + /** + * @param keyRetriever determines if it immediately goes to a {@link IndexerBackend} or if it uses a {@link CompositeKey}. + * @param downstreamIndexerSupplier the supplier of the downstream indexer + */ + public ContainAnyIndexer(KeyRetriever keyRetriever, Supplier> downstreamIndexerSupplier) { + this.modifyKeyRetriever = Objects.requireNonNull((KeyRetriever) keyRetriever); + this.queryKeyRetriever = Objects.requireNonNull((KeyRetriever) keyRetriever); + this.downstreamIndexerSupplier = Objects.requireNonNull(downstreamIndexerSupplier); + } + + @Override + public ListEntry put(Object modifyCompositeKey, T tuple) { + KeyCollection_ indexKeyCollection = modifyKeyRetriever.apply(modifyCompositeKey); + List>> children = new ArrayList<>(indexKeyCollection.size()); + for (Key_ indexKey : indexKeyCollection) { + // Avoids computeIfAbsent in order to not create lambdas on the hot path. + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer == null) { + downstreamIndexer = downstreamIndexerSupplier.get(); + downstreamIndexerMap.put(indexKey, downstreamIndexer); + } + // Even though this method puts a tuple in multiple downstreamIndexers, it does not break size() or forEach() + // because at most one of those downstreamIndexers matches for a particular compositeKey + ListEntry childListEntry = downstreamIndexer.put(modifyCompositeKey, tuple); + children.add(new Pair<>(indexKey, childListEntry)); + } + return new CompositeListEntry<>(tuple, children); + } + + @Override + public void remove(Object modifyCompositeKey, ListEntry entry) { + KeyCollection_ indexKeyCollection = modifyKeyRetriever.apply(modifyCompositeKey); + List>> children = ((CompositeListEntry) entry).getChildren(); + if (indexKeyCollection.size() != children.size()) { + throw new IllegalStateException( + ("Impossible state: the tuple (%s) with composite key (%s) has a different number of children (%d)" + + " than the index key collection size (%d).") + .formatted(entry, modifyCompositeKey, children.size(), indexKeyCollection.size())); + } + for (Pair> child : children) { + Key_ indexKey = child.key(); + ListEntry childListEntry = child.value(); + // Avoids removeIfAbsent in order to not create lambdas on the hot path. + Indexer downstreamIndexer = getDownstreamIndexer(modifyCompositeKey, indexKey); + downstreamIndexer.remove(modifyCompositeKey, childListEntry); + if (downstreamIndexer.isEmpty()) { + downstreamIndexerMap.remove(indexKey); + } + } + } + + private Indexer getDownstreamIndexer(Object compositeKey, Key_ indexerKey) { + Indexer downstreamIndexer = downstreamIndexerMap.get(indexerKey); + if (downstreamIndexer == null) { + throw new IllegalStateException( + "Impossible state: the composite key (%s) doesn't exist in the indexer %s." + .formatted(compositeKey, this)); + } + return downstreamIndexer; + } + + @Override + public int size(Object queryCompositeKey) { + KeyCollection_ indexKeyCollection = queryKeyRetriever.apply(queryCompositeKey); + if (indexKeyCollection.isEmpty()) { + return 0; + } else if (indexKeyCollection.size() == 1) { + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKeyCollection.iterator().next()); + return (downstreamIndexer == null) ? 0 : downstreamIndexer.size(queryCompositeKey); + } else { + AtomicInteger size = new AtomicInteger(0); + forEach(queryCompositeKey, tuple -> size.incrementAndGet()); + return size.get(); + } + } + + @Override + public void forEach(Object queryCompositeKey, Consumer tupleConsumer) { + KeyCollection_ indexKeyCollection = queryKeyRetriever.apply(queryCompositeKey); + if (indexKeyCollection.isEmpty()) { + return; + } else if (indexKeyCollection.size() == 1) { + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKeyCollection.iterator().next()); + if (downstreamIndexer != null) { + downstreamIndexer.forEach(queryCompositeKey, tupleConsumer); + } + } else { + Set distinctingSet = new HashSet<>(indexKeyCollection.size() * 16); + for (Key_ indexKey : indexKeyCollection) { + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer != null) { + downstreamIndexer.forEach(queryCompositeKey, tuple -> { + if (distinctingSet.add(tuple)) { + tupleConsumer.accept(tuple); + } + }); + } + } + } + } + + @Override + public boolean isEmpty() { + return downstreamIndexerMap.isEmpty(); + } + + @Override + public List> asList(Object queryCompositeKey) { + KeyCollection_ indexKeyCollection = queryKeyRetriever.apply(queryCompositeKey); + if (indexKeyCollection.isEmpty()) { + return List.of(); + } else if (indexKeyCollection.size() == 1) { + Key_ indexKey = indexKeyCollection.iterator().next(); + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer == null) { + return List.of(); + } + return downstreamIndexer.asList(queryCompositeKey); + } else { + List> list = new ArrayList<>(downstreamIndexerMap.size() * 16); + Set distinctingSet = new HashSet<>(indexKeyCollection.size() * 16); + for (Key_ indexKey : indexKeyCollection) { + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer != null) { + downstreamIndexer.forEach(queryCompositeKey, tuple -> { + if (distinctingSet.add(tuple)) { + list.addAll(downstreamIndexer.asList(queryCompositeKey)); + } + }); + } + } + return list; + } + } + + @Override + public String toString() { + return "size = " + downstreamIndexerMap.size(); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainIndexer.java new file mode 100644 index 0000000000..0a80a448ba --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainIndexer.java @@ -0,0 +1,128 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import ai.timefold.solver.core.impl.util.CompositeListEntry; +import ai.timefold.solver.core.impl.util.ListEntry; +import ai.timefold.solver.core.impl.util.Pair; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +final class ContainIndexer> implements Indexer { + + private final KeyRetriever modifyKeyRetriever; + private final KeyRetriever queryKeyRetriever; + private final Supplier> downstreamIndexerSupplier; + private final Map> downstreamIndexerMap = new HashMap<>(); + + /** + * @param keyRetriever determines if it immediately goes to a {@link IndexerBackend} or if it uses a {@link CompositeKey}. + * @param downstreamIndexerSupplier the supplier of the downstream indexer + */ + public ContainIndexer(KeyRetriever keyRetriever, Supplier> downstreamIndexerSupplier) { + this.modifyKeyRetriever = Objects.requireNonNull((KeyRetriever) keyRetriever); + this.queryKeyRetriever = Objects.requireNonNull(keyRetriever); + this.downstreamIndexerSupplier = Objects.requireNonNull(downstreamIndexerSupplier); + } + + @Override + public ListEntry put(Object modifyCompositeKey, T tuple) { + KeyCollection_ indexKeyCollection = modifyKeyRetriever.apply(modifyCompositeKey); + List>> children = new ArrayList<>(indexKeyCollection.size()); + for (Key_ indexKey : indexKeyCollection) { + // Avoids computeIfAbsent in order to not create lambdas on the hot path. + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer == null) { + downstreamIndexer = downstreamIndexerSupplier.get(); + downstreamIndexerMap.put(indexKey, downstreamIndexer); + } + // Even though this method puts a tuple in multiple downstreamIndexers, it does not break size() or forEach() + // because at most one of those downstreamIndexers matches for a particular compositeKey + ListEntry childListEntry = downstreamIndexer.put(modifyCompositeKey, tuple); + children.add(new Pair<>(indexKey, childListEntry)); + } + return new CompositeListEntry<>(tuple, children); + } + + @Override + public void remove(Object modifyCompositeKey, ListEntry entry) { + KeyCollection_ indexKeyCollection = modifyKeyRetriever.apply(modifyCompositeKey); + List>> children = ((CompositeListEntry) entry).getChildren(); + if (indexKeyCollection.size() != children.size()) { + throw new IllegalStateException( + ("Impossible state: the tuple (%s) with composite key (%s) has a different number of children (%d)" + + " than the index key collection size (%d).") + .formatted(entry, modifyCompositeKey, children.size(), indexKeyCollection.size())); + } + for (Pair> child : children) { + Key_ indexKey = child.key(); + ListEntry childListEntry = child.value(); + // Avoids removeIfAbsent in order to not create lambdas on the hot path. + Indexer downstreamIndexer = getDownstreamIndexer(modifyCompositeKey, indexKey); + downstreamIndexer.remove(modifyCompositeKey, childListEntry); + if (downstreamIndexer.isEmpty()) { + downstreamIndexerMap.remove(indexKey); + } + } + } + + private Indexer getDownstreamIndexer(Object compositeKey, Key_ indexerKey) { + Indexer downstreamIndexer = downstreamIndexerMap.get(indexerKey); + if (downstreamIndexer == null) { + throw new IllegalStateException( + "Impossible state: the composite key (%s) doesn't exist in the indexer %s." + .formatted(compositeKey, this)); + } + return downstreamIndexer; + } + + @Override + public int size(Object queryCompositeKey) { + Key_ indexKey = queryKeyRetriever.apply(queryCompositeKey); + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer == null) { + return 0; + } + return downstreamIndexer.size(queryCompositeKey); + } + + @Override + public void forEach(Object queryCompositeKey, Consumer tupleConsumer) { + Key_ indexKey = queryKeyRetriever.apply(queryCompositeKey); + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer == null) { + return; + } + downstreamIndexer.forEach(queryCompositeKey, tupleConsumer); + } + + @Override + public boolean isEmpty() { + return downstreamIndexerMap.isEmpty(); + } + + @Override + public List> asList(Object queryCompositeKey) { + Key_ indexKey = queryKeyRetriever.apply(queryCompositeKey); + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer == null) { + return Collections.emptyList(); + } + return downstreamIndexer.asList(queryCompositeKey); + } + + @Override + public String toString() { + return "size = " + downstreamIndexerMap.size(); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainedInIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainedInIndexer.java new file mode 100644 index 0000000000..c0b3936b9e --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ContainedInIndexer.java @@ -0,0 +1,113 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import ai.timefold.solver.core.impl.util.ListEntry; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +final class ContainedInIndexer> implements Indexer { + + private final KeyRetriever modifyKeyRetriever; + private final KeyRetriever queryKeyRetriever; + private final Supplier> downstreamIndexerSupplier; + private final Map> downstreamIndexerMap = new HashMap<>(); + + /** + * @param keyRetriever determines if it immediately goes to a {@link IndexerBackend} or if it uses a {@link CompositeKey}. + * @param downstreamIndexerSupplier the supplier of the downstream indexer + */ + public ContainedInIndexer(KeyRetriever keyRetriever, Supplier> downstreamIndexerSupplier) { + this.modifyKeyRetriever = Objects.requireNonNull(keyRetriever); + this.queryKeyRetriever = Objects.requireNonNull((KeyRetriever) keyRetriever); + this.downstreamIndexerSupplier = Objects.requireNonNull(downstreamIndexerSupplier); + } + + @Override + public ListEntry put(Object modifyCompositeKey, T tuple) { + Key_ indexKey = modifyKeyRetriever.apply(modifyCompositeKey); + // Avoids computeIfAbsent in order to not create lambdas on the hot path. + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer == null) { + downstreamIndexer = downstreamIndexerSupplier.get(); + downstreamIndexerMap.put(indexKey, downstreamIndexer); + } + return downstreamIndexer.put(modifyCompositeKey, tuple); + } + + @Override + public void remove(Object modifyCompositeKey, ListEntry entry) { + Key_ indexKey = modifyKeyRetriever.apply(modifyCompositeKey); + Indexer downstreamIndexer = getDownstreamIndexer(modifyCompositeKey, indexKey, entry); + downstreamIndexer.remove(modifyCompositeKey, entry); + if (downstreamIndexer.isEmpty()) { + downstreamIndexerMap.remove(indexKey); + } + } + + private Indexer getDownstreamIndexer(Object compositeKey, Key_ indexerKey, ListEntry entry) { + Indexer downstreamIndexer = downstreamIndexerMap.get(indexerKey); + if (downstreamIndexer == null) { + throw new IllegalStateException( + "Impossible state: the tuple (%s) with composite key (%s) doesn't exist in the indexer %s." + .formatted(entry, compositeKey, this)); + } + return downstreamIndexer; + } + + @Override + public int size(Object queryCompositeKey) { + KeyCollection_ indexKeyCollection = queryKeyRetriever.apply(queryCompositeKey); + int size = 0; + for (Key_ indexKey : indexKeyCollection) { + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer != null) { + size += downstreamIndexer.size(queryCompositeKey); + } + } + return size; + } + + @Override + public void forEach(Object queryCompositeKey, Consumer tupleConsumer) { + KeyCollection_ indexKeyCollection = queryKeyRetriever.apply(queryCompositeKey); + for (Key_ indexKey : indexKeyCollection) { + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer != null) { + downstreamIndexer.forEach(queryCompositeKey, tupleConsumer); + } + } + } + + @Override + public boolean isEmpty() { + return downstreamIndexerMap.isEmpty(); + } + + @Override + public List> asList(Object queryCompositeKey) { + KeyCollection_ indexKeyCollection = queryKeyRetriever.apply(queryCompositeKey); + List> list = new ArrayList<>(downstreamIndexerMap.size() * 16); + for (Key_ indexKey : indexKeyCollection) { + Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); + if (downstreamIndexer != null) { + list.addAll(downstreamIndexer.asList(queryCompositeKey)); + } + } + return list; + } + + @Override + public String toString() { + return "size = " + downstreamIndexerMap.size(); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexer.java similarity index 80% rename from core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java rename to core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexer.java index c5e89bd1a4..d220edbae0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexer.java @@ -13,30 +13,18 @@ import org.jspecify.annotations.NullMarked; @NullMarked -final class EqualsIndexer implements Indexer { +final class EqualIndexer implements Indexer { private final KeyRetriever keyRetriever; private final Supplier> downstreamIndexerSupplier; private final Map> downstreamIndexerMap = new HashMap<>(); /** - * Construct an {@link EqualsIndexer} which immediately ends in the backend. - * This means {@code compositeKey} must be a single key. - */ - public EqualsIndexer(Supplier> downstreamIndexerSupplier) { - this.keyRetriever = new SingleKeyRetriever<>(); - this.downstreamIndexerSupplier = downstreamIndexerSupplier; - } - - /** - * Construct an {@link EqualsIndexer} which does not immediately go to a {@link IndexerBackend}. - * This means {@code compositeKey} must be an instance of {@link CompositeKey}. - * - * @param keyIndex the index of the key to use within {@link CompositeKey}. + * @param keyRetriever determines if it immediately goes to a {@link IndexerBackend} or if it uses a {@link CompositeKey}. * @param downstreamIndexerSupplier the supplier of the downstream indexer */ - public EqualsIndexer(int keyIndex, Supplier> downstreamIndexerSupplier) { - this.keyRetriever = new CompositeKeyRetriever<>(keyIndex); + public EqualIndexer(KeyRetriever keyRetriever, Supplier> downstreamIndexerSupplier) { + this.keyRetriever = Objects.requireNonNull(keyRetriever); this.downstreamIndexerSupplier = Objects.requireNonNull(downstreamIndexerSupplier); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java index bc0064c2eb..0088909872 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java @@ -12,12 +12,16 @@ * An indexer for entity or fact {@code X}, * maps a property or a combination of properties of {@code X}, denoted by {@code compositeKey}, * to all instances of {@code X} that match those properties, - * depending on the the indexer type (equal, lower than, ...). + * depending on the indexer type (equal, lower than, contain, ...). * For example for {@code {Lesson(id=1, room=A), Lesson(id=2, room=B), Lesson(id=3, room=A)}}, * calling {@code visit(room=A)} would visit lesson 1 and 3. *

* The fact X is wrapped in a Tuple, because the {@link TupleState} is needed by clients of * {@link #forEach(Object, Consumer)}. + *

+ * Some indexer types (such as contain, containedIn, ...) have two different key types (modify key vs query key), + * depending on the operation type (modify operation vs query operation). + * For example, for a contain indexer the modify key is a collection, but the query key is not. * * @param The element type. Often a tuple. * For example for {@code from(A).join(B)}, the tuple is {@code UniTuple} xor {@code UniTuple}. @@ -25,16 +29,44 @@ */ @NullMarked public sealed interface Indexer - permits ComparisonIndexer, EqualsIndexer, IndexerBackend { + permits EqualIndexer, ComparisonIndexer, ContainIndexer, ContainedInIndexer, ContainAnyIndexer, IndexerBackend { - ListEntry put(Object compositeKey, T tuple); + /** + * Modify operation. + * + * @param modifyCompositeKey modify composite key, never null + * @param tuple never null + * @return the entry to allow remove it from the index directly, never null + */ + ListEntry put(Object modifyCompositeKey, T tuple); - void remove(Object compositeKey, ListEntry entry); + /** + * Modify operation. + * + * @param modifyCompositeKey modify composite key, never null + * @param entry never null + */ + void remove(Object modifyCompositeKey, ListEntry entry); - int size(Object compositeKey); + /** + * Query operation. + * + * @param queryCompositeKey query composite key, never null + * @return at least 0 + */ + int size(Object queryCompositeKey); - void forEach(Object compositeKey, Consumer tupleConsumer); + /** + * Query operation. + * + * @param queryCompositeKey query composite key, never null + * @param tupleConsumer never null + */ + void forEach(Object queryCompositeKey, Consumer tupleConsumer); + /** + * @return true if empty + */ boolean isEmpty(); /** @@ -43,10 +75,10 @@ public sealed interface Indexer * If the index is modified, a new instance of this list must be retrieved; * the previous instance is no longer valid and its behavior is undefined. * - * @param compositeKey the composite key + * @param queryCompositeKey query composite key, never null * @return all entries for a given composite key; * the caller must not modify the list */ - List> asList(Object compositeKey); + List> asList(Object queryCompositeKey); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java index b30cc7960a..a0505bf6f6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java @@ -79,6 +79,8 @@ public final class IndexerFactory { public IndexerFactory(AbstractJoiner joiner) { this.joiner = joiner; + // TODO Code encapsulation: remove the field requiresRandomAccess and call joiner.requireRandomAccess() instead? + // TODO It also impacts the flip(). Is requiresRandomAccess a good name? this.requiresRandomAccess = joiner instanceof DefaultBiEnumeratingJoiner; var joinerCount = joiner.getJoinerCount(); if (joinerCount < 2) { @@ -497,48 +499,63 @@ public Indexer buildIndexer(boolean isLeftBridge) { return backendSupplier.get(); } else if (joiner.getJoinerCount() == 1) { // Single joiner maps directly to EqualsIndexer or ComparisonIndexer. var joinerType = joiner.getJoinerType(0); - if (joinerType == JoinerType.EQUAL) { - return new EqualsIndexer<>(backendSupplier); - } else { - // Note that if creating indexer for a right bridge node, the joiner type has to be flipped. - // ( becomes .) - // This does not apply if random access is required, - // because in that case we create a right bridge only, - // and we query it from the left. - var actualJoinerType = isLeftBridge || requiresRandomAccess ? joinerType : joinerType.flip(); - return new ComparisonIndexer<>(actualJoinerType, backendSupplier); - } + KeyRetriever keyRetriever = new SingleKeyRetriever<>(); + return buildIndexerPart(isLeftBridge, joinerType, keyRetriever, backendSupplier); } - // The following code builds the children first, so it needs to iterate over the joiners in reverse order. + // The following code builds the children first, so it iterates over the joiners in reverse order. var descendingJoinerTypeMap = joinerTypeMap.descendingMap(); Supplier> downstreamIndexerSupplier = backendSupplier; var indexPropertyId = descendingJoinerTypeMap.size() - 1; for (var entry : descendingJoinerTypeMap.entrySet()) { var joinerType = entry.getValue(); if (downstreamIndexerSupplier == backendSupplier && indexPropertyId == 0) { - if (joinerType == JoinerType.EQUAL) { - downstreamIndexerSupplier = () -> new EqualsIndexer<>(backendSupplier); - } else { - var actualJoinerType = isLeftBridge ? joinerType : joinerType.flip(); - downstreamIndexerSupplier = () -> new ComparisonIndexer<>(actualJoinerType, backendSupplier); - } + KeyRetriever keyRetriever = new SingleKeyRetriever<>(); + downstreamIndexerSupplier = () -> buildIndexerPart(isLeftBridge, joinerType, keyRetriever, backendSupplier); } else { + KeyRetriever keyRetriever = new CompositeKeyRetriever<>(indexPropertyId); var actualDownstreamIndexerSupplier = downstreamIndexerSupplier; - var effectivelyFinalIndexPropertyId = indexPropertyId; - if (joinerType == JoinerType.EQUAL) { - downstreamIndexerSupplier = - () -> new EqualsIndexer<>(effectivelyFinalIndexPropertyId, actualDownstreamIndexerSupplier); - } else { - var actualJoinerType = isLeftBridge ? joinerType : joinerType.flip(); - downstreamIndexerSupplier = () -> new ComparisonIndexer<>(actualJoinerType, effectivelyFinalIndexPropertyId, - actualDownstreamIndexerSupplier); - } + downstreamIndexerSupplier = () -> buildIndexerPart(isLeftBridge, joinerType, keyRetriever, + actualDownstreamIndexerSupplier); } indexPropertyId--; } return downstreamIndexerSupplier.get(); } + private Indexer buildIndexerPart(boolean isLeftBridge, JoinerType joinerType, KeyRetriever keyRetriever, + Supplier> downstreamIndexerSupplier) { + // Note that if creating indexer for a right bridge node, the joiner type has to be flipped. + // ( becomes .) + // TODO Does the requiresRandomAccess check make sense? + // Shouldn't a right bridge always flip, even if there is no left bridge? + // TODO For neighborhoods, why create a left bridge index and keep it up to date at all? + // This does not apply if random access is required, + // because in that case we create a right bridge only, + // and we query it from the left. + if (!isLeftBridge && !requiresRandomAccess) { + joinerType = joinerType.flip(); + } + switch (joinerType) { + case EQUAL -> { + return new EqualIndexer<>(keyRetriever, downstreamIndexerSupplier); + } + case LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL -> { + return new ComparisonIndexer<>(joinerType, keyRetriever, downstreamIndexerSupplier); + } + case CONTAIN -> { + return new ContainIndexer<>(keyRetriever, downstreamIndexerSupplier); + } + case CONTAINED_IN -> { + return new ContainedInIndexer<>(keyRetriever, downstreamIndexerSupplier); + } + case CONTAIN_ANY -> { + return new ContainAnyIndexer<>(keyRetriever, downstreamIndexerSupplier); + } + default -> throw new IllegalStateException( + "Impossible state: The joiner type (" + joinerType + ") is not implemented."); + } + } + /** * Represents a function which extracts index keys from a tuple. * diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/KeyRetriever.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/KeyRetriever.java index 233185b2a3..b86f1d382f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/KeyRetriever.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/KeyRetriever.java @@ -2,6 +2,14 @@ import java.util.function.Function; +// TODO Naming confusion: KeyRetriever and KeyExtractor are too similar for separate concepts. +/** + * A function that retrieves keys of a composite key for an {@link Indexer}. + * For example, {@code join(..., equals(), lessThan(), greaterThan())} has 3 keys. + * Given {@code ("a", 7, 9)} the key retriever for {@code lessThan()} retrieves {@code 7}. + * + * @param + */ sealed interface KeyRetriever extends Function permits CompositeKeyRetriever, SingleKeyRetriever { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/joiner/AbstractJoiner.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/joiner/AbstractJoiner.java index c809ac6e94..e35dcf1175 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/joiner/AbstractJoiner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/joiner/AbstractJoiner.java @@ -8,11 +8,11 @@ public abstract class AbstractJoiner { protected final Function[] rightMappings; protected final JoinerType[] joinerTypes; - protected AbstractJoiner(Function rightMapping, JoinerType joinerType) { + protected AbstractJoiner(Function rightMapping, JoinerType joinerType) { this(new Function[] { rightMapping }, new JoinerType[] { joinerType }); } - protected AbstractJoiner(Function[] rightMappings, JoinerType[] joinerTypes) { + protected AbstractJoiner(Function[] rightMappings, JoinerType[] joinerTypes) { this.rightMappings = (Function[]) Objects.requireNonNull(rightMappings); this.joinerTypes = Objects.requireNonNull(joinerTypes); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/joiner/JoinerType.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/joiner/JoinerType.java index b6a7ea0a09..23e9b81fe3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/joiner/JoinerType.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/joiner/JoinerType.java @@ -10,9 +10,11 @@ public enum JoinerType { LESS_THAN_OR_EQUAL((a, b) -> ((Comparable) a).compareTo(b) <= 0), GREATER_THAN((a, b) -> ((Comparable) a).compareTo(b) > 0), GREATER_THAN_OR_EQUAL((a, b) -> ((Comparable) a).compareTo(b) >= 0), - CONTAINING((a, b) -> ((Collection) a).contains(b)), - INTERSECTING((a, b) -> intersecting((Collection) a, (Collection) b)), - DISJOINT((a, b) -> disjoint((Collection) a, (Collection) b)); + CONTAIN((a, b) -> ((Collection) a).contains(b)), + CONTAINED_IN((a, b) -> ((Collection) b).contains(a)), + CONTAIN_ANY((a, b) -> containAny((Collection) a, (Collection) b)), + CONTAIN_ALL((a, b) -> containAll((Collection) a, (Collection) b)), + CONTAIN_NONE((a, b) -> containNone((Collection) a, (Collection) b)); private final BiPredicate matcher; @@ -22,10 +24,16 @@ public enum JoinerType { public JoinerType flip() { return switch (this) { + case EQUAL -> this; case LESS_THAN -> GREATER_THAN; case LESS_THAN_OR_EQUAL -> GREATER_THAN_OR_EQUAL; case GREATER_THAN -> LESS_THAN; case GREATER_THAN_OR_EQUAL -> LESS_THAN_OR_EQUAL; + case CONTAIN -> CONTAINED_IN; + case CONTAINED_IN -> CONTAIN; + case CONTAIN_ANY -> this; + case CONTAIN_ALL -> this; + case CONTAIN_NONE -> this; default -> throw new IllegalStateException("The joinerType (%s) cannot be flipped." .formatted(this)); }; @@ -41,14 +49,20 @@ public boolean matches(Object left, Object right) { } } - private static boolean disjoint(Collection leftCollection, Collection rightCollection) { - return leftCollection.stream().noneMatch(rightCollection::contains) && - rightCollection.stream().noneMatch(leftCollection::contains); + private static boolean containAny(Collection leftCollection, Collection rightCollection) { + if (leftCollection.isEmpty() && rightCollection.isEmpty()) { + // Deliberately not aligned with anyMatch() because this is a very common case in constraints + return true; + } + return leftCollection.stream().anyMatch(rightCollection::contains); + } + + private static boolean containAll(Collection leftCollection, Collection rightCollection) { + return leftCollection.containsAll(rightCollection); } - private static boolean intersecting(Collection leftCollection, Collection rightCollection) { - return leftCollection.stream().anyMatch(rightCollection::contains) || - rightCollection.stream().anyMatch(leftCollection::contains); + private static boolean containNone(Collection leftCollection, Collection rightCollection) { + return leftCollection.stream().noneMatch(rightCollection::contains); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/penta/joiner/DefaultPentaJoiner.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/penta/joiner/DefaultPentaJoiner.java index 06da312d9a..c25457444e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/penta/joiner/DefaultPentaJoiner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/penta/joiner/DefaultPentaJoiner.java @@ -16,18 +16,20 @@ public final class DefaultPentaJoiner extends AbstractJoiner i private static final DefaultPentaJoiner NONE = new DefaultPentaJoiner(new QuadFunction[0], new JoinerType[0], new Function[0]); - private final QuadFunction[] leftMappings; - public DefaultPentaJoiner(QuadFunction leftMapping, JoinerType joinerType, - Function rightMapping) { + private final QuadFunction[] leftMappings; + + public DefaultPentaJoiner(QuadFunction leftMapping, JoinerType joinerType, + Function rightMapping) { super(rightMapping, joinerType); this.leftMappings = new QuadFunction[] { leftMapping }; } - private DefaultPentaJoiner(QuadFunction[] leftMappings, JoinerType[] joinerTypes, - Function[] rightMappings) { + private DefaultPentaJoiner(QuadFunction[] leftMappings, JoinerType[] joinerTypes, + Function[] rightMappings) { super(rightMappings, joinerTypes); - this.leftMappings = leftMappings; + this.leftMappings = (QuadFunction[]) Objects.requireNonNull(leftMappings); + ; } public static DefaultPentaJoiner merge(List> joinerList) { @@ -56,7 +58,7 @@ public static DefaultPentaJoiner merge(List getLeftMapping(int index) { - return (QuadFunction) leftMappings[index]; + return leftMappings[index]; } public boolean matches(A a, B b, C c, D d, E e) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/joiner/DefaultQuadJoiner.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/joiner/DefaultQuadJoiner.java index 04f8fe51db..0947b225a8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/joiner/DefaultQuadJoiner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/joiner/DefaultQuadJoiner.java @@ -17,18 +17,18 @@ public final class DefaultQuadJoiner extends AbstractJoiner imple private static final DefaultQuadJoiner NONE = new DefaultQuadJoiner(new TriFunction[0], new JoinerType[0], new Function[0]); - private final TriFunction[] leftMappings; + private final TriFunction[] leftMappings; - public DefaultQuadJoiner(TriFunction leftMapping, JoinerType joinerType, - Function rightMapping) { + public DefaultQuadJoiner(TriFunction leftMapping, JoinerType joinerType, + Function rightMapping) { super(rightMapping, joinerType); this.leftMappings = new TriFunction[] { leftMapping }; } - private DefaultQuadJoiner(TriFunction[] leftMappings, JoinerType[] joinerTypes, - Function[] rightMappings) { + private DefaultQuadJoiner(TriFunction[] leftMappings, JoinerType[] joinerTypes, + Function[] rightMappings) { super(rightMappings, joinerTypes); - this.leftMappings = leftMappings; + this.leftMappings = (TriFunction[]) Objects.requireNonNull(leftMappings); } public static DefaultQuadJoiner merge(List> joinerList) { @@ -57,7 +57,7 @@ public static DefaultQuadJoiner merge(List getLeftMapping(int index) { - return (TriFunction) leftMappings[index]; + return leftMappings[index]; } public boolean matches(A a, B b, C c, D d) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/joiner/DefaultTriJoiner.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/joiner/DefaultTriJoiner.java index c6ae1dcb43..755a6d815d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/joiner/DefaultTriJoiner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/joiner/DefaultTriJoiner.java @@ -17,18 +17,17 @@ public final class DefaultTriJoiner extends AbstractJoiner implement private static final DefaultTriJoiner NONE = new DefaultTriJoiner(new BiFunction[0], new JoinerType[0], new Function[0]); - private final BiFunction[] leftMappings; + private final BiFunction[] leftMappings; - public DefaultTriJoiner(BiFunction leftMapping, JoinerType joinerType, - Function rightMapping) { + public DefaultTriJoiner(BiFunction leftMapping, JoinerType joinerType, Function rightMapping) { super(rightMapping, joinerType); this.leftMappings = new BiFunction[] { leftMapping }; } - private DefaultTriJoiner(BiFunction[] leftMappings, JoinerType[] joinerTypes, - Function[] rightMappings) { + private DefaultTriJoiner(BiFunction[] leftMappings, JoinerType[] joinerTypes, + Function[] rightMappings) { super(rightMappings, joinerTypes); - this.leftMappings = leftMappings; + this.leftMappings = (BiFunction[]) Objects.requireNonNull(leftMappings); } public static DefaultTriJoiner merge(List> joinerList) { @@ -57,7 +56,7 @@ public static DefaultTriJoiner merge(List getLeftMapping(int index) { - return (BiFunction) leftMappings[index]; + return leftMappings[index]; } public boolean matches(A a, B b, C c) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/util/CompositeListEntry.java b/core/src/main/java/ai/timefold/solver/core/impl/util/CompositeListEntry.java new file mode 100644 index 0000000000..2f8160db50 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/util/CompositeListEntry.java @@ -0,0 +1,42 @@ +package ai.timefold.solver.core.impl.util; + +import java.util.List; + +import ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType; + +import org.jspecify.annotations.NullMarked; + +/** + * Allows to store the same tuple in multiple downstream {@link ElementAwareLinkedList}s. + * For example, {@link JoinerType#CONTAIN} need. + * + * @param the tuple type + */ +// TODO turn into record? +// TODO Is this a dirty hack or acceptable design? If the latter, is the name ListEntry ok? +@NullMarked +public final class CompositeListEntry implements ListEntry { + + private final T element; + private final List>> children; + + public CompositeListEntry(T element, List>> children) { + this.element = element; + this.children = children; + } + + @Override + public T getElement() { + return element; + } + + public List>> getChildren() { + return children; + } + + @Override + public String toString() { + return element.toString(); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareArrayList.java b/core/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareArrayList.java index d239b23c93..8ad062a3af 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareArrayList.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareArrayList.java @@ -219,7 +219,6 @@ private Entry(T element, int position) { this.position = position; } - @Override public boolean isRemoved() { return position < 0; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareLinkedList.java b/core/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareLinkedList.java index 37acf509bd..4cd51ee5c5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareLinkedList.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/util/ElementAwareLinkedList.java @@ -325,7 +325,6 @@ public void remove() { list = null; } - @Override public boolean isRemoved() { return list == null; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/util/ListEntry.java b/core/src/main/java/ai/timefold/solver/core/impl/util/ListEntry.java index 8f96bfbf42..290855c270 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/util/ListEntry.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/util/ListEntry.java @@ -4,9 +4,7 @@ @NullMarked public sealed interface ListEntry - permits ElementAwareLinkedList.Entry, ElementAwareArrayList.Entry { - - boolean isRemoved(); + permits ElementAwareLinkedList.Entry, ElementAwareArrayList.Entry, CompositeListEntry { T getElement(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/AbstractIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/AbstractIndexerTest.java index 269b8704f9..0de09b2dd4 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/AbstractIndexerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/AbstractIndexerTest.java @@ -5,11 +5,7 @@ abstract class AbstractIndexerTest { - record Person(String gender, int age) { - - } - - protected List getTuples(Indexer indexer, Object... objectProperties) { + protected static List forEachToTuples(Indexer indexer, Object... objectProperties) { var properties = switch (objectProperties.length) { case 0 -> CompositeKey.none(); case 1 -> CompositeKey.of(objectProperties[0]); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/ContainAnyIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/ContainAnyIndexerTest.java new file mode 100644 index 0000000000..16445fc127 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/ContainAnyIndexerTest.java @@ -0,0 +1,96 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import ai.timefold.solver.core.api.score.stream.Joiners; +import ai.timefold.solver.core.impl.bavet.bi.joiner.DefaultBiJoiner; +import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ContainAnyIndexerTest extends AbstractIndexerTest { + + record TestWorker(String name, List skills, String department) { + } + + record TestJob(String department, List skills) { + } + + private final DefaultBiJoiner joiner = + (DefaultBiJoiner) Joiners.containAny(TestWorker::skills, TestJob::skills) + .and(Joiners.equal(TestWorker::department, TestJob::department)); + + @Test + void isEmpty() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + assertThat(indexer.isEmpty()).isTrue(); + } + + @Test @Disabled + void size() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X", "Y"), "1"))).isEqualTo(0); + + indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "1"), newTuple("Ann")); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X"), "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X", "W"), "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany(List.of("Y"), "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany(List.of("Y", "W"), "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X", "Y"), "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany(List.of("V", "W"), "1"))).isEqualTo(0); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X"), "3"))).isEqualTo(0); + + indexer.put(CompositeKey.ofMany(List.of("X", "Z"), "1"), newTuple("Beth")); + indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "2"), newTuple("Carl")); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X"), "1"))).isEqualTo(2); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X", "W"), "1"))).isEqualTo(2); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X", "Y"), "1"))).isEqualTo(2); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X", "Y", "Z"), "1"))).isEqualTo(2); + assertThat(indexer.size(CompositeKey.ofMany(List.of("Y"), "1"))).isEqualTo(2); + assertThat(indexer.size(CompositeKey.ofMany(List.of("Z"), "1"))).isEqualTo(2); + assertThat(indexer.size(CompositeKey.ofMany(List.of("W"), "1"))).isEqualTo(0); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X"), "3"))).isEqualTo(0); + } + + @Test + void removeTwice() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var annTuple = newTuple("Ann"); + var annEntry = indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "1"), annTuple); + + indexer.remove(CompositeKey.ofMany(List.of("X", "Y"), "1"), annEntry); + assertThatThrownBy(() -> indexer.remove(CompositeKey.ofMany(List.of("X", "Y"), "1"), annEntry)) + .isInstanceOf(IllegalStateException.class); + } + + @Test @Disabled + void forEach() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + + var annXY1 = newTuple("Ann"); + indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "1"), annXY1); + var bethXZ1 = newTuple("Beth"); + indexer.put(CompositeKey.ofMany(List.of("X", "Z"), "1"), bethXZ1); + indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "2"), newTuple("Carl")); + indexer.put(CompositeKey.ofMany(List.of("X", "Z"), "3"), newTuple("Dan")); + var ednaYZ1 = newTuple("Edna"); + indexer.put(CompositeKey.ofMany(List.of("Y", "Z"), "1"), ednaYZ1); + + assertThat(forEachToTuples(indexer, List.of("X"), "1")).containsExactlyInAnyOrder(annXY1, bethXZ1); + assertThat(forEachToTuples(indexer, List.of("X", "W"), "1")).containsExactlyInAnyOrder(annXY1, bethXZ1); + assertThat(forEachToTuples(indexer, List.of("Y"), "1")).containsExactlyInAnyOrder(annXY1, ednaYZ1); + assertThat(forEachToTuples(indexer, List.of("Z"), "1")).containsExactlyInAnyOrder(bethXZ1, ednaYZ1); + assertThat(forEachToTuples(indexer, List.of("X", "Y"), "1")).containsExactlyInAnyOrder(annXY1, bethXZ1, ednaYZ1); + assertThat(forEachToTuples(indexer, List.of("X", "Z"), "1")).containsExactlyInAnyOrder(annXY1, bethXZ1, ednaYZ1); + assertThat(forEachToTuples(indexer, List.of("W"), "1")).isEmpty(); + assertThat(forEachToTuples(indexer, List.of("Y"), "3")).isEmpty(); + } + + private static UniTuple newTuple(String factA) { + return new UniTuple<>(factA, 0); + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/ContainIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/ContainIndexerTest.java new file mode 100644 index 0000000000..beb5a2b2e6 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/ContainIndexerTest.java @@ -0,0 +1,106 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import ai.timefold.solver.core.api.score.stream.Joiners; +import ai.timefold.solver.core.impl.bavet.bi.joiner.DefaultBiJoiner; +import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; + +import org.junit.jupiter.api.Test; + +class ContainIndexerTest extends AbstractIndexerTest { + + record TestWorker(String name, List skills, String department, String affinity) { + } + + record TestJob(String department, String skill, List affinities) { + } + + private final DefaultBiJoiner joiner = + (DefaultBiJoiner) Joiners.contain(TestWorker::skills, TestJob::skill) + .and(Joiners.equal(TestWorker::department, TestJob::department)); + + @Test + void isEmpty() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + assertThat(indexer.isEmpty()).isTrue(); + } + + @Test + void size() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + assertThat(indexer.size(CompositeKey.ofMany("X", "1"))).isEqualTo(0); + + indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "1"), newTuple("Ann")); + assertThat(indexer.size(CompositeKey.ofMany("X", "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany("Y", "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany("Z", "1"))).isEqualTo(0); + assertThat(indexer.size(CompositeKey.ofMany("X", "3"))).isEqualTo(0); + + indexer.put(CompositeKey.ofMany(List.of("X", "Z"), "1"), newTuple("Beth")); + indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "2"), newTuple("Carl")); + assertThat(indexer.size(CompositeKey.ofMany("X", "1"))).isEqualTo(2); + assertThat(indexer.size(CompositeKey.ofMany("Y", "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany("Z", "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany("X", "3"))).isEqualTo(0); + } + + @Test + void removeTwice() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var annTuple = newTuple("Ann"); + var annEntry = indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "1"), annTuple); + + indexer.remove(CompositeKey.ofMany(List.of("X", "Y"), "1"), annEntry); + assertThatThrownBy(() -> indexer.remove(CompositeKey.ofMany(List.of("X", "Y"), "1"), annEntry)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void forEach() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + + var annXY1 = newTuple("Ann"); + indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "1"), annXY1); + var bethXZ1 = newTuple("Beth"); + indexer.put(CompositeKey.ofMany(List.of("X", "Z"), "1"), bethXZ1); + indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "2"), newTuple("Carl")); + indexer.put(CompositeKey.ofMany(List.of("X", "Z"), "3"), newTuple("Dan")); + var ednaYZ1 = newTuple("Edna"); + indexer.put(CompositeKey.ofMany(List.of("Y", "Z"), "1"), ednaYZ1); + + assertThat(forEachToTuples(indexer, "X", "1")).containsExactlyInAnyOrder(annXY1, bethXZ1); + assertThat(forEachToTuples(indexer, "Y", "1")).containsExactlyInAnyOrder(annXY1, ednaYZ1); + assertThat(forEachToTuples(indexer, "Z", "1")).containsExactlyInAnyOrder(bethXZ1, ednaYZ1); + assertThat(forEachToTuples(indexer, "W", "1")).isEmpty(); + assertThat(forEachToTuples(indexer, "Y", "3")).isEmpty(); + } + + private final DefaultBiJoiner containedInComboJoiner = + (DefaultBiJoiner) Joiners.contain(TestWorker::skills, TestJob::skill) + .and(Joiners.containedIn(TestWorker::affinity, TestJob::affinities)); + + @Test + void forEach_containedInCombo() { + var indexer = new IndexerFactory<>(containedInComboJoiner).buildIndexer(true); + + var annXY1 = newTuple("Ann"); + indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "1"), annXY1); + var bethXY2 = newTuple("Beth"); + indexer.put(CompositeKey.ofMany(List.of("X", "Y"), "2"), bethXY2); + var ednaYZ1 = newTuple("Edna"); + indexer.put(CompositeKey.ofMany(List.of("Y", "Z"), "1"), ednaYZ1); + + assertThat(forEachToTuples(indexer, "X", List.of("1"))).containsExactlyInAnyOrder(annXY1); + assertThat(forEachToTuples(indexer, "X", List.of("1", "2"))).containsExactlyInAnyOrder(annXY1, bethXY2); + assertThat(forEachToTuples(indexer, "Y", List.of("1", "2"))).containsExactlyInAnyOrder(annXY1, bethXY2, ednaYZ1); + } + + private static UniTuple newTuple(String factA) { + return new UniTuple<>(factA, 0); + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/ContainedInIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/ContainedInIndexerTest.java new file mode 100644 index 0000000000..bf745ca7bd --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/ContainedInIndexerTest.java @@ -0,0 +1,110 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import ai.timefold.solver.core.api.score.stream.Joiners; +import ai.timefold.solver.core.impl.bavet.bi.joiner.DefaultBiJoiner; +import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; + +import org.junit.jupiter.api.Test; + +class ContainedInIndexerTest extends AbstractIndexerTest { + + record TestWorker(String name, List skills, String department, String affinity) { + } + + record TestJob(String department, String skill, List affinities) { + } + + private final DefaultBiJoiner joiner = + (DefaultBiJoiner) Joiners.containedIn(TestJob::skill, TestWorker::skills) + .and(Joiners.equal(TestJob::department, TestWorker::department)); + + @Test + void isEmpty() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + assertThat(indexer.isEmpty()).isTrue(); + } + + @Test + void size() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X"), "1"))).isEqualTo(0); + + indexer.put(CompositeKey.ofMany("X", "1"), newTuple("Ann")); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X"), "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X", "Y"), "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany(List.of("Y"), "1"))).isEqualTo(0); + assertThat(indexer.size(CompositeKey.ofMany(List.of("Y", "Z"), "1"))).isEqualTo(0); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X"), "3"))).isEqualTo(0); + + indexer.put(CompositeKey.ofMany("Y", "1"), newTuple("Beth")); + indexer.put(CompositeKey.ofMany("X", "2"), newTuple("Carl")); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X"), "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X", "Y"), "1"))).isEqualTo(2); + assertThat(indexer.size(CompositeKey.ofMany(List.of("Y"), "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany(List.of("Y", "Z"), "1"))).isEqualTo(1); + assertThat(indexer.size(CompositeKey.ofMany(List.of("X"), "3"))).isEqualTo(0); + } + + @Test + void removeTwice() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var annTuple = newTuple("Ann"); + var annEntry = indexer.put(CompositeKey.ofMany("X", "1"), annTuple); + + indexer.remove(CompositeKey.ofMany("X", "1"), annEntry); + assertThatThrownBy(() -> indexer.remove(CompositeKey.ofMany("X", "1"), annEntry)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void forEach() { + var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + + var annX1 = newTuple("Ann"); + indexer.put(CompositeKey.ofMany("X", "1"), annX1); + var bethY1 = newTuple("Beth"); + indexer.put(CompositeKey.ofMany("Y", "1"), bethY1); + indexer.put(CompositeKey.ofMany("X", "2"), newTuple("Carl")); + indexer.put(CompositeKey.ofMany("Z", "3"), newTuple("Dan")); + var ednaX1 = newTuple("Edna"); + indexer.put(CompositeKey.ofMany("X", "1"), ednaX1); + + assertThat(forEachToTuples(indexer, List.of("X"), "1")).containsExactlyInAnyOrder(annX1, ednaX1); + assertThat(forEachToTuples(indexer, List.of("X", "Y"), "1")).containsExactlyInAnyOrder(annX1, bethY1, ednaX1); + assertThat(forEachToTuples(indexer, List.of("Y"), "1")).containsExactlyInAnyOrder(bethY1); + assertThat(forEachToTuples(indexer, List.of("Y", "W"), "1")).containsExactlyInAnyOrder(bethY1); + assertThat(forEachToTuples(indexer, List.of("W"), "1")).isEmpty(); + assertThat(forEachToTuples(indexer, List.of(), "1")).isEmpty(); + assertThat(forEachToTuples(indexer, List.of("X"), "3")).isEmpty(); + } + + private final DefaultBiJoiner containComboJoiner = + (DefaultBiJoiner) Joiners.containedIn(TestJob::skill, TestWorker::skills) + .and(Joiners.contain(TestJob::affinities, TestWorker::affinity)); + + @Test + void forEach_containCombo() { + var indexer = new IndexerFactory<>(containComboJoiner).buildIndexer(true); + + var annX12 = newTuple("Ann"); + indexer.put(CompositeKey.ofMany("X", List.of("1", "2")), annX12); + var bethY13 = newTuple("Beth"); + indexer.put(CompositeKey.ofMany("Y", List.of("1", "2")), bethY13); + var ednaX23 = newTuple("Edna"); + indexer.put(CompositeKey.ofMany("X", List.of("2", "3")), ednaX23); + + assertThat(forEachToTuples(indexer, List.of("X"), "1")).containsExactlyInAnyOrder(annX12); + assertThat(forEachToTuples(indexer, List.of("X", "Y"), "1")).containsExactlyInAnyOrder(annX12, bethY13); + assertThat(forEachToTuples(indexer, List.of("X", "Y"), "2")).containsExactlyInAnyOrder(annX12, bethY13, ednaX23); + } + + private static UniTuple newTuple(String factA) { + return new UniTuple<>(factA, 0); + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexerTest.java similarity index 62% rename from core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexerTest.java index f418b7bbb2..7d6425007a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualIndexerTest.java @@ -6,20 +6,22 @@ import ai.timefold.solver.core.api.score.stream.Joiners; import ai.timefold.solver.core.impl.bavet.bi.joiner.DefaultBiJoiner; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.Pair; import org.junit.jupiter.api.Test; -class EqualsIndexerTest extends AbstractIndexerTest { +class EqualIndexerTest extends AbstractIndexerTest { - private final DefaultBiJoiner joiner = - (DefaultBiJoiner) Joiners.equal(Person::gender) - .and(Joiners.equal(Person::age)); + record TestPerson(String gender, int age) { + } + + private final DefaultBiJoiner joiner = + (DefaultBiJoiner) Joiners.equal(TestPerson::gender) + .and(Joiners.equal(TestPerson::age)); @Test void isEmpty() { var indexer = new IndexerFactory<>(joiner).buildIndexer(true); - assertThat(getTuples(indexer, "F", 40)).isEmpty(); + assertThat(forEachToTuples(indexer, "F", 40)).isEmpty(); } @Test @@ -43,21 +45,21 @@ void removeTwice() { } @Test - void visit() { + void forEach() { var indexer = new IndexerFactory<>(joiner).buildIndexer(true); var annTuple = newTuple("Ann-F-40"); - indexer.put(CompositeKey.of(new Pair<>("F", 40)), annTuple); + indexer.put(CompositeKey.ofMany("F", 40), annTuple); var bethTuple = newTuple("Beth-F-30"); - indexer.put(CompositeKey.of(new Pair<>("F", 30)), bethTuple); - indexer.put(CompositeKey.of(new Pair<>("M", 40)), newTuple("Carl-M-40")); - indexer.put(CompositeKey.of(new Pair<>("M", 30)), newTuple("Dan-M-30")); + indexer.put(CompositeKey.ofMany("F", 30), bethTuple); + indexer.put(CompositeKey.ofMany("M", 40), newTuple("Carl-M-40")); + indexer.put(CompositeKey.ofMany("M", 30), newTuple("Dan-M-30")); var ednaTuple = newTuple("Edna-F-40"); - indexer.put(CompositeKey.of(new Pair<>("F", 40)), ednaTuple); + indexer.put(CompositeKey.ofMany("F", 40), ednaTuple); - assertThat(getTuples(indexer, new Pair<>("F", 40))).containsOnly(annTuple, ednaTuple); - assertThat(getTuples(indexer, new Pair<>("F", 30))).containsOnly(bethTuple); - assertThat(getTuples(indexer, new Pair<>("F", 20))).isEmpty(); + assertThat(forEachToTuples(indexer, "F", 40)).containsOnly(annTuple, ednaTuple); + assertThat(forEachToTuples(indexer, "F", 30)).containsOnly(bethTuple); + assertThat(forEachToTuples(indexer, "F", 20)).isEmpty(); } private static UniTuple newTuple(String factA) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java index 1c2b25f314..04ca7f6f45 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java @@ -11,14 +11,17 @@ class EqualsAndComparisonIndexerTest extends AbstractIndexerTest { - private final DefaultBiJoiner joiner = - (DefaultBiJoiner) Joiners.equal(Person::gender) - .and(Joiners.lessThanOrEqual(Person::age)); + record TestPerson(String gender, int age) { + } + + private final DefaultBiJoiner joiner = + (DefaultBiJoiner) Joiners.equal(TestPerson::gender) + .and(Joiners.lessThanOrEqual(TestPerson::age)); @Test void iEmpty() { var indexer = new IndexerFactory<>(joiner).buildIndexer(true); - assertThat(getTuples(indexer, "F", 40)).isEmpty(); + assertThat(forEachToTuples(indexer, "F", 40)).isEmpty(); } @Test @@ -42,7 +45,7 @@ void removeTwice() { } @Test - void visit() { + void forEach() { var indexer = new IndexerFactory<>(joiner).buildIndexer(true); var annTuple = newTuple("Ann-F-40"); @@ -54,10 +57,10 @@ void visit() { var ednaTuple = newTuple("Edna-F-40"); indexer.put(CompositeKey.ofMany("F", 40), ednaTuple); - assertThat(getTuples(indexer, "F", 40)).containsOnly(annTuple, bethTuple, ednaTuple); - assertThat(getTuples(indexer, "F", 35)).containsOnly(bethTuple); - assertThat(getTuples(indexer, "F", 30)).containsOnly(bethTuple); - assertThat(getTuples(indexer, "F", 20)).isEmpty(); + assertThat(forEachToTuples(indexer, "F", 40)).containsOnly(annTuple, bethTuple, ednaTuple); + assertThat(forEachToTuples(indexer, "F", 35)).containsOnly(bethTuple); + assertThat(forEachToTuples(indexer, "F", 30)).containsOnly(bethTuple); + assertThat(forEachToTuples(indexer, "F", 20)).isEmpty(); } private static UniTuple newTuple(String factA) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/RandomAccessIndexerBackendTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/RandomAccessIndexerBackendTest.java index 23a50aecf4..9349802a06 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/RandomAccessIndexerBackendTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/RandomAccessIndexerBackendTest.java @@ -14,7 +14,7 @@ class RandomAccessIndexerBackendTest extends AbstractIndexerTest { void isEmpty() { var indexer = new RandomAccessIndexerBackend<>(); assertSoftly(softly -> { - softly.assertThat(getTuples(indexer)).isEmpty(); + softly.assertThat(forEachToTuples(indexer)).isEmpty(); softly.assertThat(indexer.isEmpty()).isTrue(); }); } @@ -28,7 +28,7 @@ void put() { assertThat(indexer.size(CompositeKey.none())).isEqualTo(1); assertSoftly(softly -> { softly.assertThat(indexer.isEmpty()).isFalse(); - softly.assertThat(getTuples(indexer)).containsExactly(annTuple); + softly.assertThat(forEachToTuples(indexer)).containsExactly(annTuple); }); } @@ -39,20 +39,20 @@ void removeTwice() { var annEntry = indexer.put(CompositeKey.none(), annTuple); assertSoftly(softly -> { softly.assertThat(indexer.isEmpty()).isFalse(); - softly.assertThat(getTuples(indexer)).containsExactly(annTuple); + softly.assertThat(forEachToTuples(indexer)).containsExactly(annTuple); }); indexer.remove(CompositeKey.none(), annEntry); assertSoftly(softly -> { softly.assertThat(indexer.isEmpty()).isTrue(); - softly.assertThat(getTuples(indexer)).isEmpty(); + softly.assertThat(forEachToTuples(indexer)).isEmpty(); }); assertThatThrownBy(() -> indexer.remove(CompositeKey.none(), annEntry)) .isInstanceOf(IllegalStateException.class); } @Test - void visit() { + void forEach() { var indexer = new RandomAccessIndexerBackend<>(); var annTuple = newTuple("Ann-F-40"); @@ -60,7 +60,7 @@ void visit() { var bethTuple = newTuple("Beth-F-30"); indexer.put(CompositeKey.none(), bethTuple); - assertThat(getTuples(indexer)).containsOnly(annTuple, bethTuple); + assertThat(forEachToTuples(indexer)).containsOnly(annTuple, bethTuple); } private static UniTuple newTuple(String factA) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/joiner/JoinerTypeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/joiner/JoinerTypeTest.java index fbce30bdd6..4630bb1951 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/joiner/JoinerTypeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/joiner/JoinerTypeTest.java @@ -1,18 +1,12 @@ package ai.timefold.solver.core.impl.bavet.common.joiner; -import static ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType.CONTAINING; -import static ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType.DISJOINT; -import static ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType.EQUAL; -import static ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType.GREATER_THAN; -import static ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType.GREATER_THAN_OR_EQUAL; -import static ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType.INTERSECTING; -import static ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType.LESS_THAN; -import static ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType.LESS_THAN_OR_EQUAL; +import static ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType.*; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.Test; @@ -55,32 +49,46 @@ void greaterThanOrEquals() { } @Test - void containing() { - Collection collection = Arrays.asList(1); - assertThat(CONTAINING.matches(collection, 1)).isTrue(); - assertThat(CONTAINING.matches(collection, 2)).isFalse(); + void contain() { + assertThat(CONTAIN.matches(List.of(1, 3), 1)).isTrue(); + assertThat(CONTAIN.matches(List.of(1, 3), 2)).isFalse(); } @Test - void intersecting() { - Collection left = Arrays.asList(1, 2, 3); - Collection right = Arrays.asList(3, 4, 5); - assertThat(INTERSECTING.matches(left, right)).isTrue(); - assertThat(INTERSECTING.matches(right, left)).isTrue(); - assertThat(INTERSECTING.matches(left, Collections.emptySet())).isFalse(); + void containedIn() { + assertThat(CONTAINED_IN.matches(1, List.of(1, 3))).isTrue(); + assertThat(CONTAINED_IN.matches(2, List.of(1, 3))).isFalse(); } @Test - void disjoint() { - Collection first = Arrays.asList(1, 2, 3); - Collection second = Arrays.asList(3, 4, 5); - assertThat(DISJOINT.matches(first, second)).isFalse(); - assertThat(DISJOINT.matches(second, first)).isFalse(); - Collection third = Arrays.asList(4, 5); - assertThat(DISJOINT.matches(first, third)).isTrue(); - assertThat(DISJOINT.matches(third, first)).isTrue(); - // empty sets are disjoint - assertThat(DISJOINT.matches(Collections.emptyList(), Collections.emptySet())).isTrue(); - assertThat(DISJOINT.matches(first, Collections.emptySet())).isTrue(); + void containAny() { + assertThat(CONTAIN_ANY.matches(List.of(1, 2, 3), List.of(2))).isTrue(); + assertThat(CONTAIN_ANY.matches(List.of(1, 2, 3), List.of(6))).isFalse(); + assertThat(CONTAIN_ANY.matches(List.of(1, 2, 3), List.of(3, 4, 5))).isTrue(); + assertThat(CONTAIN_ANY.matches(List.of(3, 4, 5), List.of(1, 2, 3))).isTrue(); + assertThat(CONTAIN_ANY.matches(List.of(1, 2, 3), List.of(4, 5, 6))).isFalse(); + assertThat(CONTAIN_ANY.matches(List.of(1, 2, 3), List.of())).isFalse(); + assertThat(CONTAIN_ANY.matches(List.of(), List.of(1))).isFalse(); + assertThat(CONTAIN_ANY.matches(List.of(), List.of())).isTrue(); } + @Test + void containAll() { + assertThat(CONTAIN_ALL.matches(List.of(1, 2, 3), List.of(1, 2, 3))).isTrue(); + assertThat(CONTAIN_ALL.matches(List.of(1, 3, 4), List.of(1, 2, 3))).isFalse(); + assertThat(CONTAIN_ALL.matches(List.of(1, 2, 3), List.of(1, 2))).isTrue(); + assertThat(CONTAIN_ALL.matches(List.of(1, 2), List.of(1, 2, 3))).isFalse(); + assertThat(CONTAIN_ALL.matches(List.of(1, 2, 3), List.of())).isTrue(); + assertThat(CONTAIN_ALL.matches(List.of(), List.of(1))).isFalse(); + assertThat(CONTAIN_ALL.matches(List.of(), List.of())).isTrue(); + } + + @Test + void containNone() { + assertThat(CONTAIN_NONE.matches(List.of(1, 2, 3), List.of(3, 4, 5))).isFalse(); + assertThat(CONTAIN_NONE.matches(List.of(1, 2, 3), List.of(4, 5))).isTrue(); + assertThat(CONTAIN_NONE.matches(List.of(1, 2, 3), List.of())).isTrue(); + assertThat(CONTAIN_NONE.matches(List.of(), List.of())).isTrue(); + assertThat(CONTAIN_NONE.matches(List.of(), List.of(1))).isTrue(); + } + }