diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintStream.java index 677bde791f..9fc3167cc2 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintStream.java @@ -7,6 +7,7 @@ import static ai.timefold.solver.core.impl.util.ConstantLambdaUtils.uniConstantNull; import java.math.BigDecimal; +import java.util.Collection; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.BiPredicate; @@ -1076,33 +1077,49 @@ public interface BiConstraintStream extends ConstraintStream { @NonNull BiFunction mappingD); /** - * Takes each tuple and applies a mapping on the last fact, which turns it into {@link Iterable}. - * Returns a constraint stream consisting of tuples of the first fact - * and the contents of the {@link Iterable} one after another. + * Takes each tuple and applies a mapping on its facts, which turns it into {@link Iterable}. + * Returns a constraint stream consisting of new tuples, + * each made of the original facts and one item from that iterable. * In other words, it will replace the current tuple with new tuples, - * a cartesian product of A and the individual items from the {@link Iterable}. + * a Cartesian product of (A, B) and the individual items from the {@link Iterable}. * *

* This may produce a stream with duplicate tuples. * See {@link #distinct()} for details. * *

- * In cases where the last fact is already {@link Iterable}, use {@link Function#identity()} as the argument. - * - *

* Simple example: assuming a constraint stream of {@code (PersonName, Person)} * {@code [(Ann, (name = Ann, roles = [USER, ADMIN])), (Beth, (name = Beth, roles = [USER])), * (Cathy, (name = Cathy, roles = [ADMIN, AUDITOR]))]}, - * calling {@code flattenLast(Person::getRoles)} on such stream will produce a stream of - * {@code [(Ann, USER), (Ann, ADMIN), (Beth, USER), (Cathy, ADMIN), (Cathy, AUDITOR)]}. - * - * @param mapping function to convert the last fact in the original tuple into {@link Iterable}. - * For performance, returning an implementation of {@link java.util.Collection} is preferred. - * @param the type of the last fact in the resulting tuples. + * calling {@code flatten((name, person) -> person.getRoles()))} on such stream will produce a stream of + * {@code [(Ann, (name = Ann, roles = [USER, ADMIN]), USER), + * (Ann, (name = Ann, roles = [USER, ADMIN]), ADMIN), + * (Beth, (name = Beth, roles = [USER]), USER), + * (Cathy, (name = Cathy, roles = [ADMIN, AUDITOR]), ADMIN), + * (Cathy, (name = Cathy, roles = [ADMIN, AUDITOR]), AUDITOR)]}. + * + * @param mapping function to convert the original tuple into {@link Iterable}. + * For performance, returning an implementation of {@link Collection} is preferred. + * @param the type of the last fact in the resulting tuples. * It is recommended that this type be deeply immutable. * Not following this recommendation may lead to hard-to-debug hashing issues down the stream, * especially if this value is ever used as a group key. */ + @NonNull TriConstraintStream + flatten(@NonNull BiFunction> mapping); + + /** + * As defined by {@link #flatten(BiFunction)}, + * only replacing the last fact in the original tuple by an item from the iterable. + * This means the resulting stream will still be a {@link BiConstraintStream}, + * not a {@link TriConstraintStream}. + *

+ * Simple example: assuming a constraint stream of {@code (PersonName, Person)} + * {@code [(Ann, (name = Ann, roles = [USER, ADMIN])), (Beth, (name = Beth, roles = [USER])), + * (Cathy, (name = Cathy, roles = [ADMIN, AUDITOR]))]}, + * calling {@code flattenLast(Person::getRoles)} on such stream will produce a stream of + * {@code [(Ann, USER), (Ann, ADMIN), (Beth, USER), (Cathy, ADMIN), (Cathy, AUDITOR)]}. + */ @NonNull BiConstraintStream flattenLast(@NonNull Function> mapping); /** diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintStream.java index 3a97cef99e..8ed2b62c99 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintStream.java @@ -8,6 +8,7 @@ import static ai.timefold.solver.core.impl.util.ConstantLambdaUtils.uniConstantNull; import java.math.BigDecimal; +import java.util.Collection; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; @@ -1094,6 +1095,19 @@ public interface TriConstraintStream extends ConstraintStream { @NonNull TriFunction mappingA, @NonNull TriFunction mappingB, @NonNull TriFunction mappingC, @NonNull TriFunction mappingD); + /** + * As defined by {@link BiConstraintStream#flatten(BiFunction)}. + * + * @param the type of the last fact in the resulting tuples. + * It is recommended that this type be deeply immutable. + * Not following this recommendation may lead to hard-to-debug hashing issues down the stream, + * especially if this value is ever used as a group key. + * @param mapping function to convert the original tuple into {@link Iterable}. + * For performance, returning an implementation of {@link Collection} is preferred. + */ + @NonNull QuadConstraintStream + flatten(@NonNull TriFunction> mapping); + /** * As defined by {@link BiConstraintStream#flattenLast(Function)}. * @@ -1102,7 +1116,7 @@ public interface TriConstraintStream extends ConstraintStream { * Not following this recommendation may lead to hard-to-debug hashing issues down the stream, * especially if this value is ever used as a group key. * @param mapping function to convert the last fact in the original tuple into {@link Iterable}. - * For performance, returning an implementation of {@link java.util.Collection} is preferred. + * For performance, returning an implementation of {@link Collection} is preferred. */ @NonNull TriConstraintStream flattenLast(@NonNull Function> mapping); diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintStream.java index 5ffe2b56c7..48f14e7a89 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintStream.java @@ -8,6 +8,7 @@ import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collection; import java.util.Objects; import java.util.function.BiPredicate; import java.util.function.Function; @@ -1497,27 +1498,37 @@ default UniConstraintStream ifNotExistsOtherIncludingNullVars(Class otherC /** * Takes each tuple and applies a mapping on it, which turns the tuple into a {@link Iterable}. - * Returns a constraint stream consisting of contents of those iterables. + * Returns a constraint stream consisting of new tuples, + * each made of the original fact and one item from that iterable. * This may produce a stream with duplicate tuples. * See {@link #distinct()} for details. * *

- * In cases where the original tuple is already an {@link Iterable}, - * use {@link Function#identity()} as the argument. - * - *

* Simple example: assuming a constraint stream of tuples of {@code Person}s * {@code [Ann(roles = [USER, ADMIN]]), Beth(roles = [USER]), Cathy(roles = [ADMIN, AUDITOR])]}, - * calling {@code flattenLast(Person::getRoles)} on such stream will produce - * a stream of {@code [USER, ADMIN, USER, ADMIN, AUDITOR]}. + * calling {@code flatten(Person::getRoles)} on such stream will produce + * a stream of {@code [(Ann, USER), (Ann, ADMIN), (Beth, USER), (Cathy, ADMIN), (Cathy, AUDITOR)]}. * * @param mapping function to convert the original tuple into {@link Iterable}. - * For performance, returning an implementation of {@link java.util.Collection} is preferred. - * @param the type of facts in the resulting tuples. + * For performance, returning an implementation of {@link Collection} is preferred. + * @param the type of the last fact in the resulting tuples. * It is recommended that this type be deeply immutable. * Not following this recommendation may lead to hard-to-debug hashing issues down the stream, * especially if this value is ever used as a group key. */ + @NonNull BiConstraintStream flatten(@NonNull Function> mapping); + + /** + * As defined by {@link #flatten(Function)}, + * only replacing the only fact in the original tuple by an item from the iterable. + * This means the resulting stream is a {@link UniConstraintStream}, + * not a {@link BiConstraintStream}. + *

+ * Simple example: assuming a constraint stream of tuples of {@code Person}s + * {@code [Ann(roles = [USER, ADMIN]]), Beth(roles = [USER]), Cathy(roles = [ADMIN, AUDITOR])]}, + * calling {@code flattenLast(Person::getRoles)} on such stream will produce + * a stream of {@code [(USER), (ADMIN), (USER), (ADMIN), (AUDITOR)]}. + */ @NonNull UniConstraintStream flattenLast(@NonNull Function> mapping); /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/FlattenBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/FlattenBiNode.java new file mode 100644 index 0000000000..aaf4846a58 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/FlattenBiNode.java @@ -0,0 +1,33 @@ +package ai.timefold.solver.core.impl.bavet.bi; + +import java.util.Objects; +import java.util.function.BiFunction; + +import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenNode; +import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; + +public final class FlattenBiNode extends AbstractFlattenNode, TriTuple, NewC> { + + private final BiFunction> mappingFunction; + private final int outputStoreSize; + + public FlattenBiNode(int flattenStoreIndex, BiFunction> mappingFunction, + TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { + super(flattenStoreIndex, nextNodesTupleLifecycle); + this.mappingFunction = Objects.requireNonNull(mappingFunction); + this.outputStoreSize = outputStoreSize; + } + + @Override + protected TriTuple createTuple(BiTuple originalTuple, NewC newC) { + return TriTuple.of(originalTuple.getA(), originalTuple.getB(), newC, outputStoreSize); + } + + @Override + protected Iterable extractIterable(BiTuple tuple) { + return mappingFunction.apply(tuple.getA(), tuple.getB()); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/FlattenLastBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/FlattenLastBiNode.java index 160b96027b..f47b4d3c73 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/FlattenLastBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/FlattenLastBiNode.java @@ -1,18 +1,21 @@ package ai.timefold.solver.core.impl.bavet.bi; +import java.util.Objects; import java.util.function.Function; -import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenLastNode; +import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenNode; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; -public final class FlattenLastBiNode extends AbstractFlattenLastNode, BiTuple, B, NewB> { +public final class FlattenLastBiNode extends AbstractFlattenNode, BiTuple, NewB> { + private final Function> mappingFunction; private final int outputStoreSize; public FlattenLastBiNode(int flattenLastStoreIndex, Function> mappingFunction, TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { - super(flattenLastStoreIndex, mappingFunction, nextNodesTupleLifecycle); + super(flattenLastStoreIndex, nextNodesTupleLifecycle); + this.mappingFunction = Objects.requireNonNull(mappingFunction); this.outputStoreSize = outputStoreSize; } @@ -22,8 +25,8 @@ protected BiTuple createTuple(BiTuple originalTuple, NewB newB) { } @Override - protected B getEffectiveFactIn(BiTuple tuple) { - return tuple.getB(); + protected Iterable extractIterable(BiTuple tuple) { + return mappingFunction.apply(tuple.getB()); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenLastNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java similarity index 87% rename from core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenLastNode.java rename to core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java index 2c440f44e7..c09dffb84a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenLastNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java @@ -7,7 +7,6 @@ import java.util.Map; import java.util.Objects; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; @@ -15,30 +14,26 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.util.CollectionUtils; -public abstract class AbstractFlattenLastNode +public abstract class AbstractFlattenNode extends AbstractNode implements TupleLifecycle { - private final int flattenLastStoreIndex; - private final Function> mappingFunction; + private final int flattenStoreIndex; private final StaticPropagationQueue propagationQueue; - protected AbstractFlattenLastNode(int flattenLastStoreIndex, - Function> mappingFunction, - TupleLifecycle nextNodesTupleLifecycle) { - this.flattenLastStoreIndex = flattenLastStoreIndex; - this.mappingFunction = Objects.requireNonNull(mappingFunction); + protected AbstractFlattenNode(int flattenStoreIndex, TupleLifecycle nextNodesTupleLifecycle) { + this.flattenStoreIndex = flattenStoreIndex; this.propagationQueue = new StaticPropagationQueue<>(nextNodesTupleLifecycle); } @Override public final void insert(InTuple_ tuple) { - if (tuple.getStore(flattenLastStoreIndex) != null) { + if (tuple.getStore(flattenStoreIndex) != null) { throw new IllegalStateException( "Impossible state: the input for the tuple (%s) was already added in the tupleStore." .formatted(tuple)); } - var iterable = mappingFunction.apply(getEffectiveFactIn(tuple)); + var iterable = extractIterable(tuple); if (iterable instanceof Collection collection) { // Optimization for Collection, where we know the size. var size = collection.size(); @@ -49,7 +44,7 @@ public final void insert(InTuple_ tuple) { for (var item : collection) { addTuple(tuple, item, bagByItem); } - tuple.setStore(flattenLastStoreIndex, bagByItem); + tuple.setStore(flattenStoreIndex, bagByItem); } else { var iterator = iterable.iterator(); if (!iterator.hasNext()) { @@ -59,7 +54,7 @@ public final void insert(InTuple_ tuple) { while (iterator.hasNext()) { addTuple(tuple, iterator.next(), bagByItem); } - tuple.setStore(flattenLastStoreIndex, bagByItem); + tuple.setStore(flattenStoreIndex, bagByItem); } } @@ -75,7 +70,7 @@ private void addTuple(InTuple_ originalTuple, FlattenedItem_ item, @Override public final void update(InTuple_ tuple) { - FlattenBagByItem bagByItem = tuple.getStore(flattenLastStoreIndex); + FlattenBagByItem bagByItem = tuple.getStore(flattenStoreIndex); if (bagByItem == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s). insert(tuple); @@ -83,18 +78,18 @@ public final void update(InTuple_ tuple) { } bagByItem.resetAll(); - for (var item : mappingFunction.apply(getEffectiveFactIn(tuple))) { + for (var item : extractIterable(tuple)) { addTuple(tuple, item, bagByItem); } bagByItem.getAllBags() .removeIf(bag -> bag.removeExtras(this::removeTuple)); } - protected abstract EffectiveItem_ getEffectiveFactIn(InTuple_ tuple); + protected abstract Iterable extractIterable(InTuple_ tuple); @Override public final void retract(InTuple_ tuple) { - FlattenBagByItem bagByItem = tuple.removeStore(flattenLastStoreIndex); + FlattenBagByItem bagByItem = tuple.removeStore(flattenStoreIndex); if (bagByItem == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/FlattenLastQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/FlattenLastQuadNode.java index 5bfa22f381..0aabbbcb4b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/FlattenLastQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/FlattenLastQuadNode.java @@ -1,19 +1,22 @@ package ai.timefold.solver.core.impl.bavet.quad; +import java.util.Objects; import java.util.function.Function; -import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenLastNode; +import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenNode; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; public final class FlattenLastQuadNode - extends AbstractFlattenLastNode, QuadTuple, D, NewD> { + extends AbstractFlattenNode, QuadTuple, NewD> { + private final Function> mappingFunction; private final int outputStoreSize; public FlattenLastQuadNode(int flattenLastStoreIndex, Function> mappingFunction, TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { - super(flattenLastStoreIndex, mappingFunction, nextNodesTupleLifecycle); + super(flattenLastStoreIndex, nextNodesTupleLifecycle); + this.mappingFunction = Objects.requireNonNull(mappingFunction); this.outputStoreSize = outputStoreSize; } @@ -23,8 +26,8 @@ protected QuadTuple createTuple(QuadTuple originalTup } @Override - protected D getEffectiveFactIn(QuadTuple tuple) { - return tuple.getD(); + protected Iterable extractIterable(QuadTuple tuple) { + return mappingFunction.apply(tuple.getD()); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/FlattenLastTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/FlattenLastTriNode.java index 48b71a3f51..7dd7c5d042 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/FlattenLastTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/FlattenLastTriNode.java @@ -1,19 +1,22 @@ package ai.timefold.solver.core.impl.bavet.tri; +import java.util.Objects; import java.util.function.Function; -import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenLastNode; +import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenNode; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; public final class FlattenLastTriNode - extends AbstractFlattenLastNode, TriTuple, C, NewC> { + extends AbstractFlattenNode, TriTuple, NewC> { + private final Function> mappingFunction; private final int outputStoreSize; public FlattenLastTriNode(int flattenLastStoreIndex, Function> mappingFunction, TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { - super(flattenLastStoreIndex, mappingFunction, nextNodesTupleLifecycle); + super(flattenLastStoreIndex, nextNodesTupleLifecycle); + this.mappingFunction = Objects.requireNonNull(mappingFunction); this.outputStoreSize = outputStoreSize; } @@ -23,8 +26,8 @@ protected TriTuple createTuple(TriTuple originalTuple, NewC } @Override - protected C getEffectiveFactIn(TriTuple tuple) { - return tuple.getC(); + protected Iterable extractIterable(TriTuple tuple) { + return mappingFunction.apply(tuple.getC()); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/FlattenTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/FlattenTriNode.java new file mode 100644 index 0000000000..9c1fd0dc69 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/FlattenTriNode.java @@ -0,0 +1,34 @@ +package ai.timefold.solver.core.impl.bavet.tri; + +import java.util.Objects; + +import ai.timefold.solver.core.api.function.TriFunction; +import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenNode; +import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; + +public final class FlattenTriNode + extends AbstractFlattenNode, QuadTuple, NewD> { + + private final TriFunction> mappingFunction; + private final int outputStoreSize; + + public FlattenTriNode(int flattenStoreIndex, TriFunction> mappingFunction, + TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { + super(flattenStoreIndex, nextNodesTupleLifecycle); + this.mappingFunction = Objects.requireNonNull(mappingFunction); + this.outputStoreSize = outputStoreSize; + } + + @Override + protected QuadTuple createTuple(TriTuple originalTuple, NewD newD) { + return QuadTuple.of(originalTuple.getA(), originalTuple.getB(), originalTuple.getC(), newD, outputStoreSize); + } + + @Override + protected Iterable extractIterable(TriTuple tuple) { + return mappingFunction.apply(tuple.getA(), tuple.getB(), tuple.getC()); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/FlattenLastUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/FlattenLastUniNode.java index 378db61d4e..1c7e7afbcf 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/FlattenLastUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/FlattenLastUniNode.java @@ -1,18 +1,21 @@ package ai.timefold.solver.core.impl.bavet.uni; +import java.util.Objects; import java.util.function.Function; -import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenLastNode; +import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenNode; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -public final class FlattenLastUniNode extends AbstractFlattenLastNode, UniTuple, A, NewA> { +public final class FlattenLastUniNode extends AbstractFlattenNode, UniTuple, NewA> { + private final Function> mappingFunction; private final int outputStoreSize; public FlattenLastUniNode(int flattenLastStoreIndex, Function> mappingFunction, TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { - super(flattenLastStoreIndex, mappingFunction, nextNodesTupleLifecycle); + super(flattenLastStoreIndex, nextNodesTupleLifecycle); + this.mappingFunction = Objects.requireNonNull(mappingFunction); this.outputStoreSize = outputStoreSize; } @@ -22,8 +25,8 @@ protected UniTuple createTuple(UniTuple originalTuple, NewA item) { } @Override - protected A getEffectiveFactIn(UniTuple tuple) { - return tuple.getA(); + protected Iterable extractIterable(UniTuple tuple) { + return mappingFunction.apply(tuple.getA()); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/FlattenUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/FlattenUniNode.java new file mode 100644 index 0000000000..3e2e9829cb --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/FlattenUniNode.java @@ -0,0 +1,33 @@ +package ai.timefold.solver.core.impl.bavet.uni; + +import java.util.Objects; +import java.util.function.Function; + +import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenNode; +import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; + +public final class FlattenUniNode extends AbstractFlattenNode, BiTuple, NewB> { + + private final Function> mappingFunction; + private final int outputStoreSize; + + public FlattenUniNode(int flattenStoreIndex, Function> mappingFunction, + TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { + super(flattenStoreIndex, nextNodesTupleLifecycle); + this.mappingFunction = Objects.requireNonNull(mappingFunction); + this.outputStoreSize = outputStoreSize; + } + + @Override + protected BiTuple createTuple(UniTuple originalTuple, NewB newB) { + return BiTuple.of(originalTuple.getA(), newB, outputStoreSize); + } + + @Override + protected Iterable extractIterable(UniTuple tuple) { + return mappingFunction.apply(tuple.getA()); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetAbstractBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetAbstractBiConstraintStream.java index 819ba58127..38f9091c29 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetAbstractBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetAbstractBiConstraintStream.java @@ -425,6 +425,14 @@ private UniConstraintStream buildUniGroupBy(GroupNodeConstructor TriConstraintStream + flatten(@NonNull BiFunction> mapping) { + var stream = shareAndAddChild(new BavetFlattenBiConstraintStream<>(constraintFactory, this, mapping)); + return constraintFactory.share(new BavetAftBridgeTriConstraintStream<>(constraintFactory, stream), + stream::setAftBridge); + } + @Override public @NonNull BiConstraintStream flattenLast(@NonNull Function> mapping) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetFlattenBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetFlattenBiConstraintStream.java new file mode 100644 index 0000000000..5c9fabb6ce --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetFlattenBiConstraintStream.java @@ -0,0 +1,73 @@ +package ai.timefold.solver.core.impl.score.stream.bavet.bi; + +import java.util.Objects; +import java.util.function.BiFunction; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.bavet.bi.FlattenBiNode; +import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; +import ai.timefold.solver.core.impl.score.stream.bavet.common.ConstraintNodeBuildHelper; +import ai.timefold.solver.core.impl.score.stream.bavet.common.bridge.BavetAftBridgeTriConstraintStream; + +final class BavetFlattenBiConstraintStream extends BavetAbstractBiConstraintStream { + + private final BiFunction> mappingFunction; + private BavetAftBridgeTriConstraintStream flattenStream; + + public BavetFlattenBiConstraintStream(BavetConstraintFactory constraintFactory, + BavetAbstractBiConstraintStream parent, BiFunction> mappingFunction) { + super(constraintFactory, parent); + this.mappingFunction = mappingFunction; + } + + public void setAftBridge(BavetAftBridgeTriConstraintStream flattenStream) { + this.flattenStream = flattenStream; + } + + // ************************************************************************ + // Node creation + // ************************************************************************ + + @Override + public boolean guaranteesDistinct() { + return false; + } + + @Override + public > void buildNode(ConstraintNodeBuildHelper buildHelper) { + assertEmptyChildStreamList(); + var node = new FlattenBiNode<>(buildHelper.reserveTupleStoreIndex(parent.getTupleSource()), mappingFunction, + buildHelper.getAggregatedTupleLifecycle(flattenStream.getChildStreamList()), + buildHelper.extractTupleStoreSize(flattenStream)); + buildHelper.addNode(node, this); + } + + // ************************************************************************ + // Equality for node sharing + // ************************************************************************ + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (BavetFlattenBiConstraintStream) object; + return Objects.equals(parent, that.parent) && Objects.equals(mappingFunction, that.mappingFunction); + } + + @Override + public int hashCode() { + return Objects.hash(parent, mappingFunction); + } + + // ************************************************************************ + // Getters/setters + // ************************************************************************ + + @Override + public String toString() { + return "Flatten()"; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetFlattenLastBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetFlattenLastBiConstraintStream.java index 783557e86f..fc7e8dbf06 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetFlattenLastBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetFlattenLastBiConstraintStream.java @@ -5,21 +5,17 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.bavet.bi.FlattenLastBiNode; -import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenLastNode; -import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; import ai.timefold.solver.core.impl.score.stream.bavet.common.ConstraintNodeBuildHelper; import ai.timefold.solver.core.impl.score.stream.bavet.common.bridge.BavetAftBridgeBiConstraintStream; -final class BavetFlattenLastBiConstraintStream - extends BavetAbstractBiConstraintStream { +final class BavetFlattenLastBiConstraintStream extends BavetAbstractBiConstraintStream { private final Function> mappingFunction; private BavetAftBridgeBiConstraintStream flattenLastStream; public BavetFlattenLastBiConstraintStream(BavetConstraintFactory constraintFactory, - BavetAbstractBiConstraintStream parent, - Function> mappingFunction) { + BavetAbstractBiConstraintStream parent, Function> mappingFunction) { super(constraintFactory, parent); this.mappingFunction = mappingFunction; } @@ -40,12 +36,9 @@ public boolean guaranteesDistinct() { @Override public > void buildNode(ConstraintNodeBuildHelper buildHelper) { assertEmptyChildStreamList(); - int inputStoreIndex = buildHelper.reserveTupleStoreIndex(parent.getTupleSource()); - int outputStoreSize = buildHelper.extractTupleStoreSize(flattenLastStream); - AbstractFlattenLastNode, BiTuple, B, NewB> node = new FlattenLastBiNode<>( - inputStoreIndex, mappingFunction, + var node = new FlattenLastBiNode<>(buildHelper.reserveTupleStoreIndex(parent.getTupleSource()), mappingFunction, buildHelper.getAggregatedTupleLifecycle(flattenLastStream.getChildStreamList()), - outputStoreSize); + buildHelper.extractTupleStoreSize(flattenLastStream)); buildHelper.addNode(node, this); } @@ -59,7 +52,7 @@ public boolean equals(Object object) { return true; if (object == null || getClass() != object.getClass()) return false; - BavetFlattenLastBiConstraintStream that = (BavetFlattenLastBiConstraintStream) object; + var that = (BavetFlattenLastBiConstraintStream) object; return Objects.equals(parent, that.parent) && Objects.equals(mappingFunction, that.mappingFunction); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetFlattenLastQuadConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetFlattenLastQuadConstraintStream.java index a4b8ef4187..af35aba152 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetFlattenLastQuadConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetFlattenLastQuadConstraintStream.java @@ -16,8 +16,7 @@ final class BavetFlattenLastQuadConstraintStream private BavetAftBridgeQuadConstraintStream flattenLastStream; public BavetFlattenLastQuadConstraintStream(BavetConstraintFactory constraintFactory, - BavetAbstractQuadConstraintStream parent, - Function> mappingFunction) { + BavetAbstractQuadConstraintStream parent, Function> mappingFunction) { super(constraintFactory, parent); this.mappingFunction = mappingFunction; } @@ -38,11 +37,9 @@ public boolean guaranteesDistinct() { @Override public > void buildNode(ConstraintNodeBuildHelper buildHelper) { assertEmptyChildStreamList(); - int inputStoreIndex = buildHelper.reserveTupleStoreIndex(parent.getTupleSource()); - int outputStoreSize = buildHelper.extractTupleStoreSize(flattenLastStream); - var node = new FlattenLastQuadNode<>(inputStoreIndex, mappingFunction, + var node = new FlattenLastQuadNode<>(buildHelper.reserveTupleStoreIndex(parent.getTupleSource()), mappingFunction, buildHelper.getAggregatedTupleLifecycle(flattenLastStream.getChildStreamList()), - outputStoreSize); + buildHelper.extractTupleStoreSize(flattenLastStream)); buildHelper.addNode(node, this); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetAbstractTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetAbstractTriConstraintStream.java index 6cbe3d96ad..1a809d6eb2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetAbstractTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetAbstractTriConstraintStream.java @@ -430,6 +430,14 @@ QuadConstraintStream groupBy( stream::setAftBridge); } + @Override + public @NonNull QuadConstraintStream + flatten(@NonNull TriFunction> mapping) { + var stream = shareAndAddChild(new BavetFlattenTriConstraintStream<>(constraintFactory, this, mapping)); + return constraintFactory.share(new BavetAftBridgeQuadConstraintStream<>(constraintFactory, stream), + stream::setAftBridge); + } + @Override public @NonNull TriConstraintStream flattenLast(@NonNull Function> mapping) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetFlattenLastTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetFlattenLastTriConstraintStream.java index 643b0a2026..d07afc5175 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetFlattenLastTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetFlattenLastTriConstraintStream.java @@ -16,8 +16,7 @@ final class BavetFlattenLastTriConstraintStream private BavetAftBridgeTriConstraintStream flattenLastStream; public BavetFlattenLastTriConstraintStream(BavetConstraintFactory constraintFactory, - BavetAbstractTriConstraintStream parent, - Function> mappingFunction) { + BavetAbstractTriConstraintStream parent, Function> mappingFunction) { super(constraintFactory, parent); this.mappingFunction = mappingFunction; } @@ -38,11 +37,9 @@ public boolean guaranteesDistinct() { @Override public > void buildNode(ConstraintNodeBuildHelper buildHelper) { assertEmptyChildStreamList(); - int inputStoreIndex = buildHelper.reserveTupleStoreIndex(parent.getTupleSource()); - int outputStoreSize = buildHelper.extractTupleStoreSize(flattenLastStream); - var node = new FlattenLastTriNode<>(inputStoreIndex, mappingFunction, + var node = new FlattenLastTriNode<>(buildHelper.reserveTupleStoreIndex(parent.getTupleSource()), mappingFunction, buildHelper.getAggregatedTupleLifecycle(flattenLastStream.getChildStreamList()), - outputStoreSize); + buildHelper.extractTupleStoreSize(flattenLastStream)); buildHelper.addNode(node, this); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetFlattenTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetFlattenTriConstraintStream.java new file mode 100644 index 0000000000..2ca6bde8ef --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetFlattenTriConstraintStream.java @@ -0,0 +1,74 @@ +package ai.timefold.solver.core.impl.score.stream.bavet.tri; + +import java.util.Objects; + +import ai.timefold.solver.core.api.function.TriFunction; +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.bavet.tri.FlattenTriNode; +import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; +import ai.timefold.solver.core.impl.score.stream.bavet.common.ConstraintNodeBuildHelper; +import ai.timefold.solver.core.impl.score.stream.bavet.common.bridge.BavetAftBridgeQuadConstraintStream; + +final class BavetFlattenTriConstraintStream + extends BavetAbstractTriConstraintStream { + + private final TriFunction> mappingFunction; + private BavetAftBridgeQuadConstraintStream flattenStream; + + public BavetFlattenTriConstraintStream(BavetConstraintFactory constraintFactory, + BavetAbstractTriConstraintStream parent, TriFunction> mappingFunction) { + super(constraintFactory, parent); + this.mappingFunction = mappingFunction; + } + + public void setAftBridge(BavetAftBridgeQuadConstraintStream flattenStream) { + this.flattenStream = flattenStream; + } + + // ************************************************************************ + // Node creation + // ************************************************************************ + + @Override + public boolean guaranteesDistinct() { + return false; + } + + @Override + public > void buildNode(ConstraintNodeBuildHelper buildHelper) { + assertEmptyChildStreamList(); + var node = new FlattenTriNode<>(buildHelper.reserveTupleStoreIndex(parent.getTupleSource()), mappingFunction, + buildHelper.getAggregatedTupleLifecycle(flattenStream.getChildStreamList()), + buildHelper.extractTupleStoreSize(flattenStream)); + buildHelper.addNode(node, this); + } + + // ************************************************************************ + // Equality for node sharing + // ************************************************************************ + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (BavetFlattenTriConstraintStream) object; + return Objects.equals(parent, that.parent) && Objects.equals(mappingFunction, that.mappingFunction); + } + + @Override + public int hashCode() { + return Objects.hash(parent, mappingFunction); + } + + // ************************************************************************ + // Getters/setters + // ************************************************************************ + + @Override + public String toString() { + return "Flatten()"; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetAbstractUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetAbstractUniConstraintStream.java index d3d5a2b64e..e178aef657 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetAbstractUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetAbstractUniConstraintStream.java @@ -426,6 +426,13 @@ QuadConstraintStream groupBy( stream::setAftBridge); } + @Override + public @NonNull BiConstraintStream flatten(@NonNull Function> mapping) { + var stream = shareAndAddChild(new BavetFlattenUniConstraintStream<>(constraintFactory, this, mapping)); + return constraintFactory.share(new BavetAftBridgeBiConstraintStream<>(constraintFactory, stream), + stream::setAftBridge); + } + @Override public @NonNull UniConstraintStream flattenLast(@NonNull Function> mapping) { var stream = shareAndAddChild(new BavetFlattenLastUniConstraintStream<>(constraintFactory, this, mapping)); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetFlattenLastUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetFlattenLastUniConstraintStream.java index 0f21ee1acd..4af41d3792 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetFlattenLastUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetFlattenLastUniConstraintStream.java @@ -9,15 +9,13 @@ import ai.timefold.solver.core.impl.score.stream.bavet.common.ConstraintNodeBuildHelper; import ai.timefold.solver.core.impl.score.stream.bavet.common.bridge.BavetAftBridgeUniConstraintStream; -final class BavetFlattenLastUniConstraintStream - extends BavetAbstractUniConstraintStream { +final class BavetFlattenLastUniConstraintStream extends BavetAbstractUniConstraintStream { private final Function> mappingFunction; private BavetAftBridgeUniConstraintStream flattenLastStream; public BavetFlattenLastUniConstraintStream(BavetConstraintFactory constraintFactory, - BavetAbstractUniConstraintStream parent, - Function> mappingFunction) { + BavetAbstractUniConstraintStream parent, Function> mappingFunction) { super(constraintFactory, parent); this.mappingFunction = mappingFunction; } @@ -38,11 +36,9 @@ public boolean guaranteesDistinct() { @Override public > void buildNode(ConstraintNodeBuildHelper buildHelper) { assertEmptyChildStreamList(); - int inputStoreIndex = buildHelper.reserveTupleStoreIndex(parent.getTupleSource()); - int outputStoreSize = buildHelper.extractTupleStoreSize(flattenLastStream); - var node = new FlattenLastUniNode<>(inputStoreIndex, mappingFunction, + var node = new FlattenLastUniNode<>(buildHelper.reserveTupleStoreIndex(parent.getTupleSource()), mappingFunction, buildHelper.getAggregatedTupleLifecycle(flattenLastStream.getChildStreamList()), - outputStoreSize); + buildHelper.extractTupleStoreSize(flattenLastStream)); buildHelper.addNode(node, this); } @@ -56,7 +52,7 @@ public boolean equals(Object object) { return true; if (object == null || getClass() != object.getClass()) return false; - BavetFlattenLastUniConstraintStream that = (BavetFlattenLastUniConstraintStream) object; + var that = (BavetFlattenLastUniConstraintStream) object; return Objects.equals(parent, that.parent) && Objects.equals(mappingFunction, that.mappingFunction); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetFlattenUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetFlattenUniConstraintStream.java new file mode 100644 index 0000000000..63ad3b2f5c --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetFlattenUniConstraintStream.java @@ -0,0 +1,73 @@ +package ai.timefold.solver.core.impl.score.stream.bavet.uni; + +import java.util.Objects; +import java.util.function.Function; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.bavet.uni.FlattenUniNode; +import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; +import ai.timefold.solver.core.impl.score.stream.bavet.common.ConstraintNodeBuildHelper; +import ai.timefold.solver.core.impl.score.stream.bavet.common.bridge.BavetAftBridgeBiConstraintStream; + +final class BavetFlattenUniConstraintStream extends BavetAbstractUniConstraintStream { + + private final Function> mappingFunction; + private BavetAftBridgeBiConstraintStream flattenStream; + + public BavetFlattenUniConstraintStream(BavetConstraintFactory constraintFactory, + BavetAbstractUniConstraintStream parent, Function> mappingFunction) { + super(constraintFactory, parent); + this.mappingFunction = mappingFunction; + } + + public void setAftBridge(BavetAftBridgeBiConstraintStream flattenStream) { + this.flattenStream = flattenStream; + } + + // ************************************************************************ + // Node creation + // ************************************************************************ + + @Override + public boolean guaranteesDistinct() { + return false; + } + + @Override + public > void buildNode(ConstraintNodeBuildHelper buildHelper) { + assertEmptyChildStreamList(); + var node = new FlattenUniNode<>(buildHelper.reserveTupleStoreIndex(parent.getTupleSource()), mappingFunction, + buildHelper.getAggregatedTupleLifecycle(flattenStream.getChildStreamList()), + buildHelper.extractTupleStoreSize(flattenStream)); + buildHelper.addNode(node, this); + } + + // ************************************************************************ + // Equality for node sharing + // ************************************************************************ + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (BavetFlattenUniConstraintStream) object; + return Objects.equals(parent, that.parent) && Objects.equals(mappingFunction, that.mappingFunction); + } + + @Override + public int hashCode() { + return Objects.hash(parent, mappingFunction); + } + + // ************************************************************************ + // Getters/setters + // ************************************************************************ + + @Override + public String toString() { + return "Flatten()"; + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/FlattenLastUniNodeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/FlattenLastUniNodeTest.java index 7d6c26a529..1fda97abc3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/FlattenLastUniNodeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/FlattenLastUniNodeTest.java @@ -12,7 +12,7 @@ import java.util.Objects; import java.util.stream.Collectors; -import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenLastNode; +import ai.timefold.solver.core.impl.bavet.common.AbstractFlattenNode; import ai.timefold.solver.core.impl.bavet.common.Propagator; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; @@ -57,7 +57,7 @@ private static UniTuple modifyTuple(UniTuple tuple, String... fa @Test void insertAndRetract() { - AbstractFlattenLastNode, UniTuple, String, String> node = + AbstractFlattenNode, UniTuple, String> node = new FlattenLastUniNode<>(0, FlattenLastUniNodeTest::split, downstream, 1); // First tuple is inserted, A and B make it downstream. @@ -110,7 +110,7 @@ void insertAndRetract() { @Test void modify() { - AbstractFlattenLastNode, UniTuple, String, String> node = + AbstractFlattenNode, UniTuple, String> node = new FlattenLastUniNode<>(0, FlattenLastUniNodeTest::split, downstream, 1); // First tuple is inserted. diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamFunctionalTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamFunctionalTest.java index 1906917499..173b264d93 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamFunctionalTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamFunctionalTest.java @@ -164,6 +164,10 @@ default void expandToQuad() { // Quad can't be expanded, so don't force it. } + default void flatten() { + // Quad can't be flattened, so don't force it. + } + void flattenLastWithDuplicates(); void flattenLastWithoutDuplicates(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamNodeSharingTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamNodeSharingTest.java index d7ddbfce91..89ade9de59 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamNodeSharingTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamNodeSharingTest.java @@ -195,6 +195,18 @@ default void sameParentSameFunctionExpand() { void sameParentSameFunctionMap(); + default void differentParentSameFunctionFlatten() { + // Quads don't have flatten, so don't force it. + } + + default void sameParentDifferentFunctionFlatten() { + // Quads don't have flatten, so don't force it. + } + + default void sameParentSameFunctionFlatten() { + // Quads don't have flatten, so don't force it. + } + void differentParentSameFunctionFlattenLast(); void sameParentDifferentFunctionFlattenLast(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamPrecomputeTest.java index a33c37dde0..5f4c78b777 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamPrecomputeTest.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common; public interface ConstraintStreamPrecomputeTest { + void filter_0_changed(); default void filter_1_changed() { @@ -21,6 +22,14 @@ default void filter_3_changed() { void groupBy(); + default void flatten() { + // Quad does not support flatten, so don't force it. + } + + default void flattenNewInstances() { + // Quad does not support flatten, so don't force it. + } + void flattenLast(); void flattenLastNewInstances(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamNodeSharingTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamNodeSharingTest.java index a1c1fb2e91..a289ece879 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamNodeSharingTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamNodeSharingTest.java @@ -465,6 +465,35 @@ public void sameParentSameFunctionMap() { .isSameAs(baseStream.map(mapper)); } + @Override + @TestTemplate + public void differentParentSameFunctionFlatten() { + BiPredicate filter1 = (a, b) -> true; + BiFunction> flattener = (a, b) -> Collections.emptyList(); + + assertThat(baseStream.flatten(flattener)) + .isNotSameAs(baseStream.filter(filter1).flatten(flattener)); + } + + @Override + @TestTemplate + public void sameParentDifferentFunctionFlatten() { + BiFunction> flattener1 = (a, b) -> Collections.emptyList(); + BiFunction> flattener2 = (a, b) -> Collections.emptySet(); + + assertThat(baseStream.flatten(flattener1)) + .isNotSameAs(baseStream.flatten(flattener2)); + } + + @Override + @TestTemplate + public void sameParentSameFunctionFlatten() { + BiFunction> flattener = (a, b) -> Collections.emptyList(); + + assertThat(baseStream.flatten(flattener)) + .isSameAs(baseStream.flatten(flattener)); + } + @Override @TestTemplate public void differentParentSameFunctionFlattenLast() { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java index d503de2a5a..5a6ca0b0d2 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java @@ -440,6 +440,47 @@ public void groupBy() { .groupBy(TestdataLavishEntity::getEntityGroup, ConstraintCollectors.count())); } + @Override + @TestTemplate + public void flatten() { + var solution = TestdataLavishSolution.generateEmptySolution(); + var entityWithoutGroup = new TestdataLavishEntity(); + var entityWithGroup = new TestdataLavishEntity(); + var entityGroup = new TestdataLavishEntityGroup(); + entityWithGroup.setEntityGroup(entityGroup); + solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); + solution.getEntityGroupList().add(entityGroup); + var value = new TestdataLavishValue(); + solution.getValueList().add(value); + + assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, entityWithoutGroup), + new Pair<>(entityWithGroup, entityWithoutGroup)), + pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) + .flatten(List::of)); + } + + @Override + @TestTemplate + public void flattenNewInstances() { + // Needed since Integers use a cache of instances that we don't want to accidentally use + record ValueHolder(int value) { + } + + var solution = TestdataLavishSolution.generateEmptySolution(); + var entity1 = new TestdataLavishEntity(); + entity1.setIntegerProperty(1); + var entity2 = new TestdataLavishEntity(); + entity2.setIntegerProperty(2); + solution.getEntityList().addAll(List.of(entity1, entity2)); + var value = new TestdataLavishValue(); + solution.getValueList().add(value); + + assertPrecompute(solution, List.of(new Pair<>(entity1, new ValueHolder(entity1.getIntegerProperty())), + new Pair<>(entity2, new ValueHolder(entity2.getIntegerProperty()))), + pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) + .flatten(entity -> List.of(new ValueHolder(entity.getIntegerProperty())))); + } + @Override @TestTemplate public void flattenLast() { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java index c7b4dfef1f..288dd56881 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java @@ -1908,6 +1908,35 @@ public void expandToQuad() { assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum12, concat12)); } + @Override + @TestTemplate + public void flatten() { + TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); + TestdataLavishEntity entity1 = solution.getFirstEntity(); + TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); + TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + + InnerScoreDirector scoreDirector = + buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) + .flatten((a, b) -> asList(a.getEntityGroup(), b.getEntityGroup(), group2)) + .penalize(SimpleScore.ONE) + .asConstraint(TEST_CONSTRAINT_NAME)); + + // From scratch + scoreDirector.setWorkingSolution(solution); + assertScore(scoreDirector, + assertMatch(entity1, entity2, group1), + assertMatch(entity1, entity2, group1), + assertMatch(entity1, entity2, group2)); + + // Incremental + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); + assertScore(scoreDirector); + } + @Override @TestTemplate public void flattenLastWithDuplicates() { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamNodeSharingTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamNodeSharingTest.java index e29b24b4e9..ac1e03718d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamNodeSharingTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamNodeSharingTest.java @@ -494,6 +494,39 @@ public void sameParentSameFunctionMap() { .isSameAs(baseStream.map(mapper)); } + @Override + @TestTemplate + public void differentParentSameFunctionFlatten() { + TriPredicate filter1 = (a, b, c) -> true; + TriFunction> flattener = + (a, b, c) -> Collections.emptyList(); + + assertThat(baseStream.flatten(flattener)) + .isNotSameAs(baseStream.filter(filter1).flatten(flattener)); + } + + @Override + @TestTemplate + public void sameParentDifferentFunctionFlatten() { + TriFunction> flattener1 = + (a, b, c) -> Collections.emptyList(); + TriFunction> flattener2 = + (a, b, c) -> Collections.emptySet(); + + assertThat(baseStream.flatten(flattener1)) + .isNotSameAs(baseStream.flatten(flattener2)); + } + + @Override + @TestTemplate + public void sameParentSameFunctionFlatten() { + TriFunction> flattener = + (a, b, c) -> Collections.emptyList(); + + assertThat(baseStream.flatten(flattener)) + .isSameAs(baseStream.flatten(flattener)); + } + @Override @TestTemplate public void differentParentSameFunctionFlattenLast() { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java index 923beeb667..bb6a554767 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java @@ -252,6 +252,49 @@ public void groupBy() { ConstraintCollectors.count())); } + @Override + @TestTemplate + public void flatten() { + var solution = TestdataLavishSolution.generateEmptySolution(); + var entityWithoutGroup = new TestdataLavishEntity(); + var entityWithGroup = new TestdataLavishEntity(); + var entityGroup = new TestdataLavishEntityGroup(); + entityWithGroup.setEntityGroup(entityGroup); + solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); + solution.getEntityGroupList().add(entityGroup); + var value = new TestdataLavishValue(); + solution.getValueList().add(value); + + assertPrecompute(solution, List.of(new Triple<>(entityWithoutGroup, entityWithoutGroup, value), + new Triple<>(entityWithGroup, entityWithoutGroup, value)), + pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) + .flatten(List::of) + .join(TestdataLavishValue.class)); + } + + @Override + @TestTemplate + public void flattenNewInstances() { + // Needed since Integers use a cache of instances that we don't want to accidentally use + record ValueHolder(int value) { + } + + var solution = TestdataLavishSolution.generateEmptySolution(); + var entity1 = new TestdataLavishEntity(); + entity1.setIntegerProperty(1); + var entity2 = new TestdataLavishEntity(); + entity2.setIntegerProperty(2); + solution.getEntityList().addAll(List.of(entity1, entity2)); + var value = new TestdataLavishValue(); + solution.getValueList().add(value); + + assertPrecompute(solution, List.of(new Triple<>(entity1, new ValueHolder(entity1.getIntegerProperty()), value), + new Triple<>(entity2, new ValueHolder(entity2.getIntegerProperty()), value)), + pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) + .flatten(entity -> List.of(new ValueHolder(entity.getIntegerProperty()))) + .join(TestdataLavishValue.class)); + } + @Override @TestTemplate public void flattenLast() { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java index be2787d6e5..50d6c3070e 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java @@ -1586,6 +1586,43 @@ public void expandToQuad() { assertScore(scoreDirector); } + @Override + @TestTemplate + public void flatten() { + TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 2, 3); + TestdataLavishEntity entity1 = solution.getFirstEntity(); + TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entity3 = solution.getEntityList().get(2); + TestdataLavishEntityGroup group1 = solution.getFirstEntityGroup(); + TestdataLavishEntityGroup group2 = solution.getEntityGroupList().get(1); + + InnerScoreDirector scoreDirector = + buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) + .join(TestdataLavishEntity.class, Joiners.filtering((a, b, c) -> a != c && b != c)) + .flatten((a, b, c) -> asList(a.getEntityGroup(), b.getEntityGroup(), c.getEntityGroup())) + .penalize(SimpleScore.ONE) + .asConstraint(TEST_CONSTRAINT_NAME)); + + // From scratch + scoreDirector.setWorkingSolution(solution); + assertScore(scoreDirector, + assertMatch(entity1, entity2, entity3, group1), + assertMatch(entity1, entity2, entity3, group2), + assertMatch(entity1, entity2, entity3, group1), + assertMatch(entity1, entity3, entity2, group1), + assertMatch(entity1, entity3, entity2, group1), + assertMatch(entity1, entity3, entity2, group2), + assertMatch(entity2, entity3, entity1, group2), + assertMatch(entity2, entity3, entity1, group1), + assertMatch(entity2, entity3, entity1, group1)); + + // Incremental + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); + assertScore(scoreDirector); + } + @Override @TestTemplate public void flattenLastWithDuplicates() { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamNodeSharingTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamNodeSharingTest.java index 4887b36d39..0cbb031006 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamNodeSharingTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamNodeSharingTest.java @@ -638,6 +638,32 @@ public void sameParentSameFunctionMap() { .isSameAs(baseStream.map(mapper)); } + @Override + public void differentParentSameFunctionFlatten() { + Predicate filter1 = a -> true; + Function> flattener = a -> Collections.emptyList(); + + assertThat(baseStream.flatten(flattener)) + .isNotSameAs(baseStream.filter(filter1).flatten(flattener)); + } + + @Override + public void sameParentDifferentFunctionFlatten() { + Function> flattener1 = a -> Collections.emptyList(); + Function> flattener2 = a -> Collections.emptySet(); + + assertThat(baseStream.flatten(flattener1)) + .isNotSameAs(baseStream.flatten(flattener2)); + } + + @Override + public void sameParentSameFunctionFlatten() { + Function> flattener = a -> Collections.emptyList(); + + assertThat(baseStream.flatten(flattener)) + .isSameAs(baseStream.flatten(flattener)); + } + @Override @TestTemplate public void differentParentSameFunctionFlattenLast() { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java index db838a3ea6..3e74f1b648 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java @@ -7,11 +7,13 @@ import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.Joiners; import ai.timefold.solver.core.api.score.stream.PrecomputeFactory; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintStream; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamPrecomputeTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamTestExtension; +import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntity; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntityGroup; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishSolution; @@ -174,6 +176,70 @@ public void groupBy() { .groupBy(TestdataLavishEntity::getEntityGroup)); } + @Override + @TestTemplate + public void flatten() { + var solution = TestdataLavishSolution.generateEmptySolution(); + var entityWithoutGroup = new TestdataLavishEntity(); + var entityWithGroup = new TestdataLavishEntity(); + var entityGroup = new TestdataLavishEntityGroup(); + entityWithGroup.setEntityGroup(entityGroup); + solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); + solution.getEntityGroupList().add(entityGroup); + solution.getValueList().add(new TestdataLavishValue()); + + assertPrecomputeBi(solution, List.of(new Pair<>(entityWithoutGroup, entityWithoutGroup), + new Pair<>(entityWithGroup, entityWithGroup)), + pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) + .flatten(List::of)); + } + + private void assertPrecomputeBi(TestdataLavishSolution solution, List> expectedValues, + Function> entityStreamSupplier) { + var scoreDirector = + buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) + .ifExists(TestdataLavishEntity.class) + .penalize(SimpleScore.ONE) + .asConstraint(TEST_CONSTRAINT_NAME)); + + // From scratch + scoreDirector.setWorkingSolution(solution); + assertScore(scoreDirector); + + for (var entity : solution.getEntityList()) { + scoreDirector.beforeVariableChanged(entity, "value"); + entity.setValue(solution.getFirstValue()); + scoreDirector.afterVariableChanged(entity, "value"); + } + + assertScore(scoreDirector, expectedValues.stream() + .map(pair -> new Object[] { pair.key(), pair.value() }) + .map(AbstractConstraintStreamTest::assertMatch) + .toArray(AssertableMatch[]::new)); + } + + @Override + @TestTemplate + public void flattenNewInstances() { + // Needed since Integers use a cache of instances that we don't want to accidentally use + record ValueHolder(int value) { + } + + var solution = TestdataLavishSolution.generateEmptySolution(); + var entity1 = new TestdataLavishEntity(); + entity1.setIntegerProperty(1); + var entity2 = new TestdataLavishEntity(); + entity2.setIntegerProperty(2); + solution.getEntityList().addAll(List.of(entity1, entity2)); + solution.getValueList().add(new TestdataLavishValue()); + + assertPrecomputeBi(solution, List.of( + new Pair<>(entity1, new ValueHolder(1)), + new Pair<>(entity2, new ValueHolder(2))), + pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) + .flatten(entity -> List.of(new ValueHolder(entity.getIntegerProperty())))); + } + @Override @TestTemplate public void flattenLast() { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index bbe056dc59..55ee9f2dfb 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -2491,6 +2491,39 @@ public void flattenLastWithoutDuplicates() { assertMatch(group2)); } + @Override + @TestTemplate + public void flatten() { + var solution = TestdataLavishSolution.generateSolution(1, 1, 2, 2); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var group1 = solution.getFirstEntityGroup(); + var group2 = solution.getEntityGroupList().get(1); + + var scoreDirector = + buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) + .flatten(entity -> Arrays.asList(group1, group1, group2)) + .distinct() + .penalize(SimpleScore.ONE) + .asConstraint(TEST_CONSTRAINT_NAME)); + + // From scratch + scoreDirector.setWorkingSolution(solution); + assertScore(scoreDirector, + assertMatch(entity1, group1), + assertMatch(entity1, group2), + assertMatch(entity2, group1), + assertMatch(entity2, group2)); + + // Incremental + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); + assertScore(scoreDirector, + assertMatch(entity2, group1), + assertMatch(entity2, group2)); + } + @Override @TestTemplate public void flattenLastAndDistinctWithDuplicates() { diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc index 775a1526dd..a2e94fb38c 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc @@ -1425,7 +1425,7 @@ Always check your solver's move evaluation speed to see if the cost is offset by Flattening enables you to transform any Java `Iterable` (such as `List` or `Set`) into a set of tuples, which are sent downstream. (Similar to Java Stream's `flatMap(...)`) -This is done by applying a mapping function to the final element in the source tuple. +This is done by applying a mapping function to the source tuple. [tabs] ==== @@ -1437,20 +1437,37 @@ Java:: return constraintFactory.forEach(Person.class) // UniConstraintStream .join(Job.class, equal(Function.identity(), Job::getAssignee)) // BiConstraintStream - .flattenLast(Job::getRequiredRoles) // BiConstraintStream - .filter((person, requiredRole) -> ...) + .flatten((person, job) -> job.getRequiredRoles()) // TriConstraintStream + .filter((person, job, requiredRole) -> ...) ... } ---- ==== -[NOTE] -==== -In the example above, the mapping function produces duplicate tuples +NOTE: In the previous example, the mapping function produces duplicate tuples if `Job.getRequiredRoles()` contains duplicate values. Assuming that the function returns `[USER, USER, ADMIN]`, -the tuple `(SomePerson, USER)` is sent downstream twice. +the tuple `(SomePerson, SomeJob, USER)` is sent downstream twice. See <> for how to deal with duplicate tuples. + +There is also a simplified version of `flatten` called `flattenLast`, +which flattens the last element of the tuple, replacing it in the process: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- + private Constraint requiredJobRoles(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Person.class) // UniConstraintStream + .join(Job.class, + equal(Function.identity(), Job::getAssignee)) // BiConstraintStream + .flattenLast(Job::getRequiredRoles) // BiConstraintStream + .filter((person, requiredRole) -> ...) + ... + } +---- ==== [#constraintStreamsConcat]