diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintCollectors.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintCollectors.java index 69d8933fea6..8f935af8542 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintCollectors.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintCollectors.java @@ -1715,8 +1715,7 @@ public final class ConstraintCollectors { @NonNull Function endExclusiveMap, @NonNull BiFunction differenceFunction) { return InnerUniConstraintCollectors.toConnectedRanges(ConstantLambdaUtils.identity(), startInclusiveMap, - endExclusiveMap, - differenceFunction); + endExclusiveMap, differenceFunction); } /** diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintCollector.java index d2f554e0142..70f7d8a0f97 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintCollector.java @@ -8,7 +8,8 @@ import ai.timefold.solver.core.api.score.stream.ConstraintStream; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * As described by {@link UniConstraintCollector}, only for {@link BiConstraintStream}. @@ -22,28 +23,24 @@ * especially if this value is ever used as a group key. * @see ConstraintCollectors */ +@NullMarked public interface BiConstraintCollector { /** - * A lambda that creates the result container, one for each group key combination. + * As defined by {@link UniConstraintCollector#supplier()}, but for {@link BiConstraintStream}. */ - @NonNull Supplier supplier(); /** - * A lambda that extracts data from the matched facts, - * accumulates it in the result container - * and returns an undo operation for that accumulation. - * - * @return the undo operation. This lambda is called when the facts no longer matches. + * As defined by {@link UniConstraintCollector#accumulator()}, but for {@link BiConstraintStream}. + * + * @see BiConstraintCollectorAccumulator An incremental API to be returned instead of the deprecated plain tri-function. */ - @NonNull TriFunction accumulator(); /** - * A lambda that converts the result container into the result. + * As defined by {@link UniConstraintCollector#finisher()}, but for {@link BiConstraintStream}. */ - @NonNull - Function finisher(); + Function finisher(); } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintCollectorAccumulator.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintCollectorAccumulator.java new file mode 100644 index 00000000000..9b6d8df52e8 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintCollectorAccumulator.java @@ -0,0 +1,33 @@ +package ai.timefold.solver.core.api.score.stream.bi; + +import ai.timefold.solver.core.api.function.TriFunction; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; + +import org.jspecify.annotations.NullMarked; + +/** + * As defined by {@link UniConstraintCollectorAccumulator}, + * only for {@link BiConstraintCollector}. + */ +@NullMarked +@FunctionalInterface +public interface BiConstraintCollectorAccumulator + extends TriFunction { + + /** + * As defined by {@link UniConstraintCollectorAccumulator#intoGroup(Object)}, + * only for {@link BiConstraintCollector}. + */ + BiConstraintCollectorValueHandle intoGroup(ResultContainer_ resultContainer); + + /** + * @deprecated Use {@link #intoGroup(Object)} instead. + * @throws UnsupportedOperationException always + */ + @Deprecated(since = "2.2.0", forRemoval = true) + @Override + default Runnable apply(ResultContainer_ resultContainer, A a, B b) { + throw new UnsupportedOperationException("Use intoGroup() instead."); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintCollectorValueHandle.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintCollectorValueHandle.java new file mode 100644 index 00000000000..6b35c199742 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/bi/BiConstraintCollectorValueHandle.java @@ -0,0 +1,36 @@ +package ai.timefold.solver.core.api.score.stream.bi; + +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * As defined by {@link UniConstraintCollectorValueHandle}, + * only for {@link BiConstraintCollector}. + */ +@NullMarked +public interface BiConstraintCollectorValueHandle { + + /** + * As defined by {@link UniConstraintCollectorValueHandle#add(Object)}, + * only for {@link BiConstraintCollector} + */ + void add(@Nullable A a, @Nullable B b); + + /** + * As defined by {@link UniConstraintCollectorValueHandle#replaceWith(Object)}, + * only for {@link BiConstraintCollector} + */ + default void replaceWith(@Nullable A a, @Nullable B b) { + remove(); + add(a, b); + } + + /** + * As defined by {@link UniConstraintCollectorValueHandle#remove()}, + * only for {@link BiConstraintCollector} + */ + void remove(); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintCollector.java index 3e29f57037b..83474f323e3 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintCollector.java @@ -8,7 +8,8 @@ import ai.timefold.solver.core.api.score.stream.ConstraintStream; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * As described by {@link UniConstraintCollector}, only for {@link QuadConstraintStream}. @@ -24,28 +25,24 @@ * especially if this value is ever used as a group key. * @see ConstraintCollectors */ +@NullMarked public interface QuadConstraintCollector { /** - * A lambda that creates the result container, one for each group key combination. + * As defined by {@link UniConstraintCollector#supplier()}, but for {@link QuadConstraintStream}. */ - @NonNull Supplier supplier(); /** - * A lambda that extracts data from the matched facts, - * accumulates it in the result container - * and returns an undo operation for that accumulation. - * - * @return the undo operation. This lambda is called when the facts no longer matches. + * As defined by {@link UniConstraintCollector#accumulator()}, but for {@link QuadConstraintStream}. + * + * @see QuadConstraintCollectorAccumulator An incremental API to be returned instead of the deprecated plain penta-function. */ - @NonNull PentaFunction accumulator(); /** - * A lambda that converts the result container into the result. + * As defined by {@link UniConstraintCollector#finisher()}, but for {@link QuadConstraintStream}. */ - @NonNull - Function finisher(); + Function finisher(); } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintCollectorAccumulator.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintCollectorAccumulator.java new file mode 100644 index 00000000000..7b652be290d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintCollectorAccumulator.java @@ -0,0 +1,33 @@ +package ai.timefold.solver.core.api.score.stream.quad; + +import ai.timefold.solver.core.api.function.PentaFunction; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; + +import org.jspecify.annotations.NullMarked; + +/** + * As defined by {@link UniConstraintCollectorAccumulator}, + * only for {@link QuadConstraintCollector}. + */ +@NullMarked +@FunctionalInterface +public interface QuadConstraintCollectorAccumulator + extends PentaFunction { + + /** + * As defined by {@link UniConstraintCollectorAccumulator#intoGroup(Object)}, + * only for {@link QuadConstraintCollector}. + */ + QuadConstraintCollectorValueHandle intoGroup(ResultContainer_ resultContainer); + + /** + * @deprecated Use {@link #intoGroup(Object)} instead. + * @throws UnsupportedOperationException always + */ + @Deprecated(since = "2.2.0", forRemoval = true) + @Override + default Runnable apply(ResultContainer_ resultContainer, A a, B b, C c, D d) { + throw new UnsupportedOperationException("Use intoGroup() instead."); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintCollectorValueHandle.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintCollectorValueHandle.java new file mode 100644 index 00000000000..075ff1c9e92 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/quad/QuadConstraintCollectorValueHandle.java @@ -0,0 +1,36 @@ +package ai.timefold.solver.core.api.score.stream.quad; + +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * As defined by {@link UniConstraintCollectorValueHandle}, + * only for {@link QuadConstraintCollector}. + */ +@NullMarked +public interface QuadConstraintCollectorValueHandle { + + /** + * As defined by {@link UniConstraintCollectorValueHandle#add(Object)}, + * only for {@link QuadConstraintCollector} + */ + void add(@Nullable A a, @Nullable B b, @Nullable C c, @Nullable D d); + + /** + * As defined by {@link UniConstraintCollectorValueHandle#replaceWith(Object)}, + * only for {@link QuadConstraintCollector} + */ + default void replaceWith(@Nullable A a, @Nullable B b, @Nullable C c, @Nullable D d) { + remove(); + add(a, b, c, d); + } + + /** + * As defined by {@link UniConstraintCollectorValueHandle#remove()}, + * only for {@link QuadConstraintCollector} + */ + void remove(); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintCollector.java index 4d075f32873..42cba184471 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintCollector.java @@ -8,7 +8,8 @@ import ai.timefold.solver.core.api.score.stream.ConstraintStream; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * As described by {@link UniConstraintCollector}, only for {@link TriConstraintStream}. @@ -23,28 +24,24 @@ * especially if this value is ever used as a group key. * @see ConstraintCollectors */ +@NullMarked public interface TriConstraintCollector { /** - * A lambda that creates the result container, one for each group key combination. + * As defined by {@link UniConstraintCollector#supplier()}, but for {@link TriConstraintStream}. */ - @NonNull Supplier supplier(); /** - * A lambda that extracts data from the matched facts, - * accumulates it in the result container - * and returns an undo operation for that accumulation. - * - * @return the undo operation. This lambda is called when the facts no longer matches. + * As defined by {@link UniConstraintCollector#accumulator()}, but for {@link TriConstraintStream}. + * + * @see TriConstraintCollectorAccumulator An incremental API to be returned instead of the deprecated plain quad-function. */ - @NonNull QuadFunction accumulator(); /** - * A lambda that converts the result container into the result. + * As defined by {@link UniConstraintCollector#finisher()}, but for {@link TriConstraintStream}. */ - @NonNull - Function finisher(); + Function finisher(); } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintCollectorAccumulator.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintCollectorAccumulator.java new file mode 100644 index 00000000000..4cf567b96a8 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintCollectorAccumulator.java @@ -0,0 +1,33 @@ +package ai.timefold.solver.core.api.score.stream.tri; + +import ai.timefold.solver.core.api.function.QuadFunction; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; + +import org.jspecify.annotations.NullMarked; + +/** + * As defined by {@link UniConstraintCollectorAccumulator}, + * only for {@link TriConstraintCollector}. + */ +@NullMarked +@FunctionalInterface +public interface TriConstraintCollectorAccumulator + extends QuadFunction { + + /** + * As defined by {@link UniConstraintCollectorAccumulator#intoGroup(Object)}, + * only for {@link TriConstraintCollector}. + */ + TriConstraintCollectorValueHandle intoGroup(ResultContainer_ resultContainer); + + /** + * @deprecated Use {@link #intoGroup(Object)} instead. + * @throws UnsupportedOperationException always + */ + @Deprecated(since = "2.2.0", forRemoval = true) + @Override + default Runnable apply(ResultContainer_ resultContainer, A a, B b, C c) { + throw new UnsupportedOperationException("Use intoGroup() instead."); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintCollectorValueHandle.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintCollectorValueHandle.java new file mode 100644 index 00000000000..eb46797f4ee --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/tri/TriConstraintCollectorValueHandle.java @@ -0,0 +1,36 @@ +package ai.timefold.solver.core.api.score.stream.tri; + +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * As defined by {@link UniConstraintCollectorValueHandle}, + * only for {@link TriConstraintCollector}. + */ +@NullMarked +public interface TriConstraintCollectorValueHandle { + + /** + * As defined by {@link UniConstraintCollectorValueHandle#add(Object)}, + * only for {@link TriConstraintCollector} + */ + void add(@Nullable A a, @Nullable B b, @Nullable C c); + + /** + * As defined by {@link UniConstraintCollectorValueHandle#replaceWith(Object)}, + * only for {@link TriConstraintCollector} + */ + default void replaceWith(@Nullable A a, @Nullable B b, @Nullable C c) { + remove(); + add(a, b, c); + } + + /** + * As defined by {@link UniConstraintCollectorValueHandle#remove()}, + * only for {@link TriConstraintCollector} + */ + void remove(); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintCollector.java index 121e59315fb..3abbbf5b7a6 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintCollector.java @@ -8,15 +8,15 @@ import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.ConstraintStream; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** * Usually created with {@link ConstraintCollectors}. * Used by {@link UniConstraintStream#groupBy(Function, UniConstraintCollector)}, ... *

- * Loosely based on JDK's {@link Collector}, but it returns an undo operation for each accumulation - * to enable incremental score calculation in {@link ConstraintStream constraint streams}. + * Loosely based on JDK's {@link Collector}, but it supports undoing each accumulation + * to enable incremental score calculation in {@link UniConstraintStream#groupBy(UniConstraintCollector)}. *

* It is recommended that if two constraint collectors implement the same functionality, * they should {@link Object#equals(Object) be equal}. @@ -28,35 +28,43 @@ * @param the type of the one and only fact of the tuple in the source {@link UniConstraintStream} * @param the mutable accumulation type (often hidden as an implementation detail) * @param the type of the fact of the tuple in the destination {@link ConstraintStream}. + * Null when the result would be invalid, such as maximum value from an empty container. * 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. + * * @see ConstraintCollectors */ +@NullMarked public interface UniConstraintCollector { /** * A lambda that creates the result container, one for each group key combination. */ - @NonNull Supplier supplier(); /** - * A lambda that extracts data from the matched fact, + * A type that extracts data from the matched fact, * accumulates it in the result container * and returns an undo operation for that accumulation. + *

+ * Implementations should return a {@link UniConstraintCollectorAccumulator} instead of a plain {@link BiFunction}; + * the solver detects this via {@code instanceof} and uses incremental insert/update/retract automatically. + * The declared return type stays {@link BiFunction} for backward compatibility + * and will be removed in a future major version of the solver. * - * @return the undo operation. This lambda is called when the fact no longer matches. + * @return the accumulator. Called to insert the match and retract it when it no longer belongs in the group. */ - @NonNull BiFunction accumulator(); /** * A lambda that converts the result container into the result. + * The result may be null, typically when there is nothing accumulated + * and the product in that situation is undefined. + * (Such as average of an empty set.) * - * @return null when the result would be invalid, such as maximum value from an empty container. + * @return the operation to compute the finished result; */ - @Nullable - Function finisher(); + Function finisher(); } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintCollectorAccumulator.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintCollectorAccumulator.java new file mode 100644 index 00000000000..cffa956249f --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintCollectorAccumulator.java @@ -0,0 +1,47 @@ +package ai.timefold.solver.core.api.score.stream.uni; + +import java.util.function.BiFunction; + +import org.jspecify.annotations.NullMarked; + +/** + * Used by {@link UniConstraintCollector#accumulator()} for incremental collectors. + * Extends {@link BiFunction} for backward compatibility, but {@link #apply(Object, Object)} always throws; + * use {@link #intoGroup(Object)} instead. + * Allows high-performance accumulation by providing an {@link UniConstraintCollectorValueHandle#replaceWith update} operation. + * + * @param + * @param + */ +@NullMarked +@FunctionalInterface +public interface UniConstraintCollectorAccumulator + extends BiFunction { + + /** + * Created every time a new value enters the group. + * + * @param resultContainer The container which represents the accumulated state of the group. + * If the group is empty, + * {@link UniConstraintCollector#supplier()} will be used to supply a fresh instance of the container; + * otherwise a pre-existing container will be used, carrying the accumulated state. + * {@link UniConstraintCollector#finisher()} will be used to extract the final accumulated value from the container. + * The method itself must not maintain any state; + * any container-level state must live in the container, + * and any state required to update or remove the value must live in the handle. + * @return the handle for the value, which will be used to insert the value to the group, + * to update it while in the group, and to remove it from the group. + */ + UniConstraintCollectorValueHandle intoGroup(ResultContainer_ resultContainer); + + /** + * @deprecated Use {@link #intoGroup(Object)} instead. + * @throws UnsupportedOperationException always + */ + @Deprecated(since = "2.2.0", forRemoval = true) + @Override + default Runnable apply(ResultContainer_ resultContainer, A a) { + throw new UnsupportedOperationException("Use intoGroup() instead."); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintCollectorValueHandle.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintCollectorValueHandle.java new file mode 100644 index 00000000000..f216d34eddd --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/uni/UniConstraintCollectorValueHandle.java @@ -0,0 +1,77 @@ +package ai.timefold.solver.core.api.score.stream.uni; + +import java.util.function.Predicate; + +import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Represents a handle for a single value in a single {@link UniConstraintCollectorAccumulator} group. + *

+ * For collectors which do not delegate to other collectors (most typical collectors) this is the expected behavior: + *

+ *

+ * For collectors which delegate to other collectors + * (such as {@link ConstraintCollectors#conditionally(Predicate, UniConstraintCollector)}) + * the nested collector may be treated as such: + *

    + *
  • {@link #add(Object)} will be called externally, when the value enters the group. + * An instance of {@link UniConstraintCollectorValueHandle} will only be created if there is a value to add.
  • + *
  • {@link #replaceWith(Object)} will be called externally zero or more times.
  • + *
  • {@link #remove()} will be called externally at most once, if the value is ever removed from the group. + * It may be followed by {@link #add(Object)} of another value, + * reusing the current instance of {@link UniConstraintCollectorValueHandle}.
  • + *
+ * This contract guarantees that the user can keep internal caches between add, update and remove + * to avoid some expensive operations; if the added object equals the updated or removed object, + * these caches most likely remain valid because we are updating the same logical element (tuple). + * When a value is removed, the user is responsible for clearing any and all related caches. + * + * @param
the fact in the tuple + */ +@NullMarked +public interface UniConstraintCollectorValueHandle { + + /** + * Add a value to the group. + * Once this method is called, a value is considered "added"; + * this method will not be called any more while the value is added. + * Instead, it will be followed by zero or more external {@link #replaceWith(Object) updates} + * and at most one external {@link #remove() removal}. + * + * @param a The component of the tuple. + */ + void add(@Nullable A a); + + /** + * Update a previously {@link #add(Object) added} value. + * This method will never be called externally unless a value is already added. + * In some cases, the default implementation (remove+add) will be enough, + * but this method exists for advanced cases where updates can be done more efficiently. + * + * @param a The component of the tuple. + * It is expected to be the component of the very same tuple that was used to {@link #add(Object)}. + * The component may be an entirely different object instance than was added. + * For other tuples, a fresh {@link UniConstraintCollectorValueHandle} will be obtained; + * an instance of this interface is guaranteed to work on one and only one tuple. + */ + default void replaceWith(@Nullable A a) { + remove(); + add(a); + } + + /** + * Remove a previously {@link #add(Object) added} value from the group; + * after this operation, the value is considered "removed" (not "added"). + * Another value can be {@link #add(Object) added} after this method is called on the currently added value. + */ + void remove(); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/AbstractGroupBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/AbstractGroupBiNode.java index 659eded5b3d..92eb3d82e7c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/AbstractGroupBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/AbstractGroupBiNode.java @@ -2,42 +2,58 @@ import java.util.function.Function; -import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.bavet.common.AbstractGroupNode; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.score.stream.collector.bi.BiCollectorUtils; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; abstract class AbstractGroupBiNode extends AbstractGroupNode, OutTuple_, GroupKey_, ResultContainer_, Result_> { - private final TriFunction accumulator; + private final int groupAccumulatorIndex; + private final @Nullable BiConstraintCollectorAccumulator incrementalAccumulator; - protected AbstractGroupBiNode(int groupStoreIndex, int undoStoreIndex, + protected AbstractGroupBiNode(int groupStoreIndex, int groupAccumulatorIndex, Function, GroupKey_> groupKeyFunction, - BiConstraintCollector collector, + @NonNull BiConstraintCollector collector, TupleLifecycle nextNodesTupleLifecycle, EnvironmentMode environmentMode) { - super(groupStoreIndex, undoStoreIndex, - groupKeyFunction, - collector == null ? null : collector.supplier(), - collector == null ? null : collector.finisher(), - nextNodesTupleLifecycle, environmentMode); - accumulator = collector == null ? null : collector.accumulator(); + super(groupStoreIndex, groupKeyFunction, collector.supplier(), collector.finisher(), nextNodesTupleLifecycle, + environmentMode); + this.groupAccumulatorIndex = groupAccumulatorIndex; + this.incrementalAccumulator = BiCollectorUtils.toIncremental(collector.accumulator()); } - protected AbstractGroupBiNode(int groupStoreIndex, - Function, GroupKey_> groupKeyFunction, TupleLifecycle nextNodesTupleLifecycle, - EnvironmentMode environmentMode) { - super(groupStoreIndex, - groupKeyFunction, nextNodesTupleLifecycle, environmentMode); - accumulator = null; + protected AbstractGroupBiNode(int groupStoreIndex, Function, GroupKey_> groupKeyFunction, + TupleLifecycle nextNodesTupleLifecycle, EnvironmentMode environmentMode) { + super(groupStoreIndex, groupKeyFunction, nextNodesTupleLifecycle, environmentMode); + this.groupAccumulatorIndex = -1; + this.incrementalAccumulator = null; } @Override - protected final Runnable accumulate(ResultContainer_ resultContainer, BiTuple tuple) { - return accumulator.apply(resultContainer, tuple.getA(), tuple.getB()); + protected void groupInsert(ResultContainer_ resultContainer, BiTuple tuple) { + var groupElement = incrementalAccumulator.intoGroup(resultContainer); + tuple.setStore(groupAccumulatorIndex, groupElement); + groupElement.add(tuple.getA(), tuple.getB()); } + @Override + protected void groupUpdate(ResultContainer_ resultContainer, BiTuple tuple) { + BiConstraintCollectorValueHandle groupElement = tuple.getStore(groupAccumulatorIndex); + groupElement.replaceWith(tuple.getA(), tuple.getB()); + } + + @Override + protected void groupRetract(BiTuple tuple) { + BiConstraintCollectorValueHandle groupElement = tuple.removeStore(groupAccumulatorIndex); + groupElement.remove(); + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java index 4fffb538587..3148738dbac 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java @@ -17,11 +17,7 @@ public abstract class AbstractGroupNode groupKeyFunction; /** @@ -36,18 +32,18 @@ public abstract class AbstractGroupNode> groupMap; /** - * Used when {@link #hasMultipleGroups} is false, otherwise {@link #groupMap} is used. + * Used when {@link #hasGroupKeyFunction} is false, otherwise {@link #groupMap} is used. * * @implNote The field is lazy initialized in order to maintain the same semantics as with the groupMap above. * When all tuples are removed, the field will be set to null, as if the group never existed. @@ -56,23 +52,22 @@ public abstract class AbstractGroupNode> propagationQueue; private final boolean useAssertingGroupKey; - protected AbstractGroupNode(int groupStoreIndex, int undoStoreIndex, - Function groupKeyFunction, Supplier supplier, + protected AbstractGroupNode(int groupStoreIndex, Function groupKeyFunction, + Supplier supplier, Function finisher, TupleLifecycle nextNodesTupleLifecycle, EnvironmentMode environmentMode) { this.groupStoreIndex = groupStoreIndex; - this.undoStoreIndex = undoStoreIndex; this.groupKeyFunction = groupKeyFunction; this.supplier = supplier; this.finisher = finisher; - this.hasMultipleGroups = groupKeyFunction != null; + this.hasGroupKeyFunction = groupKeyFunction != null; this.hasCollector = supplier != null; /* * Not using the default sizing to 1000. * The number of groups can be very small, and that situation is not unlikely. * Therefore, the size of these collections is kept default. */ - this.groupMap = hasMultipleGroups ? new HashMap<>() : null; + this.groupMap = hasGroupKeyFunction ? new HashMap<>() : null; this.propagationQueue = hasCollector ? new DynamicPropagationQueue<>(nextNodesTupleLifecycle, group -> { var outTuple = group.getTuple(); @@ -87,7 +82,7 @@ protected AbstractGroupNode(int groupStoreIndex, int undoStoreIndex, protected AbstractGroupNode(int groupStoreIndex, Function groupKeyFunction, TupleLifecycle nextNodesTupleLifecycle, EnvironmentMode environmentMode) { - this(groupStoreIndex, -1, + this(groupStoreIndex, groupKeyFunction, null, null, nextNodesTupleLifecycle, environmentMode); } @@ -100,39 +95,36 @@ public StreamKind getStreamKind() { @Override public final void insert(InTuple_ tuple) { if (tuple.getStore(groupStoreIndex) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + tuple - + ") was already added in the tupleStore."); + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(tuple)); } - var userSuppliedKey = hasMultipleGroups ? groupKeyFunction.apply(tuple) : null; + var userSuppliedKey = hasGroupKeyFunction ? groupKeyFunction.apply(tuple) : null; createTuple(tuple, userSuppliedKey); } private void createTuple(InTuple_ tuple, GroupKey_ userSuppliedKey) { - var newGroup = getOrCreateGroup(userSuppliedKey); - var outTuple = accumulate(tuple, newGroup); + var group = getOrCreateGroup(userSuppliedKey); + if (hasCollector) { + groupInsert(group.getResultContainer(), tuple); + } + tuple.setStore(groupStoreIndex, group); + var outTuple = group.getTuple(); switch (outTuple.getState()) { case CREATING, UPDATING -> { // Already in the correct state. } - case OK, DYING -> propagationQueue.update(newGroup); - case ABORTING -> propagationQueue.insert(newGroup); - default -> throw new IllegalStateException("Impossible state: The group (" + newGroup + ") in node (" + this - + ") is in an unexpected state (" + outTuple.getState() + ")."); - } - } - - private OutTuple_ accumulate(InTuple_ tuple, Group group) { - if (hasCollector) { - var undoAccumulator = accumulate(group.getResultContainer(), tuple); - tuple.setStore(undoStoreIndex, undoAccumulator); + case OK, DYING -> propagationQueue.update(group); + case ABORTING -> propagationQueue.insert(group); + default -> throw new IllegalStateException( + "Impossible state: The group (%s) in node (%s) is in an unexpected state (%s)." + .formatted(group, this, outTuple.getState())); } - tuple.setStore(groupStoreIndex, group); - return group.getTuple(); } private Group getOrCreateGroup(GroupKey_ userSuppliedKey) { var groupMapKey = useAssertingGroupKey ? new AssertingGroupKey<>(userSuppliedKey) : userSuppliedKey; - if (hasMultipleGroups) { + if (hasGroupKeyFunction) { // Avoids computeIfAbsent in order to not create lambdas on the hot path. var group = groupMap.get(groupMapKey); if (group == null) { @@ -164,8 +156,9 @@ private Group createGroupWithGroupKey(Object groupM private Group createGroupWithoutGroupKey() { var outTuple = createOutTuple(null); if (!hasCollector) { - throw new IllegalStateException("Impossible state: The node (" + this + ") has no collector, " - + "but it is still trying to create a group without a group key."); + throw new IllegalStateException( + "Impossible state: The node (%s) has no collector, but it is still trying to create a group without a group key." + .formatted(this)); } var group = Group.createWithoutGroupKey(supplier.get(), outTuple); propagationQueue.insert(group); @@ -185,11 +178,7 @@ public final void update(InTuple_ tuple) { insert(tuple); return; } - if (hasCollector) { - Runnable undoAccumulator = tuple.getStore(undoStoreIndex); - undoAccumulator.run(); - } - if (!hasMultipleGroups) { + if (!hasGroupKeyFunction) { updateGroup(tuple, oldGroup); return; } @@ -198,34 +187,46 @@ public final void update(InTuple_ tuple) { if (Objects.equals(oldUserSuppliedGroupKey, newUserSuppliedGroupKey)) { updateGroup(tuple, oldGroup); } else { - killTuple(oldGroup); + if (hasCollector) { + groupRetract(tuple); + } + var newParentCount = --oldGroup.parentCount; + killOutTuple(oldGroup, newParentCount == 0); createTuple(tuple, newUserSuppliedGroupKey); } } - private void updateGroup(InTuple_ tuple, Group oldGroup) { // No need to change parentCount because it is the same group - var outTuple = accumulate(tuple, oldGroup); + private void updateGroup(InTuple_ tuple, Group oldGroup) { + // No need to change parentCount because it is the same group. + if (hasCollector) { + groupUpdate(oldGroup.getResultContainer(), tuple); + } + var outTuple = oldGroup.getTuple(); switch (outTuple.getState()) { case CREATING, UPDATING -> { // Already in the correct state. } case OK -> propagationQueue.update(oldGroup); - default -> - throw new IllegalStateException("Impossible state: The group (%s) in node (%s) is in an unexpected state (%s)." - .formatted(oldGroup, this, outTuple.getState())); + default -> throw new IllegalStateException( + "Impossible state: The group (%s) in node (%s) is in an unexpected state (%s)." + .formatted(oldGroup, this, outTuple.getState())); } } - private void killTuple(Group group) { - var newParentCount = --group.parentCount; - var killGroup = (newParentCount == 0); + /** + * + * @param group the group which created the outTuple + * @param killGroup true if the group should be removed from downstream nodes + */ + private void killOutTuple(Group group, boolean killGroup) { if (killGroup) { - var groupKey = hasMultipleGroups ? group.getGroupKey() : null; + var groupKey = hasGroupKeyFunction ? group.getGroupKey() : null; var oldGroup = removeGroup(groupKey); if (oldGroup == null) { - throw new IllegalStateException("Impossible state: the group for the groupKey (" - + groupKey + ") doesn't exist in the groupMap.\n" + - "Maybe groupKey hashcode changed while it shouldn't have?"); + throw new IllegalStateException(""" + Impossible state: the group for the groupKey (%s) doesn't exist in the groupMap. + Maybe groupKey hashcode changed while it shouldn't have?""" + .formatted(groupKey)); } } var outTuple = group.getTuple(); @@ -247,13 +248,14 @@ private void killTuple(Group group) { propagationQueue.update(group); } } - default -> throw new IllegalStateException("Impossible state: The group (" + group + ") in node (" + this - + ") is in an unexpected state (" + outTuple.getState() + ")."); + default -> throw new IllegalStateException( + "Impossible state: The group (%s) in node (%s) is in an unexpected state (%s)." + .formatted(group, this, outTuple.getState())); } } private Group removeGroup(Object groupKey) { - if (hasMultipleGroups) { + if (hasGroupKeyFunction) { return groupMap.remove(groupKey); } else { var oldGroup = singletonGroup; @@ -270,13 +272,17 @@ public final void retract(InTuple_ tuple) { return; } if (hasCollector) { - Runnable undoAccumulator = tuple.removeStore(undoStoreIndex); - undoAccumulator.run(); + groupRetract(tuple); } - killTuple(group); + var newParentCount = --group.parentCount; + killOutTuple(group, newParentCount == 0); } - protected abstract Runnable accumulate(ResultContainer_ resultContainer, InTuple_ tuple); + protected abstract void groupInsert(ResultContainer_ resultContainer, InTuple_ tuple); + + protected abstract void groupUpdate(ResultContainer_ resultContainer, InTuple_ tuple); + + protected abstract void groupRetract(InTuple_ tuple); @Override public Propagator getPropagator() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleLifecycle.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleLifecycle.java index 24a078b0eaa..f08526a7e46 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleLifecycle.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleLifecycle.java @@ -79,12 +79,12 @@ static TupleLifecycle leftTupleLifecycle && - leftTupleLifecycle.leftTupleLifecycle() instanceof AbstractNode node) { + } else if (delegate instanceof LeftTupleLifecycleImpl(LeftTupleLifecycle lifecycle) + && lifecycle instanceof AbstractNode node) { streamKind = node.getStreamKind(); qualifier = Qualifier.LEFT_INPUT; - } else if (delegate instanceof RightTupleLifecycleImpl rightTupleLifecycle && - rightTupleLifecycle.rightTupleLifecycle() instanceof AbstractNode node) { + } else if (delegate instanceof RightTupleLifecycleImpl(RightTupleLifecycle tupleLifecycle) + && tupleLifecycle instanceof AbstractNode node) { streamKind = node.getStreamKind(); qualifier = Qualifier.RIGHT_INPUT; } else if (delegate instanceof RecordingTupleLifecycle) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/AbstractGroupQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/AbstractGroupQuadNode.java index 0ab3fb5c0c0..2c103fd829e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/AbstractGroupQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/AbstractGroupQuadNode.java @@ -2,42 +2,60 @@ import java.util.function.Function; -import ai.timefold.solver.core.api.function.PentaFunction; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.bavet.common.AbstractGroupNode; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.score.stream.collector.quad.QuadCollectorUtils; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; abstract class AbstractGroupQuadNode extends AbstractGroupNode, OutTuple_, GroupKey_, ResultContainer_, Result_> { - private final PentaFunction accumulator; + private final int groupAccumulatorIndex; + private final @Nullable QuadConstraintCollectorAccumulator incrementalAccumulator; - protected AbstractGroupQuadNode(int groupStoreIndex, int undoStoreIndex, + protected AbstractGroupQuadNode(int groupStoreIndex, int groupAccumulatorIndex, Function, GroupKey_> groupKeyFunction, - QuadConstraintCollector collector, + @NonNull QuadConstraintCollector collector, TupleLifecycle nextNodesTupleLifecycle, EnvironmentMode environmentMode) { - super(groupStoreIndex, undoStoreIndex, - groupKeyFunction, - collector == null ? null : collector.supplier(), - collector == null ? null : collector.finisher(), - nextNodesTupleLifecycle, environmentMode); - accumulator = collector == null ? null : collector.accumulator(); + super(groupStoreIndex, groupKeyFunction, collector.supplier(), collector.finisher(), nextNodesTupleLifecycle, + environmentMode); + this.groupAccumulatorIndex = groupAccumulatorIndex; + this.incrementalAccumulator = QuadCollectorUtils.toIncremental(collector.accumulator()); } protected AbstractGroupQuadNode(int groupStoreIndex, Function, GroupKey_> groupKeyFunction, TupleLifecycle nextNodesTupleLifecycle, EnvironmentMode environmentMode) { - super(groupStoreIndex, - groupKeyFunction, nextNodesTupleLifecycle, environmentMode); - accumulator = null; + super(groupStoreIndex, groupKeyFunction, nextNodesTupleLifecycle, environmentMode); + this.groupAccumulatorIndex = -1; + this.incrementalAccumulator = null; + } + + @Override + protected void groupInsert(ResultContainer_ resultContainer, QuadTuple tuple) { + var groupElement = incrementalAccumulator.intoGroup(resultContainer); + tuple.setStore(groupAccumulatorIndex, groupElement); + groupElement.add(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD()); + } + + @Override + protected void groupUpdate(ResultContainer_ resultContainer, QuadTuple tuple) { + QuadConstraintCollectorValueHandle groupElement = tuple.getStore(groupAccumulatorIndex); + groupElement.replaceWith(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD()); } @Override - protected final Runnable accumulate(ResultContainer_ resultContainer, QuadTuple tuple) { - return accumulator.apply(resultContainer, tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD()); + protected void groupRetract(QuadTuple tuple) { + QuadConstraintCollectorValueHandle groupElement = tuple.removeStore(groupAccumulatorIndex); + groupElement.remove(); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/AbstractGroupTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/AbstractGroupTriNode.java index ecf4f272dd1..871814a271d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/AbstractGroupTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/AbstractGroupTriNode.java @@ -2,41 +2,59 @@ import java.util.function.Function; -import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.bavet.common.AbstractGroupNode; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.score.stream.collector.tri.TriCollectorUtils; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; abstract class AbstractGroupTriNode extends AbstractGroupNode, OutTuple_, GroupKey_, ResultContainer_, Result_> { - private final QuadFunction accumulator; + private final int groupAccumulatorIndex; + private final @Nullable TriConstraintCollectorAccumulator incrementalAccumulator; - protected AbstractGroupTriNode(int groupStoreIndex, int undoStoreIndex, + protected AbstractGroupTriNode(int groupStoreIndex, int groupAccumulatorIndex, Function, GroupKey_> groupKeyFunction, - TriConstraintCollector collector, + @NonNull TriConstraintCollector collector, TupleLifecycle nextNodesTupleLifecycle, EnvironmentMode environmentMode) { - super(groupStoreIndex, undoStoreIndex, groupKeyFunction, - collector == null ? null : collector.supplier(), - collector == null ? null : collector.finisher(), - nextNodesTupleLifecycle, environmentMode); - accumulator = collector == null ? null : collector.accumulator(); + super(groupStoreIndex, groupKeyFunction, collector.supplier(), collector.finisher(), nextNodesTupleLifecycle, + environmentMode); + this.groupAccumulatorIndex = groupAccumulatorIndex; + this.incrementalAccumulator = TriCollectorUtils.toIncremental(collector.accumulator()); } - protected AbstractGroupTriNode(int groupStoreIndex, - Function, GroupKey_> groupKeyFunction, + protected AbstractGroupTriNode(int groupStoreIndex, Function, GroupKey_> groupKeyFunction, TupleLifecycle nextNodesTupleLifecycle, EnvironmentMode environmentMode) { - super(groupStoreIndex, - groupKeyFunction, nextNodesTupleLifecycle, environmentMode); - accumulator = null; + super(groupStoreIndex, groupKeyFunction, nextNodesTupleLifecycle, environmentMode); + this.groupAccumulatorIndex = -1; + this.incrementalAccumulator = null; + } + + @Override + protected void groupInsert(ResultContainer_ resultContainer, TriTuple tuple) { + var groupElement = incrementalAccumulator.intoGroup(resultContainer); + tuple.setStore(groupAccumulatorIndex, groupElement); + groupElement.add(tuple.getA(), tuple.getB(), tuple.getC()); + } + + @Override + protected void groupUpdate(ResultContainer_ resultContainer, TriTuple tuple) { + TriConstraintCollectorValueHandle groupElement = tuple.getStore(groupAccumulatorIndex); + groupElement.replaceWith(tuple.getA(), tuple.getB(), tuple.getC()); } @Override - protected final Runnable accumulate(ResultContainer_ resultContainer, TriTuple tuple) { - return accumulator.apply(resultContainer, tuple.getA(), tuple.getB(), tuple.getC()); + protected void groupRetract(TriTuple tuple) { + TriConstraintCollectorValueHandle groupElement = tuple.removeStore(groupAccumulatorIndex); + groupElement.remove(); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractGroupUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractGroupUniNode.java index 4b0e33da8f3..64773a5c32b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractGroupUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractGroupUniNode.java @@ -1,42 +1,60 @@ package ai.timefold.solver.core.impl.bavet.uni; -import java.util.function.BiFunction; import java.util.function.Function; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.bavet.common.AbstractGroupNode; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.score.stream.collector.uni.UniCollectorUtils; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; abstract class AbstractGroupUniNode extends AbstractGroupNode, OutTuple_, GroupKey_, ResultContainer_, Result_> { - private final BiFunction accumulator; + private final int groupAccumulatorIndex; + private final @Nullable UniConstraintCollectorAccumulator incrementalAccumulator; - protected AbstractGroupUniNode(int groupStoreIndex, int undoStoreIndex, + protected AbstractGroupUniNode(int groupStoreIndex, int groupAccumulatorIndex, Function, GroupKey_> groupKeyFunction, - UniConstraintCollector collector, + @NonNull UniConstraintCollector collector, TupleLifecycle nextNodesTupleLifecycle, EnvironmentMode environmentMode) { - super(groupStoreIndex, undoStoreIndex, - groupKeyFunction, - collector == null ? null : collector.supplier(), - collector == null ? null : collector.finisher(), - nextNodesTupleLifecycle, environmentMode); - accumulator = collector == null ? null : collector.accumulator(); + super(groupStoreIndex, groupKeyFunction, collector.supplier(), collector.finisher(), nextNodesTupleLifecycle, + environmentMode); + this.groupAccumulatorIndex = groupAccumulatorIndex; + this.incrementalAccumulator = UniCollectorUtils.toIncremental(collector.accumulator()); } - protected AbstractGroupUniNode(int groupStoreIndex, - Function, GroupKey_> groupKeyFunction, + protected AbstractGroupUniNode(int groupStoreIndex, Function, GroupKey_> groupKeyFunction, TupleLifecycle nextNodesTupleLifecycle, EnvironmentMode environmentMode) { - super(groupStoreIndex, - groupKeyFunction, nextNodesTupleLifecycle, environmentMode); - accumulator = null; + super(groupStoreIndex, groupKeyFunction, nextNodesTupleLifecycle, environmentMode); + this.groupAccumulatorIndex = -1; + this.incrementalAccumulator = null; + } + + @Override + protected void groupInsert(ResultContainer_ resultContainer, UniTuple tuple) { + var groupElement = incrementalAccumulator.intoGroup(resultContainer); + tuple.setStore(groupAccumulatorIndex, groupElement); + groupElement.add(tuple.getA()); } @Override - protected final Runnable accumulate(ResultContainer_ resultContainer, UniTuple tuple) { - return accumulator.apply(resultContainer, tuple.getA()); + protected void groupUpdate(ResultContainer_ resultContainer, UniTuple tuple) { + UniConstraintCollectorValueHandle groupElement = tuple.getStore(groupAccumulatorIndex); + groupElement.replaceWith(tuple.getA()); } + + @Override + protected void groupRetract(UniTuple tuple) { + UniConstraintCollectorValueHandle groupElement = tuple.removeStore(groupAccumulatorIndex); + groupElement.remove(); + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConnectedRangesSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConnectedRangesSlot.java new file mode 100644 index 00000000000..c54b04f4a7b --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConnectedRangesSlot.java @@ -0,0 +1,48 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain; +import ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.ConnectedRangeTracker; +import ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.Range; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractConnectedRangesSlot, Difference_ extends Comparable> { + + public static final class State, Difference_ extends Comparable> { + private final ConnectedRangeTracker context; + + public State(Function startMap, + Function endMap, + BiFunction differenceFunction) { + this.context = new ConnectedRangeTracker<>(startMap, endMap, differenceFunction); + } + + public ConnectedRangeChain result() { + return context.getConnectedRangeChain(); + } + } + + private final State state; + private @Nullable Range cachedRange; + + public AbstractConnectedRangesSlot(State state) { + this.state = state; + } + + protected void addMapped(Interval_ result) { + cachedRange = state.context.getRange(result); + state.context.add(cachedRange); + } + + protected void replaceWithMapped(Interval_ input) { + removeMapped(); + addMapped(input); + } + + protected void removeMapped() { + state.context.remove(cachedRange); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractCountSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractCountSlot.java new file mode 100644 index 00000000000..fc5debb59fa --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractCountSlot.java @@ -0,0 +1,25 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import ai.timefold.solver.core.impl.util.MutableLong; + +public abstract class AbstractCountSlot { + + private final MutableLong state; + + public AbstractCountSlot(MutableLong state) { + this.state = state; + } + + protected void addMapped() { + state.increment(); + } + + protected void replaceWithMapped() { + // count is unchanged + } + + protected void removeMapped() { + state.decrement(); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLoadBalanceSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLoadBalanceSlot.java new file mode 100644 index 00000000000..b4aed2e50b5 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLoadBalanceSlot.java @@ -0,0 +1,34 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractLoadBalanceSlot { + + private final DefaultLoadBalance container; + private @Nullable Balanced_ cachedBalanced; + private long cachedLoad; + + public AbstractLoadBalanceSlot(DefaultLoadBalance container) { + this.container = container; + } + + protected void addMapped(Balanced_ balanced, long load, long initialLoad) { + cachedBalanced = balanced; + cachedLoad = load; + container.registerBalanced(balanced, load, initialLoad); + } + + protected void replaceWithMapped(Balanced_ balanced, long load, long initialLoad) { + if (Objects.equals(cachedBalanced, balanced) && cachedLoad == load) { + return; + } + removeMapped(); + addMapped(balanced, load, initialLoad); + } + + protected void removeMapped() { + container.unregisterBalanced(cachedBalanced, cachedLoad); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLongAverageSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLongAverageSlot.java new file mode 100644 index 00000000000..8deed9e2c6a --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLongAverageSlot.java @@ -0,0 +1,39 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +public abstract class AbstractLongAverageSlot { + + public static final class State { + private long count = 0; + private long sum = 0; + + public Double result() { + if (count == 0) { + return null; + } + return sum / (double) count; + } + } + + private final State state; + private long cachedInput; + + public AbstractLongAverageSlot(State state) { + this.state = state; + } + + protected void addMapped(long input) { + cachedInput = input; + state.count++; + state.sum = Math.addExact(state.sum, input); + } + + protected void replaceWithMapped(long input) { + state.sum += Math.subtractExact(input, cachedInput); + cachedInput = input; + } + + protected void removeMapped() { + state.count--; + state.sum = Math.subtractExact(state.sum, cachedInput); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLongDistinctSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLongDistinctSlot.java new file mode 100644 index 00000000000..d2bd7daed57 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLongDistinctSlot.java @@ -0,0 +1,50 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import ai.timefold.solver.core.impl.util.MutableInt; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractLongDistinctSlot { + + public static final class State { + + private final Map countMap = new HashMap<>(); + + public Long result() { + return (long) countMap.size(); + } + + } + + private final State state; + private @Nullable Input_ cachedInput; + private @Nullable MutableInt cachedCounter; + + public AbstractLongDistinctSlot(State state) { + this.state = state; + } + + protected void addMapped(Input_ input) { + cachedInput = input; + cachedCounter = state.countMap.computeIfAbsent(input, ignored -> new MutableInt()); + cachedCounter.increment(); + } + + protected void replaceWithMapped(Input_ input) { + if (Objects.equals(cachedInput, input)) { + return; + } + removeMapped(); + addMapped(input); + } + + protected void removeMapped() { + if (cachedCounter.decrement() == 0) { + state.countMap.remove(cachedInput); + } + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLongSumSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLongSumSlot.java new file mode 100644 index 00000000000..fb48d47a9ef --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractLongSumSlot.java @@ -0,0 +1,27 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import ai.timefold.solver.core.impl.util.MutableLong; + +public abstract class AbstractLongSumSlot { + + private final MutableLong state; + private long cachedInput; + + public AbstractLongSumSlot(MutableLong state) { + this.state = state; + } + + protected void addMapped(long input) { + cachedInput = input; + state.add(input); + } + + protected void replaceWithMapped(long input) { + state.add(Math.subtractExact(input, cachedInput)); + cachedInput = input; + } + + protected void removeMapped() { + state.subtract(cachedInput); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractMinMaxSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractMinMaxSlot.java new file mode 100644 index 00000000000..9f37d199b1d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractMinMaxSlot.java @@ -0,0 +1,108 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.SequencedMap; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.function.Supplier; + +import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; +import ai.timefold.solver.core.impl.util.MutableInt; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractMinMaxSlot { + public static final class State { + private final Supplier>> firstOrLastEntrySupplier; + private final NavigableMap> propertyToItemCountNavigableMap; + private final Function propertyFunction; + + private State(boolean isMin, NavigableMap> propertyToItemCountNavigableMap, + Function propertyFunction) { + this.firstOrLastEntrySupplier = + isMin ? propertyToItemCountNavigableMap::firstEntry : propertyToItemCountNavigableMap::lastEntry; + this.propertyToItemCountNavigableMap = propertyToItemCountNavigableMap; + this.propertyFunction = propertyFunction; + } + + public Result_ result() { + if (propertyToItemCountNavigableMap.isEmpty()) { + return null; + } + var entry = firstOrLastEntrySupplier.get(); + return entry.getValue().sequencedKeySet().getFirst(); + } + + } + + public static > State minState() { + return new State<>(true, new TreeMap<>(), ConstantLambdaUtils.identity()); + } + + public static > State maxState() { + return new State<>(false, new TreeMap<>(), ConstantLambdaUtils.identity()); + } + + public static > State minState( + Function propertyMapper) { + return new State<>(true, new TreeMap<>(), propertyMapper); + } + + public static > State maxState( + Function propertyMapper) { + return new State<>(false, new TreeMap<>(), propertyMapper); + } + + private final State state; + private @Nullable Result_ cachedItem; + private @Nullable Property_ cachedKey; + private @Nullable Map cachedInnerMap; + private @Nullable MutableInt cachedCount; + + public AbstractMinMaxSlot(State state) { + this.state = state; + } + + protected void addMapped(Result_ item) { + cachedItem = item; + cachedKey = state.propertyFunction.apply(item); + cachedInnerMap = state.propertyToItemCountNavigableMap.computeIfAbsent(cachedKey, ignored -> new LinkedHashMap<>()); + cachedCount = cachedInnerMap.computeIfAbsent(item, ignored -> new MutableInt()); + cachedCount.increment(); + } + + protected void replaceWithMapped(Result_ item) { + var newKey = state.propertyFunction.apply(item); + if (Objects.equals(cachedKey, newKey)) { + if (Objects.equals(cachedItem, item)) { + return; + } + // same key, different item: swap within the same inner map + if (cachedCount.decrement() == 0) { + cachedInnerMap.remove(cachedItem); + } + cachedItem = item; + cachedCount = cachedInnerMap.computeIfAbsent(item, ignored -> new MutableInt()); + cachedCount.increment(); + return; + } + removeMapped(); + cachedItem = item; + cachedKey = newKey; + cachedInnerMap = state.propertyToItemCountNavigableMap.computeIfAbsent(newKey, ignored -> new LinkedHashMap<>()); + cachedCount = cachedInnerMap.computeIfAbsent(item, ignored -> new MutableInt()); + cachedCount.increment(); + } + + protected void removeMapped() { + if (cachedCount.decrement() == 0) { + cachedInnerMap.remove(cachedItem); + if (cachedInnerMap.isEmpty()) { + state.propertyToItemCountNavigableMap.remove(cachedKey); + } + } + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractReferenceAverageSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractReferenceAverageSlot.java new file mode 100644 index 00000000000..67033a9101d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractReferenceAverageSlot.java @@ -0,0 +1,87 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.time.Duration; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Supplier; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractReferenceAverageSlot { + + public static final class State { + private final BinaryOperator adder; + private final BinaryOperator subtractor; + private final BiFunction divider; + private int count = 0; + private Input_ sum; + + State(Input_ zero, BinaryOperator adder, BinaryOperator subtractor, + BiFunction divider) { + this.adder = adder; + this.subtractor = subtractor; + this.divider = divider; + this.sum = zero; + } + + public Output_ result() { + if (count == 0) { + return null; + } + return divider.apply(sum, count); + } + } + + private final State state; + private @Nullable Input_ cachedValue; + + private final static Supplier> BIG_DECIMAL = + () -> new State<>(BigDecimal.ZERO, BigDecimal::add, BigDecimal::subtract, + (sum, count) -> sum.divide(BigDecimal.valueOf(count), RoundingMode.HALF_EVEN)); + + private final static Supplier> BIG_INTEGER = + () -> new State<>(BigInteger.ZERO, BigInteger::add, BigInteger::subtract, + (sum, count) -> new BigDecimal(sum).divide(BigDecimal.valueOf(count), RoundingMode.HALF_EVEN)); + + private final static Supplier> DURATION = + () -> new State<>(Duration.ZERO, Duration::plus, Duration::minus, + (sum, count) -> { + long nanos = sum.toNanos(); + return Duration.ofNanos(nanos / count); + }); + + public AbstractReferenceAverageSlot(State state) { + this.state = state; + } + + public static Supplier> bigDecimalState() { + return BIG_DECIMAL; + } + + public static Supplier> bigIntegerState() { + return BIG_INTEGER; + } + + public static Supplier> durationState() { + return DURATION; + } + + protected void addMapped(Input_ input) { + cachedValue = input; + state.count++; + state.sum = state.adder.apply(state.sum, input); + } + + protected void replaceWithMapped(Input_ input) { + state.sum = state.adder.apply(state.subtractor.apply(state.sum, cachedValue), input); + cachedValue = input; + } + + protected void removeMapped() { + state.count--; + state.sum = state.subtractor.apply(state.sum, cachedValue); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractReferenceSumSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractReferenceSumSlot.java new file mode 100644 index 00000000000..f21134d2547 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractReferenceSumSlot.java @@ -0,0 +1,46 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.function.BinaryOperator; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractReferenceSumSlot { + + public static final class State { + private final BinaryOperator adder; + private final BinaryOperator subtractor; + private Result_ current; + + public State(Result_ zero, BinaryOperator adder, BinaryOperator subtractor) { + this.adder = adder; + this.subtractor = subtractor; + this.current = zero; + } + + public Result_ result() { + return current; + } + } + + private final State state; + private @Nullable Result_ cachedValue; + + public AbstractReferenceSumSlot(State state) { + this.state = state; + } + + protected void addMapped(Result_ input) { + cachedValue = input; + state.current = state.adder.apply(state.current, input); + } + + protected void replaceWithMapped(Result_ input) { + // Avoiding equals shortcut, as addition and subtraction are super-fast. + state.current = state.adder.apply(state.subtractor.apply(state.current, cachedValue), input); + cachedValue = input; + } + + protected void removeMapped() { + state.current = state.subtractor.apply(state.current, cachedValue); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractSequenceSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractSequenceSlot.java new file mode 100644 index 00000000000..4c60543d4fc --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractSequenceSlot.java @@ -0,0 +1,50 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.Objects; +import java.util.function.BinaryOperator; +import java.util.function.ToIntFunction; + +import ai.timefold.solver.core.api.score.stream.common.SequenceChain; +import ai.timefold.solver.core.impl.score.stream.collector.consecutive.ConsecutiveSetTree; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractSequenceSlot { + + public static final class State { + private static final BinaryOperator DIFFERENCE = (a, b) -> b - a; + private final ConsecutiveSetTree context = + new ConsecutiveSetTree<>(DIFFERENCE, Integer::sum, 1, 0); + private final ToIntFunction toIndexFunction; + + public State(ToIntFunction toIndexFunction) { + this.toIndexFunction = Objects.requireNonNull(toIndexFunction); + } + + public SequenceChain result() { + return context; + } + } + + private final State state; + private @Nullable Result_ cachedValue; + + public AbstractSequenceSlot(State state) { + this.state = state; + } + + protected void addMapped(Result_ result) { + cachedValue = result; + state.context.add(result, state.toIndexFunction.applyAsInt(result)); + } + + protected void replaceWithMapped(Result_ input) { + state.context.remove(cachedValue); + cachedValue = input; + state.context.add(input, state.toIndexFunction.applyAsInt(input)); + } + + protected void removeMapped() { + state.context.remove(cachedValue); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractSortedSetSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractSortedSetSlot.java new file mode 100644 index 00000000000..658e4d1b0c6 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractSortedSetSlot.java @@ -0,0 +1,53 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.Comparator; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.TreeMap; + +import ai.timefold.solver.core.impl.util.MutableInt; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractSortedSetSlot { + public static final class State { + private final NavigableMap itemToCount; + + public State(Comparator comparator) { + this.itemToCount = new TreeMap<>(comparator); + } + + public NavigableSet result() { + return itemToCount.navigableKeySet(); + } + } + + private final State state; + private @Nullable Mapped_ cachedValue; + private @Nullable MutableInt cachedCount; + + public AbstractSortedSetSlot(State state) { + this.state = state; + } + + protected void addMapped(Mapped_ result) { + this.cachedValue = result; + this.cachedCount = state.itemToCount.computeIfAbsent(result, ignored -> new MutableInt()); + this.cachedCount.increment(); + } + + protected void replaceWithMapped(Mapped_ result) { + if (Objects.equals(cachedValue, result)) { + return; + } + removeMapped(); + addMapped(result); + } + + protected void removeMapped() { + if (cachedCount.decrement() == 0) { + state.itemToCount.remove(cachedValue); + } + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToCollectionSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToCollectionSlot.java new file mode 100644 index 00000000000..5296b1fbbe7 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToCollectionSlot.java @@ -0,0 +1,47 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.Collection; +import java.util.function.IntFunction; + +import ai.timefold.solver.core.impl.util.ElementAwareArrayList; + +public abstract class AbstractToCollectionSlot> { + public static final class State> { + private final IntFunction collectionFunction; + private final ElementAwareArrayList list = new ElementAwareArrayList<>(); + + public State(IntFunction collectionFunction) { + this.collectionFunction = collectionFunction; + } + + public Collection_ result() { + var out = collectionFunction.apply(list.size()); + if (list.isEmpty()) { + // Avoid exception if out is an immutable collection + return out; + } + out.addAll(list); + return out; + } + } + + private final State state; + private ElementAwareArrayList.Entry cachedEntry; + + public AbstractToCollectionSlot(State state) { + this.state = state; + } + + protected void addMapped(Mapped_ mapped) { + cachedEntry = state.list.addEntry(mapped); + } + + protected void replaceWithMapped(Mapped_ mapped) { + cachedEntry.replaceElement(mapped); + } + + protected void removeMapped() { + cachedEntry.remove(); + cachedEntry = null; + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToListSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToListSlot.java new file mode 100644 index 00000000000..dc85122923b --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToListSlot.java @@ -0,0 +1,35 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.List; + +import ai.timefold.solver.core.impl.util.ElementAwareArrayList; + +public abstract class AbstractToListSlot { + public static final class State { + private final ElementAwareArrayList resultList = new ElementAwareArrayList<>(); + + public List result() { + return resultList; + } + } + + private final State state; + private ElementAwareArrayList.Entry cachedEntry; + + public AbstractToListSlot(State state) { + this.state = state; + } + + protected void addMapped(Mapped_ mapped) { + cachedEntry = state.resultList.addEntry(mapped); + } + + protected void replaceWithMapped(Mapped_ mapped) { + cachedEntry.replaceElement(mapped); + } + + protected void removeMapped() { + cachedEntry.remove(); + cachedEntry = null; + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToMapSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToMapSlot.java new file mode 100644 index 00000000000..2d4b0de0b7d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToMapSlot.java @@ -0,0 +1,58 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.Map; +import java.util.Set; +import java.util.function.BinaryOperator; +import java.util.function.IntFunction; +import java.util.function.Supplier; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractToMapSlot> { + public static final class State> { + private final ToMapResultContainer container; + + private State(ToMapResultContainer container) { + this.container = container; + } + + public Result_ result() { + return container.getResult(); + } + } + + public static , Result_ extends Map> + State multiMapState(Supplier resultSupplier, IntFunction setFunction) { + return new State<>(new ToMultiMapResultContainer<>(resultSupplier, setFunction)); + } + + public static > + State + mergeMapState(Supplier resultSupplier, BinaryOperator mergeFunction) { + return new State<>(new ToSimpleMapResultContainer<>(resultSupplier, mergeFunction)); + } + + private final State state; + private @Nullable ToMapPerKeyCounter cachedCounter; + private @Nullable CountHolder cachedHolder; + + public AbstractToMapSlot(State state) { + this.state = state; + } + + protected void addMapped(Key_ key, Value_ value) { + state.container.add(key, value); + cachedCounter = state.container.lastCounter(); + cachedHolder = state.container.lastHolder(); + } + + protected void replaceWithMapped(Key_ key, Value_ value) { + state.container.replaceWith(cachedCounter, cachedHolder, key, value); + cachedCounter = state.container.lastCounter(); + cachedHolder = state.container.lastHolder(); + } + + protected void removeMapped() { + state.container.remove(cachedCounter, cachedHolder); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToSetSlot.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToSetSlot.java new file mode 100644 index 00000000000..605513170a4 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractToSetSlot.java @@ -0,0 +1,48 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import ai.timefold.solver.core.impl.util.MutableInt; + +import org.jspecify.annotations.Nullable; + +public abstract class AbstractToSetSlot { + public static final class State { + private final Map itemToCount = new LinkedHashMap<>(); + + public Set result() { + return itemToCount.keySet(); + } + } + + private final State state; + private @Nullable Mapped_ cachedValue; + private @Nullable MutableInt cachedCount; + + public AbstractToSetSlot(State state) { + this.state = state; + } + + protected void addMapped(Mapped_ result) { + cachedValue = result; + cachedCount = state.itemToCount.computeIfAbsent(result, ignored -> new MutableInt()); + cachedCount.increment(); + } + + protected void replaceWithMapped(Mapped_ result) { + if (Objects.equals(cachedValue, result)) { + return; + } + removeMapped(); + addMapped(result); + } + + protected void removeMapped() { + if (cachedCount.decrement() == 0) { + state.itemToCount.remove(cachedValue); + } + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ConnectedRangesCalculator.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ConnectedRangesCalculator.java deleted file mode 100644 index 9395893bbfa..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ConnectedRangesCalculator.java +++ /dev/null @@ -1,42 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.util.function.BiFunction; -import java.util.function.Function; - -import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain; -import ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.ConnectedRangeTracker; -import ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.Range; - -public final class ConnectedRangesCalculator, Difference_ extends Comparable> - implements - ObjectCalculator, Range> { - - private final ConnectedRangeTracker context; - - public ConnectedRangesCalculator(Function startMap, - Function endMap, - BiFunction differenceFunction) { - this.context = new ConnectedRangeTracker<>( - startMap, - endMap, - differenceFunction); - } - - @Override - public Range insert(Interval_ result) { - final var saved = context.getRange(result); - context.add(saved); - return saved; - } - - @Override - public void retract(Range range) { - context.remove(range); - } - - @Override - public ConnectedRangeChain result() { - return context.getConnectedRangeChain(); - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/CountHolder.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/CountHolder.java new file mode 100644 index 00000000000..7a1605653a7 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/CountHolder.java @@ -0,0 +1,13 @@ +package ai.timefold.solver.core.impl.score.stream.collector; + +final class CountHolder { + + final Value_ value; + long count; + + CountHolder(Value_ value) { + this.value = value; + this.count = 1; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/CustomCollectionUndoableActionable.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/CustomCollectionUndoableActionable.java deleted file mode 100644 index f8ea0d453dc..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/CustomCollectionUndoableActionable.java +++ /dev/null @@ -1,33 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.IntFunction; - -public final class CustomCollectionUndoableActionable> - implements UndoableActionable { - private final IntFunction collectionFunction; - private final List resultList = new ArrayList<>(); - - public CustomCollectionUndoableActionable(IntFunction collectionFunction) { - this.collectionFunction = collectionFunction; - } - - @Override - public Runnable insert(Mapped_ result) { - resultList.add(result); - return () -> resultList.remove(result); - } - - @Override - public Result_ result() { - Result_ out = collectionFunction.apply(resultList.size()); - if (resultList.isEmpty()) { - // Avoid exception if out is an immutable collection - return out; - } - out.addAll(resultList); - return out; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LoadBalanceImpl.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/DefaultLoadBalance.java similarity index 94% rename from core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LoadBalanceImpl.java rename to core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/DefaultLoadBalance.java index 3d5a805b800..f06b4fb244f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LoadBalanceImpl.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/DefaultLoadBalance.java @@ -13,7 +13,8 @@ import org.jspecify.annotations.NonNull; -public final class LoadBalanceImpl implements LoadBalance { +public final class DefaultLoadBalance + implements LoadBalance { // If need be, precision can be made configurable on the constraint collector level. private static final MathContext RESULT_MATH_CONTEXT = new MathContext(6, RoundingMode.HALF_EVEN); @@ -25,14 +26,13 @@ public final class LoadBalanceImpl implements LoadBalance private long squaredDeviationIntegralPart = 0; private long squaredDeviationFractionNumerator = 0; - public Runnable registerBalanced(Balanced_ balanced, long metricValue, long initialMetricValue) { + public void registerBalanced(Balanced_ balanced, long metricValue, long initialMetricValue) { var balancedItemCount = balancedItemCountMap.compute(balanced, (k, v) -> v == null ? 1 : v + 1); if (balancedItemCount == 1) { addToMetric(balanced, metricValue + initialMetricValue); } else { addToMetric(balanced, metricValue); } - return () -> unregisterBalanced(balanced, metricValue); } public void unregisterBalanced(Balanced_ balanced, long metricValue) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ListUndoableActionable.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ListUndoableActionable.java deleted file mode 100644 index 77ed4992314..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ListUndoableActionable.java +++ /dev/null @@ -1,25 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.util.List; - -import ai.timefold.solver.core.impl.util.ElementAwareArrayList; - -public final class ListUndoableActionable implements UndoableActionable> { - - /** - * As long as additions and removals are performed using entry-based methods, - * removals have O(1) performance. - */ - private final ElementAwareArrayList resultList = new ElementAwareArrayList<>(); - - @Override - public Runnable insert(Mapped_ result) { - var entry = resultList.addEntry(result); - return entry::remove; - } - - @Override - public List result() { - return resultList; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongAverageCalculator.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongAverageCalculator.java deleted file mode 100644 index d272b856f9a..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongAverageCalculator.java +++ /dev/null @@ -1,26 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -public final class LongAverageCalculator implements LongCalculator { - long count = 0; - long sum = 0; - - @Override - public void insert(long input) { - count++; - sum += input; - } - - @Override - public void retract(long input) { - count--; - sum -= input; - } - - @Override - public Double result() { - if (count == 0) { - return null; - } - return sum / (double) count; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongCalculator.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongCalculator.java deleted file mode 100644 index ed5f328e5cf..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongCalculator.java +++ /dev/null @@ -1,9 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -public sealed interface LongCalculator permits LongAverageCalculator, LongSumCalculator { - void insert(long input); - - void retract(long input); - - Output_ result(); -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongCounter.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongCounter.java deleted file mode 100644 index 287de5e3d91..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongCounter.java +++ /dev/null @@ -1,17 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -public final class LongCounter { - private long count; - - public void increment() { - count++; - } - - public void decrement() { - count--; - } - - public long result() { - return count; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongDistinctCountCalculator.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongDistinctCountCalculator.java deleted file mode 100644 index 0a5eb0706f0..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongDistinctCountCalculator.java +++ /dev/null @@ -1,28 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.util.HashMap; -import java.util.Map; - -import ai.timefold.solver.core.impl.util.MutableInt; - -public final class LongDistinctCountCalculator implements ObjectCalculator { - private final Map countMap = new HashMap<>(); - - @Override - public Input_ insert(Input_ input) { - countMap.computeIfAbsent(input, ignored -> new MutableInt()).increment(); - return input; - } - - @Override - public void retract(Input_ mapped) { - if (countMap.get(mapped).decrement() == 0) { - countMap.remove(mapped); - } - } - - @Override - public Long result() { - return (long) countMap.size(); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongSumCalculator.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongSumCalculator.java deleted file mode 100644 index 2beb559da92..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/LongSumCalculator.java +++ /dev/null @@ -1,20 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -public final class LongSumCalculator implements LongCalculator { - long sum = 0; - - @Override - public void insert(long input) { - sum += input; - } - - @Override - public void retract(long input) { - sum -= input; - } - - @Override - public Long result() { - return sum; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/MapUndoableActionable.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/MapUndoableActionable.java deleted file mode 100644 index a65668b4017..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/MapUndoableActionable.java +++ /dev/null @@ -1,41 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.util.Map; -import java.util.Set; -import java.util.function.BinaryOperator; -import java.util.function.IntFunction; -import java.util.function.Supplier; - -import ai.timefold.solver.core.impl.util.Pair; - -public final class MapUndoableActionable> - implements UndoableActionable, Result_> { - ToMapResultContainer container; - - private MapUndoableActionable(ToMapResultContainer container) { - this.container = container; - } - - public static , Result_ extends Map> - MapUndoableActionable multiMap( - Supplier resultSupplier, IntFunction setFunction) { - return new MapUndoableActionable<>(new ToMultiMapResultContainer<>(resultSupplier, setFunction)); - } - - public static > MapUndoableActionable - mergeMap( - Supplier resultSupplier, BinaryOperator mergeFunction) { - return new MapUndoableActionable<>(new ToSimpleMapResultContainer<>(resultSupplier, mergeFunction)); - } - - @Override - public Runnable insert(Pair entry) { - container.add(entry.key(), entry.value()); - return () -> container.remove(entry.key(), entry.value()); - } - - @Override - public Result_ result() { - return container.getResult(); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/MinMaxUndoableActionable.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/MinMaxUndoableActionable.java deleted file mode 100644 index 57c924561f9..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/MinMaxUndoableActionable.java +++ /dev/null @@ -1,83 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.NavigableMap; -import java.util.TreeMap; -import java.util.function.Function; - -import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; -import ai.timefold.solver.core.impl.util.MutableInt; - -public final class MinMaxUndoableActionable implements UndoableActionable { - private final boolean isMin; - private final NavigableMap> propertyToItemCountMap; - private final Function propertyFunction; - - private MinMaxUndoableActionable(boolean isMin, - NavigableMap> propertyToItemCountMap, - Function propertyFunction) { - this.isMin = isMin; - this.propertyToItemCountMap = propertyToItemCountMap; - this.propertyFunction = propertyFunction; - } - - public static > MinMaxUndoableActionable minCalculator() { - return new MinMaxUndoableActionable<>(true, new TreeMap<>(), ConstantLambdaUtils.identity()); - } - - public static > MinMaxUndoableActionable maxCalculator() { - return new MinMaxUndoableActionable<>(false, new TreeMap<>(), ConstantLambdaUtils.identity()); - } - - public static MinMaxUndoableActionable minCalculator(Comparator comparator) { - return new MinMaxUndoableActionable<>(true, new TreeMap<>(comparator), ConstantLambdaUtils.identity()); - } - - public static MinMaxUndoableActionable maxCalculator(Comparator comparator) { - return new MinMaxUndoableActionable<>(false, new TreeMap<>(comparator), ConstantLambdaUtils.identity()); - } - - public static > MinMaxUndoableActionable - minCalculator( - Function propertyMapper) { - return new MinMaxUndoableActionable<>(true, new TreeMap<>(), propertyMapper); - } - - public static > MinMaxUndoableActionable - maxCalculator( - Function propertyMapper) { - return new MinMaxUndoableActionable<>(false, new TreeMap<>(), propertyMapper); - } - - @Override - public Runnable insert(Result_ item) { - Property_ key = propertyFunction.apply(item); - Map itemCountMap = propertyToItemCountMap.computeIfAbsent(key, ignored -> new LinkedHashMap<>()); - MutableInt count = itemCountMap.computeIfAbsent(item, ignored -> new MutableInt()); - count.increment(); - - return () -> { - if (count.decrement() == 0) { - itemCountMap.remove(item); - if (itemCountMap.isEmpty()) { - propertyToItemCountMap.remove(key); - } - } - }; - } - - @Override - public Result_ result() { - if (propertyToItemCountMap.isEmpty()) { - return null; - } - return isMin ? getFirstKey(propertyToItemCountMap.firstEntry().getValue()) - : getFirstKey(propertyToItemCountMap.lastEntry().getValue()); - } - - private static Key_ getFirstKey(Map map) { - return map.keySet().iterator().next(); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ObjectCalculator.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ObjectCalculator.java deleted file mode 100644 index 7a38f6cb5b6..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ObjectCalculator.java +++ /dev/null @@ -1,11 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -public sealed interface ObjectCalculator - permits ConnectedRangesCalculator, LongDistinctCountCalculator, ReferenceAverageCalculator, ReferenceSumCalculator, - SequenceCalculator { - Mapped_ insert(Input_ input); - - void retract(Mapped_ mapped); - - Output_ result(); -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ReferenceAverageCalculator.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ReferenceAverageCalculator.java deleted file mode 100644 index ce4963b1c83..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ReferenceAverageCalculator.java +++ /dev/null @@ -1,73 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.math.RoundingMode; -import java.time.Duration; -import java.util.function.BiFunction; -import java.util.function.BinaryOperator; -import java.util.function.Supplier; - -public final class ReferenceAverageCalculator implements ObjectCalculator { - int count = 0; - Input_ sum; - final BinaryOperator adder; - final BinaryOperator subtractor; - final BiFunction divider; - - private final static Supplier> BIG_DECIMAL = - () -> new ReferenceAverageCalculator<>(BigDecimal.ZERO, BigDecimal::add, BigDecimal::subtract, - (sum, count) -> sum.divide(BigDecimal.valueOf(count), RoundingMode.HALF_EVEN)); - - private final static Supplier> BIG_INTEGER = - () -> new ReferenceAverageCalculator<>(BigInteger.ZERO, BigInteger::add, BigInteger::subtract, - (sum, count) -> new BigDecimal(sum).divide(BigDecimal.valueOf(count), RoundingMode.HALF_EVEN)); - - private final static Supplier> DURATION = - () -> new ReferenceAverageCalculator<>(Duration.ZERO, Duration::plus, Duration::minus, - (sum, count) -> { - long nanos = sum.toNanos(); - return Duration.ofNanos(nanos / count); - }); - - public ReferenceAverageCalculator(Input_ zero, BinaryOperator adder, BinaryOperator subtractor, - BiFunction divider) { - this.sum = zero; - this.adder = adder; - this.subtractor = subtractor; - this.divider = divider; - } - - public static Supplier> bigDecimal() { - return BIG_DECIMAL; - } - - public static Supplier> bigInteger() { - return BIG_INTEGER; - } - - public static Supplier> duration() { - return DURATION; - } - - @Override - public Input_ insert(Input_ input) { - count++; - sum = adder.apply(sum, input); - return input; - } - - @Override - public void retract(Input_ mapped) { - count--; - sum = subtractor.apply(sum, mapped); - } - - @Override - public Output_ result() { - if (count == 0) { - return null; - } - return divider.apply(sum, count); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ReferenceSumCalculator.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ReferenceSumCalculator.java deleted file mode 100644 index dc361f153b7..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ReferenceSumCalculator.java +++ /dev/null @@ -1,31 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.util.function.BinaryOperator; - -public final class ReferenceSumCalculator implements ObjectCalculator { - private Result_ current; - private final BinaryOperator adder; - private final BinaryOperator subtractor; - - public ReferenceSumCalculator(Result_ current, BinaryOperator adder, BinaryOperator subtractor) { - this.current = current; - this.adder = adder; - this.subtractor = subtractor; - } - - @Override - public Result_ insert(Result_ input) { - current = adder.apply(current, input); - return input; - } - - @Override - public void retract(Result_ mapped) { - current = subtractor.apply(current, mapped); - } - - @Override - public Result_ result() { - return current; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/SequenceCalculator.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/SequenceCalculator.java deleted file mode 100644 index 9c56774e9c8..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/SequenceCalculator.java +++ /dev/null @@ -1,40 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.util.Objects; -import java.util.function.BinaryOperator; -import java.util.function.ToIntFunction; - -import ai.timefold.solver.core.api.score.stream.common.SequenceChain; -import ai.timefold.solver.core.impl.score.stream.collector.consecutive.ConsecutiveSetTree; - -public final class SequenceCalculator - implements ObjectCalculator, Result_> { - - private final static BinaryOperator DIFFERENCE = (a, b) -> b - a; - - private final ConsecutiveSetTree context = - new ConsecutiveSetTree<>(DIFFERENCE, Integer::sum, 1, 0); - private final ToIntFunction toIndexFunction; - - public SequenceCalculator(ToIntFunction toIndexFunction) { - this.toIndexFunction = Objects.requireNonNull(toIndexFunction); - } - - @Override - public Result_ insert(Result_ result) { - var value = toIndexFunction.applyAsInt(result); - context.add(result, value); - return result; - } - - @Override - public void retract(Result_ result) { - context.remove(result); - } - - @Override - public ConsecutiveSetTree result() { - return context; - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/SetUndoableActionable.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/SetUndoableActionable.java deleted file mode 100644 index 773e498c175..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/SetUndoableActionable.java +++ /dev/null @@ -1,27 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -import ai.timefold.solver.core.impl.util.MutableInt; - -public final class SetUndoableActionable implements UndoableActionable> { - final Map itemToCount = new LinkedHashMap<>(); - - @Override - public Runnable insert(Mapped_ result) { - MutableInt count = itemToCount.computeIfAbsent(result, ignored -> new MutableInt()); - count.increment(); - return () -> { - if (count.decrement() == 0) { - itemToCount.remove(result); - } - }; - } - - @Override - public Set result() { - return itemToCount.keySet(); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/SortedSetUndoableActionable.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/SortedSetUndoableActionable.java deleted file mode 100644 index 6ab302524d0..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/SortedSetUndoableActionable.java +++ /dev/null @@ -1,37 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -import java.util.Comparator; -import java.util.NavigableMap; -import java.util.NavigableSet; -import java.util.SortedSet; -import java.util.TreeMap; - -import ai.timefold.solver.core.impl.util.MutableInt; - -public final class SortedSetUndoableActionable implements UndoableActionable> { - private final NavigableMap itemToCount; - - private SortedSetUndoableActionable(NavigableMap itemToCount) { - this.itemToCount = itemToCount; - } - - public static SortedSetUndoableActionable orderBy(Comparator comparator) { - return new SortedSetUndoableActionable<>(new TreeMap<>(comparator)); - } - - @Override - public Runnable insert(Mapped_ result) { - MutableInt count = itemToCount.computeIfAbsent(result, ignored -> new MutableInt()); - count.increment(); - return () -> { - if (count.decrement() == 0) { - itemToCount.remove(result); - } - }; - } - - @Override - public NavigableSet result() { - return itemToCount.navigableKeySet(); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMapPerKeyCounter.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMapPerKeyCounter.java index 06c32b72609..7c860c45606 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMapPerKeyCounter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMapPerKeyCounter.java @@ -5,45 +5,46 @@ import java.util.function.BinaryOperator; import java.util.stream.Stream; -public final class ToMapPerKeyCounter { - - private final Map counts = new LinkedHashMap<>(0); - - public long add(Value_ value) { - return counts.compute(value, (k, currentCount) -> { - if (currentCount == null) { - return 1L; - } else { - return currentCount + 1; - } - }); +final class ToMapPerKeyCounter { + + final Key_ key; + private final Map> counts = new LinkedHashMap<>(0); + + ToMapPerKeyCounter(Key_ key) { + this.key = key; + } + + CountHolder add(Value_ value) { + var holder = counts.get(value); + if (holder == null) { + holder = new CountHolder<>(value); + counts.put(value, holder); + } else { + holder.count++; + } + return holder; } - public long remove(Value_ value) { - Long newCount = counts.compute(value, (k, currentCount) -> { - if (currentCount > 1L) { - return currentCount - 1; - } else { - return null; - } - }); - return newCount == null ? 0L : newCount; + void decrement(CountHolder holder) { + holder.count--; + if (holder.count == 0) { + counts.remove(holder.value); + } } - public Value_ merge(BinaryOperator mergeFunction) { - // Rebuilding the value from the collection is not incremental. - // The impact is negligible, assuming there are not too many values for the same key. - return counts.entrySet() + Value_ merge(BinaryOperator mergeFunction) { + return counts.values() .stream() - .map(e -> Stream.generate(e::getKey) - .limit(e.getValue()) + .map(h -> Stream.generate(() -> h.value) + .limit(h.count) .reduce(mergeFunction) - .orElseThrow(() -> new IllegalStateException("Impossible state: Should have had at least one value."))) + .orElseThrow( + () -> new IllegalStateException("Impossible state: Should have had at least one value."))) .reduce(mergeFunction) .orElseThrow(() -> new IllegalStateException("Impossible state: Should have had at least one value.")); } - public boolean isEmpty() { + boolean isEmpty() { return counts.isEmpty(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMapResultContainer.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMapResultContainer.java index e99b601b9c9..2b273dc4bf7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMapResultContainer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMapResultContainer.java @@ -2,12 +2,22 @@ import java.util.Map; -public sealed interface ToMapResultContainer> +import org.jspecify.annotations.Nullable; + +sealed interface ToMapResultContainer> permits ToMultiMapResultContainer, ToSimpleMapResultContainer { void add(Key_ key, Value_ value); - void remove(Key_ key, Value_ value); + void replaceWith(ToMapPerKeyCounter counter, CountHolder holder, Key_ newKey, Value_ newValue); + + void remove(ToMapPerKeyCounter counter, CountHolder holder); + + @Nullable + ToMapPerKeyCounter lastCounter(); + + @Nullable + CountHolder lastHolder(); Result_ getResult(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMultiMapResultContainer.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMultiMapResultContainer.java index 01de13fbe4d..0eba1048983 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMultiMapResultContainer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToMultiMapResultContainer.java @@ -7,46 +7,78 @@ import java.util.function.IntFunction; import java.util.function.Supplier; -public final class ToMultiMapResultContainer, Result_ extends Map> +import org.jspecify.annotations.Nullable; + +final class ToMultiMapResultContainer, Result_ extends Map> implements ToMapResultContainer { private final Supplier setSupplier; private final Result_ result; - private final Map> valueCounts = new HashMap<>(0); + private final Map> valueCounts = new HashMap<>(0); + private @Nullable ToMapPerKeyCounter lastCounter; + private @Nullable CountHolder lastHolder; - public ToMultiMapResultContainer(Supplier resultSupplier, IntFunction setFunction) { - IntFunction nonNullSetFunction = Objects.requireNonNull(setFunction); + ToMultiMapResultContainer(Supplier resultSupplier, IntFunction setFunction) { + var nonNullSetFunction = Objects.requireNonNull(setFunction); this.setSupplier = () -> nonNullSetFunction.apply(0); this.result = Objects.requireNonNull(resultSupplier).get(); } - public ToMultiMapResultContainer(IntFunction resultFunction, IntFunction setFunction) { - IntFunction nonNullSetFunction = Objects.requireNonNull(setFunction); - this.setSupplier = () -> nonNullSetFunction.apply(0); - this.result = Objects.requireNonNull(resultFunction).apply(0); + @Override + public void add(Key_ key, Value_ value) { + lastCounter = valueCounts.computeIfAbsent(key, ToMapPerKeyCounter::new); + lastHolder = lastCounter.add(value); + if (lastHolder.count == 1) { + result.computeIfAbsent(key, k -> setSupplier.get()).add(value); + } } @Override - public void add(Key_ key, Value_ value) { - ToMapPerKeyCounter counter = valueCounts.computeIfAbsent(key, k -> new ToMapPerKeyCounter<>()); - counter.add(value); - result.computeIfAbsent(key, k -> setSupplier.get()) - .add(value); + public void replaceWith(ToMapPerKeyCounter counter, CountHolder holder, + Key_ newKey, Value_ newValue) { + if (Objects.equals(counter.key, newKey) && Objects.equals(holder.value, newValue)) { + lastCounter = counter; + lastHolder = holder; + return; + } + if (Objects.equals(counter.key, newKey)) { + counter.decrement(holder); + if (holder.count == 0) { + result.get(counter.key).remove(holder.value); + } + lastHolder = counter.add(newValue); + if (lastHolder.count == 1) { + result.computeIfAbsent(counter.key, k -> setSupplier.get()).add(newValue); + } + lastCounter = counter; + } else { + remove(counter, holder); + add(newKey, newValue); + } } @Override - public void remove(Key_ key, Value_ value) { - ToMapPerKeyCounter counter = valueCounts.get(key); - long newCount = counter.remove(value); - if (newCount == 0) { - result.get(key).remove(value); + public void remove(ToMapPerKeyCounter counter, CountHolder holder) { + counter.decrement(holder); + if (holder.count == 0) { + result.get(counter.key).remove(holder.value); } if (counter.isEmpty()) { - valueCounts.remove(key); - result.remove(key); + valueCounts.remove(counter.key); + result.remove(counter.key); } } + @Override + public @Nullable ToMapPerKeyCounter lastCounter() { + return lastCounter; + } + + @Override + public @Nullable CountHolder lastHolder() { + return lastHolder; + } + @Override public Result_ getResult() { return result; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToSimpleMapResultContainer.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToSimpleMapResultContainer.java index fd078d9c3eb..0ef0e3dee2b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToSimpleMapResultContainer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/ToSimpleMapResultContainer.java @@ -4,45 +4,71 @@ import java.util.Map; import java.util.Objects; import java.util.function.BinaryOperator; -import java.util.function.IntFunction; import java.util.function.Supplier; -public final class ToSimpleMapResultContainer> +import org.jspecify.annotations.Nullable; + +final class ToSimpleMapResultContainer> implements ToMapResultContainer { private final BinaryOperator mergeFunction; private final Result_ result; - private final Map> valueCounts = new HashMap<>(0); + private final Map> valueCounts = new HashMap<>(0); + private @Nullable ToMapPerKeyCounter lastCounter; + private @Nullable CountHolder lastHolder; - public ToSimpleMapResultContainer(Supplier resultSupplier, BinaryOperator mergeFunction) { + ToSimpleMapResultContainer(Supplier resultSupplier, BinaryOperator mergeFunction) { this.mergeFunction = Objects.requireNonNull(mergeFunction); this.result = Objects.requireNonNull(resultSupplier).get(); } - public ToSimpleMapResultContainer(IntFunction resultSupplier, BinaryOperator mergeFunction) { - this.mergeFunction = Objects.requireNonNull(mergeFunction); - this.result = Objects.requireNonNull(resultSupplier).apply(0); + @Override + public void add(Key_ key, Value_ value) { + lastCounter = valueCounts.computeIfAbsent(key, ToMapPerKeyCounter::new); + lastHolder = lastCounter.add(value); + result.put(key, lastCounter.merge(mergeFunction)); } @Override - public void add(Key_ key, Value_ value) { - ToMapPerKeyCounter counter = valueCounts.computeIfAbsent(key, k -> new ToMapPerKeyCounter<>()); - counter.add(value); - result.put(key, counter.merge(mergeFunction)); + public void replaceWith(ToMapPerKeyCounter counter, CountHolder holder, + Key_ newKey, Value_ newValue) { + if (Objects.equals(counter.key, newKey) && Objects.equals(holder.value, newValue)) { + lastCounter = counter; + lastHolder = holder; + return; + } + if (Objects.equals(counter.key, newKey)) { + counter.decrement(holder); + lastHolder = counter.add(newValue); + result.put(counter.key, counter.merge(mergeFunction)); + lastCounter = counter; + } else { + remove(counter, holder); + add(newKey, newValue); + } } @Override - public void remove(Key_ key, Value_ value) { - ToMapPerKeyCounter counter = valueCounts.get(key); - counter.remove(value); + public void remove(ToMapPerKeyCounter counter, CountHolder holder) { + counter.decrement(holder); if (counter.isEmpty()) { - result.remove(key); - valueCounts.remove(key); + valueCounts.remove(counter.key); + result.remove(counter.key); } else { - result.put(key, counter.merge(mergeFunction)); + result.put(counter.key, counter.merge(mergeFunction)); } } + @Override + public @Nullable ToMapPerKeyCounter lastCounter() { + return lastCounter; + } + + @Override + public @Nullable CountHolder lastHolder() { + return lastHolder; + } + @Override public Result_ getResult() { return result; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/UndoableActionable.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/UndoableActionable.java deleted file mode 100644 index c0094f59d1a..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/UndoableActionable.java +++ /dev/null @@ -1,9 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector; - -public sealed interface UndoableActionable - permits CustomCollectionUndoableActionable, ListUndoableActionable, MapUndoableActionable, MinMaxUndoableActionable, - SetUndoableActionable, SortedSetUndoableActionable { - Runnable insert(Input_ input); - - Output_ result(); -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AbstractPrimitiveBasedBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AbstractPrimitiveBasedBiCollector.java new file mode 100644 index 00000000000..ec1009da34f --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AbstractPrimitiveBasedBiCollector.java @@ -0,0 +1,41 @@ +package ai.timefold.solver.core.impl.score.stream.collector.bi; + +import java.util.Objects; +import java.util.function.ToLongBiFunction; + +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; + +import org.jspecify.annotations.NonNull; + +abstract class AbstractPrimitiveBasedBiCollector + implements BiConstraintCollector { + protected final ToLongBiFunction mapper; + + AbstractPrimitiveBasedBiCollector(ToLongBiFunction mapper) { + this.mapper = mapper; + } + + protected abstract BiConstraintCollectorValueHandle newAccumulatedValue(State_ state); + + @Override + public @NonNull BiConstraintCollectorAccumulator accumulator() { + return this::newAccumulatedValue; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (AbstractPrimitiveBasedBiCollector) object; + return Objects.equals(mapper, that.mapper); + } + + @Override + public int hashCode() { + return Objects.hash(mapper); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AbstractReferenceBasedBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AbstractReferenceBasedBiCollector.java new file mode 100644 index 00000000000..d15dd297fcf --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AbstractReferenceBasedBiCollector.java @@ -0,0 +1,42 @@ +package ai.timefold.solver.core.impl.score.stream.collector.bi; + +import java.util.Objects; +import java.util.function.BiFunction; + +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; + +import org.jspecify.annotations.NonNull; + +abstract class AbstractReferenceBasedBiCollector + implements BiConstraintCollector { + + protected final BiFunction mapper; + + AbstractReferenceBasedBiCollector(BiFunction mapper) { + this.mapper = mapper; + } + + protected abstract BiConstraintCollectorValueHandle newAccumulatedValue(State_ state); + + @Override + public @NonNull BiConstraintCollectorAccumulator accumulator() { + return this::newAccumulatedValue; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (AbstractReferenceBasedBiCollector) object; + return Objects.equals(mapper, that.mapper); + } + + @Override + public int hashCode() { + return Objects.hash(mapper); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AndThenBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AndThenBiCollector.java index a9d84343891..c0f1ba216d5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AndThenBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AndThenBiCollector.java @@ -4,8 +4,8 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; import org.jspecify.annotations.NonNull; @@ -14,11 +14,13 @@ final class AndThenBiCollector private final BiConstraintCollector delegate; private final Function mappingFunction; + private final BiConstraintCollectorAccumulator innerIncremental; AndThenBiCollector(BiConstraintCollector delegate, Function mappingFunction) { this.delegate = Objects.requireNonNull(delegate); this.mappingFunction = Objects.requireNonNull(mappingFunction); + this.innerIncremental = BiCollectorUtils.toIncremental(delegate.accumulator()); } @Override @@ -27,8 +29,8 @@ final class AndThenBiCollector } @Override - public @NonNull TriFunction accumulator() { - return delegate.accumulator(); + public @NonNull BiConstraintCollectorAccumulator accumulator() { + return innerIncremental; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AverageBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AverageBiCollector.java index c0bacd8ca8c..8d9da10c04e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AverageBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AverageBiCollector.java @@ -1,19 +1,54 @@ package ai.timefold.solver.core.impl.score.stream.collector.bi; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToLongBiFunction; -import ai.timefold.solver.core.impl.score.stream.collector.LongAverageCalculator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongAverageSlot; import org.jspecify.annotations.NonNull; -final class AverageBiCollector extends LongCalculatorBiCollector { +final class AverageBiCollector + extends AbstractPrimitiveBasedBiCollector { AverageBiCollector(ToLongBiFunction mapper) { super(mapper); } @Override - public @NonNull Supplier supplier() { - return LongAverageCalculator::new; + public @NonNull Supplier supplier() { + return AbstractLongAverageSlot.State::new; + } + + @Override + public @NonNull Function finisher() { + return AbstractLongAverageSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue(AbstractLongAverageSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongAverageSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractLongAverageSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.applyAsLong(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.applyAsLong(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AverageReferenceBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AverageReferenceBiCollector.java index 22b4d4b56fc..6aa9aed0692 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AverageReferenceBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/AverageReferenceBiCollector.java @@ -2,25 +2,61 @@ import java.util.Objects; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceAverageSlot; import org.jspecify.annotations.NonNull; final class AverageReferenceBiCollector - extends ObjectCalculatorBiCollector> { - private final Supplier> calculatorSupplier; + extends + AbstractReferenceBasedBiCollector> { + private final Supplier> stateSupplier; AverageReferenceBiCollector(BiFunction mapper, - Supplier> calculatorSupplier) { + Supplier> stateSupplier) { super(mapper); - this.calculatorSupplier = calculatorSupplier; + this.stateSupplier = stateSupplier; } @Override - public @NonNull Supplier> supplier() { - return calculatorSupplier; + public @NonNull Supplier> supplier() { + return stateSupplier; + } + + @Override + public @NonNull Function, Average_> finisher() { + return AbstractReferenceAverageSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractReferenceAverageSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractReferenceAverageSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractReferenceAverageSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override @@ -32,11 +68,11 @@ public boolean equals(Object object) { if (!super.equals(object)) return false; AverageReferenceBiCollector that = (AverageReferenceBiCollector) object; - return Objects.equals(calculatorSupplier, that.calculatorSupplier); + return Objects.equals(stateSupplier, that.stateSupplier); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), calculatorSupplier); + return Objects.hash(super.hashCode(), stateSupplier); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/BiCollectorUtils.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/BiCollectorUtils.java new file mode 100644 index 00000000000..b66e0d9a1cf --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/BiCollectorUtils.java @@ -0,0 +1,57 @@ +package ai.timefold.solver.core.impl.score.stream.collector.bi; + +import ai.timefold.solver.core.api.function.TriFunction; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public final class BiCollectorUtils { + + public static BiConstraintCollectorAccumulator + toIncremental(TriFunction accumulator) { + if (accumulator instanceof BiConstraintCollectorAccumulator inc) { + return inc; + } + return new BiFromAccumulatorAdapter<>(accumulator); + } + + private record BiFromAccumulatorAdapter(TriFunction accumulator) + implements + BiConstraintCollectorAccumulator { + + @Override + public BiConstraintCollectorValueHandle intoGroup(ResultContainer_ container) { + return new BiValueHandle<>(accumulator, container); + } + } + + private static final class BiValueHandle + implements BiConstraintCollectorValueHandle { + private final TriFunction accumulator; + private final ResultContainer_ container; + private @Nullable Runnable undo; + + BiValueHandle(TriFunction accumulator, ResultContainer_ container) { + this.accumulator = accumulator; + this.container = container; + } + + @Override + public void add(@Nullable A a, @Nullable B b) { + undo = accumulator.apply(container, a, b); + } + + @Override + public void remove() { + undo.run(); + undo = null; + } + } + + private BiCollectorUtils() { + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeFourBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeFourBiCollector.java index 55e3f6ef671..f24b85f7fef 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeFourBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeFourBiCollector.java @@ -5,11 +5,13 @@ import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Quadruple; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; final class ComposeFourBiCollector implements @@ -25,10 +27,10 @@ final class ComposeFourBiCollector thirdSupplier; private final Supplier fourthSupplier; - private final TriFunction firstAccumulator; - private final TriFunction secondAccumulator; - private final TriFunction thirdAccumulator; - private final TriFunction fourthAccumulator; + private final BiConstraintCollectorAccumulator firstIncremental; + private final BiConstraintCollectorAccumulator secondIncremental; + private final BiConstraintCollectorAccumulator thirdIncremental; + private final BiConstraintCollectorAccumulator fourthIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -51,10 +53,10 @@ final class ComposeFourBiCollector, A, B, Runnable> + public @NonNull + BiConstraintCollectorAccumulator, A, B> accumulator() { - return (resultHolder, a, b) -> composeUndo(firstAccumulator.apply(resultHolder.a(), a, b), - secondAccumulator.apply(resultHolder.b(), a, b), - thirdAccumulator.apply(resultHolder.c(), a, b), - fourthAccumulator.apply(resultHolder.d(), a, b)); - } - - private static Runnable composeUndo(Runnable first, Runnable second, Runnable third, - Runnable fourth) { - return () -> { - first.run(); - second.run(); - third.run(); - fourth.run(); - }; + return ValueHandle::new; } @Override - public @NonNull Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> + finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.a()), secondFinisher.apply(resultHolder.b()), thirdFinisher.apply(resultHolder.c()), @@ -108,8 +99,7 @@ public boolean equals(Object object) { var that = (ComposeFourBiCollector) object; return Objects.equals(first, that.first) && Objects.equals(second, that.second) && Objects.equals(third, that.third) - && Objects.equals(fourth, - that.fourth) + && Objects.equals(fourth, that.fourth) && Objects.equals(composeFunction, that.composeFunction); } @@ -117,4 +107,42 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, third, fourth, composeFunction); } + + private final class ValueHandle implements BiConstraintCollectorValueHandle { + private final BiConstraintCollectorValueHandle v1; + private final BiConstraintCollectorValueHandle v2; + private final BiConstraintCollectorValueHandle v3; + private final BiConstraintCollectorValueHandle v4; + + ValueHandle(Quadruple container) { + this.v1 = firstIncremental.intoGroup(container.a()); + this.v2 = secondIncremental.intoGroup(container.b()); + this.v3 = thirdIncremental.intoGroup(container.c()); + this.v4 = fourthIncremental.intoGroup(container.d()); + } + + @Override + public void add(A a, B b) { + v1.add(a, b); + v2.add(a, b); + v3.add(a, b); + v4.add(a, b); + } + + @Override + public void replaceWith(A a, B b) { + v1.replaceWith(a, b); + v2.replaceWith(a, b); + v3.replaceWith(a, b); + v4.replaceWith(a, b); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + v3.remove(); + v4.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeThreeBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeThreeBiCollector.java index d954589392f..e3a121d0430 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeThreeBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeThreeBiCollector.java @@ -6,9 +6,12 @@ import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Triple; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; final class ComposeThreeBiCollector implements BiConstraintCollector, Result_> { @@ -21,9 +24,9 @@ final class ComposeThreeBiCollector secondSupplier; private final Supplier thirdSupplier; - private final TriFunction firstAccumulator; - private final TriFunction secondAccumulator; - private final TriFunction thirdAccumulator; + private final BiConstraintCollectorAccumulator firstIncremental; + private final BiConstraintCollectorAccumulator secondIncremental; + private final BiConstraintCollectorAccumulator thirdIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -42,9 +45,9 @@ final class ComposeThreeBiCollector, A, B, Runnable> accumulator() { - return (resultHolder, a, b) -> composeUndo(firstAccumulator.apply(resultHolder.a(), a, b), - secondAccumulator.apply(resultHolder.b(), a, b), - thirdAccumulator.apply(resultHolder.c(), a, b)); - } - - private static Runnable composeUndo(Runnable first, Runnable second, Runnable third) { - return () -> { - first.run(); - second.run(); - third.run(); - }; + public @NonNull BiConstraintCollectorAccumulator, A, B> + accumulator() { + return ValueHandle::new; } @Override - public @NonNull Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.a()), secondFinisher.apply(resultHolder.b()), thirdFinisher.apply(resultHolder.c())); @@ -91,12 +85,44 @@ public boolean equals(Object object) { var that = (ComposeThreeBiCollector) object; return Objects.equals(first, that.first) && Objects.equals(second, that.second) && Objects.equals(third, that.third) - && Objects.equals(composeFunction, - that.composeFunction); + && Objects.equals(composeFunction, that.composeFunction); } @Override public int hashCode() { return Objects.hash(first, second, third, composeFunction); } + + private final class ValueHandle implements BiConstraintCollectorValueHandle { + private final BiConstraintCollectorValueHandle v1; + private final BiConstraintCollectorValueHandle v2; + private final BiConstraintCollectorValueHandle v3; + + ValueHandle(Triple container) { + this.v1 = firstIncremental.intoGroup(container.a()); + this.v2 = secondIncremental.intoGroup(container.b()); + this.v3 = thirdIncremental.intoGroup(container.c()); + } + + @Override + public void add(A a, B b) { + v1.add(a, b); + v2.add(a, b); + v3.add(a, b); + } + + @Override + public void replaceWith(A a, B b) { + v1.replaceWith(a, b); + v2.replaceWith(a, b); + v3.replaceWith(a, b); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + v3.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeTwoBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeTwoBiCollector.java index 82c641e267a..2bd33303b50 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeTwoBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ComposeTwoBiCollector.java @@ -5,11 +5,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Pair; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; final class ComposeTwoBiCollector implements BiConstraintCollector, Result_> { @@ -20,8 +22,8 @@ final class ComposeTwoBiCollector firstSupplier; private final Supplier secondSupplier; - private final TriFunction firstAccumulator; - private final TriFunction secondAccumulator; + private final BiConstraintCollectorAccumulator firstIncremental; + private final BiConstraintCollectorAccumulator secondIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -36,8 +38,8 @@ final class ComposeTwoBiCollector, A, B, Runnable> accumulator() { - return (resultHolder, a, b) -> composeUndo(firstAccumulator.apply(resultHolder.key(), a, b), - secondAccumulator.apply(resultHolder.value(), a, b)); - } - - private static Runnable composeUndo(Runnable first, Runnable second) { - return () -> { - first.run(); - second.run(); - }; + public @NonNull BiConstraintCollectorAccumulator, A, B> accumulator() { + return ValueHandle::new; } @Override - public @NonNull Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.key()), secondFinisher.apply(resultHolder.value())); } @@ -82,4 +76,32 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, composeFunction); } + + private final class ValueHandle implements BiConstraintCollectorValueHandle { + private final BiConstraintCollectorValueHandle v1; + private final BiConstraintCollectorValueHandle v2; + + ValueHandle(Pair container) { + this.v1 = firstIncremental.intoGroup(container.key()); + this.v2 = secondIncremental.intoGroup(container.value()); + } + + @Override + public void add(A a, B b) { + v1.add(a, b); + v2.add(a, b); + } + + @Override + public void replaceWith(A a, B b) { + v1.replaceWith(a, b); + v2.replaceWith(a, b); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConditionalBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConditionalBiCollector.java index caee5f1e7e4..2403a5f73a3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConditionalBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConditionalBiCollector.java @@ -5,9 +5,9 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; -import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; import org.jspecify.annotations.NonNull; @@ -15,13 +15,13 @@ final class ConditionalBiCollector implements BiConstraintCollector { private final BiPredicate predicate; private final BiConstraintCollector delegate; - private final TriFunction innerAccumulator; + private final BiConstraintCollectorAccumulator innerIncremental; ConditionalBiCollector(BiPredicate predicate, BiConstraintCollector delegate) { this.predicate = predicate; this.delegate = delegate; - this.innerAccumulator = delegate.accumulator(); + this.innerIncremental = BiCollectorUtils.toIncremental(delegate.accumulator()); } @Override @@ -30,14 +30,8 @@ final class ConditionalBiCollector } @Override - public @NonNull TriFunction accumulator() { - return (resultContainer, a, b) -> { - if (predicate.test(a, b)) { - return innerAccumulator.apply(resultContainer, a, b); - } else { - return ConstantLambdaUtils.noop(); - } - }; + public @NonNull BiConstraintCollectorAccumulator accumulator() { + return ValueHandle::new; } @Override @@ -59,4 +53,45 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(predicate, delegate); } + + private final class ValueHandle implements BiConstraintCollectorValueHandle { + private final BiConstraintCollectorValueHandle innerValue; + private boolean active = false; + + ValueHandle(ResultContainer_ container) { + this.innerValue = innerIncremental.intoGroup(container); + } + + @Override + public void add(A a, B b) { + if (!predicate.test(a, b)) { + return; + } + active = true; + innerValue.add(a, b); + } + + @Override + public void replaceWith(A a, B b) { + var nowActive = predicate.test(a, b); + if (active && nowActive) { + innerValue.replaceWith(a, b); + } else if (active) { + active = false; + innerValue.remove(); + } else if (nowActive) { + active = true; + innerValue.add(a, b); + } + } + + @Override + public void remove() { + if (!active) { + return; + } + active = false; + innerValue.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConnectedRangesBiConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConnectedRangesBiConstraintCollector.java index d7fad0d070f..44a566b303b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConnectedRangesBiConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConnectedRangesBiConstraintCollector.java @@ -5,15 +5,15 @@ import java.util.function.Function; import java.util.function.Supplier; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain; -import ai.timefold.solver.core.impl.score.stream.collector.ConnectedRangesCalculator; -import ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.Range; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractConnectedRangesSlot; import org.jspecify.annotations.NonNull; final class ConnectedRangesBiConstraintCollector, Difference_ extends Comparable> extends - ObjectCalculatorBiCollector, Range, ConnectedRangesCalculator> { + AbstractReferenceBasedBiCollector, AbstractConnectedRangesSlot.State> { private final Function startMap; private final Function endMap; @@ -29,8 +29,43 @@ public ConnectedRangesBiConstraintCollector(BiFunction> supplier() { - return () -> new ConnectedRangesCalculator<>(startMap, endMap, differenceFunction); + public @NonNull Supplier> supplier() { + return () -> new AbstractConnectedRangesSlot.State<>(startMap, endMap, differenceFunction); + } + + @Override + public @NonNull + Function, ConnectedRangeChain> + finisher() { + return AbstractConnectedRangesSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractConnectedRangesSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractConnectedRangesSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractConnectedRangesSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConsecutiveSequencesBiConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConsecutiveSequencesBiConstraintCollector.java index f0f36609a0f..06786901c48 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConsecutiveSequencesBiConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ConsecutiveSequencesBiConstraintCollector.java @@ -2,17 +2,19 @@ import java.util.Objects; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToIntFunction; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; -import ai.timefold.solver.core.impl.score.stream.collector.SequenceCalculator; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractSequenceSlot; import org.jspecify.annotations.NonNull; final class ConsecutiveSequencesBiConstraintCollector extends - ObjectCalculatorBiCollector, Result_, SequenceCalculator> { + AbstractReferenceBasedBiCollector, AbstractSequenceSlot.State> { private final ToIntFunction indexMap; @@ -22,8 +24,40 @@ public ConsecutiveSequencesBiConstraintCollector(BiFunction resul } @Override - public @NonNull Supplier> supplier() { - return () -> new SequenceCalculator<>(indexMap); + public @NonNull Supplier> supplier() { + return () -> new AbstractSequenceSlot.State<>(indexMap); + } + + @Override + public @NonNull Function, SequenceChain> finisher() { + return AbstractSequenceSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue(AbstractSequenceSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractSequenceSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractSequenceSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountBiCollector.java index b294a4ec45d..fe0e4e36bfe 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountBiCollector.java @@ -3,13 +3,15 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LongCounter; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractCountSlot; +import ai.timefold.solver.core.impl.util.MutableLong; import org.jspecify.annotations.NonNull; -final class CountBiCollector implements BiConstraintCollector { +final class CountBiCollector implements BiConstraintCollector { private static final CountBiCollector INSTANCE = new CountBiCollector<>(); private CountBiCollector() { @@ -21,20 +23,40 @@ static CountBiCollector getInstance() { } @Override - public @NonNull Supplier supplier() { - return LongCounter::new; + public @NonNull Supplier supplier() { + return MutableLong::new; } @Override - public @NonNull TriFunction accumulator() { - return (counter, a, b) -> { - counter.increment(); - return counter::decrement; - }; + public @NonNull BiConstraintCollectorAccumulator accumulator() { + return Slot::new; } @Override - public @NonNull Function finisher() { - return LongCounter::result; + public @NonNull Function finisher() { + return MutableLong::longValue; + } + + private static final class Slot extends AbstractCountSlot + implements BiConstraintCollectorValueHandle { + + Slot(MutableLong state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctBiCollector.java index f0f0e8fea48..bbc3296a177 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctBiCollector.java @@ -1,20 +1,56 @@ package ai.timefold.solver.core.impl.score.stream.collector.bi; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.LongDistinctCountCalculator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongDistinctSlot; import org.jspecify.annotations.NonNull; final class CountDistinctBiCollector - extends ObjectCalculatorBiCollector> { + extends + AbstractReferenceBasedBiCollector> { CountDistinctBiCollector(BiFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return LongDistinctCountCalculator::new; + public @NonNull Supplier> supplier() { + return AbstractLongDistinctSlot.State::new; + } + + @Override + public @NonNull Function, Long> finisher() { + return AbstractLongDistinctSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractLongDistinctSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongDistinctSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractLongDistinctSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/InnerBiConstraintCollectors.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/InnerBiConstraintCollectors.java index 180922891c9..45126047cb8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/InnerBiConstraintCollectors.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/InnerBiConstraintCollectors.java @@ -24,32 +24,32 @@ import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceAverageSlot; -public class InnerBiConstraintCollectors { +public final class InnerBiConstraintCollectors { public static BiConstraintCollector average(ToLongBiFunction mapper) { return new AverageBiCollector<>(mapper); } static BiConstraintCollector average( BiFunction mapper, - Supplier> calculatorSupplier) { - return new AverageReferenceBiCollector<>(mapper, calculatorSupplier); + Supplier> stateSupplier) { + return new AverageReferenceBiCollector<>(mapper, stateSupplier); } public static BiConstraintCollector averageBigDecimal( BiFunction mapper) { - return average(mapper, ReferenceAverageCalculator.bigDecimal()); + return average(mapper, AbstractReferenceAverageSlot.bigDecimalState()); } public static BiConstraintCollector averageDuration( BiFunction mapper) { - return average(mapper, ReferenceAverageCalculator.duration()); + return average(mapper, AbstractReferenceAverageSlot.durationState()); } public static BiConstraintCollector averageBigInteger( BiFunction mapper) { - return average(mapper, ReferenceAverageCalculator.bigInteger()); + return average(mapper, AbstractReferenceAverageSlot.bigIntegerState()); } public static @@ -104,12 +104,6 @@ public static BiConstraintCollector(mapper); } - public static BiConstraintCollector max( - BiFunction mapper, - Comparator comparator) { - return new MaxComparatorBiCollector<>(mapper, comparator); - } - public static > BiConstraintCollector max( BiFunction mapper, @@ -122,12 +116,6 @@ public static BiConstraintCollector(mapper); } - public static BiConstraintCollector min( - BiFunction mapper, - Comparator comparator) { - return new MinComparatorBiCollector<>(mapper, comparator); - } - public static > BiConstraintCollector min( BiFunction mapper, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/LoadBalanceBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/LoadBalanceBiCollector.java index 37c106ad753..be5215f5fd7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/LoadBalanceBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/LoadBalanceBiCollector.java @@ -6,15 +6,17 @@ import java.util.function.Supplier; import java.util.function.ToLongBiFunction; -import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; -import ai.timefold.solver.core.impl.score.stream.collector.LoadBalanceImpl; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLoadBalanceSlot; +import ai.timefold.solver.core.impl.score.stream.collector.DefaultLoadBalance; import org.jspecify.annotations.NonNull; final class LoadBalanceBiCollector - implements BiConstraintCollector, LoadBalance> { + implements BiConstraintCollector, LoadBalance> { private final BiFunction balancedItemFunction; private final ToLongBiFunction loadFunction; @@ -28,22 +30,17 @@ public LoadBalanceBiCollector(BiFunction balancedItemFunction, } @Override - public @NonNull Supplier> supplier() { - return LoadBalanceImpl::new; + public @NonNull Supplier> supplier() { + return DefaultLoadBalance::new; } @Override - public @NonNull TriFunction, A, B, Runnable> accumulator() { - return (balanceStatistics, a, b) -> { - var balanced = balancedItemFunction.apply(a, b); - var initialLoad = initialLoadFunction.applyAsLong(a, b); - var load = loadFunction.applyAsLong(a, b); - return balanceStatistics.registerBalanced(balanced, load, initialLoad); - }; + public @NonNull BiConstraintCollectorAccumulator, A, B> accumulator() { + return Slot::new; } @Override - public @NonNull Function, LoadBalance> finisher() { + public @NonNull Function, LoadBalance> finisher() { return balanceStatistics -> balanceStatistics; } @@ -59,4 +56,29 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(balancedItemFunction, loadFunction, initialLoadFunction); } + + private final class Slot extends AbstractLoadBalanceSlot + implements BiConstraintCollectorValueHandle { + + Slot(DefaultLoadBalance container) { + super(container); + } + + @Override + public void add(A a, B b) { + addMapped(balancedItemFunction.apply(a, b), loadFunction.applyAsLong(a, b), + initialLoadFunction.applyAsLong(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(balancedItemFunction.apply(a, b), loadFunction.applyAsLong(a, b), + initialLoadFunction.applyAsLong(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/LongCalculatorBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/LongCalculatorBiCollector.java deleted file mode 100644 index 86454a2c309..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/LongCalculatorBiCollector.java +++ /dev/null @@ -1,49 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.bi; - -import java.util.Objects; -import java.util.function.Function; -import java.util.function.ToLongBiFunction; - -import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LongCalculator; - -import org.jspecify.annotations.NonNull; - -abstract sealed class LongCalculatorBiCollector> - implements BiConstraintCollector permits AverageBiCollector, SumBiCollector { - private final ToLongBiFunction mapper; - - public LongCalculatorBiCollector(ToLongBiFunction mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull TriFunction accumulator() { - return (calculator, a, b) -> { - final long mapped = mapper.applyAsLong(a, b); - calculator.insert(mapped); - return () -> calculator.retract(mapped); - }; - } - - @Override - public @NonNull Function finisher() { - return LongCalculator::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (LongCalculatorBiCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxComparableBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxComparableBiCollector.java index 5021d8c5ed7..367e14523e3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxComparableBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxComparableBiCollector.java @@ -1,20 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.bi; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MaxComparableBiCollector> - extends UndoableActionableBiCollector> { + extends AbstractReferenceBasedBiCollector> { MaxComparableBiCollector(BiFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return MinMaxUndoableActionable::maxCalculator; + public @NonNull Supplier> supplier() { + return AbstractMinMaxSlot::maxState; + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxComparatorBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxComparatorBiCollector.java deleted file mode 100644 index b91df7085c1..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxComparatorBiCollector.java +++ /dev/null @@ -1,43 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.bi; - -import java.util.Comparator; -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Supplier; - -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; - -import org.jspecify.annotations.NonNull; - -final class MaxComparatorBiCollector - extends UndoableActionableBiCollector> { - private final Comparator comparator; - - MaxComparatorBiCollector(BiFunction mapper, - Comparator comparator) { - super(mapper); - this.comparator = comparator; - } - - @Override - public @NonNull Supplier> supplier() { - return () -> MinMaxUndoableActionable.maxCalculator(comparator); - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - if (!super.equals(object)) - return false; - MaxComparatorBiCollector that = (MaxComparatorBiCollector) object; - return Objects.equals(comparator, that.comparator); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), comparator); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxPropertyBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxPropertyBiCollector.java index 820df03ab66..24bdc6ad985 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxPropertyBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MaxPropertyBiCollector.java @@ -5,12 +5,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MaxPropertyBiCollector> - extends UndoableActionableBiCollector> { + extends AbstractReferenceBasedBiCollector> { private final Function propertyMapper; MaxPropertyBiCollector(BiFunction mapper, @@ -20,8 +21,41 @@ final class MaxPropertyBiCollector> supplier() { - return () -> MinMaxUndoableActionable.maxCalculator(propertyMapper); + public @NonNull Supplier> supplier() { + return () -> AbstractMinMaxSlot.maxState(propertyMapper); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinComparableBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinComparableBiCollector.java index 8e60d8702f8..2b59f2fe8c9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinComparableBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinComparableBiCollector.java @@ -1,20 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.bi; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MinComparableBiCollector> - extends UndoableActionableBiCollector> { + extends AbstractReferenceBasedBiCollector> { MinComparableBiCollector(BiFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return MinMaxUndoableActionable::minCalculator; + public @NonNull Supplier> supplier() { + return AbstractMinMaxSlot::minState; + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinComparatorBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinComparatorBiCollector.java deleted file mode 100644 index 4848ce7e23f..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinComparatorBiCollector.java +++ /dev/null @@ -1,43 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.bi; - -import java.util.Comparator; -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Supplier; - -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; - -import org.jspecify.annotations.NonNull; - -final class MinComparatorBiCollector - extends UndoableActionableBiCollector> { - private final Comparator comparator; - - MinComparatorBiCollector(BiFunction mapper, - Comparator comparator) { - super(mapper); - this.comparator = comparator; - } - - @Override - public @NonNull Supplier> supplier() { - return () -> MinMaxUndoableActionable.minCalculator(comparator); - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - if (!super.equals(object)) - return false; - MinComparatorBiCollector that = (MinComparatorBiCollector) object; - return Objects.equals(comparator, that.comparator); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), comparator); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinPropertyBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinPropertyBiCollector.java index 0be6403f971..dbf3de58930 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinPropertyBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/MinPropertyBiCollector.java @@ -5,12 +5,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MinPropertyBiCollector> - extends UndoableActionableBiCollector> { + extends AbstractReferenceBasedBiCollector> { private final Function propertyMapper; MinPropertyBiCollector(BiFunction mapper, @@ -20,8 +21,41 @@ final class MinPropertyBiCollector> supplier() { - return () -> MinMaxUndoableActionable.minCalculator(propertyMapper); + public @NonNull Supplier> supplier() { + return () -> AbstractMinMaxSlot.minState(propertyMapper); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ObjectCalculatorBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ObjectCalculatorBiCollector.java deleted file mode 100644 index 9c72e3d43c1..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ObjectCalculatorBiCollector.java +++ /dev/null @@ -1,51 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.bi; - -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; - -import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.ObjectCalculator; - -import org.jspecify.annotations.NonNull; - -abstract sealed class ObjectCalculatorBiCollector> - implements BiConstraintCollector - permits AverageReferenceBiCollector, ConnectedRangesBiConstraintCollector, ConsecutiveSequencesBiConstraintCollector, - CountDistinctBiCollector, SumReferenceBiCollector { - protected final BiFunction mapper; - - public ObjectCalculatorBiCollector(BiFunction mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull TriFunction accumulator() { - return (calculator, a, b) -> { - final var mapped = mapper.apply(a, b); - final var saved = calculator.insert(mapped); - return () -> calculator.retract(saved); - }; - } - - @Override - public @NonNull Function finisher() { - return ObjectCalculator::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (ObjectCalculatorBiCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/SumBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/SumBiCollector.java index 084121cb614..cd9adfde95d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/SumBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/SumBiCollector.java @@ -1,19 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.bi; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToLongBiFunction; -import ai.timefold.solver.core.impl.score.stream.collector.LongSumCalculator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongSumSlot; +import ai.timefold.solver.core.impl.util.MutableLong; import org.jspecify.annotations.NonNull; -final class SumBiCollector extends LongCalculatorBiCollector { +final class SumBiCollector + extends AbstractPrimitiveBasedBiCollector { SumBiCollector(ToLongBiFunction mapper) { super(mapper); } @Override - public @NonNull Supplier supplier() { - return LongSumCalculator::new; + public @NonNull Supplier supplier() { + return MutableLong::new; + } + + @Override + public @NonNull Function finisher() { + return MutableLong::longValue; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue(MutableLong state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongSumSlot + implements BiConstraintCollectorValueHandle { + Slot(MutableLong state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.applyAsLong(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.applyAsLong(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/SumReferenceBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/SumReferenceBiCollector.java index 3b4aaae998e..d98b0c349dc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/SumReferenceBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/SumReferenceBiCollector.java @@ -3,14 +3,17 @@ import java.util.Objects; import java.util.function.BiFunction; import java.util.function.BinaryOperator; +import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceSumCalculator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceSumSlot; import org.jspecify.annotations.NonNull; final class SumReferenceBiCollector - extends ObjectCalculatorBiCollector> { + extends + AbstractReferenceBasedBiCollector> { private final Result_ zero; private final BinaryOperator adder; private final BinaryOperator subtractor; @@ -25,8 +28,40 @@ final class SumReferenceBiCollector } @Override - public @NonNull Supplier> supplier() { - return () -> new ReferenceSumCalculator<>(zero, adder, subtractor); + public @NonNull Supplier> supplier() { + return () -> new AbstractReferenceSumSlot.State<>(zero, adder, subtractor); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractReferenceSumSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue(AbstractReferenceSumSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractReferenceSumSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractReferenceSumSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToCollectionBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToCollectionBiCollector.java index 2ace0ba2c20..b742f6c58a4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToCollectionBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToCollectionBiCollector.java @@ -3,15 +3,17 @@ import java.util.Collection; import java.util.Objects; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.CustomCollectionUndoableActionable; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToCollectionSlot; import org.jspecify.annotations.NonNull; final class ToCollectionBiCollector> - extends UndoableActionableBiCollector> { + extends AbstractReferenceBasedBiCollector> { private final IntFunction collectionFunction; ToCollectionBiCollector(BiFunction mapper, @@ -21,8 +23,41 @@ final class ToCollectionBiCollector> supplier() { - return () -> new CustomCollectionUndoableActionable<>(collectionFunction); + public @NonNull Supplier> supplier() { + return () -> new AbstractToCollectionSlot.State<>(collectionFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return state -> state.result(); + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractToCollectionSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToCollectionSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractToCollectionSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToListBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToListBiCollector.java index d0df6849ea4..af96b1dd2c9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToListBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToListBiCollector.java @@ -2,20 +2,54 @@ import java.util.List; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.ListUndoableActionable; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToListSlot; import org.jspecify.annotations.NonNull; final class ToListBiCollector - extends UndoableActionableBiCollector, ListUndoableActionable> { + extends AbstractReferenceBasedBiCollector, AbstractToListSlot.State> { ToListBiCollector(BiFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return ListUndoableActionable::new; + public @NonNull Supplier> supplier() { + return AbstractToListSlot.State::new; + } + + @Override + public @NonNull Function, List> finisher() { + return AbstractToListSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue(AbstractToListSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToListSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractToListSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToMultiMapBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToMultiMapBiCollector.java index c02a1175208..4fa31e1c93b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToMultiMapBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToMultiMapBiCollector.java @@ -4,17 +4,17 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MapUndoableActionable; -import ai.timefold.solver.core.impl.util.Pair; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToMapSlot; import org.jspecify.annotations.NonNull; final class ToMultiMapBiCollector, Result_ extends Map> - extends - UndoableActionableBiCollector, Result_, MapUndoableActionable> { + extends AbstractReferenceBasedBiCollector> { private final BiFunction keyFunction; private final BiFunction valueFunction; private final Supplier mapSupplier; @@ -24,7 +24,7 @@ final class ToMultiMapBiCollector, BiFunction valueFunction, Supplier mapSupplier, IntFunction setFunction) { - super((a, b) -> new Pair<>(keyFunction.apply(a, b), valueFunction.apply(a, b))); + super(keyFunction); this.keyFunction = keyFunction; this.valueFunction = valueFunction; this.mapSupplier = mapSupplier; @@ -32,8 +32,41 @@ final class ToMultiMapBiCollector, } @Override - public @NonNull Supplier> supplier() { - return () -> MapUndoableActionable.multiMap(mapSupplier, setFunction); + public @NonNull Supplier> supplier() { + return () -> AbstractToMapSlot.multiMapState(mapSupplier, setFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return state -> state.result(); + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractToMapSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToMapSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractToMapSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(keyFunction.apply(a, b), valueFunction.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(keyFunction.apply(a, b), valueFunction.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } // Don't call super equals/hashCode; the groupingFunction is calculated from keyFunction diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSetBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSetBiCollector.java index f9887a2cd1e..e2b56be65da 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSetBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSetBiCollector.java @@ -2,20 +2,54 @@ import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.SetUndoableActionable; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToSetSlot; import org.jspecify.annotations.NonNull; final class ToSetBiCollector - extends UndoableActionableBiCollector, SetUndoableActionable> { + extends AbstractReferenceBasedBiCollector, AbstractToSetSlot.State> { ToSetBiCollector(BiFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return SetUndoableActionable::new; + public @NonNull Supplier> supplier() { + return AbstractToSetSlot.State::new; + } + + @Override + public @NonNull Function, Set> finisher() { + return AbstractToSetSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue(AbstractToSetSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToSetSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractToSetSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSimpleMapBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSimpleMapBiCollector.java index 07f5b2169ed..c2c1bd3b310 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSimpleMapBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSimpleMapBiCollector.java @@ -4,16 +4,16 @@ import java.util.Objects; import java.util.function.BiFunction; import java.util.function.BinaryOperator; +import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MapUndoableActionable; -import ai.timefold.solver.core.impl.util.Pair; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToMapSlot; import org.jspecify.annotations.NonNull; final class ToSimpleMapBiCollector> - extends - UndoableActionableBiCollector, Result_, MapUndoableActionable> { + extends AbstractReferenceBasedBiCollector> { private final BiFunction keyFunction; private final BiFunction valueFunction; private final Supplier mapSupplier; @@ -23,7 +23,7 @@ final class ToSimpleMapBiCollector valueFunction, Supplier mapSupplier, BinaryOperator mergeFunction) { - super((a, b) -> new Pair<>(keyFunction.apply(a, b), valueFunction.apply(a, b))); + super(keyFunction); this.keyFunction = keyFunction; this.valueFunction = valueFunction; this.mapSupplier = mapSupplier; @@ -31,8 +31,41 @@ final class ToSimpleMapBiCollector> supplier() { - return () -> MapUndoableActionable.mergeMap(mapSupplier, mergeFunction); + public @NonNull Supplier> supplier() { + return () -> AbstractToMapSlot.mergeMapState(mapSupplier, mergeFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return state -> state.result(); + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractToMapSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToMapSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractToMapSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(keyFunction.apply(a, b), valueFunction.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(keyFunction.apply(a, b), valueFunction.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } // Don't call super equals/hashCode; the groupingFunction is calculated from keyFunction diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSortedSetComparatorBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSortedSetComparatorBiCollector.java index 7e07297ce4e..277cd99ca57 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSortedSetComparatorBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/ToSortedSetComparatorBiCollector.java @@ -4,14 +4,16 @@ import java.util.Objects; import java.util.SortedSet; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.SortedSetUndoableActionable; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractSortedSetSlot; import org.jspecify.annotations.NonNull; final class ToSortedSetComparatorBiCollector - extends UndoableActionableBiCollector, SortedSetUndoableActionable> { + extends AbstractReferenceBasedBiCollector, AbstractSortedSetSlot.State> { private final Comparator comparator; ToSortedSetComparatorBiCollector(BiFunction mapper, @@ -21,8 +23,41 @@ final class ToSortedSetComparatorBiCollector } @Override - public @NonNull Supplier> supplier() { - return () -> SortedSetUndoableActionable.orderBy(comparator); + public @NonNull Supplier> supplier() { + return () -> new AbstractSortedSetSlot.State<>(comparator); + } + + @Override + public @NonNull Function, SortedSet> finisher() { + return AbstractSortedSetSlot.State::result; + } + + @Override + protected BiConstraintCollectorValueHandle newAccumulatedValue( + AbstractSortedSetSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractSortedSetSlot + implements BiConstraintCollectorValueHandle { + Slot(AbstractSortedSetSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b) { + addMapped(mapper.apply(a, b)); + } + + @Override + public void replaceWith(A a, B b) { + replaceWithMapped(mapper.apply(a, b)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/UndoableActionableBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/UndoableActionableBiCollector.java deleted file mode 100644 index f6ebfedb958..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/UndoableActionableBiCollector.java +++ /dev/null @@ -1,51 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.bi; - -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; - -import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.UndoableActionable; - -import org.jspecify.annotations.NonNull; - -abstract sealed class UndoableActionableBiCollector> - implements BiConstraintCollector - permits MaxComparableBiCollector, MaxComparatorBiCollector, MaxPropertyBiCollector, MinComparableBiCollector, - MinComparatorBiCollector, MinPropertyBiCollector, ToCollectionBiCollector, ToListBiCollector, ToMultiMapBiCollector, - ToSetBiCollector, ToSimpleMapBiCollector, ToSortedSetComparatorBiCollector { - private final BiFunction mapper; - - public UndoableActionableBiCollector(BiFunction mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull TriFunction accumulator() { - return (calculator, a, b) -> { - final Input_ mapped = mapper.apply(a, b); - return calculator.insert(mapped); - }; - } - - @Override - public @NonNull Function finisher() { - return UndoableActionable::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (UndoableActionableBiCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AbstractPrimitiveBasedQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AbstractPrimitiveBasedQuadCollector.java new file mode 100644 index 00000000000..4c47c8b456e --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AbstractPrimitiveBasedQuadCollector.java @@ -0,0 +1,41 @@ +package ai.timefold.solver.core.impl.score.stream.collector.quad; + +import java.util.Objects; + +import ai.timefold.solver.core.api.function.ToLongQuadFunction; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; + +import org.jspecify.annotations.NonNull; + +abstract class AbstractPrimitiveBasedQuadCollector + implements QuadConstraintCollector { + protected final ToLongQuadFunction mapper; + + public AbstractPrimitiveBasedQuadCollector(ToLongQuadFunction mapper) { + this.mapper = mapper; + } + + protected abstract QuadConstraintCollectorValueHandle newAccumulatedValue(State_ state); + + @Override + public @NonNull QuadConstraintCollectorAccumulator accumulator() { + return this::newAccumulatedValue; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (AbstractPrimitiveBasedQuadCollector) object; + return Objects.equals(mapper, that.mapper); + } + + @Override + public int hashCode() { + return Objects.hash(mapper); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AbstractReferenceBasedQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AbstractReferenceBasedQuadCollector.java new file mode 100644 index 00000000000..ff0b941736f --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AbstractReferenceBasedQuadCollector.java @@ -0,0 +1,43 @@ +package ai.timefold.solver.core.impl.score.stream.collector.quad; + +import java.util.Objects; + +import ai.timefold.solver.core.api.function.QuadFunction; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; + +import org.jspecify.annotations.NonNull; + +abstract class AbstractReferenceBasedQuadCollector + implements QuadConstraintCollector { + + protected final QuadFunction mapper; + + public AbstractReferenceBasedQuadCollector( + QuadFunction mapper) { + this.mapper = mapper; + } + + protected abstract QuadConstraintCollectorValueHandle newAccumulatedValue(State_ state); + + @Override + public @NonNull QuadConstraintCollectorAccumulator accumulator() { + return this::newAccumulatedValue; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (AbstractReferenceBasedQuadCollector) object; + return Objects.equals(mapper, that.mapper); + } + + @Override + public int hashCode() { + return Objects.hash(mapper); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AndThenQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AndThenQuadCollector.java index 0f2db574897..e7d854efe5a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AndThenQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AndThenQuadCollector.java @@ -4,8 +4,8 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.PentaFunction; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; import org.jspecify.annotations.NonNull; @@ -14,11 +14,13 @@ final class AndThenQuadCollector delegate; private final Function mappingFunction; + private final QuadConstraintCollectorAccumulator innerIncremental; AndThenQuadCollector(QuadConstraintCollector delegate, Function mappingFunction) { this.delegate = Objects.requireNonNull(delegate); this.mappingFunction = Objects.requireNonNull(mappingFunction); + this.innerIncremental = QuadCollectorUtils.toIncremental(delegate.accumulator()); } @Override @@ -27,8 +29,8 @@ final class AndThenQuadCollector accumulator() { - return delegate.accumulator(); + public @NonNull QuadConstraintCollectorAccumulator accumulator() { + return innerIncremental; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AverageQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AverageQuadCollector.java index 61b8e2534d6..529359bbead 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AverageQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AverageQuadCollector.java @@ -1,20 +1,54 @@ package ai.timefold.solver.core.impl.score.stream.collector.quad; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.ToLongQuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.LongAverageCalculator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongAverageSlot; import org.jspecify.annotations.NonNull; final class AverageQuadCollector - extends LongCalculatorQuadCollector { + extends AbstractPrimitiveBasedQuadCollector { AverageQuadCollector(ToLongQuadFunction mapper) { super(mapper); } @Override - public @NonNull Supplier supplier() { - return LongAverageCalculator::new; + public @NonNull Supplier supplier() { + return AbstractLongAverageSlot.State::new; + } + + @Override + public @NonNull Function finisher() { + return AbstractLongAverageSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue(AbstractLongAverageSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongAverageSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractLongAverageSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.applyAsLong(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.applyAsLong(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AverageReferenceQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AverageReferenceQuadCollector.java index 0c49331519c..8beed31d207 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AverageReferenceQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/AverageReferenceQuadCollector.java @@ -1,27 +1,62 @@ package ai.timefold.solver.core.impl.score.stream.collector.quad; import java.util.Objects; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceAverageSlot; import org.jspecify.annotations.NonNull; final class AverageReferenceQuadCollector extends - ObjectCalculatorQuadCollector> { - private final Supplier> calculatorSupplier; + AbstractReferenceBasedQuadCollector> { + private final Supplier> stateSupplier; AverageReferenceQuadCollector(QuadFunction mapper, - Supplier> calculatorSupplier) { + Supplier> stateSupplier) { super(mapper); - this.calculatorSupplier = calculatorSupplier; + this.stateSupplier = stateSupplier; } @Override - public @NonNull Supplier> supplier() { - return calculatorSupplier; + public @NonNull Supplier> supplier() { + return stateSupplier; + } + + @Override + public @NonNull Function, Average_> finisher() { + return AbstractReferenceAverageSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractReferenceAverageSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractReferenceAverageSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractReferenceAverageSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override @@ -33,11 +68,11 @@ public boolean equals(Object object) { if (!super.equals(object)) return false; AverageReferenceQuadCollector that = (AverageReferenceQuadCollector) object; - return Objects.equals(calculatorSupplier, that.calculatorSupplier); + return Objects.equals(stateSupplier, that.stateSupplier); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), calculatorSupplier); + return Objects.hash(super.hashCode(), stateSupplier); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeFourQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeFourQuadCollector.java index c016de03008..4fe3d651ca1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeFourQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeFourQuadCollector.java @@ -4,12 +4,14 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.PentaFunction; import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Quadruple; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; final class ComposeFourQuadCollector implements @@ -25,10 +27,10 @@ final class ComposeFourQuadCollector thirdSupplier; private final Supplier fourthSupplier; - private final PentaFunction firstAccumulator; - private final PentaFunction secondAccumulator; - private final PentaFunction thirdAccumulator; - private final PentaFunction fourthAccumulator; + private final QuadConstraintCollectorAccumulator firstIncremental; + private final QuadConstraintCollectorAccumulator secondIncremental; + private final QuadConstraintCollectorAccumulator thirdIncremental; + private final QuadConstraintCollectorAccumulator fourthIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -51,10 +53,10 @@ final class ComposeFourQuadCollector, A, B, C, D, Runnable> + QuadConstraintCollectorAccumulator, A, B, C, D> accumulator() { - return (resultHolder, a, b, c, d) -> composeUndo(firstAccumulator.apply(resultHolder.a(), a, b, c, d), - secondAccumulator.apply(resultHolder.b(), a, b, c, d), - thirdAccumulator.apply(resultHolder.c(), a, b, c, d), - fourthAccumulator.apply(resultHolder.d(), a, b, c, d)); - } - - private static Runnable composeUndo(Runnable first, Runnable second, Runnable third, - Runnable fourth) { - return () -> { - first.run(); - second.run(); - third.run(); - fourth.run(); - }; + return ValueHandle::new; } @Override - public @NonNull Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> + finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.a()), secondFinisher.apply(resultHolder.b()), thirdFinisher.apply(resultHolder.c()), @@ -118,4 +108,42 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, third, fourth, composeFunction); } + + private final class ValueHandle implements QuadConstraintCollectorValueHandle { + private final QuadConstraintCollectorValueHandle v1; + private final QuadConstraintCollectorValueHandle v2; + private final QuadConstraintCollectorValueHandle v3; + private final QuadConstraintCollectorValueHandle v4; + + ValueHandle(Quadruple container) { + this.v1 = firstIncremental.intoGroup(container.a()); + this.v2 = secondIncremental.intoGroup(container.b()); + this.v3 = thirdIncremental.intoGroup(container.c()); + this.v4 = fourthIncremental.intoGroup(container.d()); + } + + @Override + public void add(A a, B b, C c, D d) { + v1.add(a, b, c, d); + v2.add(a, b, c, d); + v3.add(a, b, c, d); + v4.add(a, b, c, d); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + v1.replaceWith(a, b, c, d); + v2.replaceWith(a, b, c, d); + v3.replaceWith(a, b, c, d); + v4.replaceWith(a, b, c, d); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + v3.remove(); + v4.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeThreeQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeThreeQuadCollector.java index 437a267e809..53e61a375e6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeThreeQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeThreeQuadCollector.java @@ -4,12 +4,14 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.PentaFunction; import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Triple; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; final class ComposeThreeQuadCollector implements QuadConstraintCollector, Result_> { @@ -22,9 +24,9 @@ final class ComposeThreeQuadCollector secondSupplier; private final Supplier thirdSupplier; - private final PentaFunction firstAccumulator; - private final PentaFunction secondAccumulator; - private final PentaFunction thirdAccumulator; + private final QuadConstraintCollectorAccumulator firstIncremental; + private final QuadConstraintCollectorAccumulator secondIncremental; + private final QuadConstraintCollectorAccumulator thirdIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -43,9 +45,9 @@ final class ComposeThreeQuadCollector, A, B, C, D, Runnable> accumulator() { - return (resultHolder, a, b, c, d) -> composeUndo(firstAccumulator.apply(resultHolder.a(), a, b, c, d), - secondAccumulator.apply(resultHolder.b(), a, b, c, d), - thirdAccumulator.apply(resultHolder.c(), a, b, c, d)); - } - - private static Runnable composeUndo(Runnable first, Runnable second, Runnable third) { - return () -> { - first.run(); - second.run(); - third.run(); - }; + public @NonNull QuadConstraintCollectorAccumulator, A, B, C, D> + accumulator() { + return ValueHandle::new; } @Override - public @NonNull Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.a()), secondFinisher.apply(resultHolder.b()), thirdFinisher.apply(resultHolder.c())); @@ -100,4 +93,37 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, third, composeFunction); } + + private final class ValueHandle implements QuadConstraintCollectorValueHandle { + private final QuadConstraintCollectorValueHandle v1; + private final QuadConstraintCollectorValueHandle v2; + private final QuadConstraintCollectorValueHandle v3; + + ValueHandle(Triple container) { + this.v1 = firstIncremental.intoGroup(container.a()); + this.v2 = secondIncremental.intoGroup(container.b()); + this.v3 = thirdIncremental.intoGroup(container.c()); + } + + @Override + public void add(A a, B b, C c, D d) { + v1.add(a, b, c, d); + v2.add(a, b, c, d); + v3.add(a, b, c, d); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + v1.replaceWith(a, b, c, d); + v2.replaceWith(a, b, c, d); + v3.replaceWith(a, b, c, d); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + v3.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeTwoQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeTwoQuadCollector.java index db4e1ece603..42113ead56d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeTwoQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ComposeTwoQuadCollector.java @@ -5,11 +5,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.PentaFunction; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Pair; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; final class ComposeTwoQuadCollector implements QuadConstraintCollector, Result_> { @@ -20,8 +22,8 @@ final class ComposeTwoQuadCollector firstSupplier; private final Supplier secondSupplier; - private final PentaFunction firstAccumulator; - private final PentaFunction secondAccumulator; + private final QuadConstraintCollectorAccumulator firstIncremental; + private final QuadConstraintCollectorAccumulator secondIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -36,8 +38,8 @@ final class ComposeTwoQuadCollector, A, B, C, D, Runnable> accumulator() { - return (resultHolder, a, b, c, d) -> composeUndo(firstAccumulator.apply(resultHolder.key(), a, b, c, d), - secondAccumulator.apply(resultHolder.value(), a, b, c, d)); - } - - private static Runnable composeUndo(Runnable first, Runnable second) { - return () -> { - first.run(); - second.run(); - }; + public @NonNull QuadConstraintCollectorAccumulator, A, B, C, D> accumulator() { + return ValueHandle::new; } @Override - public @NonNull Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.key()), secondFinisher.apply(resultHolder.value())); } @@ -82,4 +76,32 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, composeFunction); } + + private final class ValueHandle implements QuadConstraintCollectorValueHandle { + private final QuadConstraintCollectorValueHandle v1; + private final QuadConstraintCollectorValueHandle v2; + + ValueHandle(Pair container) { + this.v1 = firstIncremental.intoGroup(container.key()); + this.v2 = secondIncremental.intoGroup(container.value()); + } + + @Override + public void add(A a, B b, C c, D d) { + v1.add(a, b, c, d); + v2.add(a, b, c, d); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + v1.replaceWith(a, b, c, d); + v2.replaceWith(a, b, c, d); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConditionalQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConditionalQuadCollector.java index 6e4391b51f3..a1622bd4aed 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConditionalQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConditionalQuadCollector.java @@ -4,10 +4,10 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.PentaFunction; import ai.timefold.solver.core.api.function.QuadPredicate; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; -import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; import org.jspecify.annotations.NonNull; @@ -15,13 +15,13 @@ final class ConditionalQuadCollector implements QuadConstraintCollector { private final QuadPredicate predicate; private final QuadConstraintCollector delegate; - private final PentaFunction innerAccumulator; + private final QuadConstraintCollectorAccumulator innerIncremental; ConditionalQuadCollector(QuadPredicate predicate, QuadConstraintCollector delegate) { this.predicate = predicate; this.delegate = delegate; - this.innerAccumulator = delegate.accumulator(); + this.innerIncremental = QuadCollectorUtils.toIncremental(delegate.accumulator()); } @Override @@ -30,14 +30,8 @@ final class ConditionalQuadCollector } @Override - public @NonNull PentaFunction accumulator() { - return (resultContainer, a, b, c, d) -> { - if (predicate.test(a, b, c, d)) { - return innerAccumulator.apply(resultContainer, a, b, c, d); - } else { - return ConstantLambdaUtils.noop(); - } - }; + public @NonNull QuadConstraintCollectorAccumulator accumulator() { + return ValueHandle::new; } @Override @@ -59,4 +53,45 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(predicate, delegate); } + + private final class ValueHandle implements QuadConstraintCollectorValueHandle { + private final QuadConstraintCollectorValueHandle innerValue; + private boolean active = false; + + ValueHandle(ResultContainer_ container) { + this.innerValue = innerIncremental.intoGroup(container); + } + + @Override + public void add(A a, B b, C c, D d) { + if (!predicate.test(a, b, c, d)) { + return; + } + active = true; + innerValue.add(a, b, c, d); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + var nowActive = predicate.test(a, b, c, d); + if (active && nowActive) { + innerValue.replaceWith(a, b, c, d); + } else if (active) { + active = false; + innerValue.remove(); + } else if (nowActive) { + active = true; + innerValue.add(a, b, c, d); + } + } + + @Override + public void remove() { + if (!active) { + return; + } + active = false; + innerValue.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConnectedRangesQuadConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConnectedRangesQuadConstraintCollector.java index d8766489415..1d37493797d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConnectedRangesQuadConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConnectedRangesQuadConstraintCollector.java @@ -7,14 +7,14 @@ import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain; -import ai.timefold.solver.core.impl.score.stream.collector.ConnectedRangesCalculator; -import ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.Range; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractConnectedRangesSlot; import org.jspecify.annotations.NonNull; final class ConnectedRangesQuadConstraintCollector, Difference_ extends Comparable> extends - ObjectCalculatorQuadCollector, Range, ConnectedRangesCalculator> { + AbstractReferenceBasedQuadCollector, AbstractConnectedRangesSlot.State> { private final Function startMap; private final Function endMap; @@ -31,8 +31,43 @@ public ConnectedRangesQuadConstraintCollector( } @Override - public @NonNull Supplier> supplier() { - return () -> new ConnectedRangesCalculator<>(startMap, endMap, differenceFunction); + public @NonNull Supplier> supplier() { + return () -> new AbstractConnectedRangesSlot.State<>(startMap, endMap, differenceFunction); + } + + @Override + public @NonNull + Function, ConnectedRangeChain> + finisher() { + return AbstractConnectedRangesSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractConnectedRangesSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractConnectedRangesSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractConnectedRangesSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConsecutiveSequencesQuadConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConsecutiveSequencesQuadConstraintCollector.java index 0df054befde..53fb9753478 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConsecutiveSequencesQuadConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ConsecutiveSequencesQuadConstraintCollector.java @@ -1,18 +1,20 @@ package ai.timefold.solver.core.impl.score.stream.collector.quad; import java.util.Objects; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToIntFunction; import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; -import ai.timefold.solver.core.impl.score.stream.collector.SequenceCalculator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractSequenceSlot; import org.jspecify.annotations.NonNull; final class ConsecutiveSequencesQuadConstraintCollector extends - ObjectCalculatorQuadCollector, Result_, SequenceCalculator> { + AbstractReferenceBasedQuadCollector, AbstractSequenceSlot.State> { private final ToIntFunction indexMap; @@ -23,8 +25,41 @@ public ConsecutiveSequencesQuadConstraintCollector(QuadFunction> supplier() { - return () -> new SequenceCalculator<>(indexMap); + public @NonNull Supplier> supplier() { + return () -> new AbstractSequenceSlot.State<>(indexMap); + } + + @Override + public @NonNull Function, SequenceChain> finisher() { + return AbstractSequenceSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractSequenceSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractSequenceSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractSequenceSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctQuadCollector.java index eed2c186b7b..864060c16be 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctQuadCollector.java @@ -1,20 +1,56 @@ package ai.timefold.solver.core.impl.score.stream.collector.quad; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.LongDistinctCountCalculator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongDistinctSlot; import org.jspecify.annotations.NonNull; final class CountDistinctQuadCollector - extends ObjectCalculatorQuadCollector> { + extends + AbstractReferenceBasedQuadCollector> { CountDistinctQuadCollector(QuadFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return LongDistinctCountCalculator::new; + public @NonNull Supplier> supplier() { + return AbstractLongDistinctSlot.State::new; + } + + @Override + public @NonNull Function, Long> finisher() { + return AbstractLongDistinctSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractLongDistinctSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongDistinctSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractLongDistinctSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountQuadCollector.java index 03eab357860..115e2b262c7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountQuadCollector.java @@ -3,13 +3,16 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.PentaFunction; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LongCounter; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractCountSlot; +import ai.timefold.solver.core.impl.util.MutableLong; import org.jspecify.annotations.NonNull; -final class CountQuadCollector implements QuadConstraintCollector { +final class CountQuadCollector + implements QuadConstraintCollector { private static final CountQuadCollector INSTANCE = new CountQuadCollector<>(); private CountQuadCollector() { @@ -21,20 +24,40 @@ static CountQuadCollector getInstance() { } @Override - public @NonNull Supplier supplier() { - return LongCounter::new; + public @NonNull Supplier supplier() { + return MutableLong::new; } @Override - public @NonNull PentaFunction accumulator() { - return (counter, a, b, c, d) -> { - counter.increment(); - return counter::decrement; - }; + public @NonNull QuadConstraintCollectorAccumulator accumulator() { + return Slot::new; } @Override - public @NonNull Function finisher() { - return LongCounter::result; + public @NonNull Function finisher() { + return MutableLong::longValue; + } + + private static final class Slot extends AbstractCountSlot + implements QuadConstraintCollectorValueHandle { + + Slot(MutableLong state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/InnerQuadConstraintCollectors.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/InnerQuadConstraintCollectors.java index f5666a8a724..20bab1877d8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/InnerQuadConstraintCollectors.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/InnerQuadConstraintCollectors.java @@ -24,9 +24,9 @@ import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceAverageSlot; -public class InnerQuadConstraintCollectors { +public final class InnerQuadConstraintCollectors { public static QuadConstraintCollector average( ToLongQuadFunction mapper) { return new AverageQuadCollector<>(mapper); @@ -34,23 +34,23 @@ public class InnerQuadConstraintCollectors { static QuadConstraintCollector average( QuadFunction mapper, - Supplier> calculatorSupplier) { - return new AverageReferenceQuadCollector<>(mapper, calculatorSupplier); + Supplier> stateSupplier) { + return new AverageReferenceQuadCollector<>(mapper, stateSupplier); } public static QuadConstraintCollector averageBigDecimal( QuadFunction mapper) { - return average(mapper, ReferenceAverageCalculator.bigDecimal()); + return average(mapper, AbstractReferenceAverageSlot.bigDecimalState()); } public static QuadConstraintCollector averageBigInteger( QuadFunction mapper) { - return average(mapper, ReferenceAverageCalculator.bigInteger()); + return average(mapper, AbstractReferenceAverageSlot.bigIntegerState()); } public static QuadConstraintCollector averageDuration( QuadFunction mapper) { - return average(mapper, ReferenceAverageCalculator.duration()); + return average(mapper, AbstractReferenceAverageSlot.durationState()); } public static @@ -106,12 +106,6 @@ public class InnerQuadConstraintCollectors { return new MaxComparableQuadCollector<>(mapper); } - public static QuadConstraintCollector max( - QuadFunction mapper, - Comparator comparator) { - return new MaxComparatorQuadCollector<>(mapper, comparator); - } - public static > QuadConstraintCollector max( QuadFunction mapper, @@ -124,12 +118,6 @@ public class InnerQuadConstraintCollectors { return new MinComparableQuadCollector<>(mapper); } - public static QuadConstraintCollector min( - QuadFunction mapper, - Comparator comparator) { - return new MinComparatorQuadCollector<>(mapper, comparator); - } - public static > QuadConstraintCollector min( QuadFunction mapper, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/LoadBalanceQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/LoadBalanceQuadCollector.java index 1eee9a0b4ba..c83f0e83a25 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/LoadBalanceQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/LoadBalanceQuadCollector.java @@ -4,17 +4,19 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.PentaFunction; import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.function.ToLongQuadFunction; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LoadBalanceImpl; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLoadBalanceSlot; +import ai.timefold.solver.core.impl.score.stream.collector.DefaultLoadBalance; import org.jspecify.annotations.NonNull; final class LoadBalanceQuadCollector - implements QuadConstraintCollector, LoadBalance> { + implements QuadConstraintCollector, LoadBalance> { private final QuadFunction balancedItemFunction; private final ToLongQuadFunction loadFunction; @@ -28,22 +30,17 @@ public LoadBalanceQuadCollector(QuadFunction balancedItem } @Override - public @NonNull Supplier> supplier() { - return LoadBalanceImpl::new; + public @NonNull Supplier> supplier() { + return DefaultLoadBalance::new; } @Override - public @NonNull PentaFunction, A, B, C, D, Runnable> accumulator() { - return (balanceStatistics, a, b, c, d) -> { - var balanced = balancedItemFunction.apply(a, b, c, d); - var initialLoad = initialLoadFunction.applyAsLong(a, b, c, d); - var load = loadFunction.applyAsLong(a, b, c, d); - return balanceStatistics.registerBalanced(balanced, load, initialLoad); - }; + public @NonNull QuadConstraintCollectorAccumulator, A, B, C, D> accumulator() { + return Slot::new; } @Override - public @NonNull Function, LoadBalance> finisher() { + public @NonNull Function, LoadBalance> finisher() { return balanceStatistics -> balanceStatistics; } @@ -59,4 +56,29 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(balancedItemFunction, loadFunction, initialLoadFunction); } + + private final class Slot extends AbstractLoadBalanceSlot + implements QuadConstraintCollectorValueHandle { + + Slot(DefaultLoadBalance container) { + super(container); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(balancedItemFunction.apply(a, b, c, d), loadFunction.applyAsLong(a, b, c, d), + initialLoadFunction.applyAsLong(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(balancedItemFunction.apply(a, b, c, d), loadFunction.applyAsLong(a, b, c, d), + initialLoadFunction.applyAsLong(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/LongCalculatorQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/LongCalculatorQuadCollector.java deleted file mode 100644 index 24d5827bfb0..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/LongCalculatorQuadCollector.java +++ /dev/null @@ -1,50 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.quad; - -import java.util.Objects; -import java.util.function.Function; - -import ai.timefold.solver.core.api.function.PentaFunction; -import ai.timefold.solver.core.api.function.ToLongQuadFunction; -import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LongCalculator; - -import org.jspecify.annotations.NonNull; - -abstract sealed class LongCalculatorQuadCollector> - implements QuadConstraintCollector - permits AverageQuadCollector, SumQuadCollector { - private final ToLongQuadFunction mapper; - - public LongCalculatorQuadCollector(ToLongQuadFunction mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull PentaFunction accumulator() { - return (calculator, a, b, c, d) -> { - final long mapped = mapper.applyAsLong(a, b, c, d); - calculator.insert(mapped); - return () -> calculator.retract(mapped); - }; - } - - @Override - public @NonNull Function finisher() { - return LongCalculator::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (LongCalculatorQuadCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxComparableQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxComparableQuadCollector.java index 2ffb237cc60..653f0879245 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxComparableQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxComparableQuadCollector.java @@ -1,20 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.quad; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MaxComparableQuadCollector> - extends UndoableActionableQuadCollector> { + extends AbstractReferenceBasedQuadCollector> { MaxComparableQuadCollector(QuadFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return MinMaxUndoableActionable::maxCalculator; + public @NonNull Supplier> supplier() { + return AbstractMinMaxSlot::maxState; + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxComparatorQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxComparatorQuadCollector.java deleted file mode 100644 index b5c67600aa5..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxComparatorQuadCollector.java +++ /dev/null @@ -1,43 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.quad; - -import java.util.Comparator; -import java.util.Objects; -import java.util.function.Supplier; - -import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; - -import org.jspecify.annotations.NonNull; - -final class MaxComparatorQuadCollector - extends UndoableActionableQuadCollector> { - private final Comparator comparator; - - MaxComparatorQuadCollector(QuadFunction mapper, - Comparator comparator) { - super(mapper); - this.comparator = comparator; - } - - @Override - public @NonNull Supplier> supplier() { - return () -> MinMaxUndoableActionable.maxCalculator(comparator); - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - if (!super.equals(object)) - return false; - MaxComparatorQuadCollector that = (MaxComparatorQuadCollector) object; - return Objects.equals(comparator, that.comparator); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), comparator); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxPropertyQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxPropertyQuadCollector.java index ed72e890df1..c58948ab52b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxPropertyQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MaxPropertyQuadCollector.java @@ -5,12 +5,14 @@ import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MaxPropertyQuadCollector> - extends UndoableActionableQuadCollector> { + extends + AbstractReferenceBasedQuadCollector> { private final Function propertyMapper; MaxPropertyQuadCollector(QuadFunction mapper, @@ -20,8 +22,41 @@ final class MaxPropertyQuadCollector> supplier() { - return () -> MinMaxUndoableActionable.maxCalculator(propertyMapper); + public @NonNull Supplier> supplier() { + return () -> AbstractMinMaxSlot.maxState(propertyMapper); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinComparableQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinComparableQuadCollector.java index e907f1b2237..0bdbed14c51 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinComparableQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinComparableQuadCollector.java @@ -1,20 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.quad; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MinComparableQuadCollector> - extends UndoableActionableQuadCollector> { + extends AbstractReferenceBasedQuadCollector> { MinComparableQuadCollector(QuadFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return MinMaxUndoableActionable::minCalculator; + public @NonNull Supplier> supplier() { + return AbstractMinMaxSlot::minState; + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinComparatorQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinComparatorQuadCollector.java deleted file mode 100644 index 8eeea5e6148..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinComparatorQuadCollector.java +++ /dev/null @@ -1,43 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.quad; - -import java.util.Comparator; -import java.util.Objects; -import java.util.function.Supplier; - -import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; - -import org.jspecify.annotations.NonNull; - -final class MinComparatorQuadCollector - extends UndoableActionableQuadCollector> { - private final Comparator comparator; - - MinComparatorQuadCollector(QuadFunction mapper, - Comparator comparator) { - super(mapper); - this.comparator = comparator; - } - - @Override - public @NonNull Supplier> supplier() { - return () -> MinMaxUndoableActionable.minCalculator(comparator); - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - if (!super.equals(object)) - return false; - MinComparatorQuadCollector that = (MinComparatorQuadCollector) object; - return Objects.equals(comparator, that.comparator); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), comparator); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinPropertyQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinPropertyQuadCollector.java index da1893ef281..1d5d72e7d89 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinPropertyQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/MinPropertyQuadCollector.java @@ -5,12 +5,14 @@ import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MinPropertyQuadCollector> - extends UndoableActionableQuadCollector> { + extends + AbstractReferenceBasedQuadCollector> { private final Function propertyMapper; MinPropertyQuadCollector(QuadFunction mapper, @@ -20,8 +22,41 @@ final class MinPropertyQuadCollector> supplier() { - return () -> MinMaxUndoableActionable.minCalculator(propertyMapper); + public @NonNull Supplier> supplier() { + return () -> AbstractMinMaxSlot.minState(propertyMapper); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ObjectCalculatorQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ObjectCalculatorQuadCollector.java deleted file mode 100644 index 3d84bc08470..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ObjectCalculatorQuadCollector.java +++ /dev/null @@ -1,53 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.quad; - -import java.util.Objects; -import java.util.function.Function; - -import ai.timefold.solver.core.api.function.PentaFunction; -import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.ObjectCalculator; - -import org.jspecify.annotations.NonNull; - -abstract sealed class ObjectCalculatorQuadCollector> - implements QuadConstraintCollector - permits AverageReferenceQuadCollector, ConnectedRangesQuadConstraintCollector, - ConsecutiveSequencesQuadConstraintCollector, CountDistinctQuadCollector, - SumReferenceQuadCollector { - - protected final QuadFunction mapper; - - public ObjectCalculatorQuadCollector(QuadFunction mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull PentaFunction accumulator() { - return (calculator, a, b, c, d) -> { - final var mapped = mapper.apply(a, b, c, d); - final var saved = calculator.insert(mapped); - return () -> calculator.retract(saved); - }; - } - - @Override - public @NonNull Function finisher() { - return ObjectCalculator::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (ObjectCalculatorQuadCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/QuadCollectorUtils.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/QuadCollectorUtils.java new file mode 100644 index 00000000000..577dd95c5c0 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/QuadCollectorUtils.java @@ -0,0 +1,59 @@ +package ai.timefold.solver.core.impl.score.stream.collector.quad; + +import ai.timefold.solver.core.api.function.PentaFunction; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public final class QuadCollectorUtils { + + public static QuadConstraintCollectorAccumulator + toIncremental(PentaFunction accumulator) { + if (accumulator instanceof QuadConstraintCollectorAccumulator inc) { + return inc; + } + return new QuadFromAccumulatorAdapter<>(accumulator); + } + + private record QuadFromAccumulatorAdapter( + PentaFunction accumulator) + implements + QuadConstraintCollectorAccumulator { + + @Override + public QuadConstraintCollectorValueHandle intoGroup(ResultContainer_ container) { + return new QuadValueHandle<>(accumulator, container); + } + } + + private static final class QuadValueHandle + implements QuadConstraintCollectorValueHandle { + private final PentaFunction accumulator; + private final ResultContainer_ container; + private @Nullable Runnable undo; + + QuadValueHandle(PentaFunction accumulator, + ResultContainer_ container) { + this.accumulator = accumulator; + this.container = container; + } + + @Override + public void add(@Nullable A a, @Nullable B b, @Nullable C c, @Nullable D d) { + undo = accumulator.apply(container, a, b, c, d); + } + + @Override + public void remove() { + undo.run(); + undo = null; + } + } + + private QuadCollectorUtils() { + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/SumQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/SumQuadCollector.java index 292c4c4d94c..c7c21c26ce3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/SumQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/SumQuadCollector.java @@ -1,19 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.quad; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.ToLongQuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.LongSumCalculator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongSumSlot; +import ai.timefold.solver.core.impl.util.MutableLong; import org.jspecify.annotations.NonNull; -final class SumQuadCollector extends LongCalculatorQuadCollector { +final class SumQuadCollector + extends AbstractPrimitiveBasedQuadCollector { SumQuadCollector(ToLongQuadFunction mapper) { super(mapper); } @Override - public @NonNull Supplier supplier() { - return LongSumCalculator::new; + public @NonNull Supplier supplier() { + return MutableLong::new; + } + + @Override + public @NonNull Function finisher() { + return MutableLong::longValue; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue(MutableLong state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongSumSlot + implements QuadConstraintCollectorValueHandle { + Slot(MutableLong state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.applyAsLong(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.applyAsLong(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/SumReferenceQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/SumReferenceQuadCollector.java index 1e3f24f6c28..162b4ac1178 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/SumReferenceQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/SumReferenceQuadCollector.java @@ -2,15 +2,18 @@ import java.util.Objects; import java.util.function.BinaryOperator; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceSumCalculator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceSumSlot; import org.jspecify.annotations.NonNull; final class SumReferenceQuadCollector - extends ObjectCalculatorQuadCollector> { + extends + AbstractReferenceBasedQuadCollector> { private final Result_ zero; private final BinaryOperator adder; private final BinaryOperator subtractor; @@ -26,8 +29,41 @@ final class SumReferenceQuadCollector } @Override - public @NonNull Supplier> supplier() { - return () -> new ReferenceSumCalculator<>(zero, adder, subtractor); + public @NonNull Supplier> supplier() { + return () -> new AbstractReferenceSumSlot.State<>(zero, adder, subtractor); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractReferenceSumSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractReferenceSumSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractReferenceSumSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractReferenceSumSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToCollectionQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToCollectionQuadCollector.java index 7ae554e6a02..142c2aeba82 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToCollectionQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToCollectionQuadCollector.java @@ -2,17 +2,19 @@ import java.util.Collection; import java.util.Objects; +import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.CustomCollectionUndoableActionable; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToCollectionSlot; import org.jspecify.annotations.NonNull; final class ToCollectionQuadCollector> extends - UndoableActionableQuadCollector> { + AbstractReferenceBasedQuadCollector> { private final IntFunction collectionFunction; ToCollectionQuadCollector(QuadFunction mapper, @@ -22,8 +24,41 @@ final class ToCollectionQuadCollector> supplier() { - return () -> new CustomCollectionUndoableActionable<>(collectionFunction); + public @NonNull Supplier> supplier() { + return () -> new AbstractToCollectionSlot.State<>(collectionFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return state -> state.result(); + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractToCollectionSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToCollectionSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractToCollectionSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToListQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToListQuadCollector.java index ea3fc75aae0..c5f8777c11c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToListQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToListQuadCollector.java @@ -1,21 +1,56 @@ package ai.timefold.solver.core.impl.score.stream.collector.quad; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.ListUndoableActionable; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToListSlot; import org.jspecify.annotations.NonNull; final class ToListQuadCollector - extends UndoableActionableQuadCollector, ListUndoableActionable> { + extends AbstractReferenceBasedQuadCollector, AbstractToListSlot.State> { ToListQuadCollector(QuadFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return ListUndoableActionable::new; + public @NonNull Supplier> supplier() { + return AbstractToListSlot.State::new; + } + + @Override + public @NonNull Function, List> finisher() { + return AbstractToListSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractToListSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToListSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractToListSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToMultiMapQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToMultiMapQuadCollector.java index 9cccd28f8c1..c0a7d15d754 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToMultiMapQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToMultiMapQuadCollector.java @@ -3,18 +3,19 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MapUndoableActionable; -import ai.timefold.solver.core.impl.util.Pair; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToMapSlot; import org.jspecify.annotations.NonNull; final class ToMultiMapQuadCollector, Result_ extends Map> extends - UndoableActionableQuadCollector, Result_, MapUndoableActionable> { + AbstractReferenceBasedQuadCollector> { private final QuadFunction keyFunction; private final QuadFunction valueFunction; private final Supplier mapSupplier; @@ -24,7 +25,7 @@ final class ToMultiMapQuadCollector valueFunction, Supplier mapSupplier, IntFunction setFunction) { - super((a, b, c, d) -> new Pair<>(keyFunction.apply(a, b, c, d), valueFunction.apply(a, b, c, d))); + super(keyFunction); this.keyFunction = keyFunction; this.valueFunction = valueFunction; this.mapSupplier = mapSupplier; @@ -32,8 +33,41 @@ final class ToMultiMapQuadCollector> supplier() { - return () -> MapUndoableActionable.multiMap(mapSupplier, setFunction); + public @NonNull Supplier> supplier() { + return () -> AbstractToMapSlot.multiMapState(mapSupplier, setFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return state -> state.result(); + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractToMapSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToMapSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractToMapSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(keyFunction.apply(a, b, c, d), valueFunction.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(keyFunction.apply(a, b, c, d), valueFunction.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } // Don't call super equals/hashCode; the groupingFunction is calculated from keyFunction diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSetQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSetQuadCollector.java index 5683fac568d..b6cf2f6b571 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSetQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSetQuadCollector.java @@ -1,21 +1,56 @@ package ai.timefold.solver.core.impl.score.stream.collector.quad; import java.util.Set; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.SetUndoableActionable; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToSetSlot; import org.jspecify.annotations.NonNull; final class ToSetQuadCollector - extends UndoableActionableQuadCollector, SetUndoableActionable> { + extends AbstractReferenceBasedQuadCollector, AbstractToSetSlot.State> { ToSetQuadCollector(QuadFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return SetUndoableActionable::new; + public @NonNull Supplier> supplier() { + return AbstractToSetSlot.State::new; + } + + @Override + public @NonNull Function, Set> finisher() { + return AbstractToSetSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractToSetSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToSetSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractToSetSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSimpleMapQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSimpleMapQuadCollector.java index 7ec5be37f79..9bc9469ee09 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSimpleMapQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSimpleMapQuadCollector.java @@ -3,17 +3,18 @@ import java.util.Map; import java.util.Objects; import java.util.function.BinaryOperator; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MapUndoableActionable; -import ai.timefold.solver.core.impl.util.Pair; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToMapSlot; import org.jspecify.annotations.NonNull; final class ToSimpleMapQuadCollector> extends - UndoableActionableQuadCollector, Result_, MapUndoableActionable> { + AbstractReferenceBasedQuadCollector> { private final QuadFunction keyFunction; private final QuadFunction valueFunction; private final Supplier mapSupplier; @@ -23,7 +24,7 @@ final class ToSimpleMapQuadCollector valueFunction, Supplier mapSupplier, BinaryOperator mergeFunction) { - super((a, b, c, d) -> new Pair<>(keyFunction.apply(a, b, c, d), valueFunction.apply(a, b, c, d))); + super(keyFunction); this.keyFunction = keyFunction; this.valueFunction = valueFunction; this.mapSupplier = mapSupplier; @@ -31,8 +32,41 @@ final class ToSimpleMapQuadCollector> supplier() { - return () -> MapUndoableActionable.mergeMap(mapSupplier, mergeFunction); + public @NonNull Supplier> supplier() { + return () -> AbstractToMapSlot.mergeMapState(mapSupplier, mergeFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return state -> state.result(); + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractToMapSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToMapSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractToMapSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(keyFunction.apply(a, b, c, d), valueFunction.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(keyFunction.apply(a, b, c, d), valueFunction.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } // Don't call super equals/hashCode; the groupingFunction is calculated from keyFunction diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSortedSetComparatorQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSortedSetComparatorQuadCollector.java index ac300332fad..69eec5c8294 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSortedSetComparatorQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/ToSortedSetComparatorQuadCollector.java @@ -3,15 +3,18 @@ import java.util.Comparator; import java.util.Objects; import java.util.SortedSet; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.impl.score.stream.collector.SortedSetUndoableActionable; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractSortedSetSlot; import org.jspecify.annotations.NonNull; final class ToSortedSetComparatorQuadCollector - extends UndoableActionableQuadCollector, SortedSetUndoableActionable> { + extends + AbstractReferenceBasedQuadCollector, AbstractSortedSetSlot.State> { private final Comparator comparator; ToSortedSetComparatorQuadCollector(QuadFunction mapper, @@ -21,8 +24,41 @@ final class ToSortedSetComparatorQuadCollector } @Override - public @NonNull Supplier> supplier() { - return () -> SortedSetUndoableActionable.orderBy(comparator); + public @NonNull Supplier> supplier() { + return () -> new AbstractSortedSetSlot.State<>(comparator); + } + + @Override + public @NonNull Function, SortedSet> finisher() { + return AbstractSortedSetSlot.State::result; + } + + @Override + protected QuadConstraintCollectorValueHandle newAccumulatedValue( + AbstractSortedSetSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractSortedSetSlot + implements QuadConstraintCollectorValueHandle { + Slot(AbstractSortedSetSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c, D d) { + addMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void replaceWith(A a, B b, C c, D d) { + replaceWithMapped(mapper.apply(a, b, c, d)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/UndoableActionableQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/UndoableActionableQuadCollector.java deleted file mode 100644 index 0e7831de4c0..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/UndoableActionableQuadCollector.java +++ /dev/null @@ -1,51 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.quad; - -import java.util.Objects; -import java.util.function.Function; - -import ai.timefold.solver.core.api.function.PentaFunction; -import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.UndoableActionable; - -import org.jspecify.annotations.NonNull; - -abstract sealed class UndoableActionableQuadCollector> - implements QuadConstraintCollector - permits MaxComparableQuadCollector, MaxComparatorQuadCollector, MaxPropertyQuadCollector, MinComparableQuadCollector, - MinComparatorQuadCollector, MinPropertyQuadCollector, ToCollectionQuadCollector, ToListQuadCollector, - ToMultiMapQuadCollector, ToSetQuadCollector, ToSimpleMapQuadCollector, ToSortedSetComparatorQuadCollector { - private final QuadFunction mapper; - - public UndoableActionableQuadCollector(QuadFunction mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull PentaFunction accumulator() { - return (calculator, a, b, c, d) -> { - final Input_ mapped = mapper.apply(a, b, c, d); - return calculator.insert(mapped); - }; - } - - @Override - public @NonNull Function finisher() { - return UndoableActionable::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (UndoableActionableQuadCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AbstractPrimitiveBasedTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AbstractPrimitiveBasedTriCollector.java new file mode 100644 index 00000000000..9bb2c1c921b --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AbstractPrimitiveBasedTriCollector.java @@ -0,0 +1,41 @@ +package ai.timefold.solver.core.impl.score.stream.collector.tri; + +import java.util.Objects; + +import ai.timefold.solver.core.api.function.ToLongTriFunction; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; + +import org.jspecify.annotations.NonNull; + +abstract class AbstractPrimitiveBasedTriCollector + implements TriConstraintCollector { + protected final ToLongTriFunction mapper; + + public AbstractPrimitiveBasedTriCollector(ToLongTriFunction mapper) { + this.mapper = mapper; + } + + protected abstract TriConstraintCollectorValueHandle newAccumulatedValue(State_ state); + + @Override + public @NonNull TriConstraintCollectorAccumulator accumulator() { + return this::newAccumulatedValue; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (AbstractPrimitiveBasedTriCollector) object; + return Objects.equals(mapper, that.mapper); + } + + @Override + public int hashCode() { + return Objects.hash(mapper); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AbstractReferenceBasedTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AbstractReferenceBasedTriCollector.java new file mode 100644 index 00000000000..2814ff93864 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AbstractReferenceBasedTriCollector.java @@ -0,0 +1,42 @@ +package ai.timefold.solver.core.impl.score.stream.collector.tri; + +import java.util.Objects; + +import ai.timefold.solver.core.api.function.TriFunction; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; + +import org.jspecify.annotations.NonNull; + +abstract class AbstractReferenceBasedTriCollector + implements TriConstraintCollector { + + protected final TriFunction mapper; + + public AbstractReferenceBasedTriCollector(TriFunction mapper) { + this.mapper = mapper; + } + + protected abstract TriConstraintCollectorValueHandle newAccumulatedValue(State_ state); + + @Override + public @NonNull TriConstraintCollectorAccumulator accumulator() { + return this::newAccumulatedValue; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (AbstractReferenceBasedTriCollector) object; + return Objects.equals(mapper, that.mapper); + } + + @Override + public int hashCode() { + return Objects.hash(mapper); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AndThenTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AndThenTriCollector.java index 55cf898d2db..f596a697f86 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AndThenTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AndThenTriCollector.java @@ -4,8 +4,8 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; import org.jspecify.annotations.NonNull; @@ -14,11 +14,13 @@ final class AndThenTriCollector delegate; private final Function mappingFunction; + private final TriConstraintCollectorAccumulator innerIncremental; AndThenTriCollector(TriConstraintCollector delegate, Function mappingFunction) { this.delegate = Objects.requireNonNull(delegate); this.mappingFunction = Objects.requireNonNull(mappingFunction); + this.innerIncremental = TriCollectorUtils.toIncremental(delegate.accumulator()); } @Override @@ -27,8 +29,8 @@ final class AndThenTriCollector accumulator() { - return delegate.accumulator(); + public @NonNull TriConstraintCollectorAccumulator accumulator() { + return innerIncremental; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AverageReferenceTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AverageReferenceTriCollector.java index fd03cbc580e..b00167b4dd5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AverageReferenceTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AverageReferenceTriCollector.java @@ -1,27 +1,62 @@ package ai.timefold.solver.core.impl.score.stream.collector.tri; import java.util.Objects; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceAverageSlot; import org.jspecify.annotations.NonNull; final class AverageReferenceTriCollector extends - ObjectCalculatorTriCollector> { - private final Supplier> calculatorSupplier; + AbstractReferenceBasedTriCollector> { + private final Supplier> stateSupplier; AverageReferenceTriCollector(TriFunction mapper, - Supplier> calculatorSupplier) { + Supplier> stateSupplier) { super(mapper); - this.calculatorSupplier = calculatorSupplier; + this.stateSupplier = stateSupplier; } @Override - public @NonNull Supplier> supplier() { - return calculatorSupplier; + public @NonNull Supplier> supplier() { + return stateSupplier; + } + + @Override + public @NonNull Function, Average_> finisher() { + return AbstractReferenceAverageSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractReferenceAverageSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractReferenceAverageSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractReferenceAverageSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override @@ -33,11 +68,11 @@ public boolean equals(Object object) { if (!super.equals(object)) return false; AverageReferenceTriCollector that = (AverageReferenceTriCollector) object; - return Objects.equals(calculatorSupplier, that.calculatorSupplier); + return Objects.equals(stateSupplier, that.stateSupplier); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), calculatorSupplier); + return Objects.hash(super.hashCode(), stateSupplier); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AverageTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AverageTriCollector.java index deef34c6ea8..9e57c578b48 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AverageTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/AverageTriCollector.java @@ -1,19 +1,54 @@ package ai.timefold.solver.core.impl.score.stream.collector.tri; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.ToLongTriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.LongAverageCalculator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongAverageSlot; import org.jspecify.annotations.NonNull; -final class AverageTriCollector extends LongCalculatorTriCollector { +final class AverageTriCollector + extends AbstractPrimitiveBasedTriCollector { AverageTriCollector(ToLongTriFunction mapper) { super(mapper); } @Override - public @NonNull Supplier supplier() { - return LongAverageCalculator::new; + public @NonNull Supplier supplier() { + return AbstractLongAverageSlot.State::new; + } + + @Override + public @NonNull Function finisher() { + return AbstractLongAverageSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue(AbstractLongAverageSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongAverageSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractLongAverageSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.applyAsLong(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.applyAsLong(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeFourTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeFourTriCollector.java index 8fdcbee3571..ac66a69df46 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeFourTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeFourTriCollector.java @@ -6,9 +6,12 @@ import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Quadruple; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; final class ComposeFourTriCollector implements @@ -24,10 +27,10 @@ final class ComposeFourTriCollector thirdSupplier; private final Supplier fourthSupplier; - private final QuadFunction firstAccumulator; - private final QuadFunction secondAccumulator; - private final QuadFunction thirdAccumulator; - private final QuadFunction fourthAccumulator; + private final TriConstraintCollectorAccumulator firstIncremental; + private final TriConstraintCollectorAccumulator secondIncremental; + private final TriConstraintCollectorAccumulator thirdIncremental; + private final TriConstraintCollectorAccumulator fourthIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -50,10 +53,10 @@ final class ComposeFourTriCollector, A, B, C, Runnable> + public @NonNull + TriConstraintCollectorAccumulator, A, B, C> accumulator() { - return (resultHolder, a, b, c) -> composeUndo(firstAccumulator.apply(resultHolder.a(), a, b, c), - secondAccumulator.apply(resultHolder.b(), a, b, c), - thirdAccumulator.apply(resultHolder.c(), a, b, c), - fourthAccumulator.apply(resultHolder.d(), a, b, c)); - } - - private static Runnable composeUndo(Runnable first, Runnable second, Runnable third, - Runnable fourth) { - return () -> { - first.run(); - second.run(); - third.run(); - fourth.run(); - }; + return ValueHandle::new; } @Override - public @NonNull Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> + finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.a()), secondFinisher.apply(resultHolder.b()), thirdFinisher.apply(resultHolder.c()), @@ -116,4 +108,42 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, third, fourth, composeFunction); } + + private final class ValueHandle implements TriConstraintCollectorValueHandle { + private final TriConstraintCollectorValueHandle v1; + private final TriConstraintCollectorValueHandle v2; + private final TriConstraintCollectorValueHandle v3; + private final TriConstraintCollectorValueHandle v4; + + ValueHandle(Quadruple container) { + this.v1 = firstIncremental.intoGroup(container.a()); + this.v2 = secondIncremental.intoGroup(container.b()); + this.v3 = thirdIncremental.intoGroup(container.c()); + this.v4 = fourthIncremental.intoGroup(container.d()); + } + + @Override + public void add(A a, B b, C c) { + v1.add(a, b, c); + v2.add(a, b, c); + v3.add(a, b, c); + v4.add(a, b, c); + } + + @Override + public void replaceWith(A a, B b, C c) { + v1.replaceWith(a, b, c); + v2.replaceWith(a, b, c); + v3.replaceWith(a, b, c); + v4.replaceWith(a, b, c); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + v3.remove(); + v4.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeThreeTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeThreeTriCollector.java index 328d051287d..116d8e4bb58 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeThreeTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeThreeTriCollector.java @@ -4,12 +4,14 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Triple; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; final class ComposeThreeTriCollector implements TriConstraintCollector, Result_> { @@ -22,9 +24,9 @@ final class ComposeThreeTriCollector secondSupplier; private final Supplier thirdSupplier; - private final QuadFunction firstAccumulator; - private final QuadFunction secondAccumulator; - private final QuadFunction thirdAccumulator; + private final TriConstraintCollectorAccumulator firstIncremental; + private final TriConstraintCollectorAccumulator secondIncremental; + private final TriConstraintCollectorAccumulator thirdIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -43,9 +45,9 @@ final class ComposeThreeTriCollector, A, B, C, Runnable> accumulator() { - return (resultHolder, a, b, c) -> composeUndo(firstAccumulator.apply(resultHolder.a(), a, b, c), - secondAccumulator.apply(resultHolder.b(), a, b, c), - thirdAccumulator.apply(resultHolder.c(), a, b, c)); - } - - private static Runnable composeUndo(Runnable first, Runnable second, Runnable third) { - return () -> { - first.run(); - second.run(); - third.run(); - }; + public @NonNull TriConstraintCollectorAccumulator, A, B, C> + accumulator() { + return ValueHandle::new; } @Override - public @NonNull Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.a()), secondFinisher.apply(resultHolder.b()), thirdFinisher.apply(resultHolder.c())); @@ -100,4 +93,37 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, third, composeFunction); } + + private final class ValueHandle implements TriConstraintCollectorValueHandle { + private final TriConstraintCollectorValueHandle v1; + private final TriConstraintCollectorValueHandle v2; + private final TriConstraintCollectorValueHandle v3; + + ValueHandle(Triple container) { + this.v1 = firstIncremental.intoGroup(container.a()); + this.v2 = secondIncremental.intoGroup(container.b()); + this.v3 = thirdIncremental.intoGroup(container.c()); + } + + @Override + public void add(A a, B b, C c) { + v1.add(a, b, c); + v2.add(a, b, c); + v3.add(a, b, c); + } + + @Override + public void replaceWith(A a, B b, C c) { + v1.replaceWith(a, b, c); + v2.replaceWith(a, b, c); + v3.replaceWith(a, b, c); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + v3.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeTwoTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeTwoTriCollector.java index 7449ff9a60c..a6934c98938 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeTwoTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ComposeTwoTriCollector.java @@ -5,11 +5,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Pair; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; final class ComposeTwoTriCollector implements TriConstraintCollector, Result_> { @@ -20,8 +22,8 @@ final class ComposeTwoTriCollector firstSupplier; private final Supplier secondSupplier; - private final QuadFunction firstAccumulator; - private final QuadFunction secondAccumulator; + private final TriConstraintCollectorAccumulator firstIncremental; + private final TriConstraintCollectorAccumulator secondIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -36,8 +38,8 @@ final class ComposeTwoTriCollector, A, B, C, Runnable> accumulator() { - return (resultHolder, a, b, c) -> composeUndo(firstAccumulator.apply(resultHolder.key(), a, b, c), - secondAccumulator.apply(resultHolder.value(), a, b, c)); - } - - private static Runnable composeUndo(Runnable first, Runnable second) { - return () -> { - first.run(); - second.run(); - }; + public @NonNull TriConstraintCollectorAccumulator, A, B, C> accumulator() { + return ValueHandle::new; } @Override - public @NonNull Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.key()), secondFinisher.apply(resultHolder.value())); } @@ -82,4 +76,32 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, composeFunction); } + + private final class ValueHandle implements TriConstraintCollectorValueHandle { + private final TriConstraintCollectorValueHandle v1; + private final TriConstraintCollectorValueHandle v2; + + ValueHandle(Pair container) { + this.v1 = firstIncremental.intoGroup(container.key()); + this.v2 = secondIncremental.intoGroup(container.value()); + } + + @Override + public void add(A a, B b, C c) { + v1.add(a, b, c); + v2.add(a, b, c); + } + + @Override + public void replaceWith(A a, B b, C c) { + v1.replaceWith(a, b, c); + v2.replaceWith(a, b, c); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConditionalTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConditionalTriCollector.java index 3079a09dd82..6f22dc44300 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConditionalTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConditionalTriCollector.java @@ -4,10 +4,10 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.function.TriPredicate; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; -import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; import org.jspecify.annotations.NonNull; @@ -15,13 +15,13 @@ final class ConditionalTriCollector implements TriConstraintCollector { private final TriPredicate predicate; private final TriConstraintCollector delegate; - private final QuadFunction innerAccumulator; + private final TriConstraintCollectorAccumulator innerIncremental; ConditionalTriCollector(TriPredicate predicate, TriConstraintCollector delegate) { this.predicate = predicate; this.delegate = delegate; - this.innerAccumulator = delegate.accumulator(); + this.innerIncremental = TriCollectorUtils.toIncremental(delegate.accumulator()); } @Override @@ -30,14 +30,8 @@ final class ConditionalTriCollector } @Override - public @NonNull QuadFunction accumulator() { - return (resultContainer, a, b, c) -> { - if (predicate.test(a, b, c)) { - return innerAccumulator.apply(resultContainer, a, b, c); - } else { - return ConstantLambdaUtils.noop(); - } - }; + public @NonNull TriConstraintCollectorAccumulator accumulator() { + return ValueHandle::new; } @Override @@ -59,4 +53,45 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(predicate, delegate); } + + private final class ValueHandle implements TriConstraintCollectorValueHandle { + private final TriConstraintCollectorValueHandle innerValue; + private boolean active = false; + + ValueHandle(ResultContainer_ container) { + this.innerValue = innerIncremental.intoGroup(container); + } + + @Override + public void add(A a, B b, C c) { + if (!predicate.test(a, b, c)) { + return; + } + active = true; + innerValue.add(a, b, c); + } + + @Override + public void replaceWith(A a, B b, C c) { + var nowActive = predicate.test(a, b, c); + if (active && nowActive) { + innerValue.replaceWith(a, b, c); + } else if (active) { + active = false; + innerValue.remove(); + } else if (nowActive) { + active = true; + innerValue.add(a, b, c); + } + } + + @Override + public void remove() { + if (!active) { + return; + } + active = false; + innerValue.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConnectedRangesTriConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConnectedRangesTriConstraintCollector.java index c5a9f601033..266dbe2d4c5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConnectedRangesTriConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConnectedRangesTriConstraintCollector.java @@ -7,14 +7,14 @@ import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain; -import ai.timefold.solver.core.impl.score.stream.collector.ConnectedRangesCalculator; -import ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.Range; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractConnectedRangesSlot; import org.jspecify.annotations.NonNull; final class ConnectedRangesTriConstraintCollector, Difference_ extends Comparable> extends - ObjectCalculatorTriCollector, Range, ConnectedRangesCalculator> { + AbstractReferenceBasedTriCollector, AbstractConnectedRangesSlot.State> { private final Function startMap; private final Function endMap; @@ -30,8 +30,43 @@ public ConnectedRangesTriConstraintCollector(TriFunction> supplier() { - return () -> new ConnectedRangesCalculator<>(startMap, endMap, differenceFunction); + public @NonNull Supplier> supplier() { + return () -> new AbstractConnectedRangesSlot.State<>(startMap, endMap, differenceFunction); + } + + @Override + public @NonNull + Function, ConnectedRangeChain> + finisher() { + return AbstractConnectedRangesSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractConnectedRangesSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractConnectedRangesSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractConnectedRangesSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConsecutiveSequencesTriConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConsecutiveSequencesTriConstraintCollector.java index 25ec3075591..687c1c8703f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConsecutiveSequencesTriConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ConsecutiveSequencesTriConstraintCollector.java @@ -1,18 +1,20 @@ package ai.timefold.solver.core.impl.score.stream.collector.tri; import java.util.Objects; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToIntFunction; import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; -import ai.timefold.solver.core.impl.score.stream.collector.SequenceCalculator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractSequenceSlot; import org.jspecify.annotations.NonNull; final class ConsecutiveSequencesTriConstraintCollector extends - ObjectCalculatorTriCollector, Result_, SequenceCalculator> { + AbstractReferenceBasedTriCollector, AbstractSequenceSlot.State> { private final ToIntFunction indexMap; @@ -23,8 +25,41 @@ public ConsecutiveSequencesTriConstraintCollector(TriFunction } @Override - public @NonNull Supplier> supplier() { - return () -> new SequenceCalculator<>(indexMap); + public @NonNull Supplier> supplier() { + return () -> new AbstractSequenceSlot.State<>(indexMap); + } + + @Override + public @NonNull Function, SequenceChain> finisher() { + return AbstractSequenceSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractSequenceSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractSequenceSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractSequenceSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctTriCollector.java index fd848175312..1534a67a5a5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctTriCollector.java @@ -1,20 +1,56 @@ package ai.timefold.solver.core.impl.score.stream.collector.tri; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.LongDistinctCountCalculator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongDistinctSlot; import org.jspecify.annotations.NonNull; final class CountDistinctTriCollector - extends ObjectCalculatorTriCollector> { + extends + AbstractReferenceBasedTriCollector> { CountDistinctTriCollector(TriFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return LongDistinctCountCalculator::new; + public @NonNull Supplier> supplier() { + return AbstractLongDistinctSlot.State::new; + } + + @Override + public @NonNull Function, Long> finisher() { + return AbstractLongDistinctSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractLongDistinctSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongDistinctSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractLongDistinctSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountTriCollector.java index 949b11e4baf..f247887564c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountTriCollector.java @@ -3,13 +3,15 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LongCounter; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractCountSlot; +import ai.timefold.solver.core.impl.util.MutableLong; import org.jspecify.annotations.NonNull; -final class CountTriCollector implements TriConstraintCollector { +final class CountTriCollector implements TriConstraintCollector { private static final CountTriCollector INSTANCE = new CountTriCollector<>(); private CountTriCollector() { @@ -21,20 +23,40 @@ static CountTriCollector getInstance() { } @Override - public @NonNull Supplier supplier() { - return LongCounter::new; + public @NonNull Supplier supplier() { + return MutableLong::new; } @Override - public @NonNull QuadFunction accumulator() { - return (counter, a, b, c) -> { - counter.increment(); - return counter::decrement; - }; + public @NonNull TriConstraintCollectorAccumulator accumulator() { + return Slot::new; } @Override - public @NonNull Function finisher() { - return LongCounter::result; + public @NonNull Function finisher() { + return MutableLong::longValue; + } + + private static final class Slot extends AbstractCountSlot + implements TriConstraintCollectorValueHandle { + + Slot(MutableLong state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/InnerTriConstraintCollectors.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/InnerTriConstraintCollectors.java index 4788fbd939e..9028b862598 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/InnerTriConstraintCollectors.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/InnerTriConstraintCollectors.java @@ -24,9 +24,9 @@ import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceAverageSlot; -public class InnerTriConstraintCollectors { +public final class InnerTriConstraintCollectors { public static TriConstraintCollector average( ToLongTriFunction mapper) { return new AverageTriCollector<>(mapper); @@ -34,23 +34,23 @@ public class InnerTriConstraintCollectors { static TriConstraintCollector average( TriFunction mapper, - Supplier> calculatorSupplier) { - return new AverageReferenceTriCollector<>(mapper, calculatorSupplier); + Supplier> stateSupplier) { + return new AverageReferenceTriCollector<>(mapper, stateSupplier); } public static TriConstraintCollector averageBigDecimal( TriFunction mapper) { - return average(mapper, ReferenceAverageCalculator.bigDecimal()); + return average(mapper, AbstractReferenceAverageSlot.bigDecimalState()); } public static TriConstraintCollector averageBigInteger( TriFunction mapper) { - return average(mapper, ReferenceAverageCalculator.bigInteger()); + return average(mapper, AbstractReferenceAverageSlot.bigIntegerState()); } public static TriConstraintCollector averageDuration( TriFunction mapper) { - return average(mapper, ReferenceAverageCalculator.duration()); + return average(mapper, AbstractReferenceAverageSlot.durationState()); } public static @@ -106,12 +106,6 @@ public class InnerTriConstraintCollectors { return new MaxComparableTriCollector<>(mapper); } - public static TriConstraintCollector max( - TriFunction mapper, - Comparator comparator) { - return new MaxComparatorTriCollector<>(mapper, comparator); - } - public static > TriConstraintCollector max( TriFunction mapper, @@ -124,12 +118,6 @@ public class InnerTriConstraintCollectors { return new MinComparableTriCollector<>(mapper); } - public static TriConstraintCollector min( - TriFunction mapper, - Comparator comparator) { - return new MinComparatorTriCollector<>(mapper, comparator); - } - public static > TriConstraintCollector min( TriFunction mapper, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/LoadBalanceTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/LoadBalanceTriCollector.java index b1695e1d927..448269a3889 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/LoadBalanceTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/LoadBalanceTriCollector.java @@ -4,17 +4,19 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.function.ToLongTriFunction; import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LoadBalanceImpl; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLoadBalanceSlot; +import ai.timefold.solver.core.impl.score.stream.collector.DefaultLoadBalance; import org.jspecify.annotations.NonNull; final class LoadBalanceTriCollector - implements TriConstraintCollector, LoadBalance> { + implements TriConstraintCollector, LoadBalance> { private final TriFunction balancedItemFunction; private final ToLongTriFunction loadFunction; @@ -29,22 +31,17 @@ public LoadBalanceTriCollector(TriFunction balancedItemFunct } @Override - public @NonNull Supplier> supplier() { - return LoadBalanceImpl::new; + public @NonNull Supplier> supplier() { + return DefaultLoadBalance::new; } @Override - public @NonNull QuadFunction, A, B, C, Runnable> accumulator() { - return (balanceStatistics, a, b, c) -> { - var balanced = balancedItemFunction.apply(a, b, c); - var initialLoad = initialLoadFunction.applyAsLong(a, b, c); - var load = loadFunction.applyAsLong(a, b, c); - return balanceStatistics.registerBalanced(balanced, load, initialLoad); - }; + public @NonNull TriConstraintCollectorAccumulator, A, B, C> accumulator() { + return Slot::new; } @Override - public @NonNull Function, LoadBalance> finisher() { + public @NonNull Function, LoadBalance> finisher() { return balanceStatistics -> balanceStatistics; } @@ -60,4 +57,29 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(balancedItemFunction, loadFunction, initialLoadFunction); } + + private final class Slot extends AbstractLoadBalanceSlot + implements TriConstraintCollectorValueHandle { + + Slot(DefaultLoadBalance container) { + super(container); + } + + @Override + public void add(A a, B b, C c) { + addMapped(balancedItemFunction.apply(a, b, c), loadFunction.applyAsLong(a, b, c), + initialLoadFunction.applyAsLong(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(balancedItemFunction.apply(a, b, c), loadFunction.applyAsLong(a, b, c), + initialLoadFunction.applyAsLong(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/LongCalculatorTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/LongCalculatorTriCollector.java deleted file mode 100644 index b152afd6cb3..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/LongCalculatorTriCollector.java +++ /dev/null @@ -1,49 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.tri; - -import java.util.Objects; -import java.util.function.Function; - -import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.api.function.ToLongTriFunction; -import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LongCalculator; - -import org.jspecify.annotations.NonNull; - -abstract sealed class LongCalculatorTriCollector> - implements TriConstraintCollector permits AverageTriCollector, SumTriCollector { - private final ToLongTriFunction mapper; - - public LongCalculatorTriCollector(ToLongTriFunction mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull QuadFunction accumulator() { - return (calculator, a, b, c) -> { - final long mapped = mapper.applyAsLong(a, b, c); - calculator.insert(mapped); - return () -> calculator.retract(mapped); - }; - } - - @Override - public @NonNull Function finisher() { - return LongCalculator::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (LongCalculatorTriCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxComparableTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxComparableTriCollector.java index c394d1b210a..30921e7e962 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxComparableTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxComparableTriCollector.java @@ -1,20 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.tri; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MaxComparableTriCollector> - extends UndoableActionableTriCollector> { + extends AbstractReferenceBasedTriCollector> { MaxComparableTriCollector(TriFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return MinMaxUndoableActionable::maxCalculator; + public @NonNull Supplier> supplier() { + return AbstractMinMaxSlot::maxState; + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxComparatorTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxComparatorTriCollector.java deleted file mode 100644 index c84b9ed55ef..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxComparatorTriCollector.java +++ /dev/null @@ -1,43 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.tri; - -import java.util.Comparator; -import java.util.Objects; -import java.util.function.Supplier; - -import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; - -import org.jspecify.annotations.NonNull; - -final class MaxComparatorTriCollector - extends UndoableActionableTriCollector> { - private final Comparator comparator; - - MaxComparatorTriCollector(TriFunction mapper, - Comparator comparator) { - super(mapper); - this.comparator = comparator; - } - - @Override - public @NonNull Supplier> supplier() { - return () -> MinMaxUndoableActionable.maxCalculator(comparator); - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - if (!super.equals(object)) - return false; - MaxComparatorTriCollector that = (MaxComparatorTriCollector) object; - return Objects.equals(comparator, that.comparator); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), comparator); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxPropertyTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxPropertyTriCollector.java index dae34989ecd..32a186eb249 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxPropertyTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MaxPropertyTriCollector.java @@ -5,12 +5,13 @@ import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MaxPropertyTriCollector> - extends UndoableActionableTriCollector> { + extends AbstractReferenceBasedTriCollector> { private final Function propertyMapper; MaxPropertyTriCollector(TriFunction mapper, @@ -20,8 +21,41 @@ final class MaxPropertyTriCollector> supplier() { - return () -> MinMaxUndoableActionable.maxCalculator(propertyMapper); + public @NonNull Supplier> supplier() { + return () -> AbstractMinMaxSlot.maxState(propertyMapper); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinComparableTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinComparableTriCollector.java index 2f6b8f32cf6..b6120fe8b46 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinComparableTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinComparableTriCollector.java @@ -1,20 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.tri; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MinComparableTriCollector> - extends UndoableActionableTriCollector> { + extends AbstractReferenceBasedTriCollector> { MinComparableTriCollector(TriFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return MinMaxUndoableActionable::minCalculator; + public @NonNull Supplier> supplier() { + return AbstractMinMaxSlot::minState; + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinComparatorTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinComparatorTriCollector.java deleted file mode 100644 index dfaf20c3a0d..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinComparatorTriCollector.java +++ /dev/null @@ -1,43 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.tri; - -import java.util.Comparator; -import java.util.Objects; -import java.util.function.Supplier; - -import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; - -import org.jspecify.annotations.NonNull; - -final class MinComparatorTriCollector - extends UndoableActionableTriCollector> { - private final Comparator comparator; - - MinComparatorTriCollector(TriFunction mapper, - Comparator comparator) { - super(mapper); - this.comparator = comparator; - } - - @Override - public @NonNull Supplier> supplier() { - return () -> MinMaxUndoableActionable.minCalculator(comparator); - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - if (!super.equals(object)) - return false; - MinComparatorTriCollector that = (MinComparatorTriCollector) object; - return Objects.equals(comparator, that.comparator); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), comparator); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinPropertyTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinPropertyTriCollector.java index f2f8828c576..683be272110 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinPropertyTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/MinPropertyTriCollector.java @@ -5,12 +5,13 @@ import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MinPropertyTriCollector> - extends UndoableActionableTriCollector> { + extends AbstractReferenceBasedTriCollector> { private final Function propertyMapper; MinPropertyTriCollector(TriFunction mapper, @@ -20,8 +21,41 @@ final class MinPropertyTriCollector> supplier() { - return () -> MinMaxUndoableActionable.minCalculator(propertyMapper); + public @NonNull Supplier> supplier() { + return () -> AbstractMinMaxSlot.minState(propertyMapper); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ObjectCalculatorTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ObjectCalculatorTriCollector.java deleted file mode 100644 index 75736971a7b..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ObjectCalculatorTriCollector.java +++ /dev/null @@ -1,51 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.tri; - -import java.util.Objects; -import java.util.function.Function; - -import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.ObjectCalculator; - -import org.jspecify.annotations.NonNull; - -abstract sealed class ObjectCalculatorTriCollector> - implements TriConstraintCollector - permits AverageReferenceTriCollector, ConnectedRangesTriConstraintCollector, ConsecutiveSequencesTriConstraintCollector, - CountDistinctTriCollector, SumReferenceTriCollector { - protected final TriFunction mapper; - - public ObjectCalculatorTriCollector(TriFunction mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull QuadFunction accumulator() { - return (calculator, a, b, c) -> { - final var mapped = mapper.apply(a, b, c); - final var saved = calculator.insert(mapped); - return () -> calculator.retract(saved); - }; - } - - @Override - public @NonNull Function finisher() { - return ObjectCalculator::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (ObjectCalculatorTriCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/SumReferenceTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/SumReferenceTriCollector.java index 466c4c86578..ce67b96bd18 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/SumReferenceTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/SumReferenceTriCollector.java @@ -2,15 +2,18 @@ import java.util.Objects; import java.util.function.BinaryOperator; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceSumCalculator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceSumSlot; import org.jspecify.annotations.NonNull; final class SumReferenceTriCollector - extends ObjectCalculatorTriCollector> { + extends + AbstractReferenceBasedTriCollector> { private final Result_ zero; private final BinaryOperator adder; private final BinaryOperator subtractor; @@ -25,8 +28,41 @@ final class SumReferenceTriCollector } @Override - public @NonNull Supplier> supplier() { - return () -> new ReferenceSumCalculator<>(zero, adder, subtractor); + public @NonNull Supplier> supplier() { + return () -> new AbstractReferenceSumSlot.State<>(zero, adder, subtractor); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractReferenceSumSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractReferenceSumSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractReferenceSumSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractReferenceSumSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/SumTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/SumTriCollector.java index a533dc0af72..f2883e488d4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/SumTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/SumTriCollector.java @@ -1,19 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.tri; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.ToLongTriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.LongSumCalculator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongSumSlot; +import ai.timefold.solver.core.impl.util.MutableLong; import org.jspecify.annotations.NonNull; -final class SumTriCollector extends LongCalculatorTriCollector { +final class SumTriCollector + extends AbstractPrimitiveBasedTriCollector { SumTriCollector(ToLongTriFunction mapper) { super(mapper); } @Override - public @NonNull Supplier supplier() { - return LongSumCalculator::new; + public @NonNull Supplier supplier() { + return MutableLong::new; + } + + @Override + public @NonNull Function finisher() { + return MutableLong::longValue; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue(MutableLong state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongSumSlot + implements TriConstraintCollectorValueHandle { + Slot(MutableLong state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.applyAsLong(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.applyAsLong(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToCollectionTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToCollectionTriCollector.java index 3cf264f034c..43e9c87c49d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToCollectionTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToCollectionTriCollector.java @@ -2,17 +2,19 @@ import java.util.Collection; import java.util.Objects; +import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.CustomCollectionUndoableActionable; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToCollectionSlot; import org.jspecify.annotations.NonNull; final class ToCollectionTriCollector> extends - UndoableActionableTriCollector> { + AbstractReferenceBasedTriCollector> { private final IntFunction collectionFunction; ToCollectionTriCollector(TriFunction mapper, @@ -22,8 +24,41 @@ final class ToCollectionTriCollector> supplier() { - return () -> new CustomCollectionUndoableActionable<>(collectionFunction); + public @NonNull Supplier> supplier() { + return () -> new AbstractToCollectionSlot.State<>(collectionFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return state -> state.result(); + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractToCollectionSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToCollectionSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractToCollectionSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToListTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToListTriCollector.java index 3499eda6592..5e97eb8016d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToListTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToListTriCollector.java @@ -1,21 +1,56 @@ package ai.timefold.solver.core.impl.score.stream.collector.tri; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.ListUndoableActionable; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToListSlot; import org.jspecify.annotations.NonNull; final class ToListTriCollector - extends UndoableActionableTriCollector, ListUndoableActionable> { + extends AbstractReferenceBasedTriCollector, AbstractToListSlot.State> { ToListTriCollector(TriFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return ListUndoableActionable::new; + public @NonNull Supplier> supplier() { + return AbstractToListSlot.State::new; + } + + @Override + public @NonNull Function, List> finisher() { + return AbstractToListSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractToListSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToListSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractToListSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToMultiMapTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToMultiMapTriCollector.java index 318148f402c..7cb89455170 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToMultiMapTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToMultiMapTriCollector.java @@ -3,18 +3,19 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MapUndoableActionable; -import ai.timefold.solver.core.impl.util.Pair; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToMapSlot; import org.jspecify.annotations.NonNull; final class ToMultiMapTriCollector, Result_ extends Map> extends - UndoableActionableTriCollector, Result_, MapUndoableActionable> { + AbstractReferenceBasedTriCollector> { private final TriFunction keyFunction; private final TriFunction valueFunction; private final Supplier mapSupplier; @@ -24,7 +25,7 @@ final class ToMultiMapTriCollector valueFunction, Supplier mapSupplier, IntFunction setFunction) { - super((a, b, c) -> new Pair<>(keyFunction.apply(a, b, c), valueFunction.apply(a, b, c))); + super(keyFunction); this.keyFunction = keyFunction; this.valueFunction = valueFunction; this.mapSupplier = mapSupplier; @@ -32,8 +33,41 @@ final class ToMultiMapTriCollector> supplier() { - return () -> MapUndoableActionable.multiMap(mapSupplier, setFunction); + public @NonNull Supplier> supplier() { + return () -> AbstractToMapSlot.multiMapState(mapSupplier, setFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return state -> state.result(); + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractToMapSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToMapSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractToMapSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(keyFunction.apply(a, b, c), valueFunction.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(keyFunction.apply(a, b, c), valueFunction.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } // Don't call super equals/hashCode; the groupingFunction is calculated from keyFunction diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSetTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSetTriCollector.java index 8f8463cc51a..b363e03d7e7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSetTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSetTriCollector.java @@ -1,21 +1,56 @@ package ai.timefold.solver.core.impl.score.stream.collector.tri; import java.util.Set; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.SetUndoableActionable; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToSetSlot; import org.jspecify.annotations.NonNull; final class ToSetTriCollector - extends UndoableActionableTriCollector, SetUndoableActionable> { + extends AbstractReferenceBasedTriCollector, AbstractToSetSlot.State> { ToSetTriCollector(TriFunction mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return SetUndoableActionable::new; + public @NonNull Supplier> supplier() { + return AbstractToSetSlot.State::new; + } + + @Override + public @NonNull Function, Set> finisher() { + return AbstractToSetSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractToSetSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToSetSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractToSetSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSimpleMapTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSimpleMapTriCollector.java index d5d7f3e497b..524e50bd824 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSimpleMapTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSimpleMapTriCollector.java @@ -3,17 +3,18 @@ import java.util.Map; import java.util.Objects; import java.util.function.BinaryOperator; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.MapUndoableActionable; -import ai.timefold.solver.core.impl.util.Pair; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToMapSlot; import org.jspecify.annotations.NonNull; final class ToSimpleMapTriCollector> extends - UndoableActionableTriCollector, Result_, MapUndoableActionable> { + AbstractReferenceBasedTriCollector> { private final TriFunction keyFunction; private final TriFunction valueFunction; private final Supplier mapSupplier; @@ -23,7 +24,7 @@ final class ToSimpleMapTriCollector valueFunction, Supplier mapSupplier, BinaryOperator mergeFunction) { - super((a, b, c) -> new Pair<>(keyFunction.apply(a, b, c), valueFunction.apply(a, b, c))); + super(keyFunction); this.keyFunction = keyFunction; this.valueFunction = valueFunction; this.mapSupplier = mapSupplier; @@ -31,8 +32,41 @@ final class ToSimpleMapTriCollector> supplier() { - return () -> MapUndoableActionable.mergeMap(mapSupplier, mergeFunction); + public @NonNull Supplier> supplier() { + return () -> AbstractToMapSlot.mergeMapState(mapSupplier, mergeFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return state -> state.result(); + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractToMapSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToMapSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractToMapSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(keyFunction.apply(a, b, c), valueFunction.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(keyFunction.apply(a, b, c), valueFunction.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } // Don't call super equals/hashCode; the groupingFunction is calculated from keyFunction diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSortedSetComparatorTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSortedSetComparatorTriCollector.java index 09cd4573891..2e1a96e7dc7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSortedSetComparatorTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/ToSortedSetComparatorTriCollector.java @@ -3,15 +3,17 @@ import java.util.Comparator; import java.util.Objects; import java.util.SortedSet; +import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.impl.score.stream.collector.SortedSetUndoableActionable; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractSortedSetSlot; import org.jspecify.annotations.NonNull; final class ToSortedSetComparatorTriCollector - extends UndoableActionableTriCollector, SortedSetUndoableActionable> { + extends AbstractReferenceBasedTriCollector, AbstractSortedSetSlot.State> { private final Comparator comparator; ToSortedSetComparatorTriCollector(TriFunction mapper, @@ -21,8 +23,41 @@ final class ToSortedSetComparatorTriCollector } @Override - public @NonNull Supplier> supplier() { - return () -> SortedSetUndoableActionable.orderBy(comparator); + public @NonNull Supplier> supplier() { + return () -> new AbstractSortedSetSlot.State<>(comparator); + } + + @Override + public @NonNull Function, SortedSet> finisher() { + return AbstractSortedSetSlot.State::result; + } + + @Override + protected TriConstraintCollectorValueHandle newAccumulatedValue( + AbstractSortedSetSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractSortedSetSlot + implements TriConstraintCollectorValueHandle { + Slot(AbstractSortedSetSlot.State state) { + super(state); + } + + @Override + public void add(A a, B b, C c) { + addMapped(mapper.apply(a, b, c)); + } + + @Override + public void replaceWith(A a, B b, C c) { + replaceWithMapped(mapper.apply(a, b, c)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/TriCollectorUtils.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/TriCollectorUtils.java new file mode 100644 index 00000000000..ff31094c47e --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/TriCollectorUtils.java @@ -0,0 +1,58 @@ +package ai.timefold.solver.core.impl.score.stream.collector.tri; + +import ai.timefold.solver.core.api.function.QuadFunction; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public final class TriCollectorUtils { + + public static TriConstraintCollectorAccumulator + toIncremental(QuadFunction accumulator) { + if (accumulator instanceof TriConstraintCollectorAccumulator inc) { + return inc; + } + return new TriFromAccumulatorAdapter<>(accumulator); + } + + private record TriFromAccumulatorAdapter( + QuadFunction accumulator) + implements + TriConstraintCollectorAccumulator { + + @Override + public TriConstraintCollectorValueHandle intoGroup(ResultContainer_ container) { + return new TriValueHandle<>(accumulator, container); + } + } + + private static final class TriValueHandle + implements TriConstraintCollectorValueHandle { + private final QuadFunction accumulator; + private final ResultContainer_ container; + private @Nullable Runnable undo; + + TriValueHandle(QuadFunction accumulator, ResultContainer_ container) { + this.accumulator = accumulator; + this.container = container; + } + + @Override + public void add(@Nullable A a, @Nullable B b, @Nullable C c) { + undo = accumulator.apply(container, a, b, c); + } + + @Override + public void remove() { + undo.run(); + undo = null; + } + } + + private TriCollectorUtils() { + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/UndoableActionableTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/UndoableActionableTriCollector.java deleted file mode 100644 index 51464e0201f..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/UndoableActionableTriCollector.java +++ /dev/null @@ -1,51 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.tri; - -import java.util.Objects; -import java.util.function.Function; - -import ai.timefold.solver.core.api.function.QuadFunction; -import ai.timefold.solver.core.api.function.TriFunction; -import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.UndoableActionable; - -import org.jspecify.annotations.NonNull; - -abstract sealed class UndoableActionableTriCollector> - implements TriConstraintCollector - permits MaxComparableTriCollector, MaxComparatorTriCollector, MaxPropertyTriCollector, MinComparableTriCollector, - MinComparatorTriCollector, MinPropertyTriCollector, ToCollectionTriCollector, ToListTriCollector, - ToMultiMapTriCollector, ToSetTriCollector, ToSimpleMapTriCollector, ToSortedSetComparatorTriCollector { - private final TriFunction mapper; - - public UndoableActionableTriCollector(TriFunction mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull QuadFunction accumulator() { - return (calculator, a, b, c) -> { - final Input_ mapped = mapper.apply(a, b, c); - return calculator.insert(mapped); - }; - } - - @Override - public @NonNull Function finisher() { - return UndoableActionable::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (UndoableActionableTriCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AbstractPrimitiveBasedUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AbstractPrimitiveBasedUniCollector.java new file mode 100644 index 00000000000..e2b223cb198 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AbstractPrimitiveBasedUniCollector.java @@ -0,0 +1,42 @@ +package ai.timefold.solver.core.impl.score.stream.collector.uni; + +import java.util.Objects; +import java.util.function.ToLongFunction; + +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; + +import org.jspecify.annotations.NonNull; + +abstract class AbstractPrimitiveBasedUniCollector + implements UniConstraintCollector { + protected final ToLongFunction mapper; + + AbstractPrimitiveBasedUniCollector(ToLongFunction mapper) { + this.mapper = mapper; + } + + protected abstract UniConstraintCollectorValueHandle newAccumulatedValue(State_ state); + + @Override + public @NonNull UniConstraintCollectorAccumulator accumulator() { + return this::newAccumulatedValue; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (AbstractPrimitiveBasedUniCollector) object; + return Objects.equals(mapper, that.mapper); + } + + @Override + public int hashCode() { + return Objects.hash(mapper); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AbstractReferenceBasedUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AbstractReferenceBasedUniCollector.java new file mode 100644 index 00000000000..1d59cceaf9c --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AbstractReferenceBasedUniCollector.java @@ -0,0 +1,42 @@ +package ai.timefold.solver.core.impl.score.stream.collector.uni; + +import java.util.Objects; +import java.util.function.Function; + +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; + +import org.jspecify.annotations.NonNull; + +abstract class AbstractReferenceBasedUniCollector + implements UniConstraintCollector { + + protected final Function mapper; + + AbstractReferenceBasedUniCollector(Function mapper) { + this.mapper = Objects.requireNonNull(mapper); + } + + protected abstract UniConstraintCollectorValueHandle newAccumulatedValue(State_ state); + + @Override + public @NonNull UniConstraintCollectorAccumulator accumulator() { + return this::newAccumulatedValue; + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (AbstractReferenceBasedUniCollector) object; + return Objects.equals(mapper, that.mapper); + } + + @Override + public int hashCode() { + return Objects.hash(mapper); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AndThenUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AndThenUniCollector.java index 004dc9f5bf4..5f5db652ab1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AndThenUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AndThenUniCollector.java @@ -1,11 +1,11 @@ package ai.timefold.solver.core.impl.score.stream.collector.uni; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -15,11 +15,13 @@ final class AndThenUniCollector private final UniConstraintCollector delegate; private final Function mappingFunction; + private final UniConstraintCollectorAccumulator innerIncremental; AndThenUniCollector(UniConstraintCollector delegate, Function mappingFunction) { this.delegate = Objects.requireNonNull(delegate); this.mappingFunction = Objects.requireNonNull(mappingFunction); + this.innerIncremental = UniCollectorUtils.toIncremental(delegate.accumulator()); } @Override @@ -28,12 +30,12 @@ final class AndThenUniCollector } @Override - public @NonNull BiFunction accumulator() { - return delegate.accumulator(); + public @NonNull UniConstraintCollectorAccumulator accumulator() { + return innerIncremental; } @Override - public @Nullable Function finisher() { + public @NonNull Function finisher() { var finisher = delegate.finisher(); return container -> mappingFunction.apply(finisher.apply(container)); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AverageReferenceUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AverageReferenceUniCollector.java index cff3c9e63a6..1a728567e20 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AverageReferenceUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AverageReferenceUniCollector.java @@ -4,23 +4,59 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceAverageSlot; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; final class AverageReferenceUniCollector - extends ObjectCalculatorUniCollector> { - private final Supplier> calculatorSupplier; + extends + AbstractReferenceBasedUniCollector> { + private final Supplier> stateSupplier; AverageReferenceUniCollector(Function mapper, - Supplier> calculatorSupplier) { + Supplier> stateSupplier) { super(mapper); - this.calculatorSupplier = calculatorSupplier; + this.stateSupplier = stateSupplier; } @Override - public @NonNull Supplier> supplier() { - return calculatorSupplier; + public @NonNull Supplier> supplier() { + return stateSupplier; + } + + @Override + public @NonNull Function, @Nullable Average_> finisher() { + return AbstractReferenceAverageSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractReferenceAverageSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractReferenceAverageSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractReferenceAverageSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override @@ -32,11 +68,11 @@ public boolean equals(Object object) { if (!super.equals(object)) return false; AverageReferenceUniCollector that = (AverageReferenceUniCollector) object; - return Objects.equals(calculatorSupplier, that.calculatorSupplier); + return Objects.equals(stateSupplier, that.stateSupplier); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), calculatorSupplier); + return Objects.hash(super.hashCode(), stateSupplier); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AverageUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AverageUniCollector.java index da76625ebb5..7627a777b6a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AverageUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/AverageUniCollector.java @@ -1,19 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.uni; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToLongFunction; -import ai.timefold.solver.core.impl.score.stream.collector.LongAverageCalculator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongAverageSlot; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; -final class AverageUniCollector extends LongCalculatorUniCollector { +final class AverageUniCollector + extends AbstractPrimitiveBasedUniCollector { AverageUniCollector(ToLongFunction mapper) { super(mapper); } @Override - public @NonNull Supplier supplier() { - return LongAverageCalculator::new; + public @NonNull Supplier supplier() { + return AbstractLongAverageSlot.State::new; + } + + @Override + public @NonNull Function finisher() { + return AbstractLongAverageSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle newAccumulatedValue(AbstractLongAverageSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongAverageSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractLongAverageSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.applyAsLong(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.applyAsLong(a)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeFourUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeFourUniCollector.java index 36be7ecebd3..d9d226a44d6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeFourUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeFourUniCollector.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.collector.uni; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Quadruple; import org.jspecify.annotations.NonNull; @@ -26,10 +27,10 @@ final class ComposeFourUniCollector thirdSupplier; private final Supplier fourthSupplier; - private final BiFunction firstAccumulator; - private final BiFunction secondAccumulator; - private final BiFunction thirdAccumulator; - private final BiFunction fourthAccumulator; + private final UniConstraintCollectorAccumulator firstIncremental; + private final UniConstraintCollectorAccumulator secondIncremental; + private final UniConstraintCollectorAccumulator thirdIncremental; + private final UniConstraintCollectorAccumulator fourthIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -52,10 +53,10 @@ final class ComposeFourUniCollector, A, Runnable> + public @NonNull + UniConstraintCollectorAccumulator, A> accumulator() { - return (resultHolder, a) -> composeUndo(firstAccumulator.apply(resultHolder.a(), a), - secondAccumulator.apply(resultHolder.b(), a), - thirdAccumulator.apply(resultHolder.c(), a), - fourthAccumulator.apply(resultHolder.d(), a)); - } - - private static Runnable composeUndo(Runnable first, Runnable second, Runnable third, - Runnable fourth) { - return () -> { - first.run(); - second.run(); - third.run(); - fourth.run(); - }; + return ValueHandle::new; } @Override - public @Nullable Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> + finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.a()), secondFinisher.apply(resultHolder.b()), thirdFinisher.apply(resultHolder.c()), @@ -118,4 +108,42 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, third, fourth, composeFunction); } + + private final class ValueHandle implements UniConstraintCollectorValueHandle { + private final UniConstraintCollectorValueHandle v1; + private final UniConstraintCollectorValueHandle v2; + private final UniConstraintCollectorValueHandle v3; + private final UniConstraintCollectorValueHandle v4; + + ValueHandle(Quadruple container) { + this.v1 = firstIncremental.intoGroup(container.a()); + this.v2 = secondIncremental.intoGroup(container.b()); + this.v3 = thirdIncremental.intoGroup(container.c()); + this.v4 = fourthIncremental.intoGroup(container.d()); + } + + @Override + public void add(A a) { + v1.add(a); + v2.add(a); + v3.add(a); + v4.add(a); + } + + @Override + public void replaceWith(A a) { + v1.replaceWith(a); + v2.replaceWith(a); + v3.replaceWith(a); + v4.replaceWith(a); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + v3.remove(); + v4.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeThreeUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeThreeUniCollector.java index 932f8318460..7f41f044a13 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeThreeUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeThreeUniCollector.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.collector.uni; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Triple; import org.jspecify.annotations.NonNull; @@ -23,9 +24,9 @@ final class ComposeThreeUniCollector secondSupplier; private final Supplier thirdSupplier; - private final BiFunction firstAccumulator; - private final BiFunction secondAccumulator; - private final BiFunction thirdAccumulator; + private final UniConstraintCollectorAccumulator firstIncremental; + private final UniConstraintCollectorAccumulator secondIncremental; + private final UniConstraintCollectorAccumulator thirdIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -44,9 +45,9 @@ final class ComposeThreeUniCollector, A, Runnable> accumulator() { - return (resultHolder, a) -> composeUndo(firstAccumulator.apply(resultHolder.a(), a), - secondAccumulator.apply(resultHolder.b(), a), - thirdAccumulator.apply(resultHolder.c(), a)); - } - - private static Runnable composeUndo(Runnable first, Runnable second, Runnable third) { - return () -> { - first.run(); - second.run(); - third.run(); - }; + public @NonNull UniConstraintCollectorAccumulator, A> + accumulator() { + return ValueHandle::new; } @Override - public @Nullable Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.a()), secondFinisher.apply(resultHolder.b()), thirdFinisher.apply(resultHolder.c())); @@ -101,4 +93,37 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, third, composeFunction); } + + private final class ValueHandle implements UniConstraintCollectorValueHandle { + private final UniConstraintCollectorValueHandle v1; + private final UniConstraintCollectorValueHandle v2; + private final UniConstraintCollectorValueHandle v3; + + ValueHandle(Triple container) { + this.v1 = firstIncremental.intoGroup(container.a()); + this.v2 = secondIncremental.intoGroup(container.b()); + this.v3 = thirdIncremental.intoGroup(container.c()); + } + + @Override + public void add(A a) { + v1.add(a); + v2.add(a); + v3.add(a); + } + + @Override + public void replaceWith(A a) { + v1.replaceWith(a); + v2.replaceWith(a); + v3.replaceWith(a); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + v3.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeTwoUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeTwoUniCollector.java index 8033fe0d524..1781ae10961 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeTwoUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ComposeTwoUniCollector.java @@ -6,6 +6,8 @@ import java.util.function.Supplier; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.util.Pair; import org.jspecify.annotations.NonNull; @@ -20,8 +22,8 @@ final class ComposeTwoUniCollector firstSupplier; private final Supplier secondSupplier; - private final BiFunction firstAccumulator; - private final BiFunction secondAccumulator; + private final UniConstraintCollectorAccumulator firstIncremental; + private final UniConstraintCollectorAccumulator secondIncremental; private final Function firstFinisher; private final Function secondFinisher; @@ -36,8 +38,8 @@ final class ComposeTwoUniCollector, A, Runnable> accumulator() { - return (resultHolder, a) -> composeUndo(firstAccumulator.apply(resultHolder.key(), a), - secondAccumulator.apply(resultHolder.value(), a)); - } - - private static Runnable composeUndo(Runnable first, Runnable second) { - return () -> { - first.run(); - second.run(); - }; + public @NonNull UniConstraintCollectorAccumulator, A> accumulator() { + return ValueHandle::new; } @Override - public @Nullable Function, Result_> finisher() { + public @NonNull Function, @Nullable Result_> finisher() { return resultHolder -> composeFunction.apply(firstFinisher.apply(resultHolder.key()), secondFinisher.apply(resultHolder.value())); } @@ -82,4 +76,32 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(first, second, composeFunction); } + + private final class ValueHandle implements UniConstraintCollectorValueHandle { + private final UniConstraintCollectorValueHandle v1; + private final UniConstraintCollectorValueHandle v2; + + ValueHandle(Pair container) { + this.v1 = firstIncremental.intoGroup(container.key()); + this.v2 = secondIncremental.intoGroup(container.value()); + } + + @Override + public void add(A a) { + v1.add(a); + v2.add(a); + } + + @Override + public void replaceWith(A a) { + v1.replaceWith(a); + v2.replaceWith(a); + } + + @Override + public void remove() { + v1.remove(); + v2.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConditionalUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConditionalUniCollector.java index 610ef1c7329..8a5c449dc58 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConditionalUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConditionalUniCollector.java @@ -1,13 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.collector.uni; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -16,12 +16,12 @@ final class ConditionalUniCollector implements UniConstraintCollector { private final Predicate predicate; private final UniConstraintCollector delegate; - private final BiFunction innerAccumulator; + private final UniConstraintCollectorAccumulator innerIncremental; ConditionalUniCollector(Predicate predicate, UniConstraintCollector delegate) { this.predicate = predicate; this.delegate = delegate; - this.innerAccumulator = delegate.accumulator(); + this.innerIncremental = UniCollectorUtils.toIncremental(delegate.accumulator()); } @Override @@ -30,18 +30,12 @@ final class ConditionalUniCollector } @Override - public @NonNull BiFunction accumulator() { - return (resultContainer, a) -> { - if (predicate.test(a)) { - return innerAccumulator.apply(resultContainer, a); - } else { - return ConstantLambdaUtils.noop(); - } - }; + public @NonNull UniConstraintCollectorAccumulator accumulator() { + return ValueHandle::new; } @Override - public @Nullable Function finisher() { + public @NonNull Function finisher() { return delegate.finisher(); } @@ -59,4 +53,45 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(predicate, delegate); } + + private final class ValueHandle implements UniConstraintCollectorValueHandle { + private final UniConstraintCollectorValueHandle innerValue; + private boolean active = false; + + ValueHandle(ResultContainer_ container) { + this.innerValue = innerIncremental.intoGroup(container); + } + + @Override + public void add(A a) { + if (!predicate.test(a)) { + return; + } + active = true; + innerValue.add(a); + } + + @Override + public void replaceWith(A a) { + var nowActive = predicate.test(a); + if (active && nowActive) { + innerValue.replaceWith(a); + } else if (active) { + active = false; + innerValue.remove(); + } else if (nowActive) { + active = true; + innerValue.add(a); + } + } + + @Override + public void remove() { + if (!active) { + return; + } + active = false; + innerValue.remove(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConnectedRangesUniConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConnectedRangesUniConstraintCollector.java index e56fbe02b74..bae48035b6d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConnectedRangesUniConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConnectedRangesUniConstraintCollector.java @@ -6,14 +6,14 @@ import java.util.function.Supplier; import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain; -import ai.timefold.solver.core.impl.score.stream.collector.ConnectedRangesCalculator; -import ai.timefold.solver.core.impl.score.stream.collector.connected_ranges.Range; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractConnectedRangesSlot; import org.jspecify.annotations.NonNull; final class ConnectedRangesUniConstraintCollector, Difference_ extends Comparable> extends - ObjectCalculatorUniCollector, Range, ConnectedRangesCalculator> { + AbstractReferenceBasedUniCollector, AbstractConnectedRangesSlot.State> { private final Function startMap; private final Function endMap; @@ -29,8 +29,43 @@ public ConnectedRangesUniConstraintCollector(Function> supplier() { - return () -> new ConnectedRangesCalculator<>(startMap, endMap, differenceFunction); + public @NonNull Supplier> supplier() { + return () -> new AbstractConnectedRangesSlot.State<>(startMap, endMap, differenceFunction); + } + + @Override + public @NonNull + Function, ConnectedRangeChain> + finisher() { + return AbstractConnectedRangesSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractConnectedRangesSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractConnectedRangesSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractConnectedRangesSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConsecutiveSequencesUniConstraintCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConsecutiveSequencesUniConstraintCollector.java index 41b282952ea..be4b9e45a61 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConsecutiveSequencesUniConstraintCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ConsecutiveSequencesUniConstraintCollector.java @@ -1,17 +1,19 @@ package ai.timefold.solver.core.impl.score.stream.collector.uni; import java.util.Objects; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToIntFunction; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; -import ai.timefold.solver.core.impl.score.stream.collector.SequenceCalculator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractSequenceSlot; import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; import org.jspecify.annotations.NonNull; final class ConsecutiveSequencesUniConstraintCollector - extends ObjectCalculatorUniCollector, A, SequenceCalculator> { + extends AbstractReferenceBasedUniCollector, AbstractSequenceSlot.State> { private final ToIntFunction indexMap; @@ -21,8 +23,41 @@ public ConsecutiveSequencesUniConstraintCollector(ToIntFunction indexMap) { } @Override - public @NonNull Supplier> supplier() { - return () -> new SequenceCalculator<>(indexMap); + public @NonNull Supplier> supplier() { + return () -> new AbstractSequenceSlot.State<>(indexMap); + } + + @Override + public @NonNull Function, SequenceChain> finisher() { + return AbstractSequenceSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractSequenceSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractSequenceSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractSequenceSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctUniCollector.java index dde3c84cab8..22e2ef0ac0f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctUniCollector.java @@ -3,18 +3,52 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.LongDistinctCountCalculator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongDistinctSlot; import org.jspecify.annotations.NonNull; final class CountDistinctUniCollector - extends ObjectCalculatorUniCollector> { + extends AbstractReferenceBasedUniCollector> { CountDistinctUniCollector(Function mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return LongDistinctCountCalculator::new; + public @NonNull Supplier> supplier() { + return AbstractLongDistinctSlot.State::new; + } + + @Override + public @NonNull Function, Long> finisher() { + return AbstractLongDistinctSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractLongDistinctSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongDistinctSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractLongDistinctSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountUniCollector.java index 6cf469c6de3..cb48f4bc5c6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountUniCollector.java @@ -1,16 +1,17 @@ package ai.timefold.solver.core.impl.score.stream.collector.uni; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LongCounter; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractCountSlot; +import ai.timefold.solver.core.impl.util.MutableLong; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; -final class CountUniCollector implements UniConstraintCollector { +final class CountUniCollector implements UniConstraintCollector { private static final CountUniCollector INSTANCE = new CountUniCollector<>(); private CountUniCollector() { @@ -22,20 +23,40 @@ static CountUniCollector getInstance() { } @Override - public @NonNull Supplier supplier() { - return LongCounter::new; + public @NonNull Supplier supplier() { + return MutableLong::new; } @Override - public @NonNull BiFunction accumulator() { - return (counter, a) -> { - counter.increment(); - return counter::decrement; - }; + public @NonNull UniConstraintCollectorAccumulator accumulator() { + return Slot::new; } @Override - public @Nullable Function finisher() { - return LongCounter::result; + public @NonNull Function finisher() { + return MutableLong::longValue; + } + + private static final class Slot extends AbstractCountSlot + implements UniConstraintCollectorValueHandle { + + Slot(MutableLong state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/InnerUniConstraintCollectors.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/InnerUniConstraintCollectors.java index 3cc5848e306..bab54f17456 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/InnerUniConstraintCollectors.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/InnerUniConstraintCollectors.java @@ -25,7 +25,7 @@ import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceAverageSlot; public class InnerUniConstraintCollectors { public static UniConstraintCollector average(ToLongFunction mapper) { @@ -34,17 +34,17 @@ public class InnerUniConstraintCollectors { public static UniConstraintCollector averageBigDecimal( Function mapper) { - return new AverageReferenceUniCollector<>(mapper, ReferenceAverageCalculator.bigDecimal()); + return new AverageReferenceUniCollector<>(mapper, AbstractReferenceAverageSlot.bigDecimalState()); } public static UniConstraintCollector averageBigInteger( Function mapper) { - return new AverageReferenceUniCollector<>(mapper, ReferenceAverageCalculator.bigInteger()); + return new AverageReferenceUniCollector<>(mapper, AbstractReferenceAverageSlot.bigIntegerState()); } public static UniConstraintCollector averageDuration( Function mapper) { - return new AverageReferenceUniCollector<>(mapper, ReferenceAverageCalculator.duration()); + return new AverageReferenceUniCollector<>(mapper, AbstractReferenceAverageSlot.durationState()); } public static @@ -98,15 +98,8 @@ public static UniConstraintCollector(mapper); } - public static UniConstraintCollector max(Function mapper, - Comparator comparator) { - return new MaxComparatorUniCollector<>(mapper, comparator); - } - public static > UniConstraintCollector - max( - Function mapper, - Function propertyMapper) { + max(Function mapper, Function propertyMapper) { return new MaxPropertyUniCollector<>(mapper, propertyMapper); } @@ -115,15 +108,8 @@ public static UniConstraintCollector(mapper); } - public static UniConstraintCollector min(Function mapper, - Comparator comparator) { - return new MinComparatorUniCollector<>(mapper, comparator); - } - public static > UniConstraintCollector - min( - Function mapper, - Function propertyMapper) { + min(Function mapper, Function propertyMapper) { return new MinPropertyUniCollector<>(mapper, propertyMapper); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/LoadBalanceUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/LoadBalanceUniCollector.java index dc0511bceae..55ff43bd341 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/LoadBalanceUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/LoadBalanceUniCollector.java @@ -1,20 +1,21 @@ package ai.timefold.solver.core.impl.score.stream.collector.uni; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToLongFunction; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LoadBalanceImpl; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLoadBalanceSlot; +import ai.timefold.solver.core.impl.score.stream.collector.DefaultLoadBalance; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; final class LoadBalanceUniCollector - implements UniConstraintCollector, LoadBalance> { + implements UniConstraintCollector, LoadBalance> { private final Function balancedItemFunction; private final ToLongFunction loadFunction; @@ -28,22 +29,17 @@ public LoadBalanceUniCollector(Function balancedItemFunction, ToLo } @Override - public @NonNull Supplier> supplier() { - return LoadBalanceImpl::new; + public @NonNull Supplier> supplier() { + return DefaultLoadBalance::new; } @Override - public @NonNull BiFunction, A, Runnable> accumulator() { - return (balanceStatistics, a) -> { - var balanced = balancedItemFunction.apply(a); - var initialLoad = initialLoadFunction.applyAsLong(a); - var load = loadFunction.applyAsLong(a); - return balanceStatistics.registerBalanced(balanced, load, initialLoad); - }; + public @NonNull UniConstraintCollectorAccumulator, A> accumulator() { + return Slot::new; } @Override - public @Nullable Function, LoadBalance> finisher() { + public @NonNull Function, LoadBalance> finisher() { return balanceStatistics -> balanceStatistics; } @@ -59,4 +55,27 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(balancedItemFunction, loadFunction, initialLoadFunction); } + + private final class Slot extends AbstractLoadBalanceSlot + implements UniConstraintCollectorValueHandle { + + Slot(DefaultLoadBalance container) { + super(container); + } + + @Override + public void add(A a) { + addMapped(balancedItemFunction.apply(a), loadFunction.applyAsLong(a), initialLoadFunction.applyAsLong(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(balancedItemFunction.apply(a), loadFunction.applyAsLong(a), initialLoadFunction.applyAsLong(a)); + } + + @Override + public void remove() { + removeMapped(); + } + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/LongCalculatorUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/LongCalculatorUniCollector.java deleted file mode 100644 index d279307115c..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/LongCalculatorUniCollector.java +++ /dev/null @@ -1,50 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.uni; - -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.ToLongFunction; - -import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.LongCalculator; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -abstract sealed class LongCalculatorUniCollector> - implements UniConstraintCollector permits AverageUniCollector, SumUniCollector { - private final ToLongFunction mapper; - - public LongCalculatorUniCollector(ToLongFunction mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull BiFunction accumulator() { - return (calculator, a) -> { - final long mapped = mapper.applyAsLong(a); - calculator.insert(mapped); - return () -> calculator.retract(mapped); - }; - } - - @Override - public @Nullable Function finisher() { - return LongCalculator::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (LongCalculatorUniCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxComparableUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxComparableUniCollector.java index 2659a1b83e0..46ee6e68084 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxComparableUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxComparableUniCollector.java @@ -3,18 +3,52 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MaxComparableUniCollector> - extends UndoableActionableUniCollector> { + extends AbstractReferenceBasedUniCollector> { MaxComparableUniCollector(Function mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return MinMaxUndoableActionable::maxCalculator; + public @NonNull Supplier> supplier() { + return AbstractMinMaxSlot::maxState; + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxComparatorUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxComparatorUniCollector.java deleted file mode 100644 index a1575520f87..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxComparatorUniCollector.java +++ /dev/null @@ -1,42 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.uni; - -import java.util.Comparator; -import java.util.Objects; -import java.util.function.Function; -import java.util.function.Supplier; - -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; - -import org.jspecify.annotations.NonNull; - -final class MaxComparatorUniCollector - extends UndoableActionableUniCollector> { - private final Comparator comparator; - - MaxComparatorUniCollector(Function mapper, Comparator comparator) { - super(mapper); - this.comparator = comparator; - } - - @Override - public @NonNull Supplier> supplier() { - return () -> MinMaxUndoableActionable.maxCalculator(comparator); - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - if (!super.equals(object)) - return false; - MaxComparatorUniCollector that = (MaxComparatorUniCollector) object; - return Objects.equals(comparator, that.comparator); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), comparator); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxPropertyUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxPropertyUniCollector.java index d78f39af0d0..aca5f9bc9c5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxPropertyUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MaxPropertyUniCollector.java @@ -4,12 +4,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MaxPropertyUniCollector> - extends UndoableActionableUniCollector> { + extends AbstractReferenceBasedUniCollector> { private final Function propertyMapper; MaxPropertyUniCollector(Function mapper, @@ -19,8 +20,41 @@ final class MaxPropertyUniCollector> supplier() { - return () -> MinMaxUndoableActionable.maxCalculator(propertyMapper); + public @NonNull Supplier> supplier() { + return () -> AbstractMinMaxSlot.maxState(propertyMapper); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinComparableUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinComparableUniCollector.java index 2c7c6b27be4..70c6a474023 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinComparableUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinComparableUniCollector.java @@ -3,18 +3,52 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MinComparableUniCollector> - extends UndoableActionableUniCollector> { + extends AbstractReferenceBasedUniCollector> { MinComparableUniCollector(Function mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return MinMaxUndoableActionable::minCalculator; + public @NonNull Supplier> supplier() { + return AbstractMinMaxSlot::minState; + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinComparatorUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinComparatorUniCollector.java deleted file mode 100644 index 80df53e451e..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinComparatorUniCollector.java +++ /dev/null @@ -1,42 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.uni; - -import java.util.Comparator; -import java.util.Objects; -import java.util.function.Function; -import java.util.function.Supplier; - -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; - -import org.jspecify.annotations.NonNull; - -final class MinComparatorUniCollector - extends UndoableActionableUniCollector> { - private final Comparator comparator; - - MinComparatorUniCollector(Function mapper, Comparator comparator) { - super(mapper); - this.comparator = comparator; - } - - @Override - public @NonNull Supplier> supplier() { - return () -> MinMaxUndoableActionable.minCalculator(comparator); - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - if (!super.equals(object)) - return false; - MinComparatorUniCollector that = (MinComparatorUniCollector) object; - return Objects.equals(comparator, that.comparator); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), comparator); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinPropertyUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinPropertyUniCollector.java index 3d461845b65..4cae7be5f88 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinPropertyUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/MinPropertyUniCollector.java @@ -4,12 +4,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MinMaxUndoableActionable; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractMinMaxSlot; import org.jspecify.annotations.NonNull; final class MinPropertyUniCollector> - extends UndoableActionableUniCollector> { + extends AbstractReferenceBasedUniCollector> { private final Function propertyMapper; MinPropertyUniCollector(Function mapper, @@ -19,8 +20,41 @@ final class MinPropertyUniCollector> supplier() { - return () -> MinMaxUndoableActionable.minCalculator(propertyMapper); + public @NonNull Supplier> supplier() { + return () -> AbstractMinMaxSlot.minState(propertyMapper); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractMinMaxSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractMinMaxSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractMinMaxSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractMinMaxSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ObjectCalculatorUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ObjectCalculatorUniCollector.java deleted file mode 100644 index 48f9f09f28c..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ObjectCalculatorUniCollector.java +++ /dev/null @@ -1,52 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.uni; - -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; - -import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.ObjectCalculator; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -abstract sealed class ObjectCalculatorUniCollector> - implements UniConstraintCollector - permits AverageReferenceUniCollector, ConnectedRangesUniConstraintCollector, ConsecutiveSequencesUniConstraintCollector, - CountDistinctUniCollector, SumReferenceUniCollector { - - protected final Function mapper; - - public ObjectCalculatorUniCollector(Function mapper) { - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public @NonNull BiFunction accumulator() { - return (calculator, a) -> { - final var mapped = mapper.apply(a); - final var saved = calculator.insert(mapped); - return () -> calculator.retract(saved); - }; - } - - @Override - public @Nullable Function finisher() { - return ObjectCalculator::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (ObjectCalculatorUniCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/SumReferenceUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/SumReferenceUniCollector.java index b0f7ec014a4..2617564e3bd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/SumReferenceUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/SumReferenceUniCollector.java @@ -5,12 +5,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.ReferenceSumCalculator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractReferenceSumSlot; import org.jspecify.annotations.NonNull; final class SumReferenceUniCollector - extends ObjectCalculatorUniCollector> { + extends AbstractReferenceBasedUniCollector> { private final Result_ zero; private final BinaryOperator adder; private final BinaryOperator subtractor; @@ -24,8 +25,41 @@ final class SumReferenceUniCollector } @Override - public @NonNull Supplier> supplier() { - return () -> new ReferenceSumCalculator<>(zero, adder, subtractor); + public @NonNull Supplier> supplier() { + return () -> new AbstractReferenceSumSlot.State<>(zero, adder, subtractor); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractReferenceSumSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractReferenceSumSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractReferenceSumSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractReferenceSumSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/SumUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/SumUniCollector.java index eb057f06b6d..f2e97f22611 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/SumUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/SumUniCollector.java @@ -1,19 +1,55 @@ package ai.timefold.solver.core.impl.score.stream.collector.uni; +import java.util.function.Function; import java.util.function.Supplier; import java.util.function.ToLongFunction; -import ai.timefold.solver.core.impl.score.stream.collector.LongSumCalculator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractLongSumSlot; +import ai.timefold.solver.core.impl.util.MutableLong; import org.jspecify.annotations.NonNull; -final class SumUniCollector extends LongCalculatorUniCollector { +final class SumUniCollector extends AbstractPrimitiveBasedUniCollector { SumUniCollector(ToLongFunction mapper) { super(mapper); } @Override - public @NonNull Supplier supplier() { - return LongSumCalculator::new; + public @NonNull Supplier supplier() { + return MutableLong::new; + } + + @Override + public @NonNull Function finisher() { + return MutableLong::longValue; + } + + @Override + protected UniConstraintCollectorValueHandle newAccumulatedValue(MutableLong state) { + return new Slot(state); + } + + private final class Slot extends AbstractLongSumSlot + implements UniConstraintCollectorValueHandle { + + Slot(MutableLong state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.applyAsLong(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.applyAsLong(a)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToCollectionUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToCollectionUniCollector.java index 2ce729b69ed..b951b159a35 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToCollectionUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToCollectionUniCollector.java @@ -6,12 +6,13 @@ import java.util.function.IntFunction; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.CustomCollectionUndoableActionable; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToCollectionSlot; import org.jspecify.annotations.NonNull; final class ToCollectionUniCollector> - extends UndoableActionableUniCollector> { + extends AbstractReferenceBasedUniCollector> { private final IntFunction collectionFunction; ToCollectionUniCollector(Function mapper, @@ -21,8 +22,41 @@ final class ToCollectionUniCollector> supplier() { - return () -> new CustomCollectionUndoableActionable<>(collectionFunction); + public @NonNull Supplier> supplier() { + return () -> new AbstractToCollectionSlot.State<>(collectionFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractToCollectionSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractToCollectionSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToCollectionSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractToCollectionSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToListUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToListUniCollector.java index 97b31dd209a..72fa1f8ef6d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToListUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToListUniCollector.java @@ -4,18 +4,51 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.ListUndoableActionable; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToListSlot; import org.jspecify.annotations.NonNull; final class ToListUniCollector - extends UndoableActionableUniCollector, ListUndoableActionable> { + extends AbstractReferenceBasedUniCollector, AbstractToListSlot.State> { ToListUniCollector(Function mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return ListUndoableActionable::new; + public @NonNull Supplier> supplier() { + return AbstractToListSlot.State::new; + } + + @Override + public @NonNull Function, List> finisher() { + return AbstractToListSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle newAccumulatedValue(AbstractToListSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToListSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractToListSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToMultiMapUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToMultiMapUniCollector.java index 1d64100c3f7..5658200d69f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToMultiMapUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToMultiMapUniCollector.java @@ -7,14 +7,13 @@ import java.util.function.IntFunction; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MapUndoableActionable; -import ai.timefold.solver.core.impl.util.Pair; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToMapSlot; import org.jspecify.annotations.NonNull; final class ToMultiMapUniCollector, Result_ extends Map> - extends - UndoableActionableUniCollector, Result_, MapUndoableActionable> { + extends AbstractReferenceBasedUniCollector> { private final Function keyFunction; private final Function valueFunction; private final Supplier mapSupplier; @@ -24,7 +23,7 @@ final class ToMultiMapUniCollector, Re Function valueFunction, Supplier mapSupplier, IntFunction setFunction) { - super(a -> new Pair<>(keyFunction.apply(a), valueFunction.apply(a))); + super(keyFunction); this.keyFunction = keyFunction; this.valueFunction = valueFunction; this.mapSupplier = mapSupplier; @@ -32,8 +31,41 @@ final class ToMultiMapUniCollector, Re } @Override - public @NonNull Supplier> supplier() { - return () -> MapUndoableActionable.multiMap(mapSupplier, setFunction); + public @NonNull Supplier> supplier() { + return () -> AbstractToMapSlot.multiMapState(mapSupplier, setFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractToMapSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractToMapSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToMapSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractToMapSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(keyFunction.apply(a), valueFunction.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(keyFunction.apply(a), valueFunction.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } // Don't call super equals/hashCode; the groupingFunction is calculated from keyFunction diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSetUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSetUniCollector.java index 0463d42ae20..d2aa19ddeb6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSetUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSetUniCollector.java @@ -4,18 +4,52 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.SetUndoableActionable; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToSetSlot; import org.jspecify.annotations.NonNull; final class ToSetUniCollector - extends UndoableActionableUniCollector, SetUndoableActionable> { + extends AbstractReferenceBasedUniCollector, AbstractToSetSlot.State> { ToSetUniCollector(Function mapper) { super(mapper); } @Override - public @NonNull Supplier> supplier() { - return SetUndoableActionable::new; + public @NonNull Supplier> supplier() { + return AbstractToSetSlot.State::new; + } + + @Override + public @NonNull Function, Set> finisher() { + return AbstractToSetSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractToSetSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToSetSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractToSetSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSimpleMapUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSimpleMapUniCollector.java index d537d249103..be157442af3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSimpleMapUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSimpleMapUniCollector.java @@ -6,14 +6,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.MapUndoableActionable; -import ai.timefold.solver.core.impl.util.Pair; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractToMapSlot; import org.jspecify.annotations.NonNull; final class ToSimpleMapUniCollector> - extends - UndoableActionableUniCollector, Result_, MapUndoableActionable> { + extends AbstractReferenceBasedUniCollector> { private final Function keyFunction; private final Function valueFunction; private final Supplier mapSupplier; @@ -23,7 +22,7 @@ final class ToSimpleMapUniCollector valueFunction, Supplier mapSupplier, BinaryOperator mergeFunction) { - super(a -> new Pair<>(keyFunction.apply(a), valueFunction.apply(a))); + super(keyFunction); this.keyFunction = keyFunction; this.valueFunction = valueFunction; this.mapSupplier = mapSupplier; @@ -31,8 +30,41 @@ final class ToSimpleMapUniCollector> supplier() { - return () -> MapUndoableActionable.mergeMap(mapSupplier, mergeFunction); + public @NonNull Supplier> supplier() { + return () -> AbstractToMapSlot.mergeMapState(mapSupplier, mergeFunction); + } + + @Override + public @NonNull Function, Result_> finisher() { + return AbstractToMapSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractToMapSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractToMapSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractToMapSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(keyFunction.apply(a), valueFunction.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(keyFunction.apply(a), valueFunction.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } // Don't call super equals/hashCode; the groupingFunction is calculated from keyFunction diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSortedSetComparatorUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSortedSetComparatorUniCollector.java index 720a50fda51..07b5ab9836e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSortedSetComparatorUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/ToSortedSetComparatorUniCollector.java @@ -6,12 +6,13 @@ import java.util.function.Function; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.score.stream.collector.SortedSetUndoableActionable; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; +import ai.timefold.solver.core.impl.score.stream.collector.AbstractSortedSetSlot; import org.jspecify.annotations.NonNull; final class ToSortedSetComparatorUniCollector - extends UndoableActionableUniCollector, SortedSetUndoableActionable> { + extends AbstractReferenceBasedUniCollector, AbstractSortedSetSlot.State> { private final Comparator comparator; ToSortedSetComparatorUniCollector(Function mapper, @@ -21,8 +22,41 @@ final class ToSortedSetComparatorUniCollector } @Override - public @NonNull Supplier> supplier() { - return () -> SortedSetUndoableActionable.orderBy(comparator); + public @NonNull Supplier> supplier() { + return () -> new AbstractSortedSetSlot.State<>(comparator); + } + + @Override + public @NonNull Function, SortedSet> finisher() { + return AbstractSortedSetSlot.State::result; + } + + @Override + protected UniConstraintCollectorValueHandle + newAccumulatedValue(AbstractSortedSetSlot.State state) { + return new Slot(state); + } + + private final class Slot extends AbstractSortedSetSlot + implements UniConstraintCollectorValueHandle { + Slot(AbstractSortedSetSlot.State state) { + super(state); + } + + @Override + public void add(A a) { + addMapped(mapper.apply(a)); + } + + @Override + public void replaceWith(A a) { + replaceWithMapped(mapper.apply(a)); + } + + @Override + public void remove() { + removeMapped(); + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/UndoableActionableUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/UndoableActionableUniCollector.java deleted file mode 100644 index 947fc6f22d0..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/UndoableActionableUniCollector.java +++ /dev/null @@ -1,51 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.uni; - -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; - -import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.UndoableActionable; - -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -abstract sealed class UndoableActionableUniCollector> - implements UniConstraintCollector - permits MaxComparableUniCollector, MaxComparatorUniCollector, MaxPropertyUniCollector, MinComparableUniCollector, - MinComparatorUniCollector, MinPropertyUniCollector, ToCollectionUniCollector, ToListUniCollector, - ToMultiMapUniCollector, ToSetUniCollector, ToSimpleMapUniCollector, ToSortedSetComparatorUniCollector { - private final Function mapper; - - public UndoableActionableUniCollector(Function mapper) { - this.mapper = mapper; - } - - @Override - public @NonNull BiFunction accumulator() { - return (calculator, a) -> { - final Input_ mapped = mapper.apply(a); - return calculator.insert(mapped); - }; - } - - @Override - public @Nullable Function finisher() { - return UndoableActionable::result; - } - - @Override - public boolean equals(Object object) { - if (this == object) - return true; - if (object == null || getClass() != object.getClass()) - return false; - var that = (UndoableActionableUniCollector) object; - return Objects.equals(mapper, that.mapper); - } - - @Override - public int hashCode() { - return Objects.hash(mapper); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/UniCollectorUtils.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/UniCollectorUtils.java new file mode 100644 index 00000000000..81a4bdd8976 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/UniCollectorUtils.java @@ -0,0 +1,55 @@ +package ai.timefold.solver.core.impl.score.stream.collector.uni; + +import java.util.function.BiFunction; + +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public final class UniCollectorUtils { + + public static UniConstraintCollectorAccumulator + toIncremental(BiFunction accumulator) { + if (accumulator instanceof UniConstraintCollectorAccumulator inc) { + return inc; + } + return new UniFromAccumulatorAdapter<>(accumulator); + } + + private record UniFromAccumulatorAdapter(BiFunction accumulator) + implements + UniConstraintCollectorAccumulator { + + @Override + public UniConstraintCollectorValueHandle intoGroup(ResultContainer_ container) { + return new UniValueHandle<>(accumulator, container); + } + } + + private static final class UniValueHandle + implements UniConstraintCollectorValueHandle { + private final BiFunction accumulator; + private final ResultContainer_ container; + private @Nullable Runnable undo; + + UniValueHandle(BiFunction accumulator, ResultContainer_ container) { + this.accumulator = accumulator; + this.container = container; + } + + @Override + public void add(@Nullable A a) { + undo = accumulator.apply(container, a); + } + + @Override + public void remove() { + undo.run(); + undo = null; + } + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/util/MutableLong.java b/core/src/main/java/ai/timefold/solver/core/impl/util/MutableLong.java index 8f7a71884cd..17920e51b37 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/util/MutableLong.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/util/MutableLong.java @@ -27,12 +27,12 @@ public long decrement() { } public long add(long addend) { - value += addend; + value = Math.addExact(value, addend); return value; } public long subtract(long subtrahend) { - value -= subtrahend; + value = Math.subtractExact(value, subtrahend); return value; } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetAdvancedGroupByConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetAdvancedGroupByConstraintStreamTest.java index d2387082705..a769ce3c0d4 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetAdvancedGroupByConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetAdvancedGroupByConstraintStreamTest.java @@ -1,35 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.bavet; -import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.count; -import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.countBi; -import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.countQuad; -import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.countTri; -import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.sum; -import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.toMap; -import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.toSet; -import static ai.timefold.solver.core.api.score.stream.Joiners.equal; -import static ai.timefold.solver.core.api.score.stream.Joiners.filtering; -import static ai.timefold.solver.core.testutil.PlannerTestUtils.asMap; -import static org.assertj.core.api.Assertions.assertThatCode; - -import java.util.Arrays; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Stream; - -import ai.timefold.solver.core.api.score.SimpleScore; -import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; -import ai.timefold.solver.core.api.score.stream.Joiners; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.stream.common.AbstractAdvancedGroupByConstraintStreamTest; -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; - -import org.junit.jupiter.api.TestTemplate; final class BavetAdvancedGroupByConstraintStreamTest extends AbstractAdvancedGroupByConstraintStreamTest { @@ -37,553 +9,4 @@ public BavetAdvancedGroupByConstraintStreamTest(ConstraintMatchPolicy constraint super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } - @TestTemplate - void collectedDowngradedAndFiltered() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 7); - TestdataLavishEntity entity = new TestdataLavishEntity("MyEntity 1", solution.getFirstEntityGroup(), - solution.getFirstValue()); - solution.getEntityList().add(entity); - - InnerScoreDirector scoreDirector = buildScoreDirector( - factory -> factory.forEach(TestdataLavishEntity.class) - .groupBy(e -> e.getCode().substring(0, 1), count()) - .groupBy(Pair::new) - .filter(pair -> !pair.key().equals("G")) - .penalize(SimpleScore.ONE, Pair::value) - .asConstraint(TEST_CONSTRAINT_ID)); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatch(new Pair<>("M", 1L))); - - // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector); - } - - @TestTemplate - void collectedAndFiltered() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 5, 1, 7); - TestdataLavishEntityGroup entityGroup1 = new TestdataLavishEntityGroup("MyEntityGroup"); - solution.getEntityGroupList().add(entityGroup1); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup1, solution.getFirstValue()); - solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", entityGroup1, solution.getFirstValue()); - solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), - solution.getFirstValue()); - solution.getEntityList().add(entity3); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) - .groupBy(count()) - .filter(count -> count == 10) - .penalize(SimpleScore.ONE, i -> i) - .asConstraint(TEST_CONSTRAINT_ID)); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-10, 10L)); - - // Incremental - Stream.of(entity1, entity2).forEach(entity -> { - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - }); - assertScore(scoreDirector); // There is less than 10 entities, and therefore there are no penalties. - } - - @TestTemplate - void collectedFilteredRecollected() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 2); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) - .groupBy(toSet()) - .groupBy(sum(Set::size)) - .penalize(SimpleScore.ONE, count -> count) - .asConstraint(TEST_CONSTRAINT_ID)); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-2, 2L)); - - // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L)); - } - - @TestTemplate - void uniGroupByRecollected() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 2); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) - .groupBy(TestdataLavishEntity::getEntityGroup) - .groupBy(toSet()) - .penalize(SimpleScore.ONE, Set::size) - .asConstraint(TEST_CONSTRAINT_ID)); - - TestdataLavishEntity entity1 = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-2, asSet(entity1.getEntityGroup(), entity2.getEntityGroup()))); - - // Incremental - scoreDirector.beforeEntityRemoved(entity1); - solution.getEntityList().remove(entity1); - scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, - assertMatchWithScore(-1, asSet(entity2.getEntityGroup()))); - } - - @TestTemplate - void biGroupByRecollected() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 3, 2, 5); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory - .forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) - // Stream of all unique entity bi tuples that share a group - .groupBy((a, b) -> a.getEntityGroup(), countBi()) - .groupBy(ConstraintCollectors.toList((a, b) -> a)) - .penalize(SimpleScore.ONE) - .asConstraint(TEST_CONSTRAINT_ID)); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-1, - Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1)))); - - // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, - assertMatchWithScore(-1, - Arrays.asList(solution.getEntityGroupList().get(1), solution.getFirstEntityGroup()))); - } - - @TestTemplate - void triGroupByRecollected() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 3, 2, 6); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory - .forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) - .join(TestdataLavishEntity.class, - equal((a, b) -> a.getEntityGroup(), TestdataLavishEntity::getEntityGroup), - filtering((a, b, c) -> !Objects.equals(a, c) && !Objects.equals(b, c))) - // Stream of all unique entity tri tuples that share a group - .groupBy((a, b, c) -> a.getEntityGroup(), countTri()) - .groupBy(toMap((g, c) -> g, (g, c) -> c, Long::sum)) - .penalize(SimpleScore.ONE) - .asConstraint(TEST_CONSTRAINT_ID)); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-1, - asMap(solution.getFirstEntityGroup(), 3L, solution.getEntityGroupList().get(1), 3L))); - - // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, - assertMatchWithScore(-1, - asMap(solution.getEntityGroupList().get(1), 3L))); - } - - @TestTemplate - void quadGroupByRecollected() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 3, 2, 8); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory - .forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) - .join(TestdataLavishEntity.class, - equal((a, b) -> a.getEntityGroup(), TestdataLavishEntity::getEntityGroup), - filtering((a, b, c) -> !Objects.equals(a, c) && !Objects.equals(b, c))) - .join(TestdataLavishEntity.class, - equal((a, b, c) -> a.getEntityGroup(), TestdataLavishEntity::getEntityGroup), - filtering((a, b, c, d) -> !Objects.equals(a, d) && !Objects.equals(b, d) - && !Objects.equals(c, d))) - // Stream of all unique entity quad tuples that share a group - .groupBy((a, b, c, d) -> a.getEntityGroup(), countQuad()) - .groupBy(toMap((g, c) -> g, (g, c) -> c, Long::sum)) - .penalize(SimpleScore.ONE) - .asConstraint(TEST_CONSTRAINT_ID)); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-1, - asMap(solution.getFirstEntityGroup(), 12L, solution.getEntityGroupList().get(1), 12L))); - - // Incremental - TestdataLavishEntity entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, - assertMatchWithScore(-1, - asMap(solution.getEntityGroupList().get(1), 12L))); - } - - @TestTemplate - void biGroupByRegrouped() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 4); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory - .forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) - .groupBy((a, b) -> a.getEntityGroup()) - .groupBy(Function.identity(), count()) - .penalize(SimpleScore.ONE, (group, count) -> count) - .asConstraint(TEST_CONSTRAINT_ID)); - - TestdataLavishEntity entity = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-1, entity.getEntityGroup(), 1L), - assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); - - // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); - } - - @TestTemplate - void triGroupByRegrouped() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 6); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory - .forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) - .join(TestdataLavishEntity.class, - equal((a, b) -> a.getEntityGroup(), TestdataLavishEntity::getEntityGroup), - filtering((a, b, c) -> !Objects.equals(a, c) && !Objects.equals(b, c))) - // Stream of all unique entity tri tuples that share a group - .groupBy((a, b, c) -> a.getEntityGroup()) - .groupBy(Function.identity(), count()) - .penalize(SimpleScore.ONE, (group, count) -> count) - .asConstraint(TEST_CONSTRAINT_ID)); - - TestdataLavishEntity entity = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-1, entity.getEntityGroup(), 1L), - assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); - - // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); - } - - @TestTemplate - void quadGroupByRegrouped() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 8); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory - .forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) - .join(TestdataLavishEntity.class, - equal((a, b) -> a.getEntityGroup(), TestdataLavishEntity::getEntityGroup), - filtering((a, b, c) -> !Objects.equals(a, c) && !Objects.equals(b, c))) - .join(TestdataLavishEntity.class, - equal((a, b, c) -> a.getEntityGroup(), TestdataLavishEntity::getEntityGroup), - filtering((a, b, c, d) -> !Objects.equals(a, d) && !Objects.equals(b, d) - && !Objects.equals(c, d))) - // Stream of all unique entity quad tuples that share a group - .groupBy((a, b, c, d) -> a.getEntityGroup()) - .groupBy(Function.identity(), count()) - .penalize(SimpleScore.ONE, (group, count) -> count) - .asConstraint(TEST_CONSTRAINT_ID)); - - TestdataLavishEntity entity = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-1, entity.getEntityGroup(), 1L), - assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); - - // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); - } - - @TestTemplate - void biGroupByRegroupedDouble() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 4); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory - .forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) - .groupBy((a, b) -> a.getEntityGroup()) - .groupBy(Function.identity(), count()) - .groupBy((group, count) -> group.toString(), countBi()) - .penalize(SimpleScore.ONE, (groupName, count) -> count) - .asConstraint(TEST_CONSTRAINT_ID)); - - TestdataLavishEntity entity = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }), - assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); - - // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); - } - - @TestTemplate - void triGroupByRegroupedDouble() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 6); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory - .forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) - .join(TestdataLavishEntity.class, - equal((a, b) -> a.getEntityGroup(), TestdataLavishEntity::getEntityGroup), - filtering((a, b, c) -> !Objects.equals(a, c) && !Objects.equals(b, c))) - // Stream of all unique entity tri tuples that share a group - .groupBy((a, b, c) -> a.getEntityGroup()) - .groupBy(Function.identity(), count()) - .groupBy((group, count) -> group.toString(), countBi()) - .penalize(SimpleScore.ONE, (groupName, count) -> count) - .asConstraint(TEST_CONSTRAINT_ID)); - - TestdataLavishEntity entity = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }), - assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); - - // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); - } - - @TestTemplate - void quadGroupByRegroupedDouble() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 2, 2, 8); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory - .forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) - .join(TestdataLavishEntity.class, - equal((a, b) -> a.getEntityGroup(), TestdataLavishEntity::getEntityGroup), - filtering((a, b, c) -> !Objects.equals(a, c) && !Objects.equals(b, c))) - .join(TestdataLavishEntity.class, - equal((a, b, c) -> a.getEntityGroup(), TestdataLavishEntity::getEntityGroup), - filtering((a, b, c, d) -> !Objects.equals(a, d) && !Objects.equals(b, d) - && !Objects.equals(c, d))) - // Stream of all unique entity quad tuples that share a group - .groupBy((a, b, c, d) -> a.getEntityGroup()) - .groupBy(Function.identity(), count()) - .groupBy((group, count) -> group.toString(), countBi()) - .penalize(SimpleScore.ONE, (groupName, count) -> count) - .asConstraint(TEST_CONSTRAINT_ID)); - - TestdataLavishEntity entity = solution.getFirstEntity(); - TestdataLavishEntity entity2 = solution.getEntityList().get(1); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }), - assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); - - // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); - } - - @TestTemplate - void existsAfterGroupBy() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); - TestdataLavishEntityGroup entityGroup1 = new TestdataLavishEntityGroup("MyEntityGroup"); - solution.getEntityGroupList().add(entityGroup1); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup1, solution.getFirstValue()); - solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", entityGroup1, solution.getFirstValue()); - solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), - solution.getFirstValue()); - solution.getEntityList().add(entity3); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) - .groupBy(TestdataLavishEntity::getEntityGroup, count()) - .ifExists(TestdataLavishEntityGroup.class, equal((groupA, count) -> groupA, Function.identity())) - .penalize(SimpleScore.ONE, (groupA, count) -> count) - .asConstraint(TEST_CONSTRAINT_ID)); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L), - assertMatchWithScore(-2, entityGroup1, 2L)); - - // Incremental - scoreDirector.beforeProblemFactRemoved(entityGroup1); - solution.getEntityGroupList().remove(entityGroup1); - scoreDirector.afterProblemFactRemoved(entityGroup1); - assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L)); - } - - @TestTemplate - void groupByAfterExists() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); - TestdataLavishEntityGroup entityGroup1 = new TestdataLavishEntityGroup("MyEntityGroup"); - solution.getEntityGroupList().add(entityGroup1); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup1, solution.getFirstValue()); - solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", entityGroup1, solution.getFirstValue()); - solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), - solution.getFirstValue()); - solution.getEntityList().add(entity3); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) - .ifExists(TestdataLavishEntityGroup.class, - equal(TestdataLavishEntity::getEntityGroup, Function.identity())) - .groupBy(TestdataLavishEntity::getEntityGroup, count()) - .penalize(SimpleScore.ONE, (groupA, count) -> count) - .asConstraint(TEST_CONSTRAINT_ID)); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L), - assertMatchWithScore(-2, entityGroup1, 2L)); - - // Incremental - scoreDirector.beforeProblemFactRemoved(entityGroup1); - solution.getEntityGroupList().remove(entityGroup1); - scoreDirector.afterProblemFactRemoved(entityGroup1); - assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L)); - } - - @TestTemplate - void groupByAfterExistsBi() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); - TestdataLavishEntityGroup entityGroup1 = new TestdataLavishEntityGroup("MyEntityGroup"); - solution.getEntityGroupList().add(entityGroup1); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup1, solution.getFirstValue()); - solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", entityGroup1, solution.getFirstValue()); - solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), - solution.getFirstValue()); - solution.getEntityList().add(entity3); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) - .ifExists(TestdataLavishEntityGroup.class, - equal((e1, e2) -> e1.getEntityGroup(), Function.identity())) - .groupBy((e1, e2) -> e1.getEntityGroup(), countBi()) - .penalize(SimpleScore.ONE, (groupA, count) -> count) - .asConstraint(TEST_CONSTRAINT_ID)); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L), - assertMatchWithScore(-3, entityGroup1, 3L)); - - // Incremental - scoreDirector.beforeProblemFactRemoved(entityGroup1); - solution.getEntityGroupList().remove(entityGroup1); - scoreDirector.afterProblemFactRemoved(entityGroup1); - assertScore(scoreDirector, - assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L)); - } - - @TestTemplate - void filteredFromUniquePair() { - TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(1, 1, 1, 1); - TestdataLavishEntityGroup entityGroup1 = new TestdataLavishEntityGroup("MyEntityGroup"); - solution.getEntityGroupList().add(entityGroup1); - TestdataLavishEntity entity1 = new TestdataLavishEntity("MyEntity 1", entityGroup1, solution.getFirstValue()); - solution.getEntityList().add(entity1); - TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", entityGroup1, solution.getFirstValue()); - solution.getEntityList().add(entity2); - TestdataLavishEntity entity3 = new TestdataLavishEntity("MyEntity 3", solution.getFirstEntityGroup(), - solution.getFirstValue()); - solution.getEntityList().add(entity3); - - InnerScoreDirector scoreDirector = - buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class, - Joiners.equal(TestdataLavishEntity::getEntityGroup), - Joiners.filtering((e1, e2) -> !e1.getCode().contains("My"))) - .penalize(SimpleScore.ONE) - .asConstraint(TEST_CONSTRAINT_ID)); - - // From scratch - scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, - assertMatchWithScore(-1, entity3, solution.getFirstEntity())); - - // Incremental - scoreDirector.beforeProblemFactRemoved(entity3); - solution.getEntityList().remove(entity3); - scoreDirector.afterProblemFactRemoved(entity3); - assertScore(scoreDirector); - } - - @TestTemplate - void groupByThenJoinThenGroupBy() { - assertThatCode(() -> buildScoreDirector(factory -> factory.forEach(TestdataLavishEntity.class) - .groupBy(TestdataLavishEntity::getEntityGroup, TestdataLavishEntity::getValue) - .join(TestdataLavishEntity.class) - .groupBy((group, value, entity) -> group, - (group, value, entity) -> entity, - sum((group, count, entity) -> 1)) - .penalize(SimpleScore.ONE) - .asConstraint(TEST_CONSTRAINT_ID))).doesNotThrowAnyException(); - } - } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/bi/InnerBiConstraintCollectorsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/bi/InnerBiConstraintCollectorsTest.java index 30e3cf94d3c..8491c76e388 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/bi/InnerBiConstraintCollectorsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/bi/InnerBiConstraintCollectorsTest.java @@ -23,6 +23,7 @@ import java.time.Duration; import java.time.LocalDateTime; import java.time.Period; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -32,6 +33,8 @@ import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollectorValueHandle; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.impl.score.stream.collector.AbstractConstraintCollectorsTest; import ai.timefold.solver.core.impl.util.Pair; @@ -1065,7 +1068,501 @@ public void collectAndThen() { private static Runnable accumulate( BiConstraintCollector collector, Object container, A valueA, B valueB) { - return collector.accumulator().apply((Container_) container, valueA, valueB); + var slot = ((BiConstraintCollectorAccumulator) collector.accumulator()) + .intoGroup((Container_) container); + slot.add(valueA, valueB); + return slot::remove; + } + + private static BiConstraintCollectorValueHandle insert( + BiConstraintCollector collector, Object container, A a, B b) { + var slot = ((BiConstraintCollectorAccumulator) collector.accumulator()) + .intoGroup((Container_) container); + slot.add(a, b); + return slot; + } + + @Test + public void countUpdate() { + BiConstraintCollector collector = countBi(); + Object container = collector.supplier().get(); + var slot = insert(collector, container, 1, 0); + assertResult(collector, container, 1L); + slot.replaceWith(42, 0); // no-op for count + assertResult(collector, container, 1L); + slot.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void conditionallyUpdate() { + BiConstraintCollector collector = + ConstraintCollectors.conditionally((Integer a, Integer b) -> a < 2, + min(Integer::sum, i -> i)); + Object container = collector.supplier().get(); + var slot = insert(collector, container, 1, 0); // active (1 < 2) + assertResult(collector, container, 1); + slot.replaceWith(2, 0); // active → inactive (2 is not < 2) + assertResult(collector, container, null); + slot.replaceWith(0, 0); // inactive → active (0 < 2) + assertResult(collector, container, 0); + slot.replaceWith(3, 0); // active → inactive + assertResult(collector, container, null); + slot.remove(); + assertResult(collector, container, null); + } + + @Test + public void compose2Update() { + BiConstraintCollector> collector = + compose(min(Integer::sum, i -> i), + max(Integer::sum, i -> i), + (BiFunction>) Pair::new); + Object container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0); + var slot2 = insert(collector, container, 4, 0); + assertResult(collector, container, new Pair<>(2, 4)); + slot1.replaceWith(3, 0); // 2 → 3; min becomes 3 + assertResult(collector, container, new Pair<>(3, 4)); + slot2.remove(); + slot1.remove(); + assertResult(collector, container, new Pair<>(null, null)); + } + + @Test + public void collectAndThenUpdate() { + var collector = ConstraintCollectors.collectAndThen(countBi(), i -> i * 10); + var container = collector.supplier().get(); + var slot = insert(collector, container, 1, 0); + assertResult(collector, container, 10L); + slot.replaceWith(99, 0); // count no-op; result unchanged + assertResult(collector, container, 10L); + slot.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void sumUpdate() { + BiConstraintCollector collector = ConstraintCollectors.sum(Integer::sum); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0); + assertResult(collector, container, 2L); + slot1.replaceWith(5, 0); + assertResult(collector, container, 5L); + slot1.replaceWith(5, 0); // no-op + assertResult(collector, container, 5L); + var slot2 = insert(collector, container, 3, 0); + assertResult(collector, container, 8L); + slot1.replaceWith(1, 0); + assertResult(collector, container, 4L); + slot2.remove(); + assertResult(collector, container, 1L); + slot1.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void averageUpdate() { + BiConstraintCollector collector = ConstraintCollectors.average(Integer::sum); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0); + var slot2 = insert(collector, container, 2, 0); + assertResult(collector, container, 3.0D); + slot1.replaceWith(6, 0); // (6+2)/2 = 4.0; count unchanged + assertResult(collector, container, 4.0D); + slot1.replaceWith(6, 0); // no-op + assertResult(collector, container, 4.0D); + slot2.remove(); + assertResult(collector, container, 6.0D); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void countDistinctUpdate() { + BiConstraintCollector collector = ConstraintCollectors.countDistinct((a, b) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "a", null); + var slot2 = insert(collector, container, "b", null); + assertResult(collector, container, 2L); + slot1.replaceWith("b", null); // both map to "b" + assertResult(collector, container, 1L); + slot1.replaceWith("b", null); // no-op: Objects.equals short-circuit + assertResult(collector, container, 1L); + slot1.replaceWith("c", null); // "b" and "c" + assertResult(collector, container, 2L); + slot2.remove(); + assertResult(collector, container, 1L); + slot1.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void sumBigDecimalUpdate() { + BiConstraintCollector collector = + ConstraintCollectors.sumBigDecimal((a, b) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, BigDecimal.ONE, null); + var slot2 = insert(collector, container, BigDecimal.TEN, null); + assertResult(collector, container, BigDecimal.valueOf(11)); + var bd4 = BigDecimal.valueOf(4); + slot1.replaceWith(bd4, null); + assertResult(collector, container, BigDecimal.valueOf(14)); + slot1.replaceWith(bd4, null); // no-op: same reference, == short-circuit + assertResult(collector, container, BigDecimal.valueOf(14)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.remove(); + assertResult(collector, container, BigDecimal.ZERO); + } + + @Test + public void sumBigIntegerUpdate() { + BiConstraintCollector collector = + ConstraintCollectors.sumBigInteger((a, b) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, BigInteger.ONE, null); + var slot2 = insert(collector, container, BigInteger.TEN, null); + assertResult(collector, container, BigInteger.valueOf(11)); + var bi4 = BigInteger.valueOf(4); + slot1.replaceWith(bi4, null); + assertResult(collector, container, BigInteger.valueOf(14)); + slot1.replaceWith(bi4, null); // no-op: same reference, == short-circuit + assertResult(collector, container, BigInteger.valueOf(14)); + slot2.remove(); + assertResult(collector, container, BigInteger.valueOf(4)); + slot1.remove(); + assertResult(collector, container, BigInteger.ZERO); + } + + @Test + public void sumDurationUpdate() { + BiConstraintCollector collector = + ConstraintCollectors.sumDuration((a, b) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, Duration.ofSeconds(1), null); + var slot2 = insert(collector, container, Duration.ofSeconds(2), null); + assertResult(collector, container, Duration.ofSeconds(3)); + var d4 = Duration.ofSeconds(4); + slot1.replaceWith(d4, null); + assertResult(collector, container, Duration.ofSeconds(6)); + slot1.replaceWith(d4, null); // no-op: same reference, == short-circuit + assertResult(collector, container, Duration.ofSeconds(6)); + slot2.remove(); + assertResult(collector, container, Duration.ofSeconds(4)); + slot1.remove(); + assertResult(collector, container, Duration.ZERO); + } + + @Test + public void sumPeriodUpdate() { + BiConstraintCollector collector = + ConstraintCollectors.sumPeriod((a, b) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, Period.ofDays(1), null); + var slot2 = insert(collector, container, Period.ofDays(2), null); + assertResult(collector, container, Period.ofDays(3)); + var p4 = Period.ofDays(4); + slot1.replaceWith(p4, null); + assertResult(collector, container, Period.ofDays(6)); + slot1.replaceWith(p4, null); // no-op: same reference, == short-circuit + assertResult(collector, container, Period.ofDays(6)); + slot2.remove(); + assertResult(collector, container, Period.ofDays(4)); + slot1.remove(); + assertResult(collector, container, Period.ZERO); + } + + @Test + public void averageBigDecimalUpdate() { + BiConstraintCollector collector = + ConstraintCollectors.averageBigDecimal((a, b) -> BigDecimal.valueOf(a + b)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0); + var slot2 = insert(collector, container, 2, 0); + assertResult(collector, container, BigDecimal.valueOf(3)); + slot1.replaceWith(6, 0); // (6+2)/2 = 4; count unchanged + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.replaceWith(6, 0); // no-op: same input value + assertResult(collector, container, BigDecimal.valueOf(4)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void averageBigIntegerUpdate() { + BiConstraintCollector collector = + ConstraintCollectors.averageBigInteger((a, b) -> BigInteger.valueOf(a + b)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0); + var slot2 = insert(collector, container, 2, 0); + assertResult(collector, container, BigDecimal.valueOf(3)); + slot1.replaceWith(6, 0); + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.replaceWith(6, 0); // no-op: same input value + assertResult(collector, container, BigDecimal.valueOf(4)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void averageDurationUpdate() { + BiConstraintCollector collector = + ConstraintCollectors.averageDuration((a, b) -> Duration.ofSeconds(a + b)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0); + var slot2 = insert(collector, container, 2, 0); + assertResult(collector, container, Duration.ofSeconds(3)); + slot1.replaceWith(6, 0); // (6+2)/2 = 4; count unchanged + assertResult(collector, container, Duration.ofSeconds(4)); + slot1.replaceWith(6, 0); // no-op: same input value + assertResult(collector, container, Duration.ofSeconds(4)); + slot2.remove(); + assertResult(collector, container, Duration.ofSeconds(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void toConsecutiveSequencesUpdate() { + var collector = ConstraintCollectors.toConsecutiveSequences(Integer::sum, Integer::intValue); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0); + var slot2 = insert(collector, container, 3, 0); // gap of 2 — two sequences + assertResultRecursive(collector, container, buildSequenceChain(1, 3)); + slot1.replaceWith(2, 0); // 2 and 3 are consecutive — one sequence + assertResultRecursive(collector, container, buildSequenceChain(2, 3)); + slot1.replaceWith(2, 0); // same value → result unchanged + assertResultRecursive(collector, container, buildSequenceChain(2, 3)); + slot2.remove(); + assertResultRecursive(collector, container, buildSequenceChain(2)); + slot1.remove(); + assertResultRecursive(collector, container, buildSequenceChain()); + } + + @Test + public void consecutiveUsageUpdate() { + var collector = ConstraintCollectors.toConnectedRanges(Interval::new, + Interval::start, + Interval::end, (a, b) -> b - a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 3); + var slot2 = insert(collector, container, 10, 20); // disjoint + assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(10, 20))); + var i812 = new Interval(8, 12); + slot1.replaceWith(8, 12); // now overlaps with (10,20) + assertResult(collector, container, buildConsecutiveUsage(new Interval(8, 12), new Interval(10, 20))); + slot1.replaceWith(8, 12); // same value → result unchanged + assertResult(collector, container, buildConsecutiveUsage(new Interval(8, 12), new Interval(10, 20))); + slot2.remove(); + assertResult(collector, container, buildConsecutiveUsage(i812)); + slot1.remove(); + assertResult(collector, container, buildConsecutiveUsage()); + } + + @Test + public void toListUpdate() { + var collector = ConstraintCollectors. toList((a, b) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0); + var slot2 = insert(collector, container, 2, 0); + assertResult(collector, container, asList(1, 2)); + slot1.replaceWith(3, 0); + assertResult(collector, container, asList(3, 2)); + slot1.replaceWith(3, 0); // no-op + assertResult(collector, container, asList(3, 2)); + slot2.remove(); + assertResult(collector, container, singletonList(3)); + slot1.remove(); + assertResult(collector, container, emptyList()); + } + + @Test + public void toSetUpdate() { + var collector = ConstraintCollectors. toSet((a, b) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0); + var slot2 = insert(collector, container, 2, 0); + assertResult(collector, container, asSet(1, 2)); + slot1.replaceWith(3, 0); + assertResult(collector, container, asSet(2, 3)); + slot1.replaceWith(3, 0); // Objects.equals short-circuit + assertResult(collector, container, asSet(2, 3)); + slot2.remove(); + assertResult(collector, container, asSet(3)); + slot1.remove(); + assertResult(collector, container, emptySet()); + } + + @Test + public void toSortedSetUpdate() { + var collector = ConstraintCollectors. toSortedSet((a, b) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0); + var slot2 = insert(collector, container, 2, 0); + assertResult(collector, container, asSortedSet(1, 2)); + slot1.replaceWith(3, 0); + assertResult(collector, container, asSortedSet(2, 3)); + slot1.replaceWith(3, 0); // Objects.equals short-circuit + assertResult(collector, container, asSortedSet(2, 3)); + slot2.remove(); + assertResult(collector, container, asSortedSet(3)); + slot1.remove(); + assertResult(collector, container, emptySortedSet()); + } + + @Test + public void toCollectionUpdate() { + var collector = InnerBiConstraintCollectors.> toCollection( + (a, b) -> a, ArrayList::new); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0); + var slot2 = insert(collector, container, 2, 0); + assertResult(collector, container, new ArrayList<>(asList(1, 2))); + slot1.replaceWith(3, 0); + assertResult(collector, container, new ArrayList<>(asList(3, 2))); + slot1.replaceWith(3, 0); // no-op + assertResult(collector, container, new ArrayList<>(asList(3, 2))); + slot2.remove(); + assertResult(collector, container, new ArrayList<>(singletonList(3))); + slot1.remove(); + assertResult(collector, container, new ArrayList<>()); + } + + @Test + public void toMapUpdate() { + var collector = ConstraintCollectors. toMap( + Integer::sum, Integer::sum); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0); + var slot2 = insert(collector, container, 1, 0); + assertResult(collector, container, asMap(2, singleton(2), 1, singleton(1))); + slot1.replaceWith(3, 0); + assertResult(collector, container, asMap(1, singleton(1), 3, singleton(3))); + slot1.replaceWith(3, 0); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asMap(1, singleton(1), 3, singleton(3))); + slot2.remove(); + assertResult(collector, container, asMap(3, singleton(3))); + slot1.remove(); + assertResult(collector, container, emptyMap()); + } + + @Test + public void toMapMergedUpdate() { + var collector = ConstraintCollectors. toMap( + Integer::sum, Integer::sum, Integer::sum); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0); + var slot2 = insert(collector, container, 1, 0); + assertResult(collector, container, asMap(2, 2, 1, 1)); + slot1.replaceWith(3, 0); + assertResult(collector, container, asMap(1, 1, 3, 3)); + slot1.replaceWith(3, 0); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asMap(1, 1, 3, 3)); + slot2.remove(); + assertResult(collector, container, asMap(3, 3)); + slot1.remove(); + assertResult(collector, container, emptyMap()); + } + + @Test + public void toSortedMapUpdate() { + var collector = ConstraintCollectors. toSortedMap( + Integer::sum, Integer::sum); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0); + var slot2 = insert(collector, container, 1, 0); + assertResult(collector, container, asSortedMap(1, singleton(1), 2, singleton(2))); + slot1.replaceWith(3, 0); + assertResult(collector, container, asSortedMap(1, singleton(1), 3, singleton(3))); + slot1.replaceWith(3, 0); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asSortedMap(1, singleton(1), 3, singleton(3))); + slot2.remove(); + assertResult(collector, container, asSortedMap(3, singleton(3))); + slot1.remove(); + assertResult(collector, container, emptySortedMap()); + } + + @Test + public void minComparableUpdate() { + var baseLocalDateTime = LocalDateTime.of(2023, 1, 1, 0, 0); + var collector = min((Integer a, Integer b) -> baseLocalDateTime.plusMinutes(a + b)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 5, 0); + var slot2 = insert(collector, container, 3, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(3)); + slot2.replaceWith(6, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(5)); + slot2.replaceWith(6, 0); // Objects.equals short-circuit + assertResult(collector, container, baseLocalDateTime.plusMinutes(5)); + slot1.replaceWith(1, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(1)); + slot2.remove(); + assertResult(collector, container, baseLocalDateTime.plusMinutes(1)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void maxComparableUpdate() { + var baseLocalDateTime = LocalDateTime.of(2023, 1, 1, 0, 0); + var collector = max((Integer a, Integer b) -> baseLocalDateTime.plusMinutes(a + b)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 3, 0); + var slot2 = insert(collector, container, 5, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(5)); + slot2.replaceWith(2, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(3)); + slot2.replaceWith(2, 0); // Objects.equals short-circuit + assertResult(collector, container, baseLocalDateTime.plusMinutes(3)); + slot1.replaceWith(7, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(7)); + slot2.remove(); + assertResult(collector, container, baseLocalDateTime.plusMinutes(7)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void minNotComparableUpdate() { + var collector = min((String a, String b) -> a, o -> o); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "b", null); + var slot2 = insert(collector, container, "a", null); + assertResult(collector, container, "a"); + slot2.replaceWith("c", null); + assertResult(collector, container, "b"); + slot2.replaceWith("c", null); // Objects.equals short-circuit + assertResult(collector, container, "b"); + slot1.replaceWith("a", null); + assertResult(collector, container, "a"); + slot2.remove(); + assertResult(collector, container, "a"); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void maxNotComparableUpdate() { + var collector = max((String a, String b) -> a, o -> o); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "b", null); + var slot2 = insert(collector, container, "a", null); + assertResult(collector, container, "b"); + slot2.replaceWith("c", null); + assertResult(collector, container, "c"); + slot2.replaceWith("c", null); // Objects.equals short-circuit + assertResult(collector, container, "c"); + slot1.replaceWith("a", null); + assertResult(collector, container, "c"); + slot2.remove(); + assertResult(collector, container, "a"); + slot1.remove(); + assertResult(collector, container, null); } private static void assertResult( diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/quad/InnerQuadConstraintCollectorsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/quad/InnerQuadConstraintCollectorsTest.java index 42a794a1e6d..fb58931e9c3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/quad/InnerQuadConstraintCollectorsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/quad/InnerQuadConstraintCollectorsTest.java @@ -23,6 +23,7 @@ import java.time.Duration; import java.time.LocalDateTime; import java.time.Period; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -32,6 +33,8 @@ import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.score.stream.collector.AbstractConstraintCollectorsTest; import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.impl.util.Quadruple; @@ -1100,7 +1103,511 @@ public void collectAndThen() { private static Runnable accumulate( QuadConstraintCollector collector, Object container, A valueA, B valueB, C valueC, D valueD) { - return collector.accumulator().apply((Container_) container, valueA, valueB, valueC, valueD); + var slot = ((QuadConstraintCollectorAccumulator) collector.accumulator()) + .intoGroup((Container_) container); + slot.add(valueA, valueB, valueC, valueD); + return slot::remove; + } + + private static QuadConstraintCollectorValueHandle insert( + QuadConstraintCollector collector, Object container, A a, B b, C c, + D d) { + var slot = ((QuadConstraintCollectorAccumulator) collector.accumulator()) + .intoGroup((Container_) container); + slot.add(a, b, c, d); + return slot; + } + + @Test + public void countUpdate() { + QuadConstraintCollector collector = countQuad(); + Object container = collector.supplier().get(); + var slot = insert(collector, container, 1, 0, 0, 0); + assertResult(collector, container, 1L); + slot.replaceWith(42, 0, 0, 0); // no-op for count + assertResult(collector, container, 1L); + slot.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void conditionallyUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.conditionally((Integer a, Integer b, Integer c, Integer d) -> a < 2, + max((i, i2, i3, i4) -> i + i2 + i3 + i4, i -> i)); + Object container = collector.supplier().get(); + var slot = insert(collector, container, 1, 0, 0, 0); // active (1 < 2) + assertResult(collector, container, 1); + slot.replaceWith(2, 0, 0, 0); // active → inactive (2 is not < 2) + assertResult(collector, container, null); + slot.replaceWith(0, 0, 0, 0); // inactive → active (0 < 2) + assertResult(collector, container, 0); + slot.replaceWith(3, 0, 0, 0); // active → inactive + assertResult(collector, container, null); + slot.remove(); + assertResult(collector, container, null); + } + + @Test + public void compose2Update() { + QuadConstraintCollector> collector = + compose(min((i, i2, i3, i4) -> i + i2 + i3 + i4, i -> i), + max((i, i2, i3, i4) -> i + i2 + i3 + i4, i -> i), + Pair::new); + Object container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0, 0, 0); + var slot2 = insert(collector, container, 4, 0, 0, 0); + assertResult(collector, container, new Pair<>(2, 4)); + slot1.replaceWith(3, 0, 0, 0); // 2 → 3; min becomes 3 + assertResult(collector, container, new Pair<>(3, 4)); + slot2.remove(); + slot1.remove(); + assertResult(collector, container, new Pair<>(null, null)); + } + + @Test + public void collectAndThenUpdate() { + var collector = ConstraintCollectors.collectAndThen(countQuad(), i -> i * 10); + var container = collector.supplier().get(); + var slot = insert(collector, container, 1, 0, 0, 0); + assertResult(collector, container, 10L); + slot.replaceWith(99, 0, 0, 0); // count no-op; result unchanged + assertResult(collector, container, 10L); + slot.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void sumUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.sum((a, b, c, d) -> a + b + c + d); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0, 0, 0); + assertResult(collector, container, 2L); + slot1.replaceWith(5, 0, 0, 0); + assertResult(collector, container, 5L); + slot1.replaceWith(5, 0, 0, 0); // no-op + assertResult(collector, container, 5L); + var slot2 = insert(collector, container, 3, 0, 0, 0); + assertResult(collector, container, 8L); + slot1.replaceWith(1, 0, 0, 0); + assertResult(collector, container, 4L); + slot2.remove(); + assertResult(collector, container, 1L); + slot1.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void averageUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.average((a, b, c, d) -> a + b + c + d); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0, 0); + assertResult(collector, container, 3.0D); + slot1.replaceWith(6, 0, 0, 0); // (6+2)/2 = 4.0; count unchanged + assertResult(collector, container, 4.0D); + slot1.replaceWith(6, 0, 0, 0); // no-op + assertResult(collector, container, 4.0D); + slot2.remove(); + assertResult(collector, container, 6.0D); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void countDistinctUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.countDistinct((a, b, c, d) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "a", null, null, null); + var slot2 = insert(collector, container, "b", null, null, null); + assertResult(collector, container, 2L); + slot1.replaceWith("b", null, null, null); // both map to "b" + assertResult(collector, container, 1L); + slot1.replaceWith("b", null, null, null); // no-op: Objects.equals short-circuit + assertResult(collector, container, 1L); + slot1.replaceWith("c", null, null, null); // "b" and "c" + assertResult(collector, container, 2L); + slot2.remove(); + assertResult(collector, container, 1L); + slot1.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void sumBigDecimalUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.sumBigDecimal((a, b, c, d) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, BigDecimal.ONE, null, null, null); + var slot2 = insert(collector, container, BigDecimal.TEN, null, null, null); + assertResult(collector, container, BigDecimal.valueOf(11)); + var bd4 = BigDecimal.valueOf(4); + slot1.replaceWith(bd4, null, null, null); + assertResult(collector, container, BigDecimal.valueOf(14)); + slot1.replaceWith(bd4, null, null, null); // no-op: same reference, == short-circuit + assertResult(collector, container, BigDecimal.valueOf(14)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.remove(); + assertResult(collector, container, BigDecimal.ZERO); + } + + @Test + public void sumBigIntegerUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.sumBigInteger((a, b, c, d) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, BigInteger.ONE, null, null, null); + var slot2 = insert(collector, container, BigInteger.TEN, null, null, null); + assertResult(collector, container, BigInteger.valueOf(11)); + var bi4 = BigInteger.valueOf(4); + slot1.replaceWith(bi4, null, null, null); + assertResult(collector, container, BigInteger.valueOf(14)); + slot1.replaceWith(bi4, null, null, null); // no-op: same reference, == short-circuit + assertResult(collector, container, BigInteger.valueOf(14)); + slot2.remove(); + assertResult(collector, container, BigInteger.valueOf(4)); + slot1.remove(); + assertResult(collector, container, BigInteger.ZERO); + } + + @Test + public void sumDurationUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.sumDuration((a, b, c, d) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, Duration.ofSeconds(1), null, null, null); + var slot2 = insert(collector, container, Duration.ofSeconds(2), null, null, null); + assertResult(collector, container, Duration.ofSeconds(3)); + var d4 = Duration.ofSeconds(4); + slot1.replaceWith(d4, null, null, null); + assertResult(collector, container, Duration.ofSeconds(6)); + slot1.replaceWith(d4, null, null, null); // no-op: same reference, == short-circuit + assertResult(collector, container, Duration.ofSeconds(6)); + slot2.remove(); + assertResult(collector, container, Duration.ofSeconds(4)); + slot1.remove(); + assertResult(collector, container, Duration.ZERO); + } + + @Test + public void sumPeriodUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.sumPeriod((a, b, c, d) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, Period.ofDays(1), null, null, null); + var slot2 = insert(collector, container, Period.ofDays(2), null, null, null); + assertResult(collector, container, Period.ofDays(3)); + var p4 = Period.ofDays(4); + slot1.replaceWith(p4, null, null, null); + assertResult(collector, container, Period.ofDays(6)); + slot1.replaceWith(p4, null, null, null); // no-op: same reference, == short-circuit + assertResult(collector, container, Period.ofDays(6)); + slot2.remove(); + assertResult(collector, container, Period.ofDays(4)); + slot1.remove(); + assertResult(collector, container, Period.ZERO); + } + + @Test + public void averageBigDecimalUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.averageBigDecimal((a, b, c, d) -> BigDecimal.valueOf(a + b + c + d)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0, 0); + assertResult(collector, container, BigDecimal.valueOf(3)); + slot1.replaceWith(6, 0, 0, 0); // (6+2)/2 = 4; count unchanged + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.replaceWith(6, 0, 0, 0); // no-op: same input value + assertResult(collector, container, BigDecimal.valueOf(4)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void averageBigIntegerUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.averageBigInteger((a, b, c, d) -> BigInteger.valueOf(a + b + c + d)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0, 0); + assertResult(collector, container, BigDecimal.valueOf(3)); + slot1.replaceWith(6, 0, 0, 0); + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.replaceWith(6, 0, 0, 0); // no-op: same input value + assertResult(collector, container, BigDecimal.valueOf(4)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void averageDurationUpdate() { + QuadConstraintCollector collector = + ConstraintCollectors.averageDuration((a, b, c, d) -> Duration.ofSeconds(a + b + c + d)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0, 0); + assertResult(collector, container, Duration.ofSeconds(3)); + slot1.replaceWith(6, 0, 0, 0); // (6+2)/2 = 4; count unchanged + assertResult(collector, container, Duration.ofSeconds(4)); + slot1.replaceWith(6, 0, 0, 0); // no-op: same input value + assertResult(collector, container, Duration.ofSeconds(4)); + slot2.remove(); + assertResult(collector, container, Duration.ofSeconds(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void toConsecutiveSequencesUpdate() { + var collector = ConstraintCollectors.toConsecutiveSequences( + (Integer a, Integer b, Integer c, Integer d) -> a + b + c + d, Integer::intValue); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0, 0, 0); + var slot2 = insert(collector, container, 3, 0, 0, 0); // gap of 2 — two sequences + assertResultRecursive(collector, container, buildSequenceChain(1, 3)); + slot1.replaceWith(2, 0, 0, 0); // 2 and 3 are consecutive — one sequence + assertResultRecursive(collector, container, buildSequenceChain(2, 3)); + slot1.replaceWith(2, 0, 0, 0); // same value → result unchanged + assertResultRecursive(collector, container, buildSequenceChain(2, 3)); + slot2.remove(); + assertResultRecursive(collector, container, buildSequenceChain(2)); + slot1.remove(); + assertResultRecursive(collector, container, buildSequenceChain()); + } + + @Test + public void consecutiveUsageUpdate() { + var collector = ConstraintCollectors.toConnectedRanges( + (Integer a, Integer b, Object c, Object d) -> new Interval(a, b), + Interval::start, + Interval::end, (a, b) -> b - a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 3, null, null); + var slot2 = insert(collector, container, 10, 20, null, null); // disjoint + assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(10, 20))); + var i812 = new Interval(8, 12); + slot1.replaceWith(8, 12, null, null); // now overlaps with (10,20) + assertResult(collector, container, buildConsecutiveUsage(new Interval(8, 12), new Interval(10, 20))); + slot1.replaceWith(8, 12, null, null); // same value → result unchanged + assertResult(collector, container, buildConsecutiveUsage(new Interval(8, 12), new Interval(10, 20))); + slot2.remove(); + assertResult(collector, container, buildConsecutiveUsage(i812)); + slot1.remove(); + assertResult(collector, container, buildConsecutiveUsage()); + } + + @Test + public void toListUpdate() { + var collector = ConstraintCollectors. toList((a, b, c, d) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0, 0); + assertResult(collector, container, asList(1, 2)); + slot1.replaceWith(3, 0, 0, 0); + assertResult(collector, container, asList(3, 2)); + slot1.replaceWith(3, 0, 0, 0); // no-op + assertResult(collector, container, asList(3, 2)); + slot2.remove(); + assertResult(collector, container, singletonList(3)); + slot1.remove(); + assertResult(collector, container, emptyList()); + } + + @Test + public void toSetUpdate() { + var collector = ConstraintCollectors. toSet((a, b, c, d) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0, 0); + assertResult(collector, container, asSet(1, 2)); + slot1.replaceWith(3, 0, 0, 0); + assertResult(collector, container, asSet(2, 3)); + slot1.replaceWith(3, 0, 0, 0); // Objects.equals short-circuit + assertResult(collector, container, asSet(2, 3)); + slot2.remove(); + assertResult(collector, container, asSet(3)); + slot1.remove(); + assertResult(collector, container, emptySet()); + } + + @Test + public void toSortedSetUpdate() { + var collector = + ConstraintCollectors. toSortedSet((a, b, c, d) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0, 0); + assertResult(collector, container, asSortedSet(1, 2)); + slot1.replaceWith(3, 0, 0, 0); + assertResult(collector, container, asSortedSet(2, 3)); + slot1.replaceWith(3, 0, 0, 0); // Objects.equals short-circuit + assertResult(collector, container, asSortedSet(2, 3)); + slot2.remove(); + assertResult(collector, container, asSortedSet(3)); + slot1.remove(); + assertResult(collector, container, emptySortedSet()); + } + + @Test + public void toCollectionUpdate() { + var collector = + InnerQuadConstraintCollectors.> toCollection( + (a, b, c, d) -> a, ArrayList::new); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0, 0); + assertResult(collector, container, new ArrayList<>(asList(1, 2))); + slot1.replaceWith(3, 0, 0, 0); + assertResult(collector, container, new ArrayList<>(asList(3, 2))); + slot1.replaceWith(3, 0, 0, 0); // no-op + assertResult(collector, container, new ArrayList<>(asList(3, 2))); + slot2.remove(); + assertResult(collector, container, new ArrayList<>(singletonList(3))); + slot1.remove(); + assertResult(collector, container, new ArrayList<>()); + } + + @Test + public void toMapUpdate() { + var collector = ConstraintCollectors. toMap( + (a, b, c, d) -> a + b + c + d, (a, b, c, d) -> a + b + c + d); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0, 0, 0); + var slot2 = insert(collector, container, 1, 0, 0, 0); + assertResult(collector, container, asMap(2, singleton(2), 1, singleton(1))); + slot1.replaceWith(3, 0, 0, 0); + assertResult(collector, container, asMap(1, singleton(1), 3, singleton(3))); + slot1.replaceWith(3, 0, 0, 0); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asMap(1, singleton(1), 3, singleton(3))); + slot2.remove(); + assertResult(collector, container, asMap(3, singleton(3))); + slot1.remove(); + assertResult(collector, container, emptyMap()); + } + + @Test + public void toMapMergedUpdate() { + var collector = ConstraintCollectors. toMap( + (a, b, c, d) -> a + b + c + d, (a, b, c, d) -> a + b + c + d, Integer::sum); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0, 0, 0); + var slot2 = insert(collector, container, 1, 0, 0, 0); + assertResult(collector, container, asMap(2, 2, 1, 1)); + slot1.replaceWith(3, 0, 0, 0); + assertResult(collector, container, asMap(1, 1, 3, 3)); + slot1.replaceWith(3, 0, 0, 0); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asMap(1, 1, 3, 3)); + slot2.remove(); + assertResult(collector, container, asMap(3, 3)); + slot1.remove(); + assertResult(collector, container, emptyMap()); + } + + @Test + public void toSortedMapUpdate() { + var collector = ConstraintCollectors. toSortedMap( + (a, b, c, d) -> a + b + c + d, (a, b, c, d) -> a + b + c + d); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0, 0, 0); + var slot2 = insert(collector, container, 1, 0, 0, 0); + assertResult(collector, container, asSortedMap(1, singleton(1), 2, singleton(2))); + slot1.replaceWith(3, 0, 0, 0); + assertResult(collector, container, asSortedMap(1, singleton(1), 3, singleton(3))); + slot1.replaceWith(3, 0, 0, 0); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asSortedMap(1, singleton(1), 3, singleton(3))); + slot2.remove(); + assertResult(collector, container, asSortedMap(3, singleton(3))); + slot1.remove(); + assertResult(collector, container, emptySortedMap()); + } + + @Test + public void minComparableUpdate() { + var baseLocalDateTime = LocalDateTime.of(2023, 1, 1, 0, 0); + var collector = min( + (Integer a, Integer b, Integer c, Integer d) -> baseLocalDateTime.plusMinutes(a + b + c + d)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 5, 0, 0, 0); + var slot2 = insert(collector, container, 3, 0, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(3)); + slot2.replaceWith(6, 0, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(5)); + slot2.replaceWith(6, 0, 0, 0); // Objects.equals short-circuit + assertResult(collector, container, baseLocalDateTime.plusMinutes(5)); + slot1.replaceWith(1, 0, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(1)); + slot2.remove(); + assertResult(collector, container, baseLocalDateTime.plusMinutes(1)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void maxComparableUpdate() { + var baseLocalDateTime = LocalDateTime.of(2023, 1, 1, 0, 0); + var collector = max( + (Integer a, Integer b, Integer c, Integer d) -> baseLocalDateTime.plusMinutes(a + b + c + d)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 3, 0, 0, 0); + var slot2 = insert(collector, container, 5, 0, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(5)); + slot2.replaceWith(2, 0, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(3)); + slot2.replaceWith(2, 0, 0, 0); // Objects.equals short-circuit + assertResult(collector, container, baseLocalDateTime.plusMinutes(3)); + slot1.replaceWith(7, 0, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(7)); + slot2.remove(); + assertResult(collector, container, baseLocalDateTime.plusMinutes(7)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void minNotComparableUpdate() { + var collector = min((String a, String b, String c, String d) -> a, o -> o); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "b", null, null, null); + var slot2 = insert(collector, container, "a", null, null, null); + assertResult(collector, container, "a"); + slot2.replaceWith("c", null, null, null); + assertResult(collector, container, "b"); + slot2.replaceWith("c", null, null, null); // Objects.equals short-circuit + assertResult(collector, container, "b"); + slot1.replaceWith("a", null, null, null); + assertResult(collector, container, "a"); + slot2.remove(); + assertResult(collector, container, "a"); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void maxNotComparableUpdate() { + var collector = max((String a, String b, String c, String d) -> a, o -> o); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "b", null, null, null); + var slot2 = insert(collector, container, "a", null, null, null); + assertResult(collector, container, "b"); + slot2.replaceWith("c", null, null, null); + assertResult(collector, container, "c"); + slot2.replaceWith("c", null, null, null); // Objects.equals short-circuit + assertResult(collector, container, "c"); + slot1.replaceWith("a", null, null, null); + assertResult(collector, container, "c"); + slot2.remove(); + assertResult(collector, container, "a"); + slot1.remove(); + assertResult(collector, container, null); } private static void assertResult( diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/tri/InnerTriConstraintCollectorsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/tri/InnerTriConstraintCollectorsTest.java index 105df71da9a..907f6e187d8 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/tri/InnerTriConstraintCollectorsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/tri/InnerTriConstraintCollectorsTest.java @@ -23,6 +23,7 @@ import java.time.Duration; import java.time.LocalDateTime; import java.time.Period; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -32,6 +33,8 @@ import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.score.stream.collector.AbstractConstraintCollectorsTest; import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.impl.util.Quadruple; @@ -1062,7 +1065,507 @@ public void collectAndThen() { private static Runnable accumulate( TriConstraintCollector collector, Object container, A valueA, B valueB, C valueC) { - return collector.accumulator().apply((Container_) container, valueA, valueB, valueC); + var slot = ((TriConstraintCollectorAccumulator) collector.accumulator()) + .intoGroup((Container_) container); + slot.add(valueA, valueB, valueC); + return slot::remove; + } + + private static TriConstraintCollectorValueHandle insert( + TriConstraintCollector collector, Object container, A a, B b, C c) { + var slot = ((TriConstraintCollectorAccumulator) collector.accumulator()) + .intoGroup((Container_) container); + slot.add(a, b, c); + return slot; + } + + @Test + public void countUpdate() { + TriConstraintCollector collector = countTri(); + Object container = collector.supplier().get(); + var slot = insert(collector, container, 1, 0, 0); + assertResult(collector, container, 1L); + slot.replaceWith(42, 0, 0); // no-op for count + assertResult(collector, container, 1L); + slot.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void conditionallyUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.conditionally((Integer a, Integer b, Integer c) -> a < 2, + max((i, i2, i3) -> i + i2 + i3, i -> i)); + Object container = collector.supplier().get(); + var slot = insert(collector, container, 1, 0, 0); // active (1 < 2) + assertResult(collector, container, 1); + slot.replaceWith(2, 0, 0); // active → inactive (2 is not < 2) + assertResult(collector, container, null); + slot.replaceWith(0, 0, 0); // inactive → active (0 < 2) + assertResult(collector, container, 0); + slot.replaceWith(3, 0, 0); // active → inactive + assertResult(collector, container, null); + slot.remove(); + assertResult(collector, container, null); + } + + @Test + public void compose2Update() { + TriConstraintCollector> collector = + compose(min((i, i2, i3) -> i + i2 + i3, i -> i), + max((i, i2, i3) -> i + i2 + i3, i -> i), + Pair::new); + Object container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0, 0); + var slot2 = insert(collector, container, 4, 0, 0); + assertResult(collector, container, new Pair<>(2, 4)); + slot1.replaceWith(3, 0, 0); // 2 → 3; min becomes 3 + assertResult(collector, container, new Pair<>(3, 4)); + slot2.remove(); + slot1.remove(); + assertResult(collector, container, new Pair<>(null, null)); + } + + @Test + public void collectAndThenUpdate() { + var collector = ConstraintCollectors.collectAndThen(countTri(), i -> i * 10); + var container = collector.supplier().get(); + var slot = insert(collector, container, 1, 0, 0); + assertResult(collector, container, 10L); + slot.replaceWith(99, 0, 0); // count no-op; result unchanged + assertResult(collector, container, 10L); + slot.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void sumUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.sum((a, b, c) -> a + b + c); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0, 0); + assertResult(collector, container, 2L); + slot1.replaceWith(5, 0, 0); + assertResult(collector, container, 5L); + slot1.replaceWith(5, 0, 0); // no-op + assertResult(collector, container, 5L); + var slot2 = insert(collector, container, 3, 0, 0); + assertResult(collector, container, 8L); + slot1.replaceWith(1, 0, 0); + assertResult(collector, container, 4L); + slot2.remove(); + assertResult(collector, container, 1L); + slot1.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void averageUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.average((a, b, c) -> a + b + c); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0); + assertResult(collector, container, 3.0D); + slot1.replaceWith(6, 0, 0); // (6+2)/2 = 4.0; count unchanged + assertResult(collector, container, 4.0D); + slot1.replaceWith(6, 0, 0); // no-op + assertResult(collector, container, 4.0D); + slot2.remove(); + assertResult(collector, container, 6.0D); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void countDistinctUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.countDistinct((a, b, c) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "a", null, null); + var slot2 = insert(collector, container, "b", null, null); + assertResult(collector, container, 2L); + slot1.replaceWith("b", null, null); // both map to "b" + assertResult(collector, container, 1L); + slot1.replaceWith("b", null, null); // no-op: Objects.equals short-circuit + assertResult(collector, container, 1L); + slot1.replaceWith("c", null, null); // "b" and "c" + assertResult(collector, container, 2L); + slot2.remove(); + assertResult(collector, container, 1L); + slot1.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void sumBigDecimalUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.sumBigDecimal((a, b, c) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, BigDecimal.ONE, null, null); + var slot2 = insert(collector, container, BigDecimal.TEN, null, null); + assertResult(collector, container, BigDecimal.valueOf(11)); + var bd4 = BigDecimal.valueOf(4); + slot1.replaceWith(bd4, null, null); + assertResult(collector, container, BigDecimal.valueOf(14)); + slot1.replaceWith(bd4, null, null); // no-op: same reference, == short-circuit + assertResult(collector, container, BigDecimal.valueOf(14)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.remove(); + assertResult(collector, container, BigDecimal.ZERO); + } + + @Test + public void sumBigIntegerUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.sumBigInteger((a, b, c) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, BigInteger.ONE, null, null); + var slot2 = insert(collector, container, BigInteger.TEN, null, null); + assertResult(collector, container, BigInteger.valueOf(11)); + var bi4 = BigInteger.valueOf(4); + slot1.replaceWith(bi4, null, null); + assertResult(collector, container, BigInteger.valueOf(14)); + slot1.replaceWith(bi4, null, null); // no-op: same reference, == short-circuit + assertResult(collector, container, BigInteger.valueOf(14)); + slot2.remove(); + assertResult(collector, container, BigInteger.valueOf(4)); + slot1.remove(); + assertResult(collector, container, BigInteger.ZERO); + } + + @Test + public void sumDurationUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.sumDuration((a, b, c) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, Duration.ofSeconds(1), null, null); + var slot2 = insert(collector, container, Duration.ofSeconds(2), null, null); + assertResult(collector, container, Duration.ofSeconds(3)); + var d4 = Duration.ofSeconds(4); + slot1.replaceWith(d4, null, null); + assertResult(collector, container, Duration.ofSeconds(6)); + slot1.replaceWith(d4, null, null); // no-op: same reference, == short-circuit + assertResult(collector, container, Duration.ofSeconds(6)); + slot2.remove(); + assertResult(collector, container, Duration.ofSeconds(4)); + slot1.remove(); + assertResult(collector, container, Duration.ZERO); + } + + @Test + public void sumPeriodUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.sumPeriod((a, b, c) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, Period.ofDays(1), null, null); + var slot2 = insert(collector, container, Period.ofDays(2), null, null); + assertResult(collector, container, Period.ofDays(3)); + var p4 = Period.ofDays(4); + slot1.replaceWith(p4, null, null); + assertResult(collector, container, Period.ofDays(6)); + slot1.replaceWith(p4, null, null); // no-op: same reference, == short-circuit + assertResult(collector, container, Period.ofDays(6)); + slot2.remove(); + assertResult(collector, container, Period.ofDays(4)); + slot1.remove(); + assertResult(collector, container, Period.ZERO); + } + + @Test + public void averageBigDecimalUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.averageBigDecimal((a, b, c) -> BigDecimal.valueOf(a + b + c)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0); + assertResult(collector, container, BigDecimal.valueOf(3)); + slot1.replaceWith(6, 0, 0); // (6+2)/2 = 4; count unchanged + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.replaceWith(6, 0, 0); // no-op: same input value + assertResult(collector, container, BigDecimal.valueOf(4)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void averageBigIntegerUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.averageBigInteger((a, b, c) -> BigInteger.valueOf(a + b + c)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0); + assertResult(collector, container, BigDecimal.valueOf(3)); + slot1.replaceWith(6, 0, 0); + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.replaceWith(6, 0, 0); // no-op: same input value + assertResult(collector, container, BigDecimal.valueOf(4)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void averageDurationUpdate() { + TriConstraintCollector collector = + ConstraintCollectors.averageDuration((a, b, c) -> Duration.ofSeconds(a + b + c)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0); + assertResult(collector, container, Duration.ofSeconds(3)); + slot1.replaceWith(6, 0, 0); // (6+2)/2 = 4; count unchanged + assertResult(collector, container, Duration.ofSeconds(4)); + slot1.replaceWith(6, 0, 0); // no-op: same input value + assertResult(collector, container, Duration.ofSeconds(4)); + slot2.remove(); + assertResult(collector, container, Duration.ofSeconds(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void toConsecutiveSequencesUpdate() { + var collector = ConstraintCollectors.toConsecutiveSequences( + (Integer a, Integer b, Integer c) -> a + b + c, Integer::intValue); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0, 0); + var slot2 = insert(collector, container, 3, 0, 0); // gap of 2 — two sequences + assertResultRecursive(collector, container, buildSequenceChain(1, 3)); + slot1.replaceWith(2, 0, 0); // 2 and 3 are consecutive — one sequence + assertResultRecursive(collector, container, buildSequenceChain(2, 3)); + slot1.replaceWith(2, 0, 0); // same value → result unchanged + assertResultRecursive(collector, container, buildSequenceChain(2, 3)); + slot2.remove(); + assertResultRecursive(collector, container, buildSequenceChain(2)); + slot1.remove(); + assertResultRecursive(collector, container, buildSequenceChain()); + } + + @Test + public void consecutiveUsageUpdate() { + var collector = ConstraintCollectors.toConnectedRanges( + (Integer a, Integer b, Object c) -> new Interval(a, b), + Interval::start, + Interval::end, (a, b) -> b - a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 3, null); + var slot2 = insert(collector, container, 10, 20, null); // disjoint + assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(10, 20))); + var i812 = new Interval(8, 12); + slot1.replaceWith(8, 12, null); // now overlaps with (10,20) + assertResult(collector, container, buildConsecutiveUsage(new Interval(8, 12), new Interval(10, 20))); + slot1.replaceWith(8, 12, null); // same value → result unchanged + assertResult(collector, container, buildConsecutiveUsage(new Interval(8, 12), new Interval(10, 20))); + slot2.remove(); + assertResult(collector, container, buildConsecutiveUsage(i812)); + slot1.remove(); + assertResult(collector, container, buildConsecutiveUsage()); + } + + @Test + public void toListUpdate() { + var collector = ConstraintCollectors. toList((a, b, c) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0); + assertResult(collector, container, asList(1, 2)); + slot1.replaceWith(3, 0, 0); + assertResult(collector, container, asList(3, 2)); + slot1.replaceWith(3, 0, 0); // no-op + assertResult(collector, container, asList(3, 2)); + slot2.remove(); + assertResult(collector, container, singletonList(3)); + slot1.remove(); + assertResult(collector, container, emptyList()); + } + + @Test + public void toSetUpdate() { + var collector = ConstraintCollectors. toSet((a, b, c) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0); + assertResult(collector, container, asSet(1, 2)); + slot1.replaceWith(3, 0, 0); + assertResult(collector, container, asSet(2, 3)); + slot1.replaceWith(3, 0, 0); // Objects.equals short-circuit + assertResult(collector, container, asSet(2, 3)); + slot2.remove(); + assertResult(collector, container, asSet(3)); + slot1.remove(); + assertResult(collector, container, emptySet()); + } + + @Test + public void toSortedSetUpdate() { + var collector = ConstraintCollectors. toSortedSet((a, b, c) -> a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0); + assertResult(collector, container, asSortedSet(1, 2)); + slot1.replaceWith(3, 0, 0); + assertResult(collector, container, asSortedSet(2, 3)); + slot1.replaceWith(3, 0, 0); // Objects.equals short-circuit + assertResult(collector, container, asSortedSet(2, 3)); + slot2.remove(); + assertResult(collector, container, asSortedSet(3)); + slot1.remove(); + assertResult(collector, container, emptySortedSet()); + } + + @Test + public void toCollectionUpdate() { + var collector = + InnerTriConstraintCollectors.> toCollection( + (a, b, c) -> a, ArrayList::new); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1, 0, 0); + var slot2 = insert(collector, container, 2, 0, 0); + assertResult(collector, container, new ArrayList<>(asList(1, 2))); + slot1.replaceWith(3, 0, 0); + assertResult(collector, container, new ArrayList<>(asList(3, 2))); + slot1.replaceWith(3, 0, 0); // no-op + assertResult(collector, container, new ArrayList<>(asList(3, 2))); + slot2.remove(); + assertResult(collector, container, new ArrayList<>(singletonList(3))); + slot1.remove(); + assertResult(collector, container, new ArrayList<>()); + } + + @Test + public void toMapUpdate() { + var collector = ConstraintCollectors. toMap( + (a, b, c) -> a + b + c, (a, b, c) -> a + b + c); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0, 0); + var slot2 = insert(collector, container, 1, 0, 0); + assertResult(collector, container, asMap(2, singleton(2), 1, singleton(1))); + slot1.replaceWith(3, 0, 0); + assertResult(collector, container, asMap(1, singleton(1), 3, singleton(3))); + slot1.replaceWith(3, 0, 0); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asMap(1, singleton(1), 3, singleton(3))); + slot2.remove(); + assertResult(collector, container, asMap(3, singleton(3))); + slot1.remove(); + assertResult(collector, container, emptyMap()); + } + + @Test + public void toMapMergedUpdate() { + var collector = ConstraintCollectors. toMap( + (a, b, c) -> a + b + c, (a, b, c) -> a + b + c, Integer::sum); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0, 0); + var slot2 = insert(collector, container, 1, 0, 0); + assertResult(collector, container, asMap(2, 2, 1, 1)); + slot1.replaceWith(3, 0, 0); + assertResult(collector, container, asMap(1, 1, 3, 3)); + slot1.replaceWith(3, 0, 0); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asMap(1, 1, 3, 3)); + slot2.remove(); + assertResult(collector, container, asMap(3, 3)); + slot1.remove(); + assertResult(collector, container, emptyMap()); + } + + @Test + public void toSortedMapUpdate() { + var collector = ConstraintCollectors. toSortedMap( + (a, b, c) -> a + b + c, (a, b, c) -> a + b + c); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2, 0, 0); + var slot2 = insert(collector, container, 1, 0, 0); + assertResult(collector, container, asSortedMap(1, singleton(1), 2, singleton(2))); + slot1.replaceWith(3, 0, 0); + assertResult(collector, container, asSortedMap(1, singleton(1), 3, singleton(3))); + slot1.replaceWith(3, 0, 0); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asSortedMap(1, singleton(1), 3, singleton(3))); + slot2.remove(); + assertResult(collector, container, asSortedMap(3, singleton(3))); + slot1.remove(); + assertResult(collector, container, emptySortedMap()); + } + + @Test + public void minComparableUpdate() { + var baseLocalDateTime = LocalDateTime.of(2023, 1, 1, 0, 0); + var collector = min((Integer a, Integer b, Integer c) -> baseLocalDateTime.plusMinutes(a + b + c)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 5, 0, 0); + var slot2 = insert(collector, container, 3, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(3)); + slot2.replaceWith(6, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(5)); + slot2.replaceWith(6, 0, 0); // Objects.equals short-circuit + assertResult(collector, container, baseLocalDateTime.plusMinutes(5)); + slot1.replaceWith(1, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(1)); + slot2.remove(); + assertResult(collector, container, baseLocalDateTime.plusMinutes(1)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void maxComparableUpdate() { + var baseLocalDateTime = LocalDateTime.of(2023, 1, 1, 0, 0); + var collector = max((Integer a, Integer b, Integer c) -> baseLocalDateTime.plusMinutes(a + b + c)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 3, 0, 0); + var slot2 = insert(collector, container, 5, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(5)); + slot2.replaceWith(2, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(3)); + slot2.replaceWith(2, 0, 0); // Objects.equals short-circuit + assertResult(collector, container, baseLocalDateTime.plusMinutes(3)); + slot1.replaceWith(7, 0, 0); + assertResult(collector, container, baseLocalDateTime.plusMinutes(7)); + slot2.remove(); + assertResult(collector, container, baseLocalDateTime.plusMinutes(7)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void minNotComparableUpdate() { + var collector = min((String a, String b, String c) -> a, o -> o); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "b", null, null); + var slot2 = insert(collector, container, "a", null, null); + assertResult(collector, container, "a"); + slot2.replaceWith("c", null, null); + assertResult(collector, container, "b"); + slot2.replaceWith("c", null, null); // Objects.equals short-circuit + assertResult(collector, container, "b"); + slot1.replaceWith("a", null, null); + assertResult(collector, container, "a"); + slot2.remove(); + assertResult(collector, container, "a"); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void maxNotComparableUpdate() { + var collector = max((String a, String b, String c) -> a, o -> o); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "b", null, null); + var slot2 = insert(collector, container, "a", null, null); + assertResult(collector, container, "b"); + slot2.replaceWith("c", null, null); + assertResult(collector, container, "c"); + slot2.replaceWith("c", null, null); // Objects.equals short-circuit + assertResult(collector, container, "c"); + slot1.replaceWith("a", null, null); + assertResult(collector, container, "c"); + slot2.remove(); + assertResult(collector, container, "a"); + slot1.remove(); + assertResult(collector, container, null); } private static void assertResult( diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/uni/InnerUniConstraintCollectorsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/uni/InnerUniConstraintCollectorsTest.java index cb3192e238e..1325ceb4816 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/uni/InnerUniConstraintCollectorsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/uni/InnerUniConstraintCollectorsTest.java @@ -21,6 +21,7 @@ import java.math.BigInteger; import java.time.Duration; import java.time.Period; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -35,6 +36,8 @@ import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorValueHandle; import ai.timefold.solver.core.impl.score.stream.collector.AbstractConstraintCollectorsTest; import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.impl.util.Quadruple; @@ -1008,7 +1011,18 @@ public void collectAndThen() { private static Runnable accumulate( UniConstraintCollector collector, Object container, A value) { - return collector.accumulator().apply((Container_) container, value); + var slot = ((UniConstraintCollectorAccumulator) collector.accumulator()) + .intoGroup((Container_) container); + slot.add(value); + return slot::remove; + } + + private static UniConstraintCollectorValueHandle insert( + UniConstraintCollector collector, Object container, A value) { + var slot = + ((UniConstraintCollectorAccumulator) collector.accumulator()).intoGroup((Container_) container); + slot.add(value); + return slot; } private static void assertResult( @@ -1038,4 +1052,477 @@ private static void assertUnfairness( .isEqualTo(expectedValue); } + @Test + public void countUpdate() { + UniConstraintCollector collector = ConstraintCollectors.count(); + Object container = collector.supplier().get(); + var slot = insert(collector, container, 1L); + assertResult(collector, container, 1L); + slot.replaceWith(42L); // no-op for count + assertResult(collector, container, 1L); + slot.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void conditionallyUpdate() { + UniConstraintCollector collector = + ConstraintCollectors.conditionally((Integer i) -> i > 1, min()); + Object container = collector.supplier().get(); + var slot = insert(collector, container, 2); // active (2 > 1) + assertResult(collector, container, 2); + slot.replaceWith(1); // active → inactive (1 is not > 1) + assertResult(collector, container, null); + slot.replaceWith(3); // inactive → active (3 > 1) + assertResult(collector, container, 3); + slot.replaceWith(4); // active → active + assertResult(collector, container, 4); + slot.remove(); + assertResult(collector, container, null); + } + + @Test + public void compose2Update() { + UniConstraintCollector> collector = + compose(min(i -> i), max(i -> i), + (BiFunction>) Pair::new); + Object container = collector.supplier().get(); + var slot1 = insert(collector, container, 2); + var slot2 = insert(collector, container, 4); + assertResult(collector, container, new Pair<>(2, 4)); + slot1.replaceWith(3); // 2 → 3; min becomes 3 + assertResult(collector, container, new Pair<>(3, 4)); + slot2.remove(); + slot1.remove(); + assertResult(collector, container, new Pair<>(null, null)); + } + + @Test + public void collectAndThenUpdate() { + var collector = ConstraintCollectors.collectAndThen(ConstraintCollectors.count(), i -> i * 10); + var container = collector.supplier().get(); + var slot = insert(collector, container, 1); + assertResult(collector, container, 10L); + slot.replaceWith(99); // count no-op; result unchanged + assertResult(collector, container, 10L); + slot.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void sumUpdate() { + UniConstraintCollector collector = ConstraintCollectors.sum(l -> l); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2L); + assertResult(collector, container, 2L); + slot1.replaceWith(5L); + assertResult(collector, container, 5L); + slot1.replaceWith(5L); // no-op + assertResult(collector, container, 5L); + var slot2 = insert(collector, container, 3L); + assertResult(collector, container, 8L); + slot1.replaceWith(1L); + assertResult(collector, container, 4L); + slot2.remove(); + assertResult(collector, container, 1L); + slot1.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void averageUpdate() { + UniConstraintCollector collector = ConstraintCollectors.average(i -> i); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4); + var slot2 = insert(collector, container, 2); + assertResult(collector, container, 3.0D); + slot1.replaceWith(6); // (6+2)/2 = 4.0; count unchanged + assertResult(collector, container, 4.0D); + slot1.replaceWith(6); // no-op + assertResult(collector, container, 4.0D); + slot2.remove(); + assertResult(collector, container, 6.0D); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void countDistinctUpdate() { + UniConstraintCollector collector = ConstraintCollectors.countDistinct(Function.identity()); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "a"); + var slot2 = insert(collector, container, "b"); + assertResult(collector, container, 2L); + slot1.replaceWith("b"); // both map to "b" + assertResult(collector, container, 1L); + slot1.replaceWith("b"); // no-op: Objects.equals short-circuit + assertResult(collector, container, 1L); + slot1.replaceWith("c"); // "b" and "c" + assertResult(collector, container, 2L); + slot2.remove(); + assertResult(collector, container, 1L); + slot1.remove(); + assertResult(collector, container, 0L); + } + + @Test + public void sumBigDecimalUpdate() { + UniConstraintCollector collector = ConstraintCollectors.sumBigDecimal(l -> l); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, BigDecimal.ONE); + var slot2 = insert(collector, container, BigDecimal.TEN); + assertResult(collector, container, BigDecimal.valueOf(11)); + var bd4 = BigDecimal.valueOf(4); + slot1.replaceWith(bd4); + assertResult(collector, container, BigDecimal.valueOf(14)); + slot1.replaceWith(bd4); // no-op: same reference, == short-circuit + assertResult(collector, container, BigDecimal.valueOf(14)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.remove(); + assertResult(collector, container, BigDecimal.ZERO); + } + + @Test + public void sumBigIntegerUpdate() { + UniConstraintCollector collector = ConstraintCollectors.sumBigInteger(l -> l); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, BigInteger.ONE); + var slot2 = insert(collector, container, BigInteger.TEN); + assertResult(collector, container, BigInteger.valueOf(11)); + var bi4 = BigInteger.valueOf(4); + slot1.replaceWith(bi4); + assertResult(collector, container, BigInteger.valueOf(14)); + slot1.replaceWith(bi4); // no-op: same reference, == short-circuit + assertResult(collector, container, BigInteger.valueOf(14)); + slot2.remove(); + assertResult(collector, container, BigInteger.valueOf(4)); + slot1.remove(); + assertResult(collector, container, BigInteger.ZERO); + } + + @Test + public void sumDurationUpdate() { + UniConstraintCollector collector = ConstraintCollectors.sumDuration(l -> l); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, Duration.ofSeconds(1)); + var slot2 = insert(collector, container, Duration.ofSeconds(2)); + assertResult(collector, container, Duration.ofSeconds(3)); + var d4 = Duration.ofSeconds(4); + slot1.replaceWith(d4); + assertResult(collector, container, Duration.ofSeconds(6)); + slot1.replaceWith(d4); // no-op: same reference, == short-circuit + assertResult(collector, container, Duration.ofSeconds(6)); + slot2.remove(); + assertResult(collector, container, Duration.ofSeconds(4)); + slot1.remove(); + assertResult(collector, container, Duration.ZERO); + } + + @Test + public void sumPeriodUpdate() { + UniConstraintCollector collector = ConstraintCollectors.sumPeriod(l -> l); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, Period.ofDays(1)); + var slot2 = insert(collector, container, Period.ofDays(2)); + assertResult(collector, container, Period.ofDays(3)); + var p4 = Period.ofDays(4); + slot1.replaceWith(p4); + assertResult(collector, container, Period.ofDays(6)); + slot1.replaceWith(p4); // no-op: same reference, == short-circuit + assertResult(collector, container, Period.ofDays(6)); + slot2.remove(); + assertResult(collector, container, Period.ofDays(4)); + slot1.remove(); + assertResult(collector, container, Period.ZERO); + } + + @Test + public void averageBigDecimalUpdate() { + UniConstraintCollector collector = + ConstraintCollectors.averageBigDecimal(i -> BigDecimal.valueOf(i)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4); + var slot2 = insert(collector, container, 2); + assertResult(collector, container, BigDecimal.valueOf(3)); + slot1.replaceWith(6); // (6+2)/2 = 4; count unchanged + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.replaceWith(6); // no-op: same input value + assertResult(collector, container, BigDecimal.valueOf(4)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void averageBigIntegerUpdate() { + UniConstraintCollector collector = + ConstraintCollectors.averageBigInteger(i -> BigInteger.valueOf(i)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4); + var slot2 = insert(collector, container, 2); + assertResult(collector, container, BigDecimal.valueOf(3)); + slot1.replaceWith(6); + assertResult(collector, container, BigDecimal.valueOf(4)); + slot1.replaceWith(6); // no-op: same input value + assertResult(collector, container, BigDecimal.valueOf(4)); + slot2.remove(); + assertResult(collector, container, BigDecimal.valueOf(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void averageDurationUpdate() { + UniConstraintCollector collector = + ConstraintCollectors.averageDuration(i -> Duration.ofSeconds(i)); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 4); + var slot2 = insert(collector, container, 2); + assertResult(collector, container, Duration.ofSeconds(3)); + slot1.replaceWith(6); // (6+2)/2 = 4; count unchanged + assertResult(collector, container, Duration.ofSeconds(4)); + slot1.replaceWith(6); // no-op: same input value + assertResult(collector, container, Duration.ofSeconds(4)); + slot2.remove(); + assertResult(collector, container, Duration.ofSeconds(6)); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void toConsecutiveSequencesUpdate() { + var collector = ConstraintCollectors.toConsecutiveSequences(Integer::intValue); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1); + var slot2 = insert(collector, container, 3); // gap of 2 — two sequences + assertResultRecursive(collector, container, buildSequenceChain(1, 3)); + slot1.replaceWith(2); // 2 and 3 are consecutive — one sequence + assertResultRecursive(collector, container, buildSequenceChain(2, 3)); + slot1.replaceWith(2); // same value → result unchanged + assertResultRecursive(collector, container, buildSequenceChain(2, 3)); + slot2.remove(); + assertResultRecursive(collector, container, buildSequenceChain(2)); + slot1.remove(); + assertResultRecursive(collector, container, buildSequenceChain()); + } + + @Test + public void consecutiveUsageUpdate() { + var collector = ConstraintCollectors.toConnectedRanges(Interval::start, Interval::end, (a, b) -> b - a); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, new Interval(1, 3)); + var slot2 = insert(collector, container, new Interval(10, 20)); // disjoint + assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(10, 20))); + var i812 = new Interval(8, 12); + slot1.replaceWith(i812); // now overlaps with (10,20) + assertResult(collector, container, buildConsecutiveUsage(new Interval(8, 12), new Interval(10, 20))); + slot1.replaceWith(i812); // same value → result unchanged + assertResult(collector, container, buildConsecutiveUsage(new Interval(8, 12), new Interval(10, 20))); + slot2.remove(); + assertResult(collector, container, buildConsecutiveUsage(new Interval(8, 12))); + slot1.remove(); + assertResult(collector, container, buildConsecutiveUsage()); + } + + @Test + public void toListUpdate() { + var collector = ConstraintCollectors. toList(); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1); + var slot2 = insert(collector, container, 2); + assertResult(collector, container, asList(1, 2)); + slot1.replaceWith(3); + assertResult(collector, container, asList(3, 2)); + slot1.replaceWith(3); // no-op + assertResult(collector, container, asList(3, 2)); + slot2.remove(); + assertResult(collector, container, singletonList(3)); + slot1.remove(); + assertResult(collector, container, emptyList()); + } + + @Test + public void toSetUpdate() { + var collector = ConstraintCollectors. toSet(); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1); + var slot2 = insert(collector, container, 2); + assertResult(collector, container, asSet(1, 2)); + slot1.replaceWith(3); + assertResult(collector, container, asSet(2, 3)); + slot1.replaceWith(3); // Objects.equals short-circuit + assertResult(collector, container, asSet(2, 3)); + slot2.remove(); + assertResult(collector, container, singleton(3)); + slot1.remove(); + assertResult(collector, container, emptySet()); + } + + @Test + public void toSortedSetUpdate() { + var collector = ConstraintCollectors. toSortedSet(); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1); + var slot2 = insert(collector, container, 2); + assertResult(collector, container, asSortedSet(1, 2)); + slot1.replaceWith(3); + assertResult(collector, container, asSortedSet(2, 3)); + slot1.replaceWith(3); // Objects.equals short-circuit + assertResult(collector, container, asSortedSet(2, 3)); + slot2.remove(); + assertResult(collector, container, asSortedSet(3)); + slot1.remove(); + assertResult(collector, container, emptySortedSet()); + } + + @Test + public void toCollectionUpdate() { + var collector = InnerUniConstraintCollectors.> toCollection( + Function.identity(), ArrayList::new); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 1); + var slot2 = insert(collector, container, 2); + assertResult(collector, container, new ArrayList<>(asList(1, 2))); + slot1.replaceWith(3); + assertResult(collector, container, new ArrayList<>(asList(3, 2))); + slot1.replaceWith(3); // no-op + assertResult(collector, container, new ArrayList<>(asList(3, 2))); + slot2.remove(); + assertResult(collector, container, new ArrayList<>(singletonList(3))); + slot1.remove(); + assertResult(collector, container, new ArrayList<>()); + } + + @Test + public void toMapUpdate() { + var collector = ConstraintCollectors. toMap( + Function.identity(), Function.identity()); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2); + var slot2 = insert(collector, container, 1); + assertResult(collector, container, asMap(2, singleton(2), 1, singleton(1))); + slot1.replaceWith(3); + assertResult(collector, container, asMap(1, singleton(1), 3, singleton(3))); + slot1.replaceWith(3); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asMap(1, singleton(1), 3, singleton(3))); + slot2.remove(); + assertResult(collector, container, asMap(3, singleton(3))); + slot1.remove(); + assertResult(collector, container, emptyMap()); + } + + @Test + public void toMapMergedUpdate() { + var collector = ConstraintCollectors. toMap( + Function.identity(), Function.identity(), Integer::sum); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2); + var slot2 = insert(collector, container, 1); + assertResult(collector, container, asMap(2, 2, 1, 1)); + slot1.replaceWith(3); + assertResult(collector, container, asMap(1, 1, 3, 3)); + slot1.replaceWith(3); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asMap(1, 1, 3, 3)); + slot2.remove(); + assertResult(collector, container, asMap(3, 3)); + slot1.remove(); + assertResult(collector, container, emptyMap()); + } + + @Test + public void toSortedMapUpdate() { + var collector = ConstraintCollectors. toSortedMap( + Function.identity(), Function.identity()); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 2); + var slot2 = insert(collector, container, 1); + assertResult(collector, container, asSortedMap(1, singleton(1), 2, singleton(2))); + slot1.replaceWith(3); + assertResult(collector, container, asSortedMap(1, singleton(1), 3, singleton(3))); + slot1.replaceWith(3); // Objects.equals short-circuit on Pair(3,3) + assertResult(collector, container, asSortedMap(1, singleton(1), 3, singleton(3))); + slot2.remove(); + assertResult(collector, container, asSortedMap(3, singleton(3))); + slot1.remove(); + assertResult(collector, container, emptySortedMap()); + } + + @Test + public void minComparableUpdate() { + UniConstraintCollector collector = min(); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 5); + var slot2 = insert(collector, container, 3); + assertResult(collector, container, 3); + slot2.replaceWith(6); + assertResult(collector, container, 5); + slot2.replaceWith(6); // Objects.equals short-circuit + assertResult(collector, container, 5); + slot1.replaceWith(1); + assertResult(collector, container, 1); + slot2.remove(); + assertResult(collector, container, 1); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void maxComparableUpdate() { + UniConstraintCollector collector = max(); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, 3); + var slot2 = insert(collector, container, 5); + assertResult(collector, container, 5); + slot2.replaceWith(2); + assertResult(collector, container, 3); + slot2.replaceWith(2); // Objects.equals short-circuit + assertResult(collector, container, 3); + slot1.replaceWith(7); + assertResult(collector, container, 7); + slot2.remove(); + assertResult(collector, container, 7); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void minNotComparableUpdate() { + var collector = min(Function.identity(), o -> (String) o); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, (Object) "b"); + var slot2 = insert(collector, container, (Object) "a"); + assertResult(collector, container, "a"); + slot2.replaceWith("c"); + assertResult(collector, container, "b"); + slot2.replaceWith("c"); // Objects.equals short-circuit + assertResult(collector, container, "b"); + slot1.replaceWith("a"); + assertResult(collector, container, "a"); + slot2.remove(); + assertResult(collector, container, "a"); + slot1.remove(); + assertResult(collector, container, null); + } + + @Test + public void maxNotComparableUpdate() { + UniConstraintCollector collector = max(Function.identity(), o -> o); + var container = collector.supplier().get(); + var slot1 = insert(collector, container, "b"); + var slot2 = insert(collector, container, "a"); + assertResult(collector, container, "b"); + slot2.replaceWith("c"); + assertResult(collector, container, "c"); + slot2.replaceWith("c"); // Objects.equals short-circuit + assertResult(collector, container, "c"); + slot1.replaceWith("a"); + assertResult(collector, container, "c"); + slot2.remove(); + assertResult(collector, container, "a"); + slot1.remove(); + assertResult(collector, container, null); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java index 20e4b52146e..834c2a78217 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java @@ -12,6 +12,7 @@ import static ai.timefold.solver.core.testutil.PlannerTestUtils.asMap; import static org.assertj.core.api.Assertions.assertThatCode; +import java.util.Arrays; import java.util.Objects; import java.util.Set; import java.util.function.Function; @@ -19,6 +20,7 @@ import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; @@ -145,7 +147,36 @@ void uniGroupByRecollected() { } @TestTemplate - void biGroupByRecollected() { + void biGroupByRecollectedToList() { + TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 3, 2, 5); + + InnerScoreDirector scoreDirector = + buildScoreDirector(factory -> factory + .forEachUniquePair(TestdataLavishEntity.class, equal(TestdataLavishEntity::getEntityGroup)) + // Stream of all unique entity bi tuples that share a group + .groupBy((a, b) -> a.getEntityGroup(), countBi()) + .groupBy(ConstraintCollectors.toList((a, b) -> a)) + .penalize(SimpleScore.ONE) + .asConstraint(TEST_CONSTRAINT_ID)); + + // From scratch + scoreDirector.setWorkingSolution(solution); + assertScore(scoreDirector, + assertMatchWithScore(-1, + Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1)))); + + // Incremental + TestdataLavishEntity entity = solution.getFirstEntity(); + scoreDirector.beforeEntityRemoved(entity); + solution.getEntityList().remove(entity); + scoreDirector.afterEntityRemoved(entity); + assertScore(scoreDirector, + assertMatchWithScore(-1, + Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1)))); + } + + @TestTemplate + void biGroupByRecollectedToMap() { TestdataLavishSolution solution = TestdataLavishSolution.generateSolution(2, 3, 2, 5); InnerScoreDirector scoreDirector = @@ -613,4 +644,5 @@ void reusedStreamsInJoin() { assertMatchWithScore(-1, entity3, entity1), assertMatchWithScore(-1, entity3, entity3)); } + } 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 9ffac8dd852..d0a1c335eab 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 @@ -703,6 +703,7 @@ You can also provide your own collectors by implementing the or its `Bi...`, `Tri...` and `Quad...` counterparts. + [#collectorsCount] ==== `count()` collector @@ -1186,6 +1187,146 @@ ConstraintCollectors.collectAndThen( ==== +[#constraintStreamsCustomCollector] +==== Implementing a custom collector + +When no built-in collector covers the required aggregation, +implement `UniConstraintCollector` directly +(or its `Bi...`, `Tri...`, `Quad...` counterparts for higher-arity streams). +The interface has three methods: + +[cols="1,3"] +|=== +| Method | Purpose + +| `supplier()` +| Creates a fresh mutable result container for each group. + +| `accumulator()` +| Called when a fact enters a group; returns a `UniConstraintCollectorValueHandle` + whose `add()`, `replaceWith()`, and `remove()` methods maintain the container. + +| `finisher()` +| Converts the container into the immutable group result. +|=== + +The following example shows a simplified version of the built-in `toList()` collector: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +public class SimpleToListCollector + implements UniConstraintCollector, List> { + + @Override + public Supplier> supplier() { + return ArrayList::new; + } + + @Override + public UniConstraintCollectorAccumulator, A> accumulator() { + return list -> new UniConstraintCollectorValueHandle<>() { + private A current; + + @Override + public void add(A element) { + current = element; + list.add(element); + } + + @Override + public void remove() { + list.remove(current); + current = null; + } + }; + } + + @Override + public Function, List> finisher() { + return Collections::unmodifiableList; + } +} +---- +==== + +`accumulator()` returns a factory: for each fact that enters a group, +the solver calls `intoGroup(container)` to obtain a fresh handle, +then calls `add()` exactly once, zero or more `replaceWith()` calls as the fact changes, +and `remove()` at most once when the fact leaves the group. + +`list.remove(current)` removes by value (runs in linear time). +You might consider storing the insertion index instead and removing by index (runs in constant time), +but that would be risky — +any removal can shift subsequent indices at any time, +making stored positions unreliable. +Thankfully, there is a better way, using an incremental update. + +When a planning variable changes, the solver calls `replaceWith()` on the affected handle. +The default implementation (inherited from `UniConstraintCollectorValueHandle`) does `remove()` followed by `add()`. +For `ArrayList`, this means scanning to find the element in linear time, +then shifting all subsequent elements to close the gap, and finally appending the new value at the end. + +Overriding `replaceWith()` with `list.set()` avoids the shift entirely: +`set()` replaces the element in-place at its current position — +no elements are moved, and the element's position in the list is preserved. + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +public class IncrementalToListCollector + implements UniConstraintCollector, List> { + + @Override + public Supplier> supplier() { + return ArrayList::new; + } + + @Override + public UniConstraintCollectorAccumulator, A> accumulator() { + return list -> new UniConstraintCollectorValueHandle<>() { + private A current; + + @Override + public void add(A element) { + current = element; + list.add(element); + } + + @Override + public void replaceWith(A element) { + list.set(list.indexOf(current), element); + current = element; + } + + @Override + public void remove() { + list.remove(current); + current = null; + } + }; + } + + @Override + public Function, List> finisher() { + return Collections::unmodifiableList; + } +} +---- +==== + +NOTE: The above implementation of `replaceWith()` is still not ideal, +as it requires a linear scan to find the element. +This will cause constraint scaling issues in larger lists, +and is the main reason why we do not recommend using `toList()` in performance-sensitive constraints. + + [#constraintStreamsConditionalPropagation] === Conditional propagation diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/stream/common/LoadBalanceRoundTripTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/stream/common/LoadBalanceRoundTripTest.java index 8cb4ec6635f..9f99acdf0d6 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/stream/common/LoadBalanceRoundTripTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/stream/common/LoadBalanceRoundTripTest.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; import ai.timefold.solver.jackson.api.TimefoldJacksonModule; import org.junit.jupiter.api.Test; @@ -36,9 +37,9 @@ void roundTrip() throws JacksonException { Item c = new Item("C"); var collector = (UniConstraintCollector) ConstraintCollectors.loadBalance(Function.identity()); var context = collector.supplier().get(); - var accumulator = collector.accumulator(); + var accumulator = (UniConstraintCollectorAccumulator) collector.accumulator(); for (var item : List.of(a, b, c, a, a, b, a)) { - accumulator.apply(context, item); + accumulator.intoGroup(context).add(item); } // Retrieve the instance to be serialized. diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/stream/common/SequenceRoundTripTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/stream/common/SequenceRoundTripTest.java index 35ba5818dab..17ed4d33bf6 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/stream/common/SequenceRoundTripTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/score/stream/common/SequenceRoundTripTest.java @@ -10,7 +10,7 @@ import ai.timefold.solver.core.api.score.stream.common.Sequence; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.SequenceCalculator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; import ai.timefold.solver.jackson.api.TimefoldJacksonModule; import org.junit.jupiter.api.Test; @@ -32,21 +32,21 @@ private record Item(String id, int index) { } @Test - void roundTrip() throws JacksonException { + void roundTrip() throws JacksonException { // Prepare the data to be serialized. - Item sequence1Item1 = new Item("sequence1Item1", 0); - Item sequence1Item2 = new Item("sequence1Item2", 1); - Item sequence2Item1 = new Item("sequence2Item1", 3); - Item sequence2Item2 = new Item("sequence2Item2", 4); - Item sequence2Item3 = new Item("sequence2Item3", 5); - Item sequence3Item1 = new Item("sequence3Item1", 7); - UniConstraintCollector, SequenceChain> collector = + var sequence1Item1 = new Item("sequence1Item1", 0); + var sequence1Item2 = new Item("sequence1Item2", 1); + var sequence2Item1 = new Item("sequence2Item1", 3); + var sequence2Item2 = new Item("sequence2Item2", 4); + var sequence2Item3 = new Item("sequence2Item3", 5); + var sequence3Item1 = new Item("sequence3Item1", 7); + UniConstraintCollector> collector = (UniConstraintCollector) ConstraintCollectors.toConsecutiveSequences(Item::index); var context = collector.supplier().get(); - var accumulator = collector.accumulator(); + var accumulator = (UniConstraintCollectorAccumulator) collector.accumulator(); for (var item : List.of(sequence1Item1, sequence1Item2, sequence2Item1, sequence2Item2, sequence2Item3, sequence3Item1)) { - accumulator.apply(context, item); + accumulator.intoGroup(context).add(item); } // Retrieve the instances to be serialized. diff --git a/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/score/stream/common/LoadBalanceRoundTripTest.java b/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/score/stream/common/LoadBalanceRoundTripTest.java index 88027043e4d..0890697c494 100644 --- a/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/score/stream/common/LoadBalanceRoundTripTest.java +++ b/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/score/stream/common/LoadBalanceRoundTripTest.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.common.LoadBalance; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; import ai.timefold.solver.quarkus.jackson.TimefoldJacksonModule; import org.junit.jupiter.api.Test; @@ -36,9 +37,9 @@ void roundTrip() throws JsonProcessingException { Item c = new Item("C"); var collector = (UniConstraintCollector) ConstraintCollectors.loadBalance(Function.identity()); var context = collector.supplier().get(); - var accumulator = collector.accumulator(); + var accumulator = (UniConstraintCollectorAccumulator) collector.accumulator(); for (var item : List.of(a, b, c, a, a, b, a)) { - accumulator.apply(context, item); + accumulator.intoGroup(context).add(item); } // Retrieve the instance to be serialized. diff --git a/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/score/stream/common/SequenceRoundTripTest.java b/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/score/stream/common/SequenceRoundTripTest.java index 77dbdea69f7..b94a67b5021 100644 --- a/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/score/stream/common/SequenceRoundTripTest.java +++ b/quarkus-integration/quarkus-jackson/runtime/src/test/java/ai/timefold/solver/quarkus/jackson/score/stream/common/SequenceRoundTripTest.java @@ -10,7 +10,7 @@ import ai.timefold.solver.core.api.score.stream.common.Sequence; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; -import ai.timefold.solver.core.impl.score.stream.collector.SequenceCalculator; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollectorAccumulator; import ai.timefold.solver.quarkus.jackson.TimefoldJacksonModule; import org.junit.jupiter.api.Test; @@ -31,21 +31,21 @@ private record Item(String id, int index) { } @Test - void roundTrip() throws JsonProcessingException { + void roundTrip() throws JsonProcessingException { // Prepare the data to be serialized. - Item sequence1Item1 = new Item("sequence1Item1", 0); - Item sequence1Item2 = new Item("sequence1Item2", 1); - Item sequence2Item1 = new Item("sequence2Item1", 3); - Item sequence2Item2 = new Item("sequence2Item2", 4); - Item sequence2Item3 = new Item("sequence2Item3", 5); - Item sequence3Item1 = new Item("sequence3Item1", 7); - UniConstraintCollector, SequenceChain> collector = + var sequence1Item1 = new Item("sequence1Item1", 0); + var sequence1Item2 = new Item("sequence1Item2", 1); + var sequence2Item1 = new Item("sequence2Item1", 3); + var sequence2Item2 = new Item("sequence2Item2", 4); + var sequence2Item3 = new Item("sequence2Item3", 5); + var sequence3Item1 = new Item("sequence3Item1", 7); + UniConstraintCollector> collector = (UniConstraintCollector) ConstraintCollectors.toConsecutiveSequences(Item::index); var context = collector.supplier().get(); - var accumulator = collector.accumulator(); + var accumulator = (UniConstraintCollectorAccumulator) collector.accumulator(); for (var item : List.of(sequence1Item1, sequence1Item2, sequence2Item1, sequence2Item2, sequence2Item3, sequence3Item1)) { - accumulator.apply(context, item); + accumulator.intoGroup(context).add(item); } // Retrieve the instances to be serialized.