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