From 0d0ad32cfa3c831b91801324eb04e6fe77766b72 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Mon, 26 Aug 2024 15:31:34 -0400 Subject: [PATCH 1/4] fix: Use inserted range when retracting range in `toConnectedRanges` The `ConnectedRangesCalculator` incorrectly assumes the range for each value is constant. The fix is to save the range on insert, and then pass the saved range on retract. The other calculators do not have this problem, because either: - Their input should be immutable (ex: number) - They used a map in their internal data structure that saved the original value --- .../collector/ConnectedRangesCalculator.java | 14 +- .../collector/IntDistinctCountCalculator.java | 11 +- .../LongDistinctCountCalculator.java | 11 +- .../stream/collector/ObjectCalculator.java | 6 +- .../collector/ReferenceAverageCalculator.java | 9 +- .../collector/ReferenceSumCalculator.java | 9 +- .../stream/collector/SequenceCalculator.java | 5 +- .../bi/AverageReferenceBiCollector.java | 2 +- .../ConnectedRangesBiConstraintCollector.java | 3 +- ...ecutiveSequencesBiConstraintCollector.java | 2 +- .../bi/CountDistinctIntBiCollector.java | 2 +- .../bi/CountDistinctLongBiCollector.java | 2 +- .../bi/ObjectCalculatorBiCollector.java | 10 +- .../collector/bi/SumReferenceBiCollector.java | 2 +- .../quad/AverageReferenceQuadCollector.java | 3 +- ...onnectedRangesQuadConstraintCollector.java | 3 +- ...utiveSequencesQuadConstraintCollector.java | 2 +- .../quad/CountDistinctIntQuadCollector.java | 2 +- .../quad/CountDistinctLongQuadCollector.java | 2 +- .../quad/ObjectCalculatorQuadCollector.java | 10 +- .../quad/SumReferenceQuadCollector.java | 2 +- .../tri/AverageReferenceTriCollector.java | 3 +- ...ConnectedRangesTriConstraintCollector.java | 3 +- ...cutiveSequencesTriConstraintCollector.java | 2 +- .../tri/CountDistinctIntTriCollector.java | 2 +- .../tri/CountDistinctLongTriCollector.java | 2 +- .../tri/ObjectCalculatorTriCollector.java | 10 +- .../tri/SumReferenceTriCollector.java | 2 +- .../uni/AverageReferenceUniCollector.java | 2 +- ...ConnectedRangesUniConstraintCollector.java | 3 +- ...cutiveSequencesUniConstraintCollector.java | 2 +- .../uni/CountDistinctIntUniCollector.java | 2 +- .../uni/CountDistinctLongUniCollector.java | 2 +- .../uni/ObjectCalculatorUniCollector.java | 10 +- .../uni/SumReferenceUniCollector.java | 2 +- .../ConnectedRangeSolvingTest.java | 137 ++++++++++++++++++ 36 files changed, 224 insertions(+), 72 deletions(-) create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeSolvingTest.java 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 index c49e11f76f..9395893bbf 100644 --- 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 @@ -5,9 +5,11 @@ 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> { + implements + ObjectCalculator, Range> { private final ConnectedRangeTracker context; @@ -21,13 +23,15 @@ public ConnectedRangesCalculator(Function s } @Override - public void insert(Interval_ result) { - context.add(context.getRange(result)); + public Range insert(Interval_ result) { + final var saved = context.getRange(result); + context.add(saved); + return saved; } @Override - public void retract(Interval_ result) { - context.remove(context.getRange(result)); + public void retract(Range range) { + context.remove(range); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/IntDistinctCountCalculator.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/IntDistinctCountCalculator.java index ea6866127d..0552e7dce1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/IntDistinctCountCalculator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/IntDistinctCountCalculator.java @@ -5,18 +5,19 @@ import ai.timefold.solver.core.impl.util.MutableInt; -public final class IntDistinctCountCalculator implements ObjectCalculator { +public final class IntDistinctCountCalculator implements ObjectCalculator { private final Map countMap = new HashMap<>(); @Override - public void insert(Input_ input) { + public Input_ insert(Input_ input) { countMap.computeIfAbsent(input, ignored -> new MutableInt()).increment(); + return input; } @Override - public void retract(Input_ input) { - if (countMap.get(input).decrement() == 0) { - countMap.remove(input); + public void retract(Input_ mapped) { + if (countMap.get(mapped).decrement() == 0) { + countMap.remove(mapped); } } 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 index e7f9332424..0a5eb0706f 100644 --- 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 @@ -5,18 +5,19 @@ import ai.timefold.solver.core.impl.util.MutableInt; -public final class LongDistinctCountCalculator implements ObjectCalculator { +public final class LongDistinctCountCalculator implements ObjectCalculator { private final Map countMap = new HashMap<>(); @Override - public void insert(Input_ input) { + public Input_ insert(Input_ input) { countMap.computeIfAbsent(input, ignored -> new MutableInt()).increment(); + return input; } @Override - public void retract(Input_ input) { - if (countMap.get(input).decrement() == 0) { - countMap.remove(input); + public void retract(Input_ mapped) { + if (countMap.get(mapped).decrement() == 0) { + countMap.remove(mapped); } } 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 index d3007f2c25..ae1927285f 100644 --- 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 @@ -1,11 +1,11 @@ package ai.timefold.solver.core.impl.score.stream.collector; -public sealed interface ObjectCalculator +public sealed interface ObjectCalculator permits ConnectedRangesCalculator, IntDistinctCountCalculator, LongDistinctCountCalculator, ReferenceAverageCalculator, ReferenceSumCalculator, SequenceCalculator { - void insert(Input_ input); + Mapped_ insert(Input_ input); - void retract(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 index 1cbf2cd162..ce4963b1c8 100644 --- 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 @@ -8,7 +8,7 @@ import java.util.function.BinaryOperator; import java.util.function.Supplier; -public final class ReferenceAverageCalculator implements ObjectCalculator { +public final class ReferenceAverageCalculator implements ObjectCalculator { int count = 0; Input_ sum; final BinaryOperator adder; @@ -51,15 +51,16 @@ public static Supplier> duration( } @Override - public void insert(Input_ input) { + public Input_ insert(Input_ input) { count++; sum = adder.apply(sum, input); + return input; } @Override - public void retract(Input_ input) { + public void retract(Input_ mapped) { count--; - sum = subtractor.apply(sum, input); + sum = subtractor.apply(sum, mapped); } @Override 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 index 95aeedef7b..dc361f153b 100644 --- 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 @@ -2,7 +2,7 @@ import java.util.function.BinaryOperator; -public final class ReferenceSumCalculator implements ObjectCalculator { +public final class ReferenceSumCalculator implements ObjectCalculator { private Result_ current; private final BinaryOperator adder; private final BinaryOperator subtractor; @@ -14,13 +14,14 @@ public ReferenceSumCalculator(Result_ current, BinaryOperator adder, Bi } @Override - public void insert(Result_ input) { + public Result_ insert(Result_ input) { current = adder.apply(current, input); + return input; } @Override - public void retract(Result_ input) { - current = subtractor.apply(current, input); + public void retract(Result_ mapped) { + current = subtractor.apply(current, mapped); } @Override 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 index 54db9dc5fb..2463fe4166 100644 --- 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 @@ -7,7 +7,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.consecutive.ConsecutiveSetTree; public final class SequenceCalculator - implements ObjectCalculator> { + implements ObjectCalculator, Result_> { private final ConsecutiveSetTree context = new ConsecutiveSetTree<>( (Integer a, Integer b) -> b - a, @@ -20,9 +20,10 @@ public SequenceCalculator(ToIntFunction indexMap) { } @Override - public void insert(Result_ result) { + public Result_ insert(Result_ result) { var value = indexMap.applyAsInt(result); context.add(result, value); + return result; } @Override 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 8ffb3ad19b..e11cb7a9ce 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 @@ -7,7 +7,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; final class AverageReferenceBiCollector - extends ObjectCalculatorBiCollector> { + extends ObjectCalculatorBiCollector> { private final Supplier> calculatorSupplier; AverageReferenceBiCollector(BiFunction mapper, 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 5ec943825a..cd7efd836c 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 @@ -7,10 +7,11 @@ 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; final class ConnectedRangesBiConstraintCollector, Difference_ extends Comparable> extends - ObjectCalculatorBiCollector, ConnectedRangesCalculator> { + ObjectCalculatorBiCollector, Range, ConnectedRangesCalculator> { private final Function startMap; private final Function endMap; 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 775db3c578..4831d7bca4 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 @@ -10,7 +10,7 @@ final class ConsecutiveSequencesBiConstraintCollector extends - ObjectCalculatorBiCollector, SequenceCalculator> { + ObjectCalculatorBiCollector, Result_, SequenceCalculator> { private final ToIntFunction indexMap; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctIntBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctIntBiCollector.java index 68c340fc7d..8775aa525a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctIntBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctIntBiCollector.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.IntDistinctCountCalculator; final class CountDistinctIntBiCollector - extends ObjectCalculatorBiCollector> { + extends ObjectCalculatorBiCollector> { CountDistinctIntBiCollector(BiFunction mapper) { super(mapper); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctLongBiCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctLongBiCollector.java index 20adc32787..4c83171952 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctLongBiCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/bi/CountDistinctLongBiCollector.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.LongDistinctCountCalculator; final class CountDistinctLongBiCollector - extends ObjectCalculatorBiCollector> { + extends ObjectCalculatorBiCollector> { CountDistinctLongBiCollector(BiFunction mapper) { super(mapper); } 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 index 2e197c5138..418ac70bea 100644 --- 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 @@ -8,7 +8,7 @@ import ai.timefold.solver.core.api.score.stream.bi.BiConstraintCollector; import ai.timefold.solver.core.impl.score.stream.collector.ObjectCalculator; -abstract sealed class ObjectCalculatorBiCollector> +abstract sealed class ObjectCalculatorBiCollector> implements BiConstraintCollector permits AverageReferenceBiCollector, ConnectedRangesBiConstraintCollector, ConsecutiveSequencesBiConstraintCollector, CountDistinctIntBiCollector, CountDistinctLongBiCollector, SumReferenceBiCollector { @@ -21,9 +21,9 @@ public ObjectCalculatorBiCollector(BiFunction accumulator() { return (calculator, a, b) -> { - final Input_ mapped = mapper.apply(a, b); - calculator.insert(mapped); - return () -> calculator.retract(mapped); + final var mapped = mapper.apply(a, b); + final var saved = calculator.insert(mapped); + return () -> calculator.retract(saved); }; } @@ -38,7 +38,7 @@ public boolean equals(Object object) { return true; if (object == null || getClass() != object.getClass()) return false; - var that = (ObjectCalculatorBiCollector) object; + var that = (ObjectCalculatorBiCollector) object; return Objects.equals(mapper, that.mapper); } 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 2f8a7e9717..25e499ea66 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 @@ -8,7 +8,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.ReferenceSumCalculator; final class SumReferenceBiCollector - extends ObjectCalculatorBiCollector> { + extends ObjectCalculatorBiCollector> { private final Result_ zero; private final BinaryOperator adder; private final BinaryOperator subtractor; 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 c4e3c7fabc..c1e8998dde 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 @@ -7,7 +7,8 @@ import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; final class AverageReferenceQuadCollector - extends ObjectCalculatorQuadCollector> { + extends + ObjectCalculatorQuadCollector> { private final Supplier> calculatorSupplier; AverageReferenceQuadCollector(QuadFunction mapper, 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 4902c48047..cf241a37bf 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 @@ -8,10 +8,11 @@ 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; final class ConnectedRangesQuadConstraintCollector, Difference_ extends Comparable> extends - ObjectCalculatorQuadCollector, ConnectedRangesCalculator> { + ObjectCalculatorQuadCollector, Range, ConnectedRangesCalculator> { private final Function startMap; private final Function endMap; 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 808b521783..48378a9817 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 @@ -10,7 +10,7 @@ final class ConsecutiveSequencesQuadConstraintCollector extends - ObjectCalculatorQuadCollector, SequenceCalculator> { + ObjectCalculatorQuadCollector, Result_, SequenceCalculator> { private final ToIntFunction indexMap; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctIntQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctIntQuadCollector.java index e5c1907eba..cb16d5bb31 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctIntQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctIntQuadCollector.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.IntDistinctCountCalculator; final class CountDistinctIntQuadCollector - extends ObjectCalculatorQuadCollector> { + extends ObjectCalculatorQuadCollector> { CountDistinctIntQuadCollector(QuadFunction mapper) { super(mapper); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctLongQuadCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctLongQuadCollector.java index 16f1cb7d3b..f1dcc17573 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctLongQuadCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/quad/CountDistinctLongQuadCollector.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.LongDistinctCountCalculator; final class CountDistinctLongQuadCollector - extends ObjectCalculatorQuadCollector> { + extends ObjectCalculatorQuadCollector> { CountDistinctLongQuadCollector(QuadFunction mapper) { super(mapper); } 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 index ee508a4a8f..78c300be1a 100644 --- 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 @@ -8,7 +8,7 @@ import ai.timefold.solver.core.api.score.stream.quad.QuadConstraintCollector; import ai.timefold.solver.core.impl.score.stream.collector.ObjectCalculator; -abstract sealed class ObjectCalculatorQuadCollector> +abstract sealed class ObjectCalculatorQuadCollector> implements QuadConstraintCollector permits AverageReferenceQuadCollector, ConnectedRangesQuadConstraintCollector, ConsecutiveSequencesQuadConstraintCollector, CountDistinctIntQuadCollector, CountDistinctLongQuadCollector, @@ -23,9 +23,9 @@ public ObjectCalculatorQuadCollector(QuadFunction accumulator() { return (calculator, a, b, c, d) -> { - final Input_ mapped = mapper.apply(a, b, c, d); - calculator.insert(mapped); - return () -> calculator.retract(mapped); + final var mapped = mapper.apply(a, b, c, d); + final var saved = calculator.insert(mapped); + return () -> calculator.retract(saved); }; } @@ -40,7 +40,7 @@ public boolean equals(Object object) { return true; if (object == null || getClass() != object.getClass()) return false; - var that = (ObjectCalculatorQuadCollector) object; + var that = (ObjectCalculatorQuadCollector) object; return Objects.equals(mapper, that.mapper); } 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 03fb2c3d11..da49633c8e 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 @@ -8,7 +8,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.ReferenceSumCalculator; final class SumReferenceQuadCollector - extends ObjectCalculatorQuadCollector> { + extends ObjectCalculatorQuadCollector> { private final Result_ zero; private final BinaryOperator adder; private final BinaryOperator subtractor; 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 15a3438bcc..7c16f7cbf0 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 @@ -7,7 +7,8 @@ import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; final class AverageReferenceTriCollector - extends ObjectCalculatorTriCollector> { + extends + ObjectCalculatorTriCollector> { private final Supplier> calculatorSupplier; AverageReferenceTriCollector(TriFunction mapper, 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 17851a680e..17a5299dda 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 @@ -8,10 +8,11 @@ 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; final class ConnectedRangesTriConstraintCollector, Difference_ extends Comparable> extends - ObjectCalculatorTriCollector, ConnectedRangesCalculator> { + ObjectCalculatorTriCollector, Range, ConnectedRangesCalculator> { private final Function startMap; private final Function endMap; 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 eabbb1b384..b1253b0f82 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 @@ -10,7 +10,7 @@ final class ConsecutiveSequencesTriConstraintCollector extends - ObjectCalculatorTriCollector, SequenceCalculator> { + ObjectCalculatorTriCollector, Result_, SequenceCalculator> { private final ToIntFunction indexMap; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctIntTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctIntTriCollector.java index 03a739e02b..e798f38e4d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctIntTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctIntTriCollector.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.IntDistinctCountCalculator; final class CountDistinctIntTriCollector - extends ObjectCalculatorTriCollector> { + extends ObjectCalculatorTriCollector> { CountDistinctIntTriCollector(TriFunction mapper) { super(mapper); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctLongTriCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctLongTriCollector.java index 49a57d379e..69e07017a7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctLongTriCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/tri/CountDistinctLongTriCollector.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.LongDistinctCountCalculator; final class CountDistinctLongTriCollector - extends ObjectCalculatorTriCollector> { + extends ObjectCalculatorTriCollector> { CountDistinctLongTriCollector(TriFunction mapper) { super(mapper); } 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 index d57b95b9f5..44484d0cb0 100644 --- 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 @@ -8,7 +8,7 @@ import ai.timefold.solver.core.api.score.stream.tri.TriConstraintCollector; import ai.timefold.solver.core.impl.score.stream.collector.ObjectCalculator; -abstract sealed class ObjectCalculatorTriCollector> +abstract sealed class ObjectCalculatorTriCollector> implements TriConstraintCollector permits AverageReferenceTriCollector, ConnectedRangesTriConstraintCollector, ConsecutiveSequencesTriConstraintCollector, CountDistinctIntTriCollector, CountDistinctLongTriCollector, SumReferenceTriCollector { @@ -21,9 +21,9 @@ public ObjectCalculatorTriCollector(TriFunction accumulator() { return (calculator, a, b, c) -> { - final Input_ mapped = mapper.apply(a, b, c); - calculator.insert(mapped); - return () -> calculator.retract(mapped); + final var mapped = mapper.apply(a, b, c); + final var saved = calculator.insert(mapped); + return () -> calculator.retract(saved); }; } @@ -38,7 +38,7 @@ public boolean equals(Object object) { return true; if (object == null || getClass() != object.getClass()) return false; - var that = (ObjectCalculatorTriCollector) object; + var that = (ObjectCalculatorTriCollector) object; return Objects.equals(mapper, that.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 056fd4c4b1..4aa7da1937 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 @@ -8,7 +8,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.ReferenceSumCalculator; final class SumReferenceTriCollector - extends ObjectCalculatorTriCollector> { + extends ObjectCalculatorTriCollector> { private final Result_ zero; private final BinaryOperator adder; private final BinaryOperator subtractor; 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 cd43e197a2..329d727ba0 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 @@ -7,7 +7,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.ReferenceAverageCalculator; final class AverageReferenceUniCollector - extends ObjectCalculatorUniCollector> { + extends ObjectCalculatorUniCollector> { private final Supplier> calculatorSupplier; AverageReferenceUniCollector(Function mapper, 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 20557c211b..0eb267265a 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 @@ -7,10 +7,11 @@ 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; final class ConnectedRangesUniConstraintCollector, Difference_ extends Comparable> extends - ObjectCalculatorUniCollector, ConnectedRangesCalculator> { + ObjectCalculatorUniCollector, Range, ConnectedRangesCalculator> { private final Function startMap; private final Function endMap; 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 45f641de5b..7887740990 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 @@ -9,7 +9,7 @@ import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; final class ConsecutiveSequencesUniConstraintCollector - extends ObjectCalculatorUniCollector, SequenceCalculator> { + extends ObjectCalculatorUniCollector, A, SequenceCalculator> { private final ToIntFunction indexMap; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctIntUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctIntUniCollector.java index 3965ed854e..dff9559f03 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctIntUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctIntUniCollector.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.IntDistinctCountCalculator; final class CountDistinctIntUniCollector - extends ObjectCalculatorUniCollector> { + extends ObjectCalculatorUniCollector> { CountDistinctIntUniCollector(Function mapper) { super(mapper); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctLongUniCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctLongUniCollector.java index 6c482fac9c..5c9a2c0cac 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctLongUniCollector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/uni/CountDistinctLongUniCollector.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.LongDistinctCountCalculator; final class CountDistinctLongUniCollector - extends ObjectCalculatorUniCollector> { + extends ObjectCalculatorUniCollector> { CountDistinctLongUniCollector(Function mapper) { super(mapper); } 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 index a7f6c56811..746a303da2 100644 --- 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 @@ -7,7 +7,7 @@ import ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector; import ai.timefold.solver.core.impl.score.stream.collector.ObjectCalculator; -abstract sealed class ObjectCalculatorUniCollector> +abstract sealed class ObjectCalculatorUniCollector> implements UniConstraintCollector permits AverageReferenceUniCollector, ConnectedRangesUniConstraintCollector, ConsecutiveSequencesUniConstraintCollector, CountDistinctIntUniCollector, CountDistinctLongUniCollector, SumReferenceUniCollector { @@ -21,9 +21,9 @@ public ObjectCalculatorUniCollector(Function mapper @Override public BiFunction accumulator() { return (calculator, a) -> { - final Input_ mapped = mapper.apply(a); - calculator.insert(mapped); - return () -> calculator.retract(mapped); + final var mapped = mapper.apply(a); + final var saved = calculator.insert(mapped); + return () -> calculator.retract(saved); }; } @@ -38,7 +38,7 @@ public boolean equals(Object object) { return true; if (object == null || getClass() != object.getClass()) return false; - var that = (ObjectCalculatorUniCollector) object; + var that = (ObjectCalculatorUniCollector) object; return Objects.equals(mapper, that.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 68c2f16432..b5fb706639 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 @@ -8,7 +8,7 @@ import ai.timefold.solver.core.impl.score.stream.collector.ReferenceSumCalculator; final class SumReferenceUniCollector - extends ObjectCalculatorUniCollector> { + extends ObjectCalculatorUniCollector> { private final Result_ zero; private final BinaryOperator adder; private final BinaryOperator subtractor; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeSolvingTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeSolvingTest.java new file mode 100644 index 0000000000..e1271ce8c8 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeSolvingTest.java @@ -0,0 +1,137 @@ +package ai.timefold.solver.core.impl.score.stream.collector.connected_ranges; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.lookup.PlanningId; +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.solution.PlanningScore; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeFactory; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore; +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.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.Joiners; +import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain; +import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.config.solver.EnvironmentMode; +import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.core.config.solver.termination.TerminationConfig; + +import org.junit.jupiter.api.Test; + +public class ConnectedRangeSolvingTest { + public record Equipment(int id, int capacity) { + } + + @PlanningEntity + public static class Job { + private int id; + private int requiredEquipmentId; + private Integer start; + + public Job() { + } + + public Job(int id, int requiredEquipmentId) { + this.id = id; + this.requiredEquipmentId = requiredEquipmentId; + } + + @PlanningId + public int getId() { + return id; + } + + public int getRequiredEquipmentId() { + return requiredEquipmentId; + } + + @PlanningVariable + public Integer getStart() { + return start; + } + + public void setStart(Integer start) { + this.start = start; + } + + public Integer getEnd() { + return start == null ? null : start + 10; + } + } + + @PlanningSolution + public static class Planner { + @PlanningScore + private HardMediumSoftScore score; + + @ProblemFactCollectionProperty + private final List equipments = new ArrayList<>(); + + @PlanningEntityCollectionProperty + private final List jobs = new ArrayList<>(); + + @ValueRangeProvider + public CountableValueRange getStartOffsetRange() { + return ValueRangeFactory.createIntValueRange(0, 100); + } + + public Planner() { + } + + public Planner(List equipments, List jobs) { + this.equipments.addAll(equipments); + this.jobs.addAll(jobs); + } + } + + public static class MyConstraintProvider implements ConstraintProvider { + public MyConstraintProvider() { + } + + @Override + public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { + return new Constraint[] { doNotOverAssignEquipment(constraintFactory) }; + } + + public Constraint doNotOverAssignEquipment(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Equipment.class) + .join(Job.class, Joiners.equal(Equipment::id, Job::getRequiredEquipmentId)) + .groupBy((equipment, job) -> equipment, ConstraintCollectors.toConnectedRanges((equipment, job) -> job, + Job::getStart, + Job::getEnd, + (a, b) -> b - a)) + .flattenLast(ConnectedRangeChain::getConnectedRanges) + .filter((equipment, connectedRange) -> connectedRange.getMaximumOverlap() > equipment.capacity()) + .penalize(HardMediumSoftScore.ONE_HARD) + .asConstraint("Concurrent equipment usage over capacity"); + } + } + + @Test + public void solveConnectedRanges() { + var e1 = new Equipment(1, 1); + var j1 = new Job(1, e1.id()); + var j2 = new Job(2, e1.id()); + var problem = new Planner(List.of(e1), List.of(j1, j2)); + + var config = new SolverConfig() + .withSolutionClass(Planner.class) + .withEntityClasses(Job.class) + .withConstraintProviderClass(MyConstraintProvider.class) + .withEnvironmentMode(EnvironmentMode.FULL_ASSERT) + .withTerminationConfig( + new TerminationConfig() + .withScoreCalculationCountLimit(1_000L)); + var solver = SolverFactory.create(config).buildSolver(); + solver.solve(problem); + } +} From 9167c5c0eadf776edea43144426b010517cd6d74 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 27 Aug 2024 10:29:43 -0400 Subject: [PATCH 2/4] chore: Use a unit test collector approach --- .../connected_ranges/RangeGapImpl.java | 17 + .../AbstractConstraintCollectorsTest.java | 49 ++ .../bi/InnerBiConstraintCollectorsTest.java | 41 +- .../ConnectedRangeTrackerTest.java | 468 ------------------ .../InnerQuadConstraintCollectorsTest.java | 43 +- .../tri/InnerTriConstraintCollectorsTest.java | 43 +- .../uni/InnerUniConstraintCollectorsTest.java | 41 +- 7 files changed, 212 insertions(+), 490 deletions(-) delete mode 100644 core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/RangeGapImpl.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/RangeGapImpl.java index efae2257b5..8edd9d1d7e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/RangeGapImpl.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/RangeGapImpl.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.collector.connected_ranges; +import java.util.Objects; + import ai.timefold.solver.core.api.score.stream.common.ConnectedRange; import ai.timefold.solver.core.api.score.stream.common.RangeGap; @@ -51,6 +53,21 @@ void setLength(Difference_ length) { this.length = length; } + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof RangeGapImpl rangeGap)) + return false; + return Objects.equals(getPreviousRangeEnd(), rangeGap.getPreviousRangeEnd()) && + Objects.equals(getNextRangeStart(), rangeGap.getNextRangeStart()); + } + + @Override + public int hashCode() { + return Objects.hash(getPreviousRangeEnd(), getNextRangeStart()); + } + @Override public String toString() { return "RangeGap{" + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConstraintCollectorsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConstraintCollectorsTest.java index 40ca538877..4557591cbe 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConstraintCollectorsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConstraintCollectorsTest.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.collector; import java.util.Arrays; +import java.util.Objects; import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; @@ -134,8 +135,56 @@ protected ConnectedRangeChain buildConsecutiveUsage( }).getConnectedRangeChain(); } + protected ConnectedRangeChain buildDynamicConsecutiveUsage(DynamicInterval... data) { + return Arrays.stream(data).collect( + () -> new ConnectedRangeTracker<>(DynamicInterval::getStart, DynamicInterval::getEnd, (a, b) -> b - a), + (tree, datum) -> tree.add(tree.getRange(datum)), + (a, b) -> { + throw new UnsupportedOperationException(); + }).getConnectedRangeChain(); + } + public record Interval(int start, int end) { } + public static final class DynamicInterval { + int start; + + public DynamicInterval(int start) { + this.start = start; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return start + 10; + } + + public void setStart(int start) { + this.start = start; + } + + @Override + public String toString() { + return "DynamicInterval(%d, %d)".formatted(getStart(), getEnd()); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof DynamicInterval that)) + return false; + return start == that.start; + } + + @Override + public int hashCode() { + return Objects.hashCode(start); + } + } + } 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 dd780c1cd5..7fdec968d3 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 @@ -32,7 +32,6 @@ 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.common.ConnectedRangeChain; 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; @@ -1071,19 +1070,19 @@ public void toConsecutiveSequences() { @Override @Test public void consecutiveUsage() { - BiConstraintCollector> collector = + var collector = ConstraintCollectors.toConnectedRanges(Interval::new, Interval::start, Interval::end, (a, b) -> b - a); var container = collector.supplier().get(); // Add first value, sequence is [(1,3)] - Runnable firstRetractor = accumulate(collector, container, 1, 3); + var firstRetractor = accumulate(collector, container, 1, 3); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3))); // Add second value, sequence is [(1,3),(2,4)] - Runnable secondRetractor = accumulate(collector, container, 2, 4); + var secondRetractor = accumulate(collector, container, 2, 4); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(2, 4))); // Add third value, same as the second. Sequence is [{1,1},2}] - Runnable thirdRetractor = accumulate(collector, container, 2, 4); + var thirdRetractor = accumulate(collector, container, 2, 4); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(2, 4), new Interval(2, 4))); // Retract one instance of the second value; we only have two values now. secondRetractor.run(); @@ -1094,6 +1093,38 @@ public void consecutiveUsage() { // Retract last value; there are no values now. firstRetractor.run(); assertResult(collector, container, buildConsecutiveUsage()); + + var dynamicCollector = + ConstraintCollectors.toConnectedRanges((DynamicInterval a, Object b) -> a, + DynamicInterval::getStart, + DynamicInterval::getEnd, (a, b) -> b - a); + + var first = new DynamicInterval(0); + var second = new DynamicInterval(10); + var third = new DynamicInterval(20); + container = dynamicCollector.supplier().get(); + + // Add first value, sequence is [[(0, 10)]] + firstRetractor = accumulate(dynamicCollector, container, first, null); + assertResult(dynamicCollector, container, buildDynamicConsecutiveUsage(new DynamicInterval(0))); + + // Add third value, sequence is [[(0, 10)], [(20, 30)]] + accumulate(dynamicCollector, container, third, null); + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(0), new DynamicInterval(20))); + + // Add second value, sequence is [[(0, 10), (10, 20), (20, 30)]] + accumulate(dynamicCollector, container, second, null); + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(0), new DynamicInterval(10), new DynamicInterval(20))); + + // Change first value, retract it, then re-add it + first.setStart(-5); + firstRetractor.run(); + accumulate(dynamicCollector, container, first, null); + + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(-5), new DynamicInterval(10), new DynamicInterval(20))); } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java deleted file mode 100644 index 8c9ebd8a54..0000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java +++ /dev/null @@ -1,468 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.connected_ranges; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Random; -import java.util.TreeSet; -import java.util.stream.Collectors; - -import ai.timefold.solver.core.api.score.stream.common.ConnectedRange; -import ai.timefold.solver.core.api.score.stream.common.RangeGap; - -import org.junit.jupiter.api.Test; - -class ConnectedRangeTrackerTest { - private static class TestRange { - int start; - int end; - - public TestRange(int start, int end) { - this.start = start; - this.end = end; - } - - public int getStart() { - return start; - } - - public int getEnd() { - return end; - } - - public void setStart(int start) { - this.start = start; - } - - public void setEnd(int end) { - this.end = end; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - TestRange range = (TestRange) o; - return start == range.start && end == range.end; - } - - @Override - public int hashCode() { - return Objects.hash(start, end); - } - - @Override - public String toString() { - return "(" + start + ", " + end + ")"; - } - } - - private ConnectedRangeTracker getIntegerConnectedRangeTracker() { - return new ConnectedRangeTracker<>(TestRange::getStart, TestRange::getEnd, (a, b) -> b - a); - } - - @Test - void testNonConsecutiveRanges() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - Range a = tree.getRange(new TestRange(0, 2)); - Range b = tree.getRange(new TestRange(3, 4)); - Range c = tree.getRange(new TestRange(5, 7)); - tree.add(a); - tree.add(b); - tree.add(c); - - var connectedRangeList = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); - assertThat(connectedRangeList).hasSize(3); - - assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2)); - assertThat(connectedRangeList.get(0).hasOverlap()).isFalse(); - assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(1); - - assertThat(connectedRangeList.get(1)).containsExactly(new TestRange(3, 4)); - assertThat(connectedRangeList.get(1).hasOverlap()).isFalse(); - assertThat(connectedRangeList.get(1).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRangeList.get(1).getMaximumOverlap()).isEqualTo(1); - - assertThat(connectedRangeList.get(2)).containsExactly(new TestRange(5, 7)); - assertThat(connectedRangeList.get(2).hasOverlap()).isFalse(); - assertThat(connectedRangeList.get(2).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRangeList.get(2).getMaximumOverlap()).isEqualTo(1); - - verifyGaps(tree); - } - - @Test - void testConsecutiveRanges() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - Range a = tree.getRange(new TestRange(0, 2)); - Range b = tree.getRange(new TestRange(2, 4)); - Range c = tree.getRange(new TestRange(4, 7)); - tree.add(a); - tree.add(b); - tree.add(c); - - var connectedRangeList = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); - assertThat(connectedRangeList).hasSize(1); - - assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2), new TestRange(2, 4), new TestRange(4, 7)); - assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(1); - verifyGaps(tree); - } - - @Test - void testDuplicateRanges() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - Range a = tree.getRange(new TestRange(0, 2)); - Range b = tree.getRange(new TestRange(4, 7)); - tree.add(a); - tree.add(a); - tree.add(b); - - var connectedRangeList = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); - assertThat(connectedRangeList).hasSize(2); - - assertThat(connectedRangeList.get(0)).containsExactly(a.getValue(), a.getValue()); - assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(2); - assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(2); - assertThat(connectedRangeList.get(1)).containsExactly(b.getValue()); - assertThat(connectedRangeList.get(1).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRangeList.get(1).getMaximumOverlap()).isEqualTo(1); - verifyGaps(tree); - } - - @Test - void testRangeRemoval() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - TestRange removedRange = new TestRange(2, 4); - Range a = tree.getRange(new TestRange(0, 2)); - Range b = tree.getRange(removedRange); - Range c = tree.getRange(new TestRange(4, 7)); - tree.add(a); - tree.add(b); - tree.add(c); - - // Imitate changing planning variables - removedRange.setStart(10); - removedRange.setEnd(12); - - tree.remove(b); - - var connectedRangeList = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); - assertThat(connectedRangeList).hasSize(2); - - assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2)); - assertThat(connectedRangeList.get(1)).containsExactly(new TestRange(4, 7)); - verifyGaps(tree); - } - - @Test - void testRangeAddUpdatingOldGap() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - TestRange beforeAll = new TestRange(1, 2); - TestRange newStart = new TestRange(3, 8); - TestRange oldStart = new TestRange(4, 5); - TestRange betweenOldAndNewStart = new TestRange(6, 7); - TestRange afterAll = new TestRange(9, 10); - - tree.add(tree.getRange(beforeAll)); - verifyGaps(tree); - - tree.add(tree.getRange(afterAll)); - verifyGaps(tree); - - tree.add(tree.getRange(oldStart)); - verifyGaps(tree); - - tree.add(tree.getRange(betweenOldAndNewStart)); - verifyGaps(tree); - - tree.add(tree.getRange(newStart)); - verifyGaps(tree); - } - - @Test - void testOverlappingRange() { - ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); - Range a = tree.getRange(new TestRange(0, 2)); - TestRange removedTestRange1 = new TestRange(1, 3); - Range removedRange1 = tree.getRange(removedTestRange1); - Range c = tree.getRange(new TestRange(2, 4)); - - Range d = tree.getRange(new TestRange(5, 6)); - - Range e = tree.getRange(new TestRange(7, 9)); - TestRange removedTestRange2 = new TestRange(7, 9); - Range removedRange2 = tree.getRange(removedTestRange2); - - tree.add(a); - tree.add(removedRange1); - tree.add(c); - tree.add(d); - tree.add(e); - tree.add(removedRange2); - - var connectedRanges = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); - assertThat(connectedRanges).hasSize(3); - - assertThat(connectedRanges.get(0)).containsExactly(a.getValue(), removedTestRange1, c.getValue()); - assertThat(connectedRanges.get(0).hasOverlap()).isTrue(); - assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(2); - - assertThat(connectedRanges.get(1)).containsExactly(d.getValue()); - assertThat(connectedRanges.get(1).hasOverlap()).isFalse(); - assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1); - - assertThat(connectedRanges.get(2)).containsExactly(e.getValue(), removedTestRange2); - assertThat(connectedRanges.get(2).hasOverlap()).isTrue(); - assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(2); - assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(2); - - verifyGaps(tree); - - // Simulate changing planning variables - removedTestRange1.setStart(0); - removedTestRange1.setEnd(10); - - tree.remove(removedRange1); - - connectedRanges = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); - assertThat(connectedRanges).hasSize(3); - - assertThat(connectedRanges.get(0)).containsExactly(a.getValue(), c.getValue()); - assertThat(connectedRanges.get(0).hasOverlap()).isFalse(); - assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1); - - assertThat(connectedRanges.get(1)).containsExactly(d.getValue()); - assertThat(connectedRanges.get(1).hasOverlap()).isFalse(); - assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1); - - assertThat(connectedRanges.get(2)).containsExactly(e.getValue(), removedTestRange2); - assertThat(connectedRanges.get(2).hasOverlap()).isTrue(); - assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(2); - assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(2); - - verifyGaps(tree); - - // Simulate changing planning variables - removedTestRange2.setStart(2); - removedTestRange2.setEnd(4); - - tree.remove(removedRange2); - connectedRanges = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); - assertThat(connectedRanges).hasSize(3); - - assertThat(connectedRanges.get(0)).containsExactly(a.getValue(), c.getValue()); - assertThat(connectedRanges.get(0).hasOverlap()).isFalse(); - assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1); - - assertThat(connectedRanges.get(1)).containsExactly(d.getValue()); - assertThat(connectedRanges.get(1).hasOverlap()).isFalse(); - assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1); - - assertThat(connectedRanges.get(2)).containsExactly(e.getValue()); - assertThat(connectedRanges.get(2).hasOverlap()).isFalse(); - assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(1); - - verifyGaps(tree); - Range g = tree.getRange(new TestRange(6, 7)); - tree.add(g); - connectedRanges = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); - assertThat(connectedRanges).hasSize(2); - - assertThat(connectedRanges.get(0)).containsExactly(a.getValue(), c.getValue()); - assertThat(connectedRanges.get(0).hasOverlap()).isFalse(); - assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1); - - assertThat(connectedRanges.get(1)).containsExactly(d.getValue(), g.getValue(), e.getValue()); - assertThat(connectedRanges.get(1).hasOverlap()).isFalse(); - assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1); - assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1); - } - - void verifyGaps(ConnectedRangeTracker tree) { - var connectedRangeList = - new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); - var gapList = - new IterableList<>(tree.getConnectedRangeChain().getGaps()); - - if (connectedRangeList.size() == 0) { - return; - } - assertThat(gapList).hasSize(connectedRangeList.size() - 1); - for (int i = 0; i < connectedRangeList.size() - 1; i++) { - assertThat(gapList.get(i).getPreviousRangeEnd()).isEqualTo(connectedRangeList.get(i).getEnd()); - assertThat(gapList.get(i).getNextRangeStart()).isEqualTo(connectedRangeList.get(i + 1).getStart()); - assertThat(gapList.get(i).getLength()) - .isEqualTo(connectedRangeList.get(i + 1).getStart() - connectedRangeList.get(i).getEnd()); - } - } - - private static int rangeGapCompare(RangeGap a, - RangeGap b) { - if (a == b) { - return 0; - } - if (a == null || b == null) { - return (a == null) ? -1 : 1; - } - boolean out = Objects.equals(a.getPreviousRangeEnd(), b.getPreviousRangeEnd()) && - Objects.equals(a.getNextRangeStart(), b.getNextRangeStart()) && - Objects.equals(a.getLength(), b.getLength()); - - if (out) { - return 0; - } - return a.hashCode() - b.hashCode(); - } - - private static int rangeClusterCompare(ConnectedRange a, - ConnectedRange b) { - if (a == b) { - return 0; - } - if (a == null || b == null) { - return (a == null) ? -1 : 1; - } - - if (!(a instanceof ConnectedRangeImpl) || !(b instanceof ConnectedRangeImpl)) { - throw new IllegalArgumentException("Expected (" + a + ") and (" + b + ") to both be ConnectedRangeImpl"); - } - - var first = (ConnectedRangeImpl) a; - var second = (ConnectedRangeImpl) b; - - boolean out = first.getStartSplitPoint().compareTo(second.getStartSplitPoint()) == 0 && - first.getEndSplitPoint().compareTo(second.getEndSplitPoint()) == 0 && - first.getMinimumOverlap() == second.getMinimumOverlap() && - first.getMaximumOverlap() == second.getMaximumOverlap(); - if (out) { - return 0; - } - return first.hashCode() - second.hashCode(); - } - - // Compare the mutable version with the recompute version - @Test - void testRandomRanges() { - Random random = new Random(1); - - for (int i = 0; i < 100; i++) { - Map> rangeToInstanceMap = new HashMap<>(); - TreeSet> splitPoints = new TreeSet<>(); - ConnectedRangeTracker tree = - new ConnectedRangeTracker<>(TestRange::getStart, TestRange::getEnd, (a, b) -> b - a); - for (int j = 0; j < 100; j++) { - // Create a random range - String old = formatConnectedRangeTracker(tree); - int from = random.nextInt(5); - int to = from + random.nextInt(5); - TestRange data = new TestRange(from, to); - Range range = rangeToInstanceMap.computeIfAbsent(data, tree::getRange); - Range treeRange = - new Range<>(data, TestRange::getStart, TestRange::getEnd); - splitPoints.add(treeRange.getStartSplitPoint()); - splitPoints.add(treeRange.getEndSplitPoint()); - - // Get the split points from the set (since those split points have collections) - RangeSplitPoint startSplitPoint = - splitPoints.floor(treeRange.getStartSplitPoint()); - RangeSplitPoint endSplitPoint = splitPoints.floor(treeRange.getEndSplitPoint()); - - // Create the collections if they do not exist - if (startSplitPoint.startpointRangeToCountMap == null) { - startSplitPoint.createCollections(); - } - if (endSplitPoint.endpointRangeToCountMap == null) { - endSplitPoint.createCollections(); - } - - // Either add or remove the range - String op; - if (startSplitPoint.containsRangeStarting(treeRange) && random.nextBoolean()) { - op = "Remove"; - startSplitPoint.removeRangeStartingAtSplitPoint(treeRange); - endSplitPoint.removeRangeEndingAtSplitPoint(treeRange); - if (startSplitPoint.isEmpty()) { - splitPoints.remove(startSplitPoint); - } - if (endSplitPoint.isEmpty()) { - splitPoints.remove(endSplitPoint); - } - tree.remove(range); - } else { - op = "Add"; - startSplitPoint.addRangeStartingAtSplitPoint(treeRange); - endSplitPoint.addRangeEndingAtSplitPoint(treeRange); - tree.add(range); - } - - // Recompute all connected ranges - RangeSplitPoint previous = null; - RangeSplitPoint current = splitPoints.isEmpty() ? null : splitPoints.first(); - List> rangeClusterList = new ArrayList<>(); - List> gapList = new ArrayList<>(); - while (current != null) { - rangeClusterList - .add(ConnectedRangeImpl.getConnectedRangeStartingAt(splitPoints, (a, b) -> a - b, current)); - if (previous != null) { - ConnectedRangeImpl before = - rangeClusterList.get(rangeClusterList.size() - 2); - ConnectedRangeImpl after = - rangeClusterList.get(rangeClusterList.size() - 1); - gapList.add(new RangeGapImpl<>(before, after, after.getStart() - before.getEnd())); - } - previous = current; - current = splitPoints.higher(rangeClusterList.get(rangeClusterList.size() - 1).getEndSplitPoint()); - } - - // Verify the mutable version matches the recompute version - verifyGaps(tree); - assertThat(tree.getConnectedRangeChain().getConnectedRanges()) - .as(op + " range " + range + " to " + old) - .usingElementComparator(ConnectedRangeTrackerTest::rangeClusterCompare) - .containsExactlyElementsOf(rangeClusterList); - assertThat(tree.getConnectedRangeChain().getGaps()) - .as(op + " range " + range + " to " + old) - .usingElementComparator(ConnectedRangeTrackerTest::rangeGapCompare) - .containsExactlyElementsOf(gapList); - } - } - } - - private String formatConnectedRangeTracker(ConnectedRangeTracker rangeTree) { - List> listOfConnectedRanges = new ArrayList<>(); - for (ConnectedRange cluster : rangeTree.getConnectedRangeChain() - .getConnectedRanges()) { - List rangesInCluster = new ArrayList<>(); - for (TestRange range : cluster) { - rangesInCluster.add(range); - } - listOfConnectedRanges.add(rangesInCluster); - } - return listOfConnectedRanges.stream() - .map(cluster -> cluster.stream().map(TestRange::toString).collect(Collectors.joining(",", "[", "]"))) - .collect(Collectors.joining(";", "{", "}")); - } - -} 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 4b8abe8529..c635ee4b7b 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 @@ -30,7 +30,6 @@ import java.util.SortedSet; import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; -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.quad.QuadConstraintCollector; import ai.timefold.solver.core.impl.score.stream.collector.AbstractConstraintCollectorsTest; @@ -1124,19 +1123,19 @@ public void toConsecutiveSequences() { @Override @Test public void consecutiveUsage() { - QuadConstraintCollector> collector = - ConstraintCollectors.toConnectedRanges((a, b, c, d) -> new Interval(a, b), + 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(); // Add first value, sequence is [(1,3)] - Runnable firstRetractor = accumulate(collector, container, 1, 3, null, null); + var firstRetractor = accumulate(collector, container, 1, 3, null, null); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3))); // Add second value, sequence is [(1,3),(2,4)] - Runnable secondRetractor = accumulate(collector, container, 2, 4, null, null); + var secondRetractor = accumulate(collector, container, 2, 4, null, null); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(2, 4))); // Add third value, same as the second. Sequence is [{1,1},2}] - Runnable thirdRetractor = accumulate(collector, container, 2, 4, null, null); + var thirdRetractor = accumulate(collector, container, 2, 4, null, null); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(2, 4), new Interval(2, 4))); // Retract one instance of the second value; we only have two values now. secondRetractor.run(); @@ -1147,6 +1146,38 @@ public void consecutiveUsage() { // Retract last value; there are no values now. firstRetractor.run(); assertResult(collector, container, buildConsecutiveUsage()); + + var dynamicCollector = + ConstraintCollectors.toConnectedRanges((DynamicInterval a, Object b, Object c, Object d) -> a, + DynamicInterval::getStart, + DynamicInterval::getEnd, (a, b) -> b - a); + + var first = new DynamicInterval(0); + var second = new DynamicInterval(10); + var third = new DynamicInterval(20); + container = dynamicCollector.supplier().get(); + + // Add first value, sequence is [[(0, 10)]] + firstRetractor = accumulate(dynamicCollector, container, first, null, null, null); + assertResult(dynamicCollector, container, buildDynamicConsecutiveUsage(new DynamicInterval(0))); + + // Add third value, sequence is [[(0, 10)], [(20, 30)]] + accumulate(dynamicCollector, container, third, null, null, null); + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(0), new DynamicInterval(20))); + + // Add second value, sequence is [[(0, 10), (10, 20), (20, 30)]] + accumulate(dynamicCollector, container, second, null, null, null); + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(0), new DynamicInterval(10), new DynamicInterval(20))); + + // Change first value, retract it, then re-add it + first.setStart(-5); + firstRetractor.run(); + accumulate(dynamicCollector, container, first, null, null, null); + + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(-5), new DynamicInterval(10), new DynamicInterval(20))); } @Override 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 e31ba9e85a..0a1867e87b 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 @@ -30,7 +30,6 @@ import java.util.SortedSet; import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; -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.tri.TriConstraintCollector; import ai.timefold.solver.core.impl.score.stream.collector.AbstractConstraintCollectorsTest; @@ -1076,19 +1075,19 @@ public void toConsecutiveSequences() { @Override @Test public void consecutiveUsage() { - TriConstraintCollector> collector = - ConstraintCollectors.toConnectedRanges((a, b, c) -> new Interval(a, b), + 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(); // Add first value, sequence is [(1,3)] - Runnable firstRetractor = accumulate(collector, container, 1, 3, null); + var firstRetractor = accumulate(collector, container, 1, 3, null); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3))); // Add second value, sequence is [(1,3),(2,4)] - Runnable secondRetractor = accumulate(collector, container, 2, 4, null); + var secondRetractor = accumulate(collector, container, 2, 4, null); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(2, 4))); // Add third value, same as the second. Sequence is [{1,1},2}] - Runnable thirdRetractor = accumulate(collector, container, 2, 4, null); + var thirdRetractor = accumulate(collector, container, 2, 4, null); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(2, 4), new Interval(2, 4))); // Retract one instance of the second value; we only have two values now. secondRetractor.run(); @@ -1099,6 +1098,38 @@ public void consecutiveUsage() { // Retract last value; there are no values now. firstRetractor.run(); assertResult(collector, container, buildConsecutiveUsage()); + + var dynamicCollector = + ConstraintCollectors.toConnectedRanges((DynamicInterval a, Object b, Object c) -> a, + DynamicInterval::getStart, + DynamicInterval::getEnd, (a, b) -> b - a); + + var first = new DynamicInterval(0); + var second = new DynamicInterval(10); + var third = new DynamicInterval(20); + container = dynamicCollector.supplier().get(); + + // Add first value, sequence is [[(0, 10)]] + firstRetractor = accumulate(dynamicCollector, container, first, null, null); + assertResult(dynamicCollector, container, buildDynamicConsecutiveUsage(new DynamicInterval(0))); + + // Add third value, sequence is [[(0, 10)], [(20, 30)]] + accumulate(dynamicCollector, container, third, null, null); + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(0), new DynamicInterval(20))); + + // Add second value, sequence is [[(0, 10), (10, 20), (20, 30)]] + accumulate(dynamicCollector, container, second, null, null); + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(0), new DynamicInterval(10), new DynamicInterval(20))); + + // Change first value, retract it, then re-add it + first.setStart(-5); + firstRetractor.run(); + accumulate(dynamicCollector, container, first, null, null); + + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(-5), new DynamicInterval(10), new DynamicInterval(20))); } @Override 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 22cc919a32..05ba8384e1 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 @@ -33,7 +33,6 @@ import ai.timefold.solver.core.api.function.QuadFunction; import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; -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.uni.UniConstraintCollector; import ai.timefold.solver.core.impl.score.stream.collector.AbstractConstraintCollectorsTest; @@ -1001,19 +1000,19 @@ public void toConsecutiveSequences() { @Override @Test public void consecutiveUsage() { - UniConstraintCollector> collector = + var collector = ConstraintCollectors.toConnectedRanges( Interval::start, Interval::end, (a, b) -> b - a); var container = collector.supplier().get(); // Add first value, sequence is [(1,3)] - Runnable firstRetractor = accumulate(collector, container, new Interval(1, 3)); + var firstRetractor = accumulate(collector, container, new Interval(1, 3)); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3))); // Add second value, sequence is [(1,3),(2,4)] - Runnable secondRetractor = accumulate(collector, container, new Interval(2, 4)); + var secondRetractor = accumulate(collector, container, new Interval(2, 4)); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(2, 4))); // Add third value, same as the second. Sequence is [{1,1},2}] - Runnable thirdRetractor = accumulate(collector, container, new Interval(2, 4)); + var thirdRetractor = accumulate(collector, container, new Interval(2, 4)); assertResult(collector, container, buildConsecutiveUsage(new Interval(1, 3), new Interval(2, 4), new Interval(2, 4))); // Retract one instance of the second value; we only have two values now. secondRetractor.run(); @@ -1024,6 +1023,38 @@ public void consecutiveUsage() { // Retract last value; there are no values now. firstRetractor.run(); assertResult(collector, container, buildConsecutiveUsage()); + + var dynamicCollector = + ConstraintCollectors.toConnectedRanges( + DynamicInterval::getStart, + DynamicInterval::getEnd, (a, b) -> b - a); + + var first = new DynamicInterval(0); + var second = new DynamicInterval(10); + var third = new DynamicInterval(20); + container = dynamicCollector.supplier().get(); + + // Add first value, sequence is [[(0, 10)]] + firstRetractor = accumulate(dynamicCollector, container, first); + assertResult(dynamicCollector, container, buildDynamicConsecutiveUsage(new DynamicInterval(0))); + + // Add third value, sequence is [[(0, 10)], [(20, 30)]] + accumulate(dynamicCollector, container, third); + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(0), new DynamicInterval(20))); + + // Add second value, sequence is [[(0, 10), (10, 20), (20, 30)]] + accumulate(dynamicCollector, container, second); + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(0), new DynamicInterval(10), new DynamicInterval(20))); + + // Change first value, retract it, then re-add it + first.setStart(-5); + firstRetractor.run(); + accumulate(dynamicCollector, container, first); + + assertResult(dynamicCollector, container, + buildDynamicConsecutiveUsage(new DynamicInterval(-5), new DynamicInterval(10), new DynamicInterval(20))); } @Override From 5f3987fd01124301fdee4598f9fc4c092cd07ba1 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 27 Aug 2024 10:34:32 -0400 Subject: [PATCH 3/4] fix: Remove correct file --- .../ConnectedRangeSolvingTest.java | 137 ----- .../ConnectedRangeTrackerTest.java | 468 ++++++++++++++++++ 2 files changed, 468 insertions(+), 137 deletions(-) delete mode 100644 core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeSolvingTest.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeSolvingTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeSolvingTest.java deleted file mode 100644 index e1271ce8c8..0000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeSolvingTest.java +++ /dev/null @@ -1,137 +0,0 @@ -package ai.timefold.solver.core.impl.score.stream.collector.connected_ranges; - -import java.util.ArrayList; -import java.util.List; - -import ai.timefold.solver.core.api.domain.entity.PlanningEntity; -import ai.timefold.solver.core.api.domain.lookup.PlanningId; -import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; -import ai.timefold.solver.core.api.domain.solution.PlanningScore; -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; -import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; -import ai.timefold.solver.core.api.domain.valuerange.ValueRangeFactory; -import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; -import ai.timefold.solver.core.api.domain.variable.PlanningVariable; -import ai.timefold.solver.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore; -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.ConstraintFactory; -import ai.timefold.solver.core.api.score.stream.ConstraintProvider; -import ai.timefold.solver.core.api.score.stream.Joiners; -import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain; -import ai.timefold.solver.core.api.solver.SolverFactory; -import ai.timefold.solver.core.config.solver.EnvironmentMode; -import ai.timefold.solver.core.config.solver.SolverConfig; -import ai.timefold.solver.core.config.solver.termination.TerminationConfig; - -import org.junit.jupiter.api.Test; - -public class ConnectedRangeSolvingTest { - public record Equipment(int id, int capacity) { - } - - @PlanningEntity - public static class Job { - private int id; - private int requiredEquipmentId; - private Integer start; - - public Job() { - } - - public Job(int id, int requiredEquipmentId) { - this.id = id; - this.requiredEquipmentId = requiredEquipmentId; - } - - @PlanningId - public int getId() { - return id; - } - - public int getRequiredEquipmentId() { - return requiredEquipmentId; - } - - @PlanningVariable - public Integer getStart() { - return start; - } - - public void setStart(Integer start) { - this.start = start; - } - - public Integer getEnd() { - return start == null ? null : start + 10; - } - } - - @PlanningSolution - public static class Planner { - @PlanningScore - private HardMediumSoftScore score; - - @ProblemFactCollectionProperty - private final List equipments = new ArrayList<>(); - - @PlanningEntityCollectionProperty - private final List jobs = new ArrayList<>(); - - @ValueRangeProvider - public CountableValueRange getStartOffsetRange() { - return ValueRangeFactory.createIntValueRange(0, 100); - } - - public Planner() { - } - - public Planner(List equipments, List jobs) { - this.equipments.addAll(equipments); - this.jobs.addAll(jobs); - } - } - - public static class MyConstraintProvider implements ConstraintProvider { - public MyConstraintProvider() { - } - - @Override - public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { - return new Constraint[] { doNotOverAssignEquipment(constraintFactory) }; - } - - public Constraint doNotOverAssignEquipment(ConstraintFactory constraintFactory) { - return constraintFactory.forEach(Equipment.class) - .join(Job.class, Joiners.equal(Equipment::id, Job::getRequiredEquipmentId)) - .groupBy((equipment, job) -> equipment, ConstraintCollectors.toConnectedRanges((equipment, job) -> job, - Job::getStart, - Job::getEnd, - (a, b) -> b - a)) - .flattenLast(ConnectedRangeChain::getConnectedRanges) - .filter((equipment, connectedRange) -> connectedRange.getMaximumOverlap() > equipment.capacity()) - .penalize(HardMediumSoftScore.ONE_HARD) - .asConstraint("Concurrent equipment usage over capacity"); - } - } - - @Test - public void solveConnectedRanges() { - var e1 = new Equipment(1, 1); - var j1 = new Job(1, e1.id()); - var j2 = new Job(2, e1.id()); - var problem = new Planner(List.of(e1), List.of(j1, j2)); - - var config = new SolverConfig() - .withSolutionClass(Planner.class) - .withEntityClasses(Job.class) - .withConstraintProviderClass(MyConstraintProvider.class) - .withEnvironmentMode(EnvironmentMode.FULL_ASSERT) - .withTerminationConfig( - new TerminationConfig() - .withScoreCalculationCountLimit(1_000L)); - var solver = SolverFactory.create(config).buildSolver(); - solver.solve(problem); - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java new file mode 100644 index 0000000000..8c9ebd8a54 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java @@ -0,0 +1,468 @@ +package ai.timefold.solver.core.impl.score.stream.collector.connected_ranges; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import ai.timefold.solver.core.api.score.stream.common.ConnectedRange; +import ai.timefold.solver.core.api.score.stream.common.RangeGap; + +import org.junit.jupiter.api.Test; + +class ConnectedRangeTrackerTest { + private static class TestRange { + int start; + int end; + + public TestRange(int start, int end) { + this.start = start; + this.end = end; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + public void setStart(int start) { + this.start = start; + } + + public void setEnd(int end) { + this.end = end; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TestRange range = (TestRange) o; + return start == range.start && end == range.end; + } + + @Override + public int hashCode() { + return Objects.hash(start, end); + } + + @Override + public String toString() { + return "(" + start + ", " + end + ")"; + } + } + + private ConnectedRangeTracker getIntegerConnectedRangeTracker() { + return new ConnectedRangeTracker<>(TestRange::getStart, TestRange::getEnd, (a, b) -> b - a); + } + + @Test + void testNonConsecutiveRanges() { + ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); + Range a = tree.getRange(new TestRange(0, 2)); + Range b = tree.getRange(new TestRange(3, 4)); + Range c = tree.getRange(new TestRange(5, 7)); + tree.add(a); + tree.add(b); + tree.add(c); + + var connectedRangeList = + new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + assertThat(connectedRangeList).hasSize(3); + + assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2)); + assertThat(connectedRangeList.get(0).hasOverlap()).isFalse(); + assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(1); + + assertThat(connectedRangeList.get(1)).containsExactly(new TestRange(3, 4)); + assertThat(connectedRangeList.get(1).hasOverlap()).isFalse(); + assertThat(connectedRangeList.get(1).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRangeList.get(1).getMaximumOverlap()).isEqualTo(1); + + assertThat(connectedRangeList.get(2)).containsExactly(new TestRange(5, 7)); + assertThat(connectedRangeList.get(2).hasOverlap()).isFalse(); + assertThat(connectedRangeList.get(2).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRangeList.get(2).getMaximumOverlap()).isEqualTo(1); + + verifyGaps(tree); + } + + @Test + void testConsecutiveRanges() { + ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); + Range a = tree.getRange(new TestRange(0, 2)); + Range b = tree.getRange(new TestRange(2, 4)); + Range c = tree.getRange(new TestRange(4, 7)); + tree.add(a); + tree.add(b); + tree.add(c); + + var connectedRangeList = + new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + assertThat(connectedRangeList).hasSize(1); + + assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2), new TestRange(2, 4), new TestRange(4, 7)); + assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(1); + verifyGaps(tree); + } + + @Test + void testDuplicateRanges() { + ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); + Range a = tree.getRange(new TestRange(0, 2)); + Range b = tree.getRange(new TestRange(4, 7)); + tree.add(a); + tree.add(a); + tree.add(b); + + var connectedRangeList = + new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + assertThat(connectedRangeList).hasSize(2); + + assertThat(connectedRangeList.get(0)).containsExactly(a.getValue(), a.getValue()); + assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(2); + assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(2); + assertThat(connectedRangeList.get(1)).containsExactly(b.getValue()); + assertThat(connectedRangeList.get(1).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRangeList.get(1).getMaximumOverlap()).isEqualTo(1); + verifyGaps(tree); + } + + @Test + void testRangeRemoval() { + ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); + TestRange removedRange = new TestRange(2, 4); + Range a = tree.getRange(new TestRange(0, 2)); + Range b = tree.getRange(removedRange); + Range c = tree.getRange(new TestRange(4, 7)); + tree.add(a); + tree.add(b); + tree.add(c); + + // Imitate changing planning variables + removedRange.setStart(10); + removedRange.setEnd(12); + + tree.remove(b); + + var connectedRangeList = + new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + assertThat(connectedRangeList).hasSize(2); + + assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2)); + assertThat(connectedRangeList.get(1)).containsExactly(new TestRange(4, 7)); + verifyGaps(tree); + } + + @Test + void testRangeAddUpdatingOldGap() { + ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); + TestRange beforeAll = new TestRange(1, 2); + TestRange newStart = new TestRange(3, 8); + TestRange oldStart = new TestRange(4, 5); + TestRange betweenOldAndNewStart = new TestRange(6, 7); + TestRange afterAll = new TestRange(9, 10); + + tree.add(tree.getRange(beforeAll)); + verifyGaps(tree); + + tree.add(tree.getRange(afterAll)); + verifyGaps(tree); + + tree.add(tree.getRange(oldStart)); + verifyGaps(tree); + + tree.add(tree.getRange(betweenOldAndNewStart)); + verifyGaps(tree); + + tree.add(tree.getRange(newStart)); + verifyGaps(tree); + } + + @Test + void testOverlappingRange() { + ConnectedRangeTracker tree = getIntegerConnectedRangeTracker(); + Range a = tree.getRange(new TestRange(0, 2)); + TestRange removedTestRange1 = new TestRange(1, 3); + Range removedRange1 = tree.getRange(removedTestRange1); + Range c = tree.getRange(new TestRange(2, 4)); + + Range d = tree.getRange(new TestRange(5, 6)); + + Range e = tree.getRange(new TestRange(7, 9)); + TestRange removedTestRange2 = new TestRange(7, 9); + Range removedRange2 = tree.getRange(removedTestRange2); + + tree.add(a); + tree.add(removedRange1); + tree.add(c); + tree.add(d); + tree.add(e); + tree.add(removedRange2); + + var connectedRanges = + new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + assertThat(connectedRanges).hasSize(3); + + assertThat(connectedRanges.get(0)).containsExactly(a.getValue(), removedTestRange1, c.getValue()); + assertThat(connectedRanges.get(0).hasOverlap()).isTrue(); + assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(2); + + assertThat(connectedRanges.get(1)).containsExactly(d.getValue()); + assertThat(connectedRanges.get(1).hasOverlap()).isFalse(); + assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1); + + assertThat(connectedRanges.get(2)).containsExactly(e.getValue(), removedTestRange2); + assertThat(connectedRanges.get(2).hasOverlap()).isTrue(); + assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(2); + assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(2); + + verifyGaps(tree); + + // Simulate changing planning variables + removedTestRange1.setStart(0); + removedTestRange1.setEnd(10); + + tree.remove(removedRange1); + + connectedRanges = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + assertThat(connectedRanges).hasSize(3); + + assertThat(connectedRanges.get(0)).containsExactly(a.getValue(), c.getValue()); + assertThat(connectedRanges.get(0).hasOverlap()).isFalse(); + assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1); + + assertThat(connectedRanges.get(1)).containsExactly(d.getValue()); + assertThat(connectedRanges.get(1).hasOverlap()).isFalse(); + assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1); + + assertThat(connectedRanges.get(2)).containsExactly(e.getValue(), removedTestRange2); + assertThat(connectedRanges.get(2).hasOverlap()).isTrue(); + assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(2); + assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(2); + + verifyGaps(tree); + + // Simulate changing planning variables + removedTestRange2.setStart(2); + removedTestRange2.setEnd(4); + + tree.remove(removedRange2); + connectedRanges = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + assertThat(connectedRanges).hasSize(3); + + assertThat(connectedRanges.get(0)).containsExactly(a.getValue(), c.getValue()); + assertThat(connectedRanges.get(0).hasOverlap()).isFalse(); + assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1); + + assertThat(connectedRanges.get(1)).containsExactly(d.getValue()); + assertThat(connectedRanges.get(1).hasOverlap()).isFalse(); + assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1); + + assertThat(connectedRanges.get(2)).containsExactly(e.getValue()); + assertThat(connectedRanges.get(2).hasOverlap()).isFalse(); + assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(1); + + verifyGaps(tree); + Range g = tree.getRange(new TestRange(6, 7)); + tree.add(g); + connectedRanges = new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + assertThat(connectedRanges).hasSize(2); + + assertThat(connectedRanges.get(0)).containsExactly(a.getValue(), c.getValue()); + assertThat(connectedRanges.get(0).hasOverlap()).isFalse(); + assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1); + + assertThat(connectedRanges.get(1)).containsExactly(d.getValue(), g.getValue(), e.getValue()); + assertThat(connectedRanges.get(1).hasOverlap()).isFalse(); + assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1); + assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1); + } + + void verifyGaps(ConnectedRangeTracker tree) { + var connectedRangeList = + new IterableList<>(tree.getConnectedRangeChain().getConnectedRanges()); + var gapList = + new IterableList<>(tree.getConnectedRangeChain().getGaps()); + + if (connectedRangeList.size() == 0) { + return; + } + assertThat(gapList).hasSize(connectedRangeList.size() - 1); + for (int i = 0; i < connectedRangeList.size() - 1; i++) { + assertThat(gapList.get(i).getPreviousRangeEnd()).isEqualTo(connectedRangeList.get(i).getEnd()); + assertThat(gapList.get(i).getNextRangeStart()).isEqualTo(connectedRangeList.get(i + 1).getStart()); + assertThat(gapList.get(i).getLength()) + .isEqualTo(connectedRangeList.get(i + 1).getStart() - connectedRangeList.get(i).getEnd()); + } + } + + private static int rangeGapCompare(RangeGap a, + RangeGap b) { + if (a == b) { + return 0; + } + if (a == null || b == null) { + return (a == null) ? -1 : 1; + } + boolean out = Objects.equals(a.getPreviousRangeEnd(), b.getPreviousRangeEnd()) && + Objects.equals(a.getNextRangeStart(), b.getNextRangeStart()) && + Objects.equals(a.getLength(), b.getLength()); + + if (out) { + return 0; + } + return a.hashCode() - b.hashCode(); + } + + private static int rangeClusterCompare(ConnectedRange a, + ConnectedRange b) { + if (a == b) { + return 0; + } + if (a == null || b == null) { + return (a == null) ? -1 : 1; + } + + if (!(a instanceof ConnectedRangeImpl) || !(b instanceof ConnectedRangeImpl)) { + throw new IllegalArgumentException("Expected (" + a + ") and (" + b + ") to both be ConnectedRangeImpl"); + } + + var first = (ConnectedRangeImpl) a; + var second = (ConnectedRangeImpl) b; + + boolean out = first.getStartSplitPoint().compareTo(second.getStartSplitPoint()) == 0 && + first.getEndSplitPoint().compareTo(second.getEndSplitPoint()) == 0 && + first.getMinimumOverlap() == second.getMinimumOverlap() && + first.getMaximumOverlap() == second.getMaximumOverlap(); + if (out) { + return 0; + } + return first.hashCode() - second.hashCode(); + } + + // Compare the mutable version with the recompute version + @Test + void testRandomRanges() { + Random random = new Random(1); + + for (int i = 0; i < 100; i++) { + Map> rangeToInstanceMap = new HashMap<>(); + TreeSet> splitPoints = new TreeSet<>(); + ConnectedRangeTracker tree = + new ConnectedRangeTracker<>(TestRange::getStart, TestRange::getEnd, (a, b) -> b - a); + for (int j = 0; j < 100; j++) { + // Create a random range + String old = formatConnectedRangeTracker(tree); + int from = random.nextInt(5); + int to = from + random.nextInt(5); + TestRange data = new TestRange(from, to); + Range range = rangeToInstanceMap.computeIfAbsent(data, tree::getRange); + Range treeRange = + new Range<>(data, TestRange::getStart, TestRange::getEnd); + splitPoints.add(treeRange.getStartSplitPoint()); + splitPoints.add(treeRange.getEndSplitPoint()); + + // Get the split points from the set (since those split points have collections) + RangeSplitPoint startSplitPoint = + splitPoints.floor(treeRange.getStartSplitPoint()); + RangeSplitPoint endSplitPoint = splitPoints.floor(treeRange.getEndSplitPoint()); + + // Create the collections if they do not exist + if (startSplitPoint.startpointRangeToCountMap == null) { + startSplitPoint.createCollections(); + } + if (endSplitPoint.endpointRangeToCountMap == null) { + endSplitPoint.createCollections(); + } + + // Either add or remove the range + String op; + if (startSplitPoint.containsRangeStarting(treeRange) && random.nextBoolean()) { + op = "Remove"; + startSplitPoint.removeRangeStartingAtSplitPoint(treeRange); + endSplitPoint.removeRangeEndingAtSplitPoint(treeRange); + if (startSplitPoint.isEmpty()) { + splitPoints.remove(startSplitPoint); + } + if (endSplitPoint.isEmpty()) { + splitPoints.remove(endSplitPoint); + } + tree.remove(range); + } else { + op = "Add"; + startSplitPoint.addRangeStartingAtSplitPoint(treeRange); + endSplitPoint.addRangeEndingAtSplitPoint(treeRange); + tree.add(range); + } + + // Recompute all connected ranges + RangeSplitPoint previous = null; + RangeSplitPoint current = splitPoints.isEmpty() ? null : splitPoints.first(); + List> rangeClusterList = new ArrayList<>(); + List> gapList = new ArrayList<>(); + while (current != null) { + rangeClusterList + .add(ConnectedRangeImpl.getConnectedRangeStartingAt(splitPoints, (a, b) -> a - b, current)); + if (previous != null) { + ConnectedRangeImpl before = + rangeClusterList.get(rangeClusterList.size() - 2); + ConnectedRangeImpl after = + rangeClusterList.get(rangeClusterList.size() - 1); + gapList.add(new RangeGapImpl<>(before, after, after.getStart() - before.getEnd())); + } + previous = current; + current = splitPoints.higher(rangeClusterList.get(rangeClusterList.size() - 1).getEndSplitPoint()); + } + + // Verify the mutable version matches the recompute version + verifyGaps(tree); + assertThat(tree.getConnectedRangeChain().getConnectedRanges()) + .as(op + " range " + range + " to " + old) + .usingElementComparator(ConnectedRangeTrackerTest::rangeClusterCompare) + .containsExactlyElementsOf(rangeClusterList); + assertThat(tree.getConnectedRangeChain().getGaps()) + .as(op + " range " + range + " to " + old) + .usingElementComparator(ConnectedRangeTrackerTest::rangeGapCompare) + .containsExactlyElementsOf(gapList); + } + } + } + + private String formatConnectedRangeTracker(ConnectedRangeTracker rangeTree) { + List> listOfConnectedRanges = new ArrayList<>(); + for (ConnectedRange cluster : rangeTree.getConnectedRangeChain() + .getConnectedRanges()) { + List rangesInCluster = new ArrayList<>(); + for (TestRange range : cluster) { + rangesInCluster.add(range); + } + listOfConnectedRanges.add(rangesInCluster); + } + return listOfConnectedRanges.stream() + .map(cluster -> cluster.stream().map(TestRange::toString).collect(Collectors.joining(",", "[", "]"))) + .collect(Collectors.joining(";", "{", "}")); + } + +} From c0ce6addbac78675c2aeceed7951efb1ac5caa3f Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 27 Aug 2024 10:45:11 -0400 Subject: [PATCH 4/4] chore: review comments --- .../collector/AbstractConstraintCollectorsTest.java | 3 +++ .../collector/bi/InnerBiConstraintCollectorsTest.java | 8 ++++++-- .../collector/quad/InnerQuadConstraintCollectorsTest.java | 8 ++++++-- .../collector/tri/InnerTriConstraintCollectorsTest.java | 8 ++++++-- .../collector/uni/InnerUniConstraintCollectorsTest.java | 8 ++++++-- 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConstraintCollectorsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConstraintCollectorsTest.java index 4557591cbe..535efdeb38 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConstraintCollectorsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/AbstractConstraintCollectorsTest.java @@ -111,6 +111,9 @@ public abstract class AbstractConstraintCollectorsTest { @Test public abstract void consecutiveUsage(); + @Test + public abstract void consecutiveUsageDynamic(); + @Test public abstract void loadBalance(); 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 7fdec968d3..acc5279c68 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 @@ -1093,7 +1093,11 @@ public void consecutiveUsage() { // Retract last value; there are no values now. firstRetractor.run(); assertResult(collector, container, buildConsecutiveUsage()); + } + @Override + @Test + public void consecutiveUsageDynamic() { var dynamicCollector = ConstraintCollectors.toConnectedRanges((DynamicInterval a, Object b) -> a, DynamicInterval::getStart, @@ -1102,10 +1106,10 @@ public void consecutiveUsage() { var first = new DynamicInterval(0); var second = new DynamicInterval(10); var third = new DynamicInterval(20); - container = dynamicCollector.supplier().get(); + var container = dynamicCollector.supplier().get(); // Add first value, sequence is [[(0, 10)]] - firstRetractor = accumulate(dynamicCollector, container, first, null); + var firstRetractor = accumulate(dynamicCollector, container, first, null); assertResult(dynamicCollector, container, buildDynamicConsecutiveUsage(new DynamicInterval(0))); // Add third value, sequence is [[(0, 10)], [(20, 30)]] 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 c635ee4b7b..eaee6f4c34 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 @@ -1146,7 +1146,11 @@ public void consecutiveUsage() { // Retract last value; there are no values now. firstRetractor.run(); assertResult(collector, container, buildConsecutiveUsage()); + } + @Override + @Test + public void consecutiveUsageDynamic() { var dynamicCollector = ConstraintCollectors.toConnectedRanges((DynamicInterval a, Object b, Object c, Object d) -> a, DynamicInterval::getStart, @@ -1155,10 +1159,10 @@ public void consecutiveUsage() { var first = new DynamicInterval(0); var second = new DynamicInterval(10); var third = new DynamicInterval(20); - container = dynamicCollector.supplier().get(); + var container = dynamicCollector.supplier().get(); // Add first value, sequence is [[(0, 10)]] - firstRetractor = accumulate(dynamicCollector, container, first, null, null, null); + var firstRetractor = accumulate(dynamicCollector, container, first, null, null, null); assertResult(dynamicCollector, container, buildDynamicConsecutiveUsage(new DynamicInterval(0))); // Add third value, sequence is [[(0, 10)], [(20, 30)]] 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 0a1867e87b..26154932d1 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 @@ -1098,7 +1098,11 @@ public void consecutiveUsage() { // Retract last value; there are no values now. firstRetractor.run(); assertResult(collector, container, buildConsecutiveUsage()); + } + @Override + @Test + public void consecutiveUsageDynamic() { var dynamicCollector = ConstraintCollectors.toConnectedRanges((DynamicInterval a, Object b, Object c) -> a, DynamicInterval::getStart, @@ -1107,10 +1111,10 @@ public void consecutiveUsage() { var first = new DynamicInterval(0); var second = new DynamicInterval(10); var third = new DynamicInterval(20); - container = dynamicCollector.supplier().get(); + var container = dynamicCollector.supplier().get(); // Add first value, sequence is [[(0, 10)]] - firstRetractor = accumulate(dynamicCollector, container, first, null, null); + var firstRetractor = accumulate(dynamicCollector, container, first, null, null); assertResult(dynamicCollector, container, buildDynamicConsecutiveUsage(new DynamicInterval(0))); // Add third value, sequence is [[(0, 10)], [(20, 30)]] 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 05ba8384e1..e83ec18d00 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 @@ -1023,7 +1023,11 @@ public void consecutiveUsage() { // Retract last value; there are no values now. firstRetractor.run(); assertResult(collector, container, buildConsecutiveUsage()); + } + @Override + @Test + public void consecutiveUsageDynamic() { var dynamicCollector = ConstraintCollectors.toConnectedRanges( DynamicInterval::getStart, @@ -1032,10 +1036,10 @@ public void consecutiveUsage() { var first = new DynamicInterval(0); var second = new DynamicInterval(10); var third = new DynamicInterval(20); - container = dynamicCollector.supplier().get(); + var container = dynamicCollector.supplier().get(); // Add first value, sequence is [[(0, 10)]] - firstRetractor = accumulate(dynamicCollector, container, first); + var firstRetractor = accumulate(dynamicCollector, container, first); assertResult(dynamicCollector, container, buildDynamicConsecutiveUsage(new DynamicInterval(0))); // Add third value, sequence is [[(0, 10)], [(20, 30)]]