From 4dfe33fc2f8199d38a7079e1810aaeeba19f0817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Sat, 27 Jun 2026 13:56:25 +0200 Subject: [PATCH 1/3] First punch --- .../bavet/common/AbstractFlattenNode.java | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java index 3302129f5a9..b20fe59e1c2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java @@ -7,17 +7,22 @@ import java.util.Map; import java.util.Objects; import java.util.function.Consumer; -import java.util.function.Supplier; +import java.util.function.Predicate; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; +import org.jspecify.annotations.Nullable; + public abstract class AbstractFlattenNode extends AbstractSingleInputNode { private final int flattenStoreIndex; private final StaticPropagationQueue propagationQueue; + private final Consumer removeTupleConsumer = this::removeTuple; + private final Predicate> removeExtrasPredicate = + bag -> bag.removeExtras(removeTupleConsumer); protected AbstractFlattenNode(int flattenStoreIndex, TupleLifecycle nextNodesTupleLifecycle) { super(nextNodesTupleLifecycle); @@ -64,10 +69,15 @@ public final void insert(InTuple_ tuple) { private void addTuple(InTuple_ originalTuple, FlattenedItem_ item, FlattenBagByItem bagByItem) { - var outTupleBag = bagByItem.getBag(item); - outTupleBag.add(() -> createTuple(originalTuple, outTupleBag.value), - propagationQueue::insert, - propagationQueue::update); + var bag = bagByItem.getBag(item); + var reuse = bag.reuseOrAdvance(); + if (reuse == null) { + var created = createTuple(originalTuple, bag.value); + bag.append(created); + propagationQueue.insert(created); + } else { + propagationQueue.update(reuse); + } } protected abstract OutTuple_ createTuple(InTuple_ originalTuple, FlattenedItem_ item); @@ -86,7 +96,7 @@ public final void update(InTuple_ tuple) { addTuple(tuple, item, bagByItem); } bagByItem.getAllBags() - .removeIf(bag -> bag.removeExtras(this::removeTuple)); + .removeIf(removeExtrasPredicate); } protected abstract Iterable extractIterable(InTuple_ tuple); @@ -98,7 +108,7 @@ public final void retract(InTuple_ tuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - bagByItem.applyToAll(this::removeTuple); + bagByItem.applyToAll(removeTupleConsumer); } private void removeTuple(OutTuple_ outTuple) { @@ -165,21 +175,19 @@ private static final class FlattenItemBag { /** * Increments {@link #newCount}. - * If the updated {@link #newCount} is less than or equal to the size of {@link #outTupleList}, - * the {@code updateConsumer} is called with the corresponding element from - * {@link #outTupleList}. - * Otherwise, the {@code insertConsumer} is called with a new tuple created - * with {@code outTupleSupplier}, and that tuple is added to {@link #outTupleList}. + * + * @return the existing tuple to reuse if {@link #newCount} is within the current size of + * {@link #outTupleList}, or {@code null} if a new tuple must be created and passed + * to {@link #append}. */ - void add(Supplier outTupleSupplier, Consumer insertConsumer, Consumer updateConsumer) { + @Nullable + OutTuple_ reuseOrAdvance() { var listIndex = newCount++; - if (newCount > outTupleList.size()) { - var inserted = outTupleSupplier.get(); - outTupleList.add(inserted); - insertConsumer.accept(inserted); - } else { - updateConsumer.accept(outTupleList.get(listIndex)); - } + return newCount > outTupleList.size() ? null : outTupleList.get(listIndex); + } + + void append(OutTuple_ created) { + outTupleList.add(created); } /** From 8067bc536809fa9dcbc236ac52753249ad1d4665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Sat, 27 Jun 2026 18:36:35 +0200 Subject: [PATCH 2/3] Refactor --- .../common/iterator/MappingIterator.java | 26 --- .../list/ElementDestinationSelector.java | 2 +- .../consecutive/ConsecutiveSetTree.java | 159 ++++++++++-------- .../collector/consecutive/SequenceImpl.java | 7 +- .../core/impl/util/MappingIterator.java | 20 +++ 5 files changed, 112 insertions(+), 102 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/MappingIterator.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/util/MappingIterator.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/MappingIterator.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/MappingIterator.java deleted file mode 100644 index 60ae0cf4208..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/MappingIterator.java +++ /dev/null @@ -1,26 +0,0 @@ -package ai.timefold.solver.core.impl.heuristic.selector.common.iterator; - -import java.util.Iterator; -import java.util.function.Function; - -public final class MappingIterator implements Iterator { - - private final Iterator source; - private final Function mapper; - - public MappingIterator(Iterator source, Function mapper) { - this.source = source; - this.mapper = mapper; - } - - @Override - public boolean hasNext() { - return source.hasNext(); - } - - @Override - public R next() { - return mapper.apply(source.next()); - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java index 60ca043c557..0037a9f9812 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java @@ -12,11 +12,11 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.AbstractSelector; import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.ConcatenatingIterator; -import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.MappingIterator; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.FilteringValueSelector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.util.MappingIterator; import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/ConsecutiveSetTree.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/ConsecutiveSetTree.java index 602333b4046..0ae43a5e92c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/ConsecutiveSetTree.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/ConsecutiveSetTree.java @@ -1,7 +1,10 @@ package ai.timefold.solver.core.impl.score.stream.collector.consecutive; +import java.util.AbstractCollection; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.NavigableMap; import java.util.Objects; @@ -12,8 +15,10 @@ import ai.timefold.solver.core.api.score.stream.common.Break; import ai.timefold.solver.core.api.score.stream.common.Sequence; import ai.timefold.solver.core.api.score.stream.common.SequenceChain; +import ai.timefold.solver.core.impl.util.MappingIterator; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** @@ -39,8 +44,6 @@ public final class ConsecutiveSetTree, private final NavigableMap, Value_> itemMap = new TreeMap<>(); private final NavigableMap, SequenceImpl> startItemToSequence = new TreeMap<>(); - private final NavigableMap, BreakImpl> startItemToPreviousBreak = - new TreeMap<>(); private ComparableValue firstItem; private ComparableValue lastItem; @@ -60,10 +63,9 @@ public ConsecutiveSetTree(BiFunction differenceFunc return (Collection) startItemToSequence.values(); } - @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public @NonNull Collection> getBreaks() { - return (Collection) startItemToPreviousBreak.values(); + return new BreakCollection(); // Live view of the breaks. } @Override @@ -87,7 +89,7 @@ public ConsecutiveSetTree(BiFunction differenceFunc if (startItemToSequence.size() <= 1) { return null; } - return startItemToPreviousBreak.firstEntry().getValue(); + return startItemToSequence.higherEntry(startItemToSequence.firstKey()).getValue().previousBreak; } @Override @@ -95,7 +97,7 @@ public ConsecutiveSetTree(BiFunction differenceFunc if (startItemToSequence.size() <= 1) { return null; } - return startItemToPreviousBreak.lastEntry().getValue(); + return startItemToSequence.lastEntry().getValue().previousBreak; } public boolean add(Value_ value, Point_ valueIndex) { @@ -143,7 +145,8 @@ private ComparableValue addItemToBag(Value_ value, Point_ valueI private void addSubsequentItem(ComparableValue addingItem, Map.Entry, SequenceImpl> firstBeforeItemEntry, Point_ valueIndex, Value_ value) { - var endOfBeforeSequenceItem = firstBeforeItemEntry.getValue().lastItem; + var prevBag = firstBeforeItemEntry.getValue(); + var endOfBeforeSequenceItem = prevBag.lastItem; var endOfBeforeSequenceIndex = endOfBeforeSequenceItem.index(); if (isInNaturalOrderAndHashOrderIfEqual(valueIndex, value, endOfBeforeSequenceIndex, endOfBeforeSequenceItem.value())) { @@ -151,35 +154,39 @@ private void addSubsequentItem(ComparableValue addingItem, return; } // Item is outside the bag - var firstBeforeItem = firstBeforeItemEntry.getKey(); var firstAfterItem = startItemToSequence.higherKey(addingItem); if (firstAfterItem != null) { - addBetweenItems(addingItem, firstBeforeItem, endOfBeforeSequenceItem, firstAfterItem); + addBetweenItems(addingItem, prevBag, endOfBeforeSequenceItem, firstAfterItem); } else { - var prevBag = startItemToSequence.get(firstBeforeItem); if (isFirstSuccessorOfSecond(addingItem, endOfBeforeSequenceItem)) { // We need to extend the first bag // No break since afterItem is null prevBag.setEnd(addingItem); } else { // Start a new bag of consecutive items - addSequence(addingItem, prevBag); + var newBag = new SequenceImpl<>(this, addingItem); + startItemToSequence.put(addingItem, newBag); + newBag.previousBreak = new BreakImpl<>(newBag, prevBag); } } } private void addFirstItem(ComparableValue addingItem) { - var firstAfterItem = startItemToSequence.higherKey(addingItem); - if (firstAfterItem != null) { + var firstAfterEntry = startItemToSequence.higherEntry(addingItem); + if (firstAfterEntry != null) { + var firstAfterItem = firstAfterEntry.getKey(); + var afterBag = firstAfterEntry.getValue(); if (isFirstSuccessorOfSecond(firstAfterItem, addingItem)) { // We need to move the after bag to use item as key - var afterBag = startItemToSequence.remove(firstAfterItem); + startItemToSequence.remove(firstAfterItem); afterBag.setStart(addingItem); // No break since this is the first sequence startItemToSequence.put(addingItem, afterBag); } else { - // Start a new bag of consecutive items - addSequence(addingItem, firstAfterItem, startItemToSequence.get(firstAfterItem)); + // Start a new bag of consecutive items; addingItem becomes the new first sequence + var newBag = new SequenceImpl<>(this, addingItem); + startItemToSequence.put(addingItem, newBag); + afterBag.previousBreak = new BreakImpl<>(afterBag, newBag); } } else { // Start a new bag of consecutive items @@ -189,63 +196,47 @@ private void addFirstItem(ComparableValue addingItem) { } private void addBetweenItems(ComparableValue comparableItem, - ComparableValue firstBeforeItem, ComparableValue endOfBeforeSequenceItem, + SequenceImpl prevBag, + ComparableValue endOfBeforeSequenceItem, ComparableValue firstAfterItem) { if (isFirstSuccessorOfSecond(comparableItem, endOfBeforeSequenceItem)) { // We need to extend the first bag - var prevBag = startItemToSequence.get(firstBeforeItem); if (isFirstSuccessorOfSecond(firstAfterItem, comparableItem)) { - // We need to merge the two bags - startItemToPreviousBreak.remove(firstAfterItem); + // We need to merge the two bags; afterBag.previousBreak (break between prevBag and + // afterBag) is discarded with afterBag. higherEntry is strict-greater, so it skips + // firstAfterItem (just removed) and finds the sequence after afterBag. var afterBag = startItemToSequence.remove(firstAfterItem); prevBag.merge(afterBag); - var maybeNextBreak = startItemToPreviousBreak.higherEntry(firstAfterItem); - if (maybeNextBreak != null) { - maybeNextBreak.getValue().setPreviousSequence(prevBag); + var nextSeqEntry = startItemToSequence.higherEntry(firstAfterItem); + if (nextSeqEntry != null) { + nextSeqEntry.getValue().previousBreak.setPreviousSequence(prevBag); } } else { prevBag.setEnd(comparableItem); - startItemToPreviousBreak.get(firstAfterItem).updateLength(); + startItemToSequence.get(firstAfterItem).previousBreak.updateLength(); } } else { // Don't need to extend the first bag if (isFirstSuccessorOfSecond(firstAfterItem, comparableItem)) { - // We need to move the after bag to use item as key + // We need to move the after bag to use item as key; previousBreak is a field on the + // bag, so it follows the bag without re-keying — just update the length. var afterBag = startItemToSequence.remove(firstAfterItem); afterBag.setStart(comparableItem); startItemToSequence.put(comparableItem, afterBag); - var prevBreak = startItemToPreviousBreak.remove(firstAfterItem); - prevBreak.updateLength(); - startItemToPreviousBreak.put(comparableItem, prevBreak); + afterBag.previousBreak.updateLength(); } else { - // Start a new bag of consecutive items + // Start a new bag of consecutive items; split the existing break into two var newBag = new SequenceImpl<>(this, comparableItem); startItemToSequence.put(comparableItem, newBag); - startItemToPreviousBreak.get(firstAfterItem).setPreviousSequence(newBag); - SequenceImpl previousSequence = startItemToSequence.get(firstBeforeItem); - startItemToPreviousBreak.put(comparableItem, - new BreakImpl<>(newBag, previousSequence)); + startItemToSequence.get(firstAfterItem).previousBreak.setPreviousSequence(newBag); + newBag.previousBreak = new BreakImpl<>(newBag, prevBag); } } } - private void addSequence(ComparableValue addingItem, - SequenceImpl newNextSequence) { - var newBag = new SequenceImpl<>(this, addingItem); - startItemToSequence.put(addingItem, newBag); - startItemToPreviousBreak.put(addingItem, new BreakImpl<>(newBag, newNextSequence)); - } - - private void addSequence(ComparableValue addingItem, ComparableValue firstAfterItem, - SequenceImpl newPreviousSequence) { - var newBag = new SequenceImpl<>(this, addingItem); - startItemToSequence.put(addingItem, newBag); - startItemToPreviousBreak.put(firstAfterItem, new BreakImpl<>(newPreviousSequence, newBag)); - } - private static , Value_> boolean isInNaturalOrderAndHashOrderIfEqual(T a, Value_ aItem, T b, Value_ bItem) { - int difference = a.compareTo(b); + var difference = a.compareTo(b); if (difference != 0) { return difference < 0; } @@ -266,7 +257,7 @@ public boolean remove(Value_ value) { valueCountMap.remove(value); var removingItem = valueCount.value; itemMap.remove(removingItem); - boolean noMoreItems = itemMap.isEmpty(); + var noMoreItems = itemMap.isEmpty(); if (removingItem.compareTo(firstItem) == 0) { firstItem = noMoreItems ? null : itemMap.firstEntry().getKey(); } @@ -278,15 +269,17 @@ public boolean remove(Value_ value) { var firstBeforeItem = firstBeforeItemEntry.getKey(); var bag = firstBeforeItemEntry.getValue(); if (bag.getFirstItem() == bag.getLastItem()) { // Bag is empty if first item = last item + var removedBreak = bag.previousBreak; // null if this was the first sequence startItemToSequence.remove(firstBeforeItem); - var removedBreak = startItemToPreviousBreak.remove(firstBeforeItem); - var extendedBreakEntry = startItemToPreviousBreak.higherEntry(firstBeforeItem); - if (extendedBreakEntry != null) { + var nextSeqEntry = startItemToSequence.higherEntry(firstBeforeItem); + if (nextSeqEntry != null) { + var nextSeq = nextSeqEntry.getValue(); if (removedBreak != null) { - var extendedBreak = extendedBreakEntry.getValue(); - extendedBreak.setPreviousSequence(removedBreak.previousSequence); + // Middle sequence removed: stitch the next sequence's break to removed sequence's predecessor + nextSeq.previousBreak.setPreviousSequence(removedBreak.previousSequence); } else { - startItemToPreviousBreak.remove(extendedBreakEntry.getKey()); + // First sequence removed: next sequence becomes first, so no break before it + nextSeq.previousBreak = null; } } } else { // Bag is not empty. @@ -299,25 +292,23 @@ public boolean remove(Value_ value) { private void removeItemFromBag(SequenceImpl bag, ComparableValue item, ComparableValue sequenceStart, ComparableValue sequenceEnd) { if (item.equals(sequenceStart)) { - // Change start key to the item after this one + // Change start key to the item after this one; previousBreak stays on the bag as a field — + // no re-keying needed, just recompute the length for the new gap. bag.setStart(itemMap.higherKey(item)); startItemToSequence.remove(sequenceStart); - var extendedBreak = startItemToPreviousBreak.remove(sequenceStart); var bagFirstItem = bag.firstItem; startItemToSequence.put(bagFirstItem, bag); - if (extendedBreak != null) { - extendedBreak.updateLength(); - startItemToPreviousBreak.put(bagFirstItem, extendedBreak); + if (bag.previousBreak != null) { + bag.previousBreak.updateLength(); } return; } if (item.equals(sequenceEnd)) { // Set end key to the item before this one bag.setEnd(itemMap.lowerKey(item)); - var extendedBreakEntry = startItemToPreviousBreak.higherEntry(item); - if (extendedBreakEntry != null) { - var extendedBreak = extendedBreakEntry.getValue(); - extendedBreak.updateLength(); + var nextSeqEntry = startItemToSequence.higherEntry(sequenceEnd); + if (nextSeqEntry != null) { + nextSeqEntry.getValue().previousBreak.updateLength(); } return; } @@ -336,21 +327,19 @@ private void removeItemFromBag(SequenceImpl bag, Co var splitBag = bag.split(item); var firstSplitItem = splitBag.firstItem; startItemToSequence.put(firstSplitItem, splitBag); - startItemToPreviousBreak.put(firstSplitItem, new BreakImpl<>(splitBag, bag)); - var maybeNextBreak = startItemToPreviousBreak.higherEntry(firstAfterItem); - if (maybeNextBreak != null) { - maybeNextBreak.getValue().setPreviousSequence(splitBag); + splitBag.previousBreak = new BreakImpl<>(splitBag, bag); + // higherEntry is strict-greater, so it skips firstSplitItem (just inserted) and finds the + // sequence that was already after bag; its previousBreak now references splitBag, not bag. + var nextSeqEntry = startItemToSequence.higherEntry(firstSplitItem); + if (nextSeqEntry != null) { + nextSeqEntry.getValue().previousBreak.setPreviousSequence(splitBag); } } - Break getBreakBefore(ComparableValue item) { - return startItemToPreviousBreak.get(item); - } - Break getBreakAfter(ComparableValue item) { - var entry = startItemToPreviousBreak.higherEntry(item); + var entry = startItemToSequence.higherEntry(item); if (entry != null) { - return entry.getValue(); + return entry.getValue().previousBreak; } return null; } @@ -382,6 +371,28 @@ public String toString() { '}'; } + @NullMarked + private final class BreakCollection + extends AbstractCollection> { + + @Override + public Iterator> iterator() { + var sequences = startItemToSequence.values(); + if (sequences.size() <= 1) { + return Collections.emptyIterator(); + } + var iterator = sequences.iterator(); + iterator.next(); // skip first sequence — it has no previousBreak + return new MappingIterator<>(iterator, i -> i.previousBreak); + } + + @Override + public int size() { + return Math.max(0, startItemToSequence.size() - 1); + } + } + + @NullMarked private static final class ValueCount { private final Value_ value; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/SequenceImpl.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/SequenceImpl.java index d08c6800f8b..c6f9fbfdc88 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/SequenceImpl.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/SequenceImpl.java @@ -24,6 +24,11 @@ final class SequenceImpl, Difference_ ComparableValue firstItem; ComparableValue lastItem; + // null iff this is the first sequence; otherwise previousBreak.nextSequence == this. + // The tree maintains N-1 breaks for N sequences. + @Nullable + BreakImpl previousBreak; + // Memorized calculations private Difference_ length; private NavigableMap, Value_> comparableItems; @@ -55,7 +60,7 @@ final class SequenceImpl, Difference_ @Override public @Nullable Break getPreviousBreak() { - return sourceTree.getBreakBefore(firstItem); + return previousBreak; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/util/MappingIterator.java b/core/src/main/java/ai/timefold/solver/core/impl/util/MappingIterator.java new file mode 100644 index 00000000000..f18d6975bd3 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/util/MappingIterator.java @@ -0,0 +1,20 @@ +package ai.timefold.solver.core.impl.util; + +import java.util.Iterator; +import java.util.function.Function; + +public record MappingIterator(Iterator source, Function mapper) + implements + Iterator { + + @Override + public boolean hasNext() { + return source.hasNext(); + } + + @Override + public R next() { + return mapper.apply(source.next()); + } + +} From 3df47273038d1f0f3becb8f561e470a1ebe9e662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Sat, 27 Jun 2026 19:16:20 +0200 Subject: [PATCH 3/3] One more --- .../consecutive/ConsecutiveSetTree.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/ConsecutiveSetTree.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/ConsecutiveSetTree.java index 0ae43a5e92c..bd4c6942f27 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/ConsecutiveSetTree.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/consecutive/ConsecutiveSetTree.java @@ -154,9 +154,9 @@ private void addSubsequentItem(ComparableValue addingItem, return; } // Item is outside the bag - var firstAfterItem = startItemToSequence.higherKey(addingItem); - if (firstAfterItem != null) { - addBetweenItems(addingItem, prevBag, endOfBeforeSequenceItem, firstAfterItem); + var firstAfterEntry = startItemToSequence.higherEntry(addingItem); + if (firstAfterEntry != null) { + addBetweenItems(addingItem, prevBag, endOfBeforeSequenceItem, firstAfterEntry); } else { if (isFirstSuccessorOfSecond(addingItem, endOfBeforeSequenceItem)) { // We need to extend the first bag @@ -198,14 +198,16 @@ private void addFirstItem(ComparableValue addingItem) { private void addBetweenItems(ComparableValue comparableItem, SequenceImpl prevBag, ComparableValue endOfBeforeSequenceItem, - ComparableValue firstAfterItem) { + Map.Entry, SequenceImpl> firstAfterEntry) { + var firstAfterItem = firstAfterEntry.getKey(); + var afterBag = firstAfterEntry.getValue(); if (isFirstSuccessorOfSecond(comparableItem, endOfBeforeSequenceItem)) { // We need to extend the first bag if (isFirstSuccessorOfSecond(firstAfterItem, comparableItem)) { // We need to merge the two bags; afterBag.previousBreak (break between prevBag and // afterBag) is discarded with afterBag. higherEntry is strict-greater, so it skips // firstAfterItem (just removed) and finds the sequence after afterBag. - var afterBag = startItemToSequence.remove(firstAfterItem); + startItemToSequence.remove(firstAfterItem); // delete the afterBag mapping prevBag.merge(afterBag); var nextSeqEntry = startItemToSequence.higherEntry(firstAfterItem); if (nextSeqEntry != null) { @@ -213,14 +215,14 @@ private void addBetweenItems(ComparableValue comparableItem, } } else { prevBag.setEnd(comparableItem); - startItemToSequence.get(firstAfterItem).previousBreak.updateLength(); + afterBag.previousBreak.updateLength(); } } else { // Don't need to extend the first bag if (isFirstSuccessorOfSecond(firstAfterItem, comparableItem)) { // We need to move the after bag to use item as key; previousBreak is a field on the // bag, so it follows the bag without re-keying — just update the length. - var afterBag = startItemToSequence.remove(firstAfterItem); + startItemToSequence.remove(firstAfterItem); // delete the afterBag mapping; re-keyed below afterBag.setStart(comparableItem); startItemToSequence.put(comparableItem, afterBag); afterBag.previousBreak.updateLength(); @@ -228,7 +230,7 @@ private void addBetweenItems(ComparableValue comparableItem, // Start a new bag of consecutive items; split the existing break into two var newBag = new SequenceImpl<>(this, comparableItem); startItemToSequence.put(comparableItem, newBag); - startItemToSequence.get(firstAfterItem).previousBreak.setPreviousSequence(newBag); + afterBag.previousBreak.setPreviousSequence(newBag); newBag.previousBreak = new BreakImpl<>(newBag, prevBag); } }