@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]