From 6b5e470f6610a6e15e98180117c71de2e268b526 Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 28 Oct 2025 10:56:13 -0300 Subject: [PATCH 01/13] chore: simplify the selection sorting contract --- .../decorator/ComparatorFactorySelectionSorter.java | 6 ++---- .../common/decorator/ComparatorSelectionSorter.java | 3 +-- .../selector/common/decorator/SelectionSorter.java | 9 +++++---- .../selector/entity/decorator/SortingEntitySelector.java | 2 +- .../selector/move/decorator/SortingMoveSelector.java | 2 +- .../value/decorator/FromEntitySortingValueSelector.java | 2 +- .../selector/value/decorator/SortingValueSelector.java | 2 +- .../decorator/ComparatorFactorySelectionSorterTest.java | 8 ++------ .../entity/decorator/SortingEntitySelectorTest.java | 5 +++++ .../selector/move/decorator/SortingMoveSelectorTest.java | 5 +++++ .../value/decorator/SortingValueSelectorTest.java | 5 +++++ 11 files changed, 29 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java index 0a7483c674..eac6e5fdec 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java @@ -7,7 +7,6 @@ import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; /** @@ -35,9 +34,8 @@ private Comparator getAppliedComparator(Comparator comparator) { } @Override - public void sort(ScoreDirector scoreDirector, List selectionList) { - var appliedComparator = - getAppliedComparator(selectionComparatorFactory.createComparator(scoreDirector.getWorkingSolution())); + public void sort(Solution_ solution, List selectionList) { + var appliedComparator = getAppliedComparator(selectionComparatorFactory.createComparator(solution)); selectionList.sort(appliedComparator); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java index 7c4305c6f3..92f1a1cd3b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java @@ -6,7 +6,6 @@ import java.util.Objects; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; /** @@ -34,7 +33,7 @@ public ComparatorSelectionSorter(Comparator comparator, SelectionSorterOrder } @Override - public void sort(ScoreDirector scoreDirector, List selectionList) { + public void sort(Solution_ solution, List selectionList) { selectionList.sort(appliedComparator); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java index fe33e2eac1..d700f9ecee 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java @@ -4,10 +4,11 @@ import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.heuristic.selector.Selector; +import org.jspecify.annotations.NullMarked; + /** * Decides the order of a {@link List} of selection * (which is a {@link PlanningEntity}, a planningValue, a {@link Move} or a {@link Selector}). @@ -19,15 +20,15 @@ * @param the solution type, the class with the {@link PlanningSolution} annotation * @param the selection type */ +@NullMarked @FunctionalInterface public interface SelectionSorter { /** - * @param scoreDirector never null, the {@link ScoreDirector} - * which has the {@link ScoreDirector#getWorkingSolution()} to which the selections belong or apply to + * @param solution never null, the current solution * @param selectionList never null, a {@link List} * of {@link PlanningEntity}, planningValue, {@link Move} or {@link Selector} */ - void sort(ScoreDirector scoreDirector, List selectionList); + void sort(Solution_ solution, List selectionList); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java index 7467f25f8b..f3c21da9d5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java @@ -60,7 +60,7 @@ public void constructCache(SolverScope solverScope) { return; } super.constructCache(solverScope); - sorter.sort(solverScope.getScoreDirector(), cachedEntityList); + sorter.sort(solverScope.getScoreDirector().getWorkingSolution(), cachedEntityList); logger.trace(" Sorted cachedEntityList: size ({}), entitySelector ({}).", cachedEntityList.size(), this); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelector.java index b9a30af845..5124baf609 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelector.java @@ -25,7 +25,7 @@ public SortingMoveSelector(MoveSelector childMoveSelector, SelectionC @Override public void constructCache(SolverScope solverScope) { super.constructCache(solverScope); - sorter.sort(solverScope.getScoreDirector(), cachedMoveList); + sorter.sort(solverScope.getScoreDirector().getWorkingSolution(), cachedMoveList); logger.trace(" Sorted cachedMoveList: size ({}), moveSelector ({}).", cachedMoveList.size(), this); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java index 844ecd5e0f..d34b17a1e6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java @@ -98,7 +98,7 @@ public Iterator iterator(Object entity) { childValueSelector.iterator(entity).forEachRemaining(cachedValueList::add); logger.trace(" Created cachedValueList: size ({}), valueSelector ({}).", cachedValueList.size(), this); - sorter.sort(scoreDirector, cachedValueList); + sorter.sort(scoreDirector.getWorkingSolution(), cachedValueList); logger.trace(" Sorted cachedValueList: size ({}), valueSelector ({}).", cachedValueList.size(), this); return cachedValueList.iterator(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java index beaae10d4a..a9f01c324f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java @@ -27,7 +27,7 @@ public SortingValueSelector(IterableValueSelector childValueSelector, @Override public void constructCache(SolverScope solverScope) { super.constructCache(solverScope); - sorter.sort(solverScope.getScoreDirector(), cachedValueList); + sorter.sort(solverScope.getScoreDirector().getWorkingSolution(), cachedValueList); logger.trace(" Sorted cachedValueList: size ({}), valueSelector ({}).", cachedValueList.size(), this); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java index 3ff74694ab..42a05ab5ed 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java @@ -1,14 +1,12 @@ package ai.timefold.solver.core.impl.heuristic.selector.common.decorator; import static ai.timefold.solver.core.testutil.PlannerAssert.assertCodesOfIterator; -import static org.mockito.Mockito.mock; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import ai.timefold.solver.core.api.domain.common.ComparatorFactory; -import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -24,13 +22,12 @@ void sortAscending() { ComparatorFactorySelectionSorter selectionSorter = new ComparatorFactorySelectionSorter<>( comparatorFactory, SelectionSorterOrder.ASCENDING); - ScoreDirector scoreDirector = mock(ScoreDirector.class); List selectionList = new ArrayList<>(); selectionList.add(new TestdataEntity("C")); selectionList.add(new TestdataEntity("A")); selectionList.add(new TestdataEntity("D")); selectionList.add(new TestdataEntity("B")); - selectionSorter.sort(scoreDirector, selectionList); + selectionSorter.sort(new TestdataSolution(), selectionList); assertCodesOfIterator(selectionList.iterator(), "A", "B", "C", "D"); } @@ -41,13 +38,12 @@ void sortDescending() { ComparatorFactorySelectionSorter selectionSorter = new ComparatorFactorySelectionSorter<>( comparatorFactory, SelectionSorterOrder.DESCENDING); - ScoreDirector scoreDirector = mock(ScoreDirector.class); List selectionList = new ArrayList<>(); selectionList.add(new TestdataEntity("C")); selectionList.add(new TestdataEntity("A")); selectionList.add(new TestdataEntity("D")); selectionList.add(new TestdataEntity("B")); - selectionSorter.sort(scoreDirector, selectionList); + selectionSorter.sort(new TestdataSolution(), selectionList); assertCodesOfIterator(selectionList.iterator(), "D", "C", "B", "A"); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java index a1a0c949ef..3862514873 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java @@ -4,6 +4,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.verifyPhaseLifecycle; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -17,6 +18,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataObject; @@ -56,6 +58,9 @@ public void runCacheType(SelectionCacheType cacheType, int timesCalled) { EntitySelector entitySelector = new SortingEntitySelector(childEntitySelector, cacheType, sorter); SolverScope solverScope = mock(SolverScope.class); + InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); + doReturn(scoreDirector).when(solverScope).getScoreDirector(); + doReturn(new TestdataSolution()).when(scoreDirector).getWorkingSolution(); entitySelector.solvingStarted(solverScope); AbstractPhaseScope phaseScopeA = mock(AbstractPhaseScope.class); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java index 0a06aafc71..1006ac40a4 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java @@ -5,6 +5,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.verifyPhaseLifecycle; import static ai.timefold.solver.core.testutil.PlannerTestUtils.mockScoreDirector; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -27,6 +28,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testutil.CodeAssertable; @@ -109,6 +111,9 @@ public void runCacheType(SelectionCacheType cacheType, int timesCalled) { MoveSelector moveSelector = new SortingMoveSelector(childMoveSelector, cacheType, sorter); SolverScope solverScope = mock(SolverScope.class); + InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); + doReturn(scoreDirector).when(solverScope).getScoreDirector(); + doReturn(new TestdataSolution()).when(scoreDirector).getWorkingSolution(); moveSelector.solvingStarted(solverScope); AbstractPhaseScope phaseScopeA = mock(AbstractPhaseScope.class); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java index 5ce2feb59c..fe900d8ace 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java @@ -2,6 +2,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfValueSelector; import static ai.timefold.solver.core.testutil.PlannerAssert.verifyPhaseLifecycle; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -15,6 +16,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataObject; @@ -51,6 +53,9 @@ public void runOriginalSelection(SelectionCacheType cacheType, int timesCalled) IterableValueSelector valueSelector = new SortingValueSelector(childValueSelector, cacheType, sorter); SolverScope solverScope = mock(SolverScope.class); + InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); + doReturn(scoreDirector).when(solverScope).getScoreDirector(); + doReturn(new TestdataSolution()).when(scoreDirector).getWorkingSolution(); valueSelector.solvingStarted(solverScope); AbstractPhaseScope phaseScopeA = mock(AbstractPhaseScope.class); From 12b6c14245e5bdce23b9abe639425a5740ff6842 Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 28 Oct 2025 15:44:43 -0300 Subject: [PATCH 02/13] feat: enable sorting on ValueRangeManager --- .../api/domain/valuerange/ValueRange.java | 9 +++ .../AbstractCountableValueRange.java | 10 +++ .../AbstractUncountableValueRange.java | 5 ++ .../domain/valuerange/ValueRangeCache.java | 37 +++++++++-- .../valuerange/buildin/EmptyValueRange.java | 8 +++ .../buildin/collection/ListValueRange.java | 10 +++ .../buildin/collection/SetValueRange.java | 8 +++ .../CompositeCountableValueRange.java | 19 +++++- .../NullAllowingCountableValueRange.java | 12 ++-- .../sort/SelectionSorterAdapter.java | 34 ++++++++++ .../valuerange/sort/ValueRangeSorter.java | 31 +++++++++ .../ComparatorFactorySelectionSorter.java | 16 ++++- .../decorator/ComparatorSelectionSorter.java | 16 ++++- .../common/decorator/SelectionSetSorter.java | 28 ++++++++ .../common/decorator/SelectionSorter.java | 4 +- .../score/director/ValueRangeManager.java | 66 ++++++++++++++----- 16 files changed, 280 insertions(+), 33 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSetSorter.java diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRange.java b/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRange.java index 9a39414678..1e6c9c5989 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRange.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -45,6 +46,14 @@ public interface ValueRange { */ boolean contains(@Nullable T value); + /** + * The sorting operation copies the current value range and sorts it using the provided sorter. + * + * @param sorter never null, the value range sorter + * @return A new instance of the value range, with the data sorted. + */ + ValueRange sort(ValueRangeSorter sorter); + /** * Select in random order, but without shuffling the elements. * Each element might be selected multiple times. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractCountableValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractCountableValueRange.java index 64dbc5846b..473f5b28b0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractCountableValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractCountableValueRange.java @@ -5,6 +5,9 @@ import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; import ai.timefold.solver.core.api.domain.valuerange.ValueRange; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeFactory; +import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; + +import org.jspecify.annotations.NullMarked; /** * Abstract superclass for {@link CountableValueRange} (and therefore {@link ValueRange}). @@ -13,6 +16,7 @@ * @see ValueRange * @see ValueRangeFactory */ +@NullMarked public abstract class AbstractCountableValueRange implements CountableValueRange { /** @@ -33,4 +37,10 @@ public boolean isEmpty() { return getSize() == 0L; } + @Override + public ValueRange sort(ValueRangeSorter sorter) { + // The sorting operation is not supported by default + // and must be explicitly implemented by the child classes if needed. + throw new UnsupportedOperationException(); + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractUncountableValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractUncountableValueRange.java index d71ec9380c..8272888788 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractUncountableValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractUncountableValueRange.java @@ -3,6 +3,7 @@ import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; import ai.timefold.solver.core.api.domain.valuerange.ValueRange; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeFactory; +import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; /** * Abstract superclass for {@link ValueRange} that is not a {@link CountableValueRange}). @@ -16,4 +17,8 @@ @Deprecated(forRemoval = true, since = "1.1.0") public abstract class AbstractUncountableValueRange implements ValueRange { + @Override + public ValueRange sort(ValueRangeSorter sorter) { + throw new UnsupportedOperationException(); + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java index 9ec1acde91..83afa8985d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java @@ -8,6 +8,7 @@ import java.util.Random; import java.util.Set; +import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.CachedListRandomIterator; import ai.timefold.solver.core.impl.util.CollectionUtils; @@ -25,16 +26,19 @@ public final class ValueRangeCache private final List valuesWithFastRandomAccess; private final Set valuesWithFastLookup; + private final CacheType cacheType; - private ValueRangeCache(int size, Set emptyCacheSet) { + private ValueRangeCache(int size, Set emptyCacheSet, CacheType cacheType) { this.valuesWithFastRandomAccess = new ArrayList<>(size); this.valuesWithFastLookup = emptyCacheSet; + this.cacheType = cacheType; } - private ValueRangeCache(Collection collection, Set emptyCacheSet) { + private ValueRangeCache(Collection collection, Set emptyCacheSet, CacheType cacheType) { this.valuesWithFastRandomAccess = new ArrayList<>(collection); this.valuesWithFastLookup = emptyCacheSet; this.valuesWithFastLookup.addAll(valuesWithFastRandomAccess); + this.cacheType = cacheType; } public void add(@Nullable Value_ value) { @@ -72,6 +76,20 @@ public Iterator iterator(Random workingRandom) { return new CachedListRandomIterator<>(valuesWithFastRandomAccess, workingRandom); } + /** + * Creates a copy of the cache and apply a sorting operation. + * + * @param sorter never null, the sorter + */ + public ValueRangeCache sort(ValueRangeSorter sorter) { + var valuesWithFastRandomAccessSorted = new ArrayList<>(valuesWithFastRandomAccess); + sorter.sort(valuesWithFastRandomAccessSorted); + return switch (cacheType) { + case USER_VALUES -> Builder.FOR_USER_VALUES.buildCache(valuesWithFastRandomAccessSorted); + case TRUSTED_VALUES -> Builder.FOR_TRUSTED_VALUES.buildCache(valuesWithFastRandomAccessSorted); + }; + } + public enum Builder { /** @@ -80,12 +98,13 @@ public enum Builder { FOR_USER_VALUES { @Override public ValueRangeCache buildCache(int size) { - return new ValueRangeCache<>(size, CollectionUtils.newIdentityHashSet(size)); + return new ValueRangeCache<>(size, CollectionUtils.newIdentityHashSet(size), CacheType.USER_VALUES); } @Override public ValueRangeCache buildCache(Collection collection) { - return new ValueRangeCache<>(collection, CollectionUtils.newIdentityHashSet(collection.size())); + return new ValueRangeCache<>(collection, CollectionUtils.newIdentityHashSet(collection.size()), + CacheType.USER_VALUES); } }, @@ -100,12 +119,13 @@ public ValueRangeCache buildCache(Collection collection FOR_TRUSTED_VALUES { @Override public ValueRangeCache buildCache(int size) { - return new ValueRangeCache<>(size, CollectionUtils.newHashSet(size)); + return new ValueRangeCache<>(size, CollectionUtils.newHashSet(size), CacheType.TRUSTED_VALUES); } @Override public ValueRangeCache buildCache(Collection collection) { - return new ValueRangeCache<>(collection, CollectionUtils.newHashSet(collection.size())); + return new ValueRangeCache<>(collection, CollectionUtils.newHashSet(collection.size()), + CacheType.TRUSTED_VALUES); } }; @@ -116,4 +136,9 @@ public ValueRangeCache buildCache(Collection collection } + private enum CacheType { + USER_VALUES, + TRUSTED_VALUES + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/EmptyValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/EmptyValueRange.java index c2c8bd011c..f967815189 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/EmptyValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/EmptyValueRange.java @@ -4,7 +4,9 @@ import java.util.NoSuchElementException; import java.util.Random; +import ai.timefold.solver.core.api.domain.valuerange.ValueRange; import ai.timefold.solver.core.impl.domain.valuerange.AbstractCountableValueRange; +import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullMarked; @@ -43,6 +45,12 @@ public long getSize() { return (Iterator) EmptyIterator.INSTANCE; } + @Override + public ValueRange sort(ValueRangeSorter sorter) { + // Sorting operation ignored + return this; + } + @Override public boolean contains(@Nullable T value) { return false; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRange.java index e99e369dd6..df42fe8eb6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRange.java @@ -1,11 +1,14 @@ package ai.timefold.solver.core.impl.domain.valuerange.buildin.collection; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Random; +import ai.timefold.solver.core.api.domain.valuerange.ValueRange; import ai.timefold.solver.core.impl.domain.valuerange.AbstractCountableValueRange; import ai.timefold.solver.core.impl.domain.valuerange.ValueRangeCache; +import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.CachedListRandomIterator; import org.jspecify.annotations.NullMarked; @@ -55,6 +58,13 @@ public boolean contains(@Nullable T value) { return cache.contains(value); } + @Override + public ValueRange sort(ValueRangeSorter sorter) { + var newList = new ArrayList<>(list); + sorter.sort(newList); + return new ListValueRange<>(newList, isValueImmutable); + } + @Override public Iterator createOriginalIterator() { return list.iterator(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/SetValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/SetValueRange.java index 16cef0f77c..9e814dd584 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/SetValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/SetValueRange.java @@ -5,8 +5,10 @@ import java.util.Set; import java.util.stream.Collectors; +import ai.timefold.solver.core.api.domain.valuerange.ValueRange; import ai.timefold.solver.core.impl.domain.valuerange.AbstractCountableValueRange; import ai.timefold.solver.core.impl.domain.valuerange.ValueRangeCache; +import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -59,6 +61,12 @@ public boolean contains(@Nullable T value) { return set.contains(value); } + @Override + public ValueRange sort(ValueRangeSorter sorter) { + var sortedSet = sorter.sort(set); + return new SetValueRange<>(sortedSet); + } + @Override public Iterator createOriginalIterator() { return set.iterator(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRange.java index f77cbd15c3..d3d289cbd7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRange.java @@ -4,8 +4,10 @@ import java.util.List; import java.util.Random; +import ai.timefold.solver.core.api.domain.valuerange.ValueRange; import ai.timefold.solver.core.impl.domain.valuerange.AbstractCountableValueRange; import ai.timefold.solver.core.impl.domain.valuerange.ValueRangeCache; +import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -18,13 +20,13 @@ public final class CompositeCountableValueRange extends AbstractCountableValu public CompositeCountableValueRange(List> childValueRangeList) { var maximumSize = 0L; - var isValueImmutable = true; + var isImmutable = true; for (AbstractCountableValueRange childValueRange : childValueRangeList) { - isValueImmutable &= childValueRange.isValueImmutable(); + isImmutable &= childValueRange.isValueImmutable(); maximumSize += childValueRange.getSize(); } // To eliminate duplicates, we immediately expand the child value ranges into a cache. - var cacheBuilder = isValueImmutable ? ValueRangeCache.Builder.FOR_TRUSTED_VALUES + var cacheBuilder = isImmutable ? ValueRangeCache.Builder.FOR_TRUSTED_VALUES : ValueRangeCache.Builder.FOR_USER_VALUES; this.cache = cacheBuilder.buildCache((int) maximumSize); for (var childValueRange : childValueRangeList) { @@ -35,7 +37,12 @@ public CompositeCountableValueRange(List cache, boolean isValueImmutable) { this.isValueImmutable = isValueImmutable; + this.cache = cache; } @Override @@ -53,6 +60,12 @@ public T get(long index) { return cache.get((int) index); } + @Override + public ValueRange sort(ValueRangeSorter sorter) { + var sortedCache = this.cache.sort(sorter); + return new CompositeCountableValueRange<>(sortedCache, isValueImmutable); + } + @Override public boolean contains(@Nullable T value) { return cache.contains(value); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRange.java index 0ff0c78343..46aada23e2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRange.java @@ -4,7 +4,9 @@ import java.util.Random; import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; +import ai.timefold.solver.core.api.domain.valuerange.ValueRange; import ai.timefold.solver.core.impl.domain.valuerange.AbstractCountableValueRange; +import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; import ai.timefold.solver.core.impl.domain.valuerange.util.ValueRangeIterator; import ai.timefold.solver.core.impl.solver.random.RandomUtils; @@ -25,11 +27,6 @@ public NullAllowingCountableValueRange(CountableValueRange childValueRange) { size = childValueRange.getSize() + 1L; } - @Override - public boolean isValueImmutable() { - return super.isValueImmutable(); - } - AbstractCountableValueRange getChildValueRange() { return childValueRange; } @@ -56,6 +53,11 @@ public boolean contains(T value) { return childValueRange.contains(value); } + @Override + public ValueRange sort(ValueRangeSorter sorter) { + return childValueRange.sort(sorter); + } + @Override public @NonNull Iterator createOriginalIterator() { return new OriginalNullValueRangeIterator(childValueRange.createOriginalIterator()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java new file mode 100644 index 0000000000..c9189affff --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java @@ -0,0 +1,34 @@ +package ai.timefold.solver.core.impl.domain.valuerange.sort; + +import java.util.List; +import java.util.Set; +import java.util.SortedSet; + +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSetSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record SelectionSorterAdapter(Solution_ solution, + SelectionSorter selectionSorter) implements ValueRangeSorter { + + public static ValueRangeSorter of(Solution_ solution, SelectionSorter selectionSorter) { + return new SelectionSorterAdapter<>(solution, selectionSorter); + } + + @Override + public void sort(List selectionList) { + selectionSorter.sort(solution, selectionList); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public SortedSet sort(Set selectionSet) { + if (!(selectionSorter instanceof SelectionSetSorter selectionSetSorter)) { + throw new IllegalStateException( + "Impossible state: the sorting operation cannot be performed because the sorter does not support sorting collection sets."); + } + return selectionSetSorter.sort(solution, selectionSet); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java new file mode 100644 index 0000000000..d3754be14d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java @@ -0,0 +1,31 @@ +package ai.timefold.solver.core.impl.domain.valuerange.sort; + +import java.util.List; +import java.util.Set; +import java.util.SortedSet; + +import org.jspecify.annotations.NullMarked; + +/** + * Basic contract for sorting a range of elements. + * + * @param the value range type + */ +@NullMarked +public interface ValueRangeSorter { + + /** + * Apply an in-place sorting operation. + * + * @param selectionList never null, a {@link List} of value that will be sorted. + */ + void sort(List selectionList); + + /** + * Creates a copy of the provided set and sort the data. + * + * @param selectionSet never null, a {@link Set} of values that will be used as input for sorting. + */ + SortedSet sort(Set selectionSet); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java index eac6e5fdec..2f59692fb8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java @@ -4,18 +4,25 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; +import org.jspecify.annotations.NullMarked; + /** * Sorts a selection {@link List} based on a {@link ComparatorFactory}. * * @param the solution type, the class with the {@link PlanningSolution} annotation * @param the selection type */ -public final class ComparatorFactorySelectionSorter implements SelectionSorter { +@NullMarked +public final class ComparatorFactorySelectionSorter + implements SelectionSorter, SelectionSetSorter { private final ComparatorFactory selectionComparatorFactory; private final SelectionSorterOrder selectionSorterOrder; @@ -39,6 +46,13 @@ public void sort(Solution_ solution, List selectionList) { selectionList.sort(appliedComparator); } + @Override + public SortedSet sort(Solution_ solution, Set selectionSet) { + var treeSet = new TreeSet<>(getAppliedComparator(selectionComparatorFactory.createComparator(solution))); + treeSet.addAll(selectionSet); + return Collections.unmodifiableSortedSet(treeSet); + } + @Override public boolean equals(Object o) { if (!(o instanceof ComparatorFactorySelectionSorter that)) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java index 92f1a1cd3b..a056ab84b9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java @@ -4,17 +4,24 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; +import org.jspecify.annotations.NullMarked; + /** * Sorts a selection {@link List} based on a {@link Comparator}. * * @param the solution type, the class with the {@link PlanningSolution} annotation * @param the selection type */ -public final class ComparatorSelectionSorter implements SelectionSorter { +@NullMarked +public final class ComparatorSelectionSorter + implements SelectionSorter, SelectionSetSorter { private final Comparator appliedComparator; @@ -37,6 +44,13 @@ public void sort(Solution_ solution, List selectionList) { selectionList.sort(appliedComparator); } + @Override + public SortedSet sort(Solution_ solution, Set selectionSet) { + var treeSet = new TreeSet<>(appliedComparator); + treeSet.addAll(selectionSet); + return Collections.unmodifiableSortedSet(treeSet); + } + @Override public boolean equals(Object other) { if (this == other) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSetSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSetSorter.java new file mode 100644 index 0000000000..9260107b42 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSetSorter.java @@ -0,0 +1,28 @@ +package ai.timefold.solver.core.impl.heuristic.selector.common.decorator; + +import java.util.Set; +import java.util.SortedSet; + +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; + +import org.jspecify.annotations.NullMarked; + +/** + * Decides the order of a {@link Set} of selection values. + * + * @param the solution type, the class with the {@link PlanningSolution} annotation + * @param the selection type + */ +@NullMarked +@FunctionalInterface +public interface SelectionSetSorter { + + /** + * Creates a copy of the provided set and sort the data. + * + * @param solution never null, the current solution + * @param selectionSet never null, a {@link Set} of values that will be used as input for sorting. + */ + SortedSet sort(Solution_ solution, Set selectionSet); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java index d700f9ecee..fb76aace58 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java @@ -25,9 +25,11 @@ public interface SelectionSorter { /** + * Apply an in-place sorting operation. + * * @param solution never null, the current solution * @param selectionList never null, a {@link List} - * of {@link PlanningEntity}, planningValue, {@link Move} or {@link Selector} + * of {@link PlanningEntity}, planningValue, {@link Move} or {@link Selector} that will be sorted. */ void sort(Solution_ solution, List selectionList); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java index 1b63dcffad..4c68dd1d09 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java @@ -21,20 +21,20 @@ import ai.timefold.solver.core.impl.domain.valuerange.buildin.composite.NullAllowingCountableValueRange; import ai.timefold.solver.core.impl.domain.valuerange.buildin.primdouble.DoubleValueRange; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; +import ai.timefold.solver.core.impl.domain.valuerange.sort.SelectionSorterAdapter; import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.common.ReachableValues; import ai.timefold.solver.core.impl.heuristic.selector.common.ReachableValues.ReachableItemValue; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.util.MathUtils; import ai.timefold.solver.core.impl.util.MutableInt; import ai.timefold.solver.core.impl.util.MutableLong; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Caches value ranges for the current working solution, @@ -59,12 +59,10 @@ @NullMarked public final class ValueRangeManager { - private final Logger logger = LoggerFactory.getLogger(getClass()); - private final SolutionDescriptor solutionDescriptor; - private final CountableValueRange[] fromSolution; + private final @Nullable CountableValueRangeItem[] fromSolution; private final ReachableValues[] reachableValues; - private final Map[]> fromEntityMap = + private final Map[]> fromEntityMap = new IdentityHashMap<>(); private @Nullable Solution_ cachedWorkingSolution = null; @@ -86,7 +84,7 @@ public static ValueRangeManager of(SolutionDescriptor solutionDescriptor) { this.solutionDescriptor = Objects.requireNonNull(solutionDescriptor); - this.fromSolution = new CountableValueRange[solutionDescriptor.getValueRangeDescriptorCount()]; + this.fromSolution = new CountableValueRangeItem[solutionDescriptor.getValueRangeDescriptorCount()]; this.reachableValues = new ReachableValues[solutionDescriptor.getValueRangeDescriptorCount()]; } @@ -362,8 +360,18 @@ public CountableValueRange getFromSolution(ValueRangeDescriptor CountableValueRange getFromSolution(ValueRangeDescriptor valueRangeDescriptor, Solution_ solution) { - var valueRange = fromSolution[valueRangeDescriptor.getOrdinal()]; - if (valueRange == null) { // Avoid computeIfAbsent on the hot path; creates capturing lambda instances. + return getFromSolution(valueRangeDescriptor, solution, null); + } + + public CountableValueRange getFromSolution(ValueRangeDescriptor valueRangeDescriptor, Solution_ solution, + @Nullable SelectionSorter sorter) { + var item = fromSolution[valueRangeDescriptor.getOrdinal()]; + var valueRange = + item != null ? (CountableValueRange) item.countableValueRange() : null; + var valueRangeSorter = item != null ? item.sorter() : null; + // Avoid computeIfAbsent on the hot path; creates capturing lambda instances. + // We read and sort the data again if needed + if (valueRange == null || (sorter != null && !Objects.equals(valueRangeSorter, sorter))) { var extractedValueRange = valueRangeDescriptor. extractAllValues(Objects.requireNonNull(solution)); if (!(extractedValueRange instanceof CountableValueRange countableValueRange)) { throw new UnsupportedOperationException(""" @@ -376,9 +384,13 @@ public CountableValueRange getFromSolution(ValueRangeDescriptor) valueRange.sort(sorterAdapter); + } + fromSolution[valueRangeDescriptor.getOrdinal()] = new CountableValueRangeItem<>(valueRange, sorter); } - return (CountableValueRange) valueRange; + return valueRange; } /** @@ -386,6 +398,15 @@ public CountableValueRange getFromSolution(ValueRangeDescriptor CountableValueRange getFromEntity(ValueRangeDescriptor valueRangeDescriptor, Object entity) { + return getFromEntity(valueRangeDescriptor, entity, null); + } + + /** + * @throws IllegalStateException if called before {@link #reset(Object)} is called + */ + @SuppressWarnings("unchecked") + public CountableValueRange getFromEntity(ValueRangeDescriptor valueRangeDescriptor, Object entity, + @Nullable SelectionSorter sorter) { if (cachedWorkingSolution == null) { throw new IllegalStateException( "Impossible state: value range (%s) on planning entity (%s) requested before the working solution is known." @@ -393,9 +414,13 @@ public CountableValueRange getFromEntity(ValueRangeDescriptor } var valueRangeList = fromEntityMap.computeIfAbsent(entity, - e -> new CountableValueRange[solutionDescriptor.getValueRangeDescriptorCount()]); - var valueRange = valueRangeList[valueRangeDescriptor.getOrdinal()]; - if (valueRange == null) { // Avoid computeIfAbsent on the hot path; creates capturing lambda instances. + e -> new CountableValueRangeItem[solutionDescriptor.getValueRangeDescriptorCount()]); + var item = valueRangeList[valueRangeDescriptor.getOrdinal()]; + var valueRange = item != null ? (CountableValueRange) item.countableValueRange() : null; + var valueRangeSorter = item != null ? item.sorter() : null; + // Avoid computeIfAbsent on the hot path; creates capturing lambda instances. + // We read and sort the data again if needed + if (valueRange == null || (sorter != null && !Objects.equals(valueRangeSorter, sorter))) { var extractedValueRange = valueRangeDescriptor. extractValuesFromEntity(cachedWorkingSolution, Objects.requireNonNull(entity)); if (!(extractedValueRange instanceof CountableValueRange countableValueRange)) { @@ -409,9 +434,13 @@ public CountableValueRange getFromEntity(ValueRangeDescriptor } else { valueRange = countableValueRange; } - valueRangeList[valueRangeDescriptor.getOrdinal()] = valueRange; + if (sorter != null) { + var sorterAdapter = SelectionSorterAdapter.of(cachedWorkingSolution, sorter); + valueRange = (CountableValueRange) valueRange.sort(sorterAdapter); + } + valueRangeList[valueRangeDescriptor.getOrdinal()] = new CountableValueRangeItem<>(valueRange, sorter); } - return (CountableValueRange) valueRange; + return valueRange; } public long countOnSolution(ValueRangeDescriptor valueRangeDescriptor, Solution_ solution) { @@ -506,4 +535,9 @@ public void reset(@Nullable Solution_ workingSolution) { } } + private record CountableValueRangeItem(CountableValueRange countableValueRange, + @Nullable SelectionSorter sorter) { + + } + } From 96cb14cd0c5dee2aecb7db8d7370c9ca4117d2b6 Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 28 Oct 2025 19:18:13 -0300 Subject: [PATCH 03/13] chore: add tests --- .../buildin/EmptyValueRangeTest.java | 13 +++++-- .../bigdecimal/BigDecimalValueRangeTest.java | 9 ++++- .../biginteger/BigIntegerValueRangeTest.java | 9 ++++- .../collection/ListValueRangeTest.java | 29 ++++++++++++++- .../buildin/collection/SetValueRangeTest.java | 29 ++++++++++++++- .../CompositeCountableValueRangeTest.java | 37 ++++++++++++++++++- .../NullAllowingCountableValueRangeTest.java | 31 ++++++++++++++++ .../primboolean/BooleanValueRangeTest.java | 11 +++++- .../primdouble/DoubleValueRangeTest.java | 7 ++++ .../buildin/primint/IntValueRangeTest.java | 9 ++++- .../buildin/primlong/LongValueRangeTest.java | 9 ++++- .../temporal/TemporalValueRangeTest.java | 25 ++++++++----- 12 files changed, 197 insertions(+), 21 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/EmptyValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/EmptyValueRangeTest.java index 4e3b9bbd0a..dc6430ec46 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/EmptyValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/EmptyValueRangeTest.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.domain.valuerange.buildin; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.util.Random; @@ -11,13 +12,13 @@ class EmptyValueRangeTest { @Test void getSize() { - assertThat(EmptyValueRange.instance().getSize()).isEqualTo(0L); + assertThat(EmptyValueRange.instance().getSize()).isZero(); } @Test void get() { - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> EmptyValueRange.instance().get(0L)); + var range = EmptyValueRange.instance(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> range.get(0L)); } @Test @@ -41,4 +42,10 @@ void createRandomIterator() { .isEmpty(); } + @Test + void sort() { + var range = EmptyValueRange.instance(); + assertThatCode(() -> range.sort(null)).doesNotThrowAnyException(); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/bigdecimal/BigDecimalValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/bigdecimal/BigDecimalValueRangeTest.java index 36fa6aa3f5..fb008735ee 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/bigdecimal/BigDecimalValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/bigdecimal/BigDecimalValueRangeTest.java @@ -3,6 +3,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllElementsOfIterator; import static ai.timefold.solver.core.testutil.PlannerAssert.assertElementsOfIterator; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.math.BigDecimal; @@ -17,7 +18,7 @@ void getSize() { assertThat(new BigDecimalValueRange(new BigDecimal("0"), new BigDecimal("10")).getSize()).isEqualTo(10L); assertThat(new BigDecimalValueRange(new BigDecimal("100.0"), new BigDecimal("120.0")).getSize()).isEqualTo(200L); assertThat(new BigDecimalValueRange(new BigDecimal("-15.00"), new BigDecimal("25.07")).getSize()).isEqualTo(4007L); - assertThat(new BigDecimalValueRange(new BigDecimal("7.0"), new BigDecimal("7.0")).getSize()).isEqualTo(0L); + assertThat(new BigDecimalValueRange(new BigDecimal("7.0"), new BigDecimal("7.0")).getSize()).isZero(); // IncrementUnit assertThat(new BigDecimalValueRange(new BigDecimal("0.0"), new BigDecimal("10.0"), new BigDecimal("2.0")).getSize()) .isEqualTo(5L); @@ -123,4 +124,10 @@ void createRandomIterator() { new BigDecimal("115.3"), new BigDecimal("100.0")); } + @Test + void sort() { + var range = new BigDecimalValueRange(new BigDecimal("0"), new BigDecimal("4")); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> range.sort(null)); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/biginteger/BigIntegerValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/biginteger/BigIntegerValueRangeTest.java index 4f148b184d..6a43454971 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/biginteger/BigIntegerValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/biginteger/BigIntegerValueRangeTest.java @@ -3,6 +3,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllElementsOfIterator; import static ai.timefold.solver.core.testutil.PlannerAssert.assertElementsOfIterator; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.math.BigInteger; @@ -17,7 +18,7 @@ void getSize() { assertThat(new BigIntegerValueRange(new BigInteger("0"), new BigInteger("10")).getSize()).isEqualTo(10L); assertThat(new BigIntegerValueRange(new BigInteger("100"), new BigInteger("120")).getSize()).isEqualTo(20L); assertThat(new BigIntegerValueRange(new BigInteger("-15"), new BigInteger("25")).getSize()).isEqualTo(40L); - assertThat(new BigIntegerValueRange(new BigInteger("7"), new BigInteger("7")).getSize()).isEqualTo(0L); + assertThat(new BigIntegerValueRange(new BigInteger("7"), new BigInteger("7")).getSize()).isZero(); // IncrementUnit assertThat(new BigIntegerValueRange(new BigInteger("0"), new BigInteger("10"), new BigInteger("2")).getSize()) .isEqualTo(5L); @@ -117,4 +118,10 @@ void createRandomIterator() { .createRandomIterator(new TestRandom(3, 0)), new BigInteger("115"), new BigInteger("100")); } + @Test + void sort() { + var range = new BigIntegerValueRange(new BigInteger("0"), new BigInteger("7")); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> range.sort(null)); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRangeTest.java index a21a6ab045..fb0a9ea2e7 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRangeTest.java @@ -6,8 +6,14 @@ import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.Random; +import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; +import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; +import ai.timefold.solver.core.impl.domain.valuerange.sort.SelectionSorterAdapter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorFactorySelectionSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter; import ai.timefold.solver.core.testutil.TestRandom; import org.junit.jupiter.api.Test; @@ -20,7 +26,7 @@ void getSize() { assertThat(new ListValueRange<>(Arrays.asList(100, 120, 5, 7, 8)).getSize()).isEqualTo(5L); assertThat(new ListValueRange<>(Arrays.asList(-15, 25, 0)).getSize()).isEqualTo(3L); assertThat(new ListValueRange<>(Arrays.asList("b", "z", "a")).getSize()).isEqualTo(3L); - assertThat(new ListValueRange<>(Collections.emptyList()).getSize()).isEqualTo(0L); + assertThat(new ListValueRange<>(Collections.emptyList()).getSize()).isZero(); } @Test @@ -68,4 +74,25 @@ void createRandomIterator() { assertAllElementsOfIterator(new ListValueRange<>(Collections.emptyList()).createRandomIterator(new Random(0))); } + @Test + void sort() { + var ascComparatorSorter = new SelectionSorterAdapter<>(null, new ComparatorSelectionSorter<>( + Comparator.comparingInt(Integer::intValue), SelectionSorterOrder.ASCENDING)); + Comparator integerComparator = Integer::compareTo; + var ascComparatorFactorySorter = new SelectionSorterAdapter<>(null, + new ComparatorFactorySelectionSorter<>(solution -> integerComparator, SelectionSorterOrder.ASCENDING)); + var descComparatorSorter = new SelectionSorterAdapter<>(null, new ComparatorSelectionSorter<>( + Comparator.comparingInt(Integer::intValue), SelectionSorterOrder.DESCENDING)); + var descComparatorFactorySorter = new SelectionSorterAdapter<>(null, + new ComparatorFactorySelectionSorter<>(solution -> integerComparator, SelectionSorterOrder.DESCENDING)); + assertAllElementsOfIterator(((CountableValueRange) new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)) + .sort(ascComparatorSorter)).createOriginalIterator(), -15, -1, 0, 1, 25); + assertAllElementsOfIterator(((CountableValueRange) new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)) + .sort(ascComparatorFactorySorter)).createOriginalIterator(), -15, -1, 0, 1, 25); + assertAllElementsOfIterator(((CountableValueRange) new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)) + .sort(descComparatorSorter)).createOriginalIterator(), 25, 1, 0, -1, -15); + assertAllElementsOfIterator(((CountableValueRange) new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)) + .sort(descComparatorFactorySorter)).createOriginalIterator(), 25, 1, 0, -1, -15); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/SetValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/SetValueRangeTest.java index a1ec81e73e..d60220f867 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/SetValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/SetValueRangeTest.java @@ -6,10 +6,16 @@ import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.Random; import java.util.stream.Collectors; +import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; +import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; +import ai.timefold.solver.core.impl.domain.valuerange.sort.SelectionSorterAdapter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorFactorySelectionSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter; import ai.timefold.solver.core.testutil.TestRandom; import org.junit.jupiter.api.Test; @@ -22,7 +28,7 @@ void getSize() { assertThat(createRange(100, 120, 5, 7, 8).getSize()).isEqualTo(5L); assertThat(createRange(-15, 25, 0).getSize()).isEqualTo(3L); assertThat(createRange("b", "z", "a").getSize()).isEqualTo(3L); - assertThat(new SetValueRange<>(Collections.emptySet()).getSize()).isEqualTo(0L); + assertThat(new SetValueRange<>(Collections.emptySet()).getSize()).isZero(); } @Test @@ -69,6 +75,27 @@ void createRandomIterator() { assertAllElementsOfIterator(new SetValueRange<>(Collections.emptySet()).createRandomIterator(new Random(0))); } + @Test + void sort() { + var ascComparatorSorter = new SelectionSorterAdapter<>(null, new ComparatorSelectionSorter<>( + Comparator.comparingInt(Integer::intValue), SelectionSorterOrder.ASCENDING)); + Comparator integerComparator = Integer::compareTo; + var ascComparatorFactorySorter = new SelectionSorterAdapter<>(null, + new ComparatorFactorySelectionSorter<>(solution -> integerComparator, SelectionSorterOrder.ASCENDING)); + var descComparatorSorter = new SelectionSorterAdapter<>(null, new ComparatorSelectionSorter<>( + Comparator.comparingInt(Integer::intValue), SelectionSorterOrder.DESCENDING)); + var descComparatorFactorySorter = new SelectionSorterAdapter<>(null, + new ComparatorFactorySelectionSorter<>(solution -> integerComparator, SelectionSorterOrder.DESCENDING)); + assertAllElementsOfIterator(((CountableValueRange) createRange(-15, 25, 0, 1, -1) + .sort(ascComparatorSorter)).createOriginalIterator(), -15, -1, 0, 1, 25); + assertAllElementsOfIterator(((CountableValueRange) createRange(-15, 25, 0, 1, -1) + .sort(ascComparatorFactorySorter)).createOriginalIterator(), -15, -1, 0, 1, 25); + assertAllElementsOfIterator(((CountableValueRange) createRange(-15, 25, 0, 1, -1) + .sort(descComparatorSorter)).createOriginalIterator(), 25, 1, 0, -1, -15); + assertAllElementsOfIterator(((CountableValueRange) createRange(-15, 25, 0, 1, -1) + .sort(descComparatorFactorySorter)).createOriginalIterator(), 25, 1, 0, -1, -15); + } + private static SetValueRange createRange(T... values) { var set = Arrays.stream(values) .collect(Collectors.toCollection(LinkedHashSet::new)); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRangeTest.java index d829f9889e..3ce43f8197 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRangeTest.java @@ -7,9 +7,15 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; +import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; +import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; import ai.timefold.solver.core.impl.domain.valuerange.buildin.collection.ListValueRange; +import ai.timefold.solver.core.impl.domain.valuerange.sort.SelectionSorterAdapter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorFactorySelectionSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter; import ai.timefold.solver.core.testutil.TestRandom; import org.junit.jupiter.api.Test; @@ -29,7 +35,7 @@ private static CompositeCountableValueRange createValueRange(List... l void getSize() { assertThat(createValueRange(Arrays.asList(0, 2, 5, 10), Arrays.asList(-15, 25, -1)).getSize()).isEqualTo(7L); assertThat(createValueRange(Arrays.asList("a", "b"), Arrays.asList("c"), Arrays.asList("d")).getSize()).isEqualTo(4L); - assertThat(createValueRange(Collections.emptyList()).getSize()).isEqualTo(0L); + assertThat(createValueRange(Collections.emptyList()).getSize()).isZero(); } @Test @@ -72,4 +78,33 @@ void createRandomIterator() { .createRandomIterator(new TestRandom(0))); } + @Test + void sort() { + var ascComparatorSorter = new SelectionSorterAdapter<>(null, new ComparatorSelectionSorter<>( + Comparator.comparingInt(Integer::intValue), SelectionSorterOrder.ASCENDING)); + Comparator integerComparator = Integer::compareTo; + var ascComparatorFactorySorter = new SelectionSorterAdapter<>(null, + new ComparatorFactorySelectionSorter<>(solution -> integerComparator, SelectionSorterOrder.ASCENDING)); + var descComparatorSorter = new SelectionSorterAdapter<>(null, new ComparatorSelectionSorter<>( + Comparator.comparingInt(Integer::intValue), SelectionSorterOrder.DESCENDING)); + var descComparatorFactorySorter = new SelectionSorterAdapter<>(null, + new ComparatorFactorySelectionSorter<>(solution -> integerComparator, SelectionSorterOrder.DESCENDING)); + assertAllElementsOfIterator( + ((CountableValueRange) createValueRange(Arrays.asList(0, 2, 5, 10), Arrays.asList(-15, 25, -1)) + .sort(ascComparatorSorter)).createOriginalIterator(), + -15, -1, 0, 2, 5, 10, 25); + assertAllElementsOfIterator( + ((CountableValueRange) createValueRange(Arrays.asList(0, 2, 5, 10), Arrays.asList(-15, 25, -1)) + .sort(ascComparatorFactorySorter)).createOriginalIterator(), + -15, -1, 0, 2, 5, 10, 25); + assertAllElementsOfIterator( + ((CountableValueRange) createValueRange(Arrays.asList(0, 2, 5, 10), Arrays.asList(-15, 25, -1)) + .sort(descComparatorSorter)).createOriginalIterator(), + 25, 10, 5, 2, 0, -1, -15); + assertAllElementsOfIterator( + ((CountableValueRange) createValueRange(Arrays.asList(0, 2, 5, 10), Arrays.asList(-15, 25, -1)) + .sort(descComparatorFactorySorter)).createOriginalIterator(), + 25, 10, 5, 2, 0, -1, -15); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRangeTest.java index 5808ac63b3..0c71acf1f3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRangeTest.java @@ -6,8 +6,14 @@ import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; +import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; +import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; import ai.timefold.solver.core.impl.domain.valuerange.buildin.collection.ListValueRange; +import ai.timefold.solver.core.impl.domain.valuerange.sort.SelectionSorterAdapter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorFactorySelectionSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter; import ai.timefold.solver.core.testutil.TestRandom; import org.junit.jupiter.api.Test; @@ -87,4 +93,29 @@ void createRandomIterator() { .createRandomIterator(new TestRandom(0)), new String[] { null }); } + @Test + void sort() { + var ascComparatorSorter = new SelectionSorterAdapter<>(null, new ComparatorSelectionSorter<>( + Comparator.comparingInt(Integer::intValue), SelectionSorterOrder.ASCENDING)); + Comparator integerComparator = Integer::compareTo; + var ascComparatorFactorySorter = new SelectionSorterAdapter<>(null, + new ComparatorFactorySelectionSorter<>(solution -> integerComparator, SelectionSorterOrder.ASCENDING)); + var descComparatorSorter = new SelectionSorterAdapter<>(null, new ComparatorSelectionSorter<>( + Comparator.comparingInt(Integer::intValue), SelectionSorterOrder.DESCENDING)); + var descComparatorFactorySorter = new SelectionSorterAdapter<>(null, + new ComparatorFactorySelectionSorter<>(solution -> integerComparator, SelectionSorterOrder.DESCENDING)); + assertAllElementsOfIterator(((CountableValueRange) new NullAllowingCountableValueRange<>( + (new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(ascComparatorSorter)).createOriginalIterator(), + -15, -1, 0, 1, 25); + assertAllElementsOfIterator(((CountableValueRange) new NullAllowingCountableValueRange<>( + (new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(ascComparatorFactorySorter)) + .createOriginalIterator(), -15, -1, 0, 1, 25); + assertAllElementsOfIterator(((CountableValueRange) new NullAllowingCountableValueRange<>( + (new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(descComparatorSorter)).createOriginalIterator(), + 25, 1, 0, -1, -15); + assertAllElementsOfIterator(((CountableValueRange) new NullAllowingCountableValueRange<>( + (new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(descComparatorFactorySorter)) + .createOriginalIterator(), 25, 1, 0, -1, -15); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primboolean/BooleanValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primboolean/BooleanValueRangeTest.java index 766b82e5bd..336c66237c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primboolean/BooleanValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primboolean/BooleanValueRangeTest.java @@ -45,12 +45,19 @@ void createRandomIterator() { @Test void getIndexNegative() { - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> new BooleanValueRange().get(-1)); + var range = new BooleanValueRange(); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> range.get(-1)); } @Test void getIndexGreaterThanSize() { - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> new BooleanValueRange().get(2)); + var range = new BooleanValueRange(); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> range.get(2)); } + @Test + void sort() { + var range = new BooleanValueRange(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> range.sort(null)); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primdouble/DoubleValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primdouble/DoubleValueRangeTest.java index ea8f4ecc9c..63267511b5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primdouble/DoubleValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primdouble/DoubleValueRangeTest.java @@ -3,6 +3,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllElementsOfIterator; import static ai.timefold.solver.core.testutil.PlannerAssert.assertElementsOfIterator; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import ai.timefold.solver.core.testutil.TestRandom; @@ -35,4 +36,10 @@ void createRandomIterator() { Math.nextAfter(2000000.0, Double.NEGATIVE_INFINITY)))); } + @Test + void sort() { + var range = new DoubleValueRange(0.0, 10.0); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> range.sort(null)); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primint/IntValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primint/IntValueRangeTest.java index 24c076b061..f8f8dd7bc9 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primint/IntValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primint/IntValueRangeTest.java @@ -3,6 +3,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllElementsOfIterator; import static ai.timefold.solver.core.testutil.PlannerAssert.assertElementsOfIterator; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import ai.timefold.solver.core.testutil.TestRandom; @@ -15,7 +16,7 @@ void getSize() { assertThat(new IntValueRange(0, 10).getSize()).isEqualTo(10L); assertThat(new IntValueRange(100, 120).getSize()).isEqualTo(20L); assertThat(new IntValueRange(-15, 25).getSize()).isEqualTo(40L); - assertThat(new IntValueRange(7, 7).getSize()).isEqualTo(0L); + assertThat(new IntValueRange(7, 7).getSize()).isZero(); assertThat(new IntValueRange(-1000, Integer.MAX_VALUE - 100).getSize()).isEqualTo(Integer.MAX_VALUE + 900L); // IncrementUnit assertThat(new IntValueRange(0, 10, 2).getSize()).isEqualTo(5L); @@ -78,4 +79,10 @@ void createRandomIterator() { assertElementsOfIterator(new IntValueRange(100, 120, 5).createRandomIterator(new TestRandom(3, 0)), 115, 100); } + @Test + void sort() { + var range = new IntValueRange(0, 7); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> range.sort(null)); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primlong/LongValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primlong/LongValueRangeTest.java index c9ca085fde..c39e9296b3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primlong/LongValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/primlong/LongValueRangeTest.java @@ -3,6 +3,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllElementsOfIterator; import static ai.timefold.solver.core.testutil.PlannerAssert.assertElementsOfIterator; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import ai.timefold.solver.core.testutil.TestRandom; @@ -15,7 +16,7 @@ void getSize() { assertThat(new LongValueRange(0L, 10L).getSize()).isEqualTo(10L); assertThat(new LongValueRange(100L, 120L).getSize()).isEqualTo(20L); assertThat(new LongValueRange(-15L, 25L).getSize()).isEqualTo(40L); - assertThat(new LongValueRange(7L, 7L).getSize()).isEqualTo(0L); + assertThat(new LongValueRange(7L, 7L).getSize()).isZero(); assertThat(new LongValueRange(-1000L, Long.MAX_VALUE - 3000L).getSize()).isEqualTo(Long.MAX_VALUE - 2000L); // IncrementUnit assertThat(new LongValueRange(0L, 10L, 2L).getSize()).isEqualTo(5L); @@ -78,4 +79,10 @@ void createRandomIterator() { assertElementsOfIterator(new LongValueRange(100L, 120L, 5L).createRandomIterator(new TestRandom(3, 0)), 115L, 100L); } + @Test + void sort() { + var range = new LongValueRange(0L, 7L); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> range.sort(null)); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/temporal/TemporalValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/temporal/TemporalValueRangeTest.java index c8011182ee..96e4249799 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/temporal/TemporalValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/temporal/TemporalValueRangeTest.java @@ -40,7 +40,7 @@ void getSizeForLocalDate() { assertThat(new TemporalValueRange<>(from, to, 5, ChronoUnit.DAYS).getSize()).isEqualTo(2L); from = LocalDate.of(2016, 7, 7); to = LocalDate.of(2016, 7, 7); - assertThat(new TemporalValueRange<>(from, to, 1, ChronoUnit.MONTHS).getSize()).isEqualTo(0L); + assertThat(new TemporalValueRange<>(from, to, 1, ChronoUnit.MONTHS).getSize()).isZero(); from = LocalDate.of(2017, 1, 31); to = LocalDate.of(2017, 2, 28); // Exactly 1 month later @@ -80,11 +80,11 @@ void getSizeForLocalDate() { void getSizeForLocalDateTime() { LocalDateTime fromTime = LocalDateTime.of(2016, 7, 7, 7, 7, 7); LocalDateTime toTime = LocalDateTime.of(2016, 7, 7, 7, 7, 7); - assertThat(new TemporalValueRange<>(fromTime, toTime, 1, ChronoUnit.MONTHS).getSize()).isEqualTo(0L); - assertThat(new TemporalValueRange<>(fromTime, toTime, 1, ChronoUnit.DAYS).getSize()).isEqualTo(0L); - assertThat(new TemporalValueRange<>(fromTime, toTime, 1, ChronoUnit.HOURS).getSize()).isEqualTo(0L); - assertThat(new TemporalValueRange<>(fromTime, toTime, 1, ChronoUnit.MINUTES).getSize()).isEqualTo(0L); - assertThat(new TemporalValueRange<>(fromTime, toTime, 1, ChronoUnit.SECONDS).getSize()).isEqualTo(0L); + assertThat(new TemporalValueRange<>(fromTime, toTime, 1, ChronoUnit.MONTHS).getSize()).isZero(); + assertThat(new TemporalValueRange<>(fromTime, toTime, 1, ChronoUnit.DAYS).getSize()).isZero(); + assertThat(new TemporalValueRange<>(fromTime, toTime, 1, ChronoUnit.HOURS).getSize()).isZero(); + assertThat(new TemporalValueRange<>(fromTime, toTime, 1, ChronoUnit.MINUTES).getSize()).isZero(); + assertThat(new TemporalValueRange<>(fromTime, toTime, 1, ChronoUnit.SECONDS).getSize()).isZero(); fromTime = LocalDateTime.of(2016, 7, 7, 7, 7, 7); toTime = LocalDateTime.of(2016, 7, 7, 7, 7, 8); @@ -477,15 +477,16 @@ void remainderOnIncrementTypeExceedsMaximumYear() { Year from = Year.of(Year.MIN_VALUE); Year to = Year.of(Year.MAX_VALUE - 0); // Maximum Year range is not divisible by 10 - assertThat((to.getValue() - from.getValue()) % 10).isNotEqualTo(0); + assertThat((to.getValue() - from.getValue()) % 10).isNotZero(); assertThatIllegalArgumentException() .isThrownBy(() -> new TemporalValueRange<>(from, to, 1, ChronoUnit.DECADES)); } @Test void getIndexNegative() { + var range = new TemporalValueRange<>(Year.of(0), Year.of(1), 1, ChronoUnit.YEARS); assertThatExceptionOfType(IndexOutOfBoundsException.class) - .isThrownBy(() -> new TemporalValueRange<>(Year.of(0), Year.of(1), 1, ChronoUnit.YEARS).get(-1)); + .isThrownBy(() -> range.get(-1)); } @Test @@ -495,7 +496,13 @@ void getIndexGreaterThanSize() { assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> range.get(1)); } - private static interface TemporalMock extends Temporal, Comparable { + @Test + void sort() { + var range = new TemporalValueRange<>(Year.of(0), Year.of(1), 1, ChronoUnit.YEARS); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> range.sort(null)); + } + + private interface TemporalMock extends Temporal, Comparable { } } From a05e0447312a45be7a7b5189b24ad1fe4c33e650 Mon Sep 17 00:00:00 2001 From: fred Date: Mon, 3 Nov 2025 08:15:05 -0300 Subject: [PATCH 04/13] chore: minor improvements --- .../core/impl/domain/valuerange/ValueRangeCache.java | 3 +-- .../valuerange/buildin/collection/ListValueRange.java | 6 ++---- .../buildin/composite/CompositeCountableValueRange.java | 2 +- .../domain/valuerange/sort/SelectionSorterAdapter.java | 8 ++++++-- .../impl/domain/valuerange/sort/ValueRangeSorter.java | 6 +++--- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java index 83afa8985d..9295635412 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java @@ -82,8 +82,7 @@ public Iterator iterator(Random workingRandom) { * @param sorter never null, the sorter */ public ValueRangeCache sort(ValueRangeSorter sorter) { - var valuesWithFastRandomAccessSorted = new ArrayList<>(valuesWithFastRandomAccess); - sorter.sort(valuesWithFastRandomAccessSorted); + var valuesWithFastRandomAccessSorted = sorter.sort(valuesWithFastRandomAccess); return switch (cacheType) { case USER_VALUES -> Builder.FOR_USER_VALUES.buildCache(valuesWithFastRandomAccessSorted); case TRUSTED_VALUES -> Builder.FOR_TRUSTED_VALUES.buildCache(valuesWithFastRandomAccessSorted); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRange.java index df42fe8eb6..602579bad3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/collection/ListValueRange.java @@ -1,6 +1,5 @@ package ai.timefold.solver.core.impl.domain.valuerange.buildin.collection; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Random; @@ -60,9 +59,8 @@ public boolean contains(@Nullable T value) { @Override public ValueRange sort(ValueRangeSorter sorter) { - var newList = new ArrayList<>(list); - sorter.sort(newList); - return new ListValueRange<>(newList, isValueImmutable); + var sortedList = sorter.sort(list); + return new ListValueRange<>(sortedList, isValueImmutable); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRange.java index d3d289cbd7..14657955d0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/CompositeCountableValueRange.java @@ -41,8 +41,8 @@ public CompositeCountableValueRange(List cache, boolean isValueImmutable) { - this.isValueImmutable = isValueImmutable; this.cache = cache; + this.isValueImmutable = isValueImmutable; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java index c9189affff..889947aa63 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.domain.valuerange.sort; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.SortedSet; @@ -18,8 +20,10 @@ public static ValueRangeSorter of(Solution_ solution, Selectio } @Override - public void sort(List selectionList) { - selectionSorter.sort(solution, selectionList); + public List sort(List selectionList) { + var newList = new ArrayList<>(selectionList); + selectionSorter.sort(solution, newList); + return Collections.unmodifiableList(newList); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java index d3754be14d..da26aae9e7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java @@ -15,11 +15,11 @@ public interface ValueRangeSorter { /** - * Apply an in-place sorting operation. + * Creates a copy of the provided list and sort the data. * - * @param selectionList never null, a {@link List} of value that will be sorted. + * @param selectionList never null, a {@link List} of values that will be used as input for sorting. */ - void sort(List selectionList); + List sort(List selectionList); /** * Creates a copy of the provided set and sort the data. From 00f95c06a91a3a5a3bc9f672c559b51470bdb68f Mon Sep 17 00:00:00 2001 From: fred Date: Mon, 3 Nov 2025 10:33:42 -0300 Subject: [PATCH 05/13] chore: add more tests --- .../score/director/ValueRangeManagerTest.java | 281 ++++++++++++++++++ .../composite/TestdataCompositeSolution.java | 2 +- .../TestdataListEntityProvidingSolution.java | 26 ++ ...aListCompositeEntityProvidingSolution.java | 34 +++ ...ListUnassignedEntityProvidingSolution.java | 26 ++ ...ignedCompositeEntityProvidingSolution.java | 35 +++ .../TestdataEntityProvidingSolution.java | 10 +- ...tdataCompositeEntityProvidingSolution.java | 35 +++ ...lowsUnassignedEntityProvidingSolution.java | 10 +- ...ignedCompositeEntityProvidingSolution.java | 35 +++ .../solver/core/testutil/PlannerAssert.java | 23 ++ 11 files changed, 514 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ValueRangeManagerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/ValueRangeManagerTest.java index 1287dc41eb..423f585aa2 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ValueRangeManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/ValueRangeManagerTest.java @@ -1,15 +1,24 @@ package ai.timefold.solver.core.impl.score.director; +import static ai.timefold.solver.core.testutil.PlannerAssert.assertNonNullCodesOfIterator; +import static ai.timefold.solver.core.testutil.PlannerAssert.assertReversedNonNullCodesOfIterator; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.stream.IntStream; import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; +import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; +import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorFactorySelectionSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.util.MathUtils; import ai.timefold.solver.core.testdomain.TestdataEntity; +import ai.timefold.solver.core.testdomain.TestdataObject; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; import ai.timefold.solver.core.testdomain.chained.TestdataChainedAnchor; @@ -73,6 +82,15 @@ void extractValueFromSolutionUnassignedBasicVariable() { assertThat(otherValueRange.getSize()).isEqualTo(2); } + @Test + void sortValueFromSolutionUnassignedBasicVariable() { + var solution = TestdataAllowsUnassignedSolution.generateSolution(6, 1); + var valueRangeDescriptor = TestdataAllowsUnassignedEntity.buildVariableDescriptorForValue() + .getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromSolutionCompositeUnassignedBasicVariable() { var solution = TestdataAllowsUnassignedCompositeSolution.generateSolution(2, 2); @@ -93,6 +111,16 @@ void extractValueFromSolutionCompositeUnassignedBasicVariable() { assertThat(otherValueRange.getSize()).isEqualTo(4); } + @Test + void sortValueFromSolutionCompositeUnassignedBasicVariable() { + var solution = TestdataAllowsUnassignedCompositeSolution.generateSolution(3, 1); + var valueRangeDescriptor = TestdataAllowsUnassignedCompositeEntity.buildVariableDescriptorForValue() + .getValueRangeDescriptor(); + // 3 values per range + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromSolutionAssignedBasicVariable() { var solution = TestdataSolution.generateSolution(2, 2); @@ -112,6 +140,14 @@ void extractValueFromSolutionAssignedBasicVariable() { assertThat(otherValueRange.getSize()).isEqualTo(2); } + @Test + void sortValueFromSolutionAssignedBasicVariable() { + var solution = TestdataSolution.generateSolution(6, 1); + var valueRangeDescriptor = TestdataEntity.buildVariableDescriptorForValue().getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromSolutionCompositeAssignedBasicVariable() { var solution = TestdataCompositeSolution.generateSolution(2, 2); @@ -134,6 +170,18 @@ void extractValueFromSolutionCompositeAssignedBasicVariable() { assertThat(otherValueRange.getSize()).isEqualTo(4); } + @Test + void sortValueFromSolutionCompositeAssignedBasicVariable() { + var solution = TestdataCompositeSolution.generateSolution(3, 1); + var valueRangeDescriptor = TestdataCompositeSolution.buildSolutionDescriptor() + .findEntityDescriptor(TestdataCompositeEntity.class) + .getGenuineVariableDescriptor("value") + .getValueRangeDescriptor(); + // 3 values per range + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromEntityUnassignedBasicVariable() { var solution = TestdataAllowsUnassignedEntityProvidingSolution.generateSolution(); @@ -164,6 +212,20 @@ void extractValueFromEntityUnassignedBasicVariable() { assertThat(otherEntityValueRange.getSize()).isEqualTo(2); } + @Test + void sortValueFromEntityUnassignedBasicVariable() { + var solution = TestdataAllowsUnassignedEntityProvidingSolution.generateUninitializedSolution(6, 2); + var valueRangeDescriptor = TestdataAllowsUnassignedEntityProvidingEntity.buildVariableDescriptorForValue() + .getValueRangeDescriptor(); + // 3 values per entity + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(0), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(1), valueRangeDescriptor, + List.of("Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromEntityCompositeUnassignedBasicVariable() { var solution = TestdataAllowsUnassignedCompositeEntityProvidingSolution.generateSolution(); @@ -196,6 +258,21 @@ void extractValueFromEntityCompositeUnassignedBasicVariable() { assertThat(otherEntityValueRange.getSize()).isEqualTo(3); } + @Test + void sortValueFromEntityCompositeUnassignedBasicVariable() { + var solution = TestdataAllowsUnassignedCompositeEntityProvidingSolution.generateSolution(6, 2); + var valueRangeDescriptor = TestdataAllowsUnassignedCompositeEntityProvidingEntity.buildVariableDescriptorForValue() + .getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(0), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2", "Generated Value 3", "Generated Value 4", + "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(1), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2", "Generated Value 3", "Generated Value 4", + "Generated Value 5")); + } + @Test void extractValueFromEntityAssignedBasicVariable() { var solution = TestdataEntityProvidingSolution.generateSolution(); @@ -226,6 +303,19 @@ void extractValueFromEntityAssignedBasicVariable() { assertThat(otherEntityValueRange.getSize()).isEqualTo(2); } + @Test + void sortValueFromEntityAssignedBasicVariable() { + var solution = TestdataEntityProvidingSolution.generateUninitializedSolution(6, 2); + var valueRangeDescriptor = TestdataEntityProvidingEntity.buildVariableDescriptorForValue() + .getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(0), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(1), valueRangeDescriptor, + List.of("Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromEntityCompositeAssignedBasicVariable() { var solution = TestdataCompositeEntityProvidingSolution.generateSolution(); @@ -259,6 +349,21 @@ void extractValueFromEntityCompositeAssignedBasicVariable() { assertThat(otherEntityValueRange.getSize()).isEqualTo(3); } + @Test + void sortValueFromEntityCompositeAssignedBasicVariable() { + var solution = TestdataCompositeEntityProvidingSolution.generateSolution(6, 2); + var valueRangeDescriptor = TestdataCompositeEntityProvidingEntity.buildVariableDescriptorForValue() + .getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(0), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2", "Generated Value 3", "Generated Value 4", + "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(1), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2", "Generated Value 3", "Generated Value 4", + "Generated Value 5")); + } + @Test void extractValueFromSolutionUnassignedListVariable() { var solution = TestdataAllowsUnassignedValuesListSolution.generateUninitializedSolution(2, 2); @@ -278,6 +383,15 @@ void extractValueFromSolutionUnassignedListVariable() { assertThat(otherValueRange.getSize()).isEqualTo(2); } + @Test + void sortValueFromSolutionUnassignedListVariable() { + var solution = TestdataAllowsUnassignedValuesListSolution.generateUninitializedSolution(6, 2); + var valueRangeDescriptor = TestdataAllowsUnassignedValuesListEntity.buildVariableDescriptorForValueList() + .getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromSolutionCompositeUnassignedListVariable() { var solution = TestdataAllowsUnassignedCompositeListSolution.generateSolution(2, 2); @@ -298,6 +412,16 @@ void extractValueFromSolutionCompositeUnassignedListVariable() { assertThat(otherValueRange.getSize()).isEqualTo(4); } + @Test + void sortValueFromSolutionCompositeUnassignedListVariable() { + var solution = TestdataAllowsUnassignedCompositeListSolution.generateSolution(3, 2); + var valueRangeDescriptor = TestdataAllowsUnassignedCompositeListEntity.buildVariableDescriptorForValueList() + .getValueRangeDescriptor(); + // 3 values per range + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromSolutionAssignedListVariable() { var solution = TestdataListSolution.generateUninitializedSolution(2, 2); @@ -319,6 +443,17 @@ void extractValueFromSolutionAssignedListVariable() { assertThat(otherValueRange.getSize()).isEqualTo(2); } + @Test + void sortValueFromSolutionAssignedListVariable() { + var solution = TestdataListSolution.generateUninitializedSolution(6, 2); + var valueRangeDescriptor = TestdataListSolution.buildSolutionDescriptor() + .findEntityDescriptor(TestdataListEntity.class) + .getGenuineVariableDescriptor("valueList") + .getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromSolutionCompositeAssignedListVariable() { var solution = TestdataListCompositeSolution.generateSolution(2, 2); @@ -339,6 +474,16 @@ void extractValueFromSolutionCompositeAssignedListVariable() { assertThat(otherValueRange.getSize()).isEqualTo(4); } + @Test + void sortValueFromSolutionCompositeAssignedListVariable() { + var solution = TestdataListCompositeSolution.generateSolution(3, 2); + var valueRangeDescriptor = TestdataListCompositeEntity.buildVariableDescriptorForValueList() + .getValueRangeDescriptor(); + // 3 values per range + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromEntityUnassignedListVariable() { var solution = TestdataListUnassignedEntityProvidingSolution.generateSolution(); @@ -369,6 +514,19 @@ void extractValueFromEntityUnassignedListVariable() { assertThat(otherEntityValueRange.getSize()).isEqualTo(2); } + @Test + void sortValueFromEntityUnassignedListVariable() { + var solution = TestdataListUnassignedEntityProvidingSolution.generateSolution(6, 2); + var valueRangeDescriptor = TestdataListUnassignedEntityProvidingEntity.buildVariableDescriptorForValueList() + .getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(0), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(1), valueRangeDescriptor, + List.of("Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromEntityCompositeUnassignedListVariable() { var solution = TestdataListUnassignedCompositeEntityProvidingSolution.generateSolution(); @@ -402,6 +560,21 @@ void extractValueFromEntityCompositeUnassignedListVariable() { assertThat(otherEntityValueRange.getSize()).isEqualTo(3); } + @Test + void sortValueFromEntityCompositeUnassignedListVariable() { + var solution = TestdataListUnassignedCompositeEntityProvidingSolution.generateSolution(6, 2); + var valueRangeDescriptor = TestdataListUnassignedCompositeEntityProvidingEntity.buildVariableDescriptorForValueList() + .getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(0), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2", "Generated Value 3", "Generated Value 4", + "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(1), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2", "Generated Value 3", "Generated Value 4", + "Generated Value 5")); + } + @Test void extractValueFromEntityAssignedListVariable() { var solution = TestdataListEntityProvidingSolution.generateSolution(); @@ -432,6 +605,19 @@ void extractValueFromEntityAssignedListVariable() { assertThat(otherEntityValueRange.getSize()).isEqualTo(2); } + @Test + void sortValueFromEntityAssignedListVariable() { + var solution = TestdataListEntityProvidingSolution.generateSolution(6, 2); + var valueRangeDescriptor = TestdataListEntityProvidingEntity.buildVariableDescriptorForValueList() + .getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(0), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(1), valueRangeDescriptor, + List.of("Generated Value 3", "Generated Value 4", "Generated Value 5")); + } + @Test void extractValueFromEntityCompositeAssignedListVariable() { var solution = TestdataListCompositeEntityProvidingSolution.generateSolution(); @@ -465,6 +651,21 @@ void extractValueFromEntityCompositeAssignedListVariable() { assertThat(otherEntityValueRange.getSize()).isEqualTo(3); } + @Test + void sortValueFromEntityCompositeAssignedListVariable() { + var solution = TestdataListCompositeEntityProvidingSolution.generateSolution(6, 2); + var valueRangeDescriptor = TestdataListCompositeEntityProvidingEntity.buildVariableDescriptorForValueList() + .getValueRangeDescriptor(); + assertSolutionValueRangeSortingOrder(solution, valueRangeDescriptor, List.of("Generated Value 0", "Generated Value 1", + "Generated Value 2", "Generated Value 3", "Generated Value 4", "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(0), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2", "Generated Value 3", "Generated Value 4", + "Generated Value 5")); + assertEntityValueRangeSortingOrder(solution, solution.getEntityList().get(1), valueRangeDescriptor, + List.of("Generated Value 0", "Generated Value 1", "Generated Value 2", "Generated Value 3", "Generated Value 4", + "Generated Value 5")); + } + @Test void countEntities() { var valueCount = 10; @@ -783,4 +984,84 @@ void assertProblemScaleListIsApproximatelyProblemScaleChained() { .isCloseTo(Math.pow(10, chainedPowerExponent), Percentage.withPercentage(1)); } + private void assertSolutionValueRangeSortingOrder(S solution, ValueRangeDescriptor valueRangeDescriptor, + List allValues) { + var solutionDescriptor = valueRangeDescriptor.getVariableDescriptor().getEntityDescriptor().getSolutionDescriptor(); + var valueRangeManager = ValueRangeManager.of(solutionDescriptor, solution); + + // Default order + var valueRange = (CountableValueRange) valueRangeManager.getFromSolution(valueRangeDescriptor, solution); + assertNonNullCodesOfIterator(valueRange.createOriginalIterator(), allValues.toArray(String[]::new)); + + // Desc comparator + SelectionSorter sorterComparator = + new ComparatorSelectionSorter<>(Comparator.comparing(TestdataObject::getCode), SelectionSorterOrder.DESCENDING); + var sortedValueRange = + (CountableValueRange) valueRangeManager.getFromSolution(valueRangeDescriptor, solution, sorterComparator); + assertReversedNonNullCodesOfIterator(sortedValueRange.createOriginalIterator(), allValues.toArray(String[]::new)); + assertThat(valueRange).isNotSameAs(sortedValueRange); + + // Asc comparator + // Default order is still desc + var otherValueRange = (CountableValueRange) valueRangeManager.getFromSolution(valueRangeDescriptor, solution); + assertReversedNonNullCodesOfIterator(otherValueRange.createOriginalIterator(), allValues.toArray(String[]::new)); + assertThat(otherValueRange).isSameAs(sortedValueRange); + + // Update it to asc order + SelectionSorter sorterComparatorFactory = + new ComparatorFactorySelectionSorter<>(sol -> Comparator.comparing(TestdataObject::getCode), + SelectionSorterOrder.ASCENDING); + var otherSortedValueRange = + (CountableValueRange) valueRangeManager.getFromSolution(valueRangeDescriptor, solution, + sorterComparatorFactory); + assertNonNullCodesOfIterator(otherSortedValueRange.createOriginalIterator(), allValues.toArray(String[]::new)); + assertThat(otherSortedValueRange).isNotSameAs(otherValueRange); + + // Using the same sorter + var anotherSortedValueRange = + (CountableValueRange) valueRangeManager.getFromSolution(valueRangeDescriptor, solution, + sorterComparatorFactory); + assertThat(otherSortedValueRange).isSameAs(anotherSortedValueRange); + } + + private void assertEntityValueRangeSortingOrder(S solution, E entity, ValueRangeDescriptor valueRangeDescriptor, + List allValues) { + var solutionDescriptor = valueRangeDescriptor.getVariableDescriptor().getEntityDescriptor().getSolutionDescriptor(); + var valueRangeManager = ValueRangeManager.of(solutionDescriptor, solution); + + // Default order + var valueRange = (CountableValueRange) valueRangeManager.getFromEntity(valueRangeDescriptor, entity); + assertNonNullCodesOfIterator(valueRange.createOriginalIterator(), allValues.toArray(String[]::new)); + + // Desc comparator + SelectionSorter sorterComparator = + new ComparatorSelectionSorter<>(Comparator.comparing(TestdataObject::getCode), SelectionSorterOrder.DESCENDING); + var sortedValueRange = + (CountableValueRange) valueRangeManager.getFromEntity(valueRangeDescriptor, entity, sorterComparator); + assertReversedNonNullCodesOfIterator(sortedValueRange.createOriginalIterator(), allValues.toArray(String[]::new)); + assertThat(valueRange).isNotSameAs(sortedValueRange); + + // Asc comparator + // Default order is still desc + var otherValueRange = (CountableValueRange) valueRangeManager.getFromEntity(valueRangeDescriptor, entity); + assertReversedNonNullCodesOfIterator(otherValueRange.createOriginalIterator(), allValues.toArray(String[]::new)); + assertThat(otherValueRange).isSameAs(sortedValueRange); + + // Update it to asc order + SelectionSorter sorterComparatorFactory = + new ComparatorFactorySelectionSorter<>(sol -> Comparator.comparing(TestdataObject::getCode), + SelectionSorterOrder.ASCENDING); + var otherSortedValueRange = + (CountableValueRange) valueRangeManager.getFromEntity(valueRangeDescriptor, entity, + sorterComparatorFactory); + assertNonNullCodesOfIterator(otherSortedValueRange.createOriginalIterator(), allValues.toArray(String[]::new)); + assertThat(otherSortedValueRange).isNotSameAs(otherValueRange); + + // Using the same sorter + var anotherSortedValueRange = + (CountableValueRange) valueRangeManager.getFromEntity(valueRangeDescriptor, entity, + sorterComparatorFactory); + assertThat(otherSortedValueRange).isSameAs(anotherSortedValueRange); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/composite/TestdataCompositeSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/composite/TestdataCompositeSolution.java index e550e7a5ed..a2f99be25c 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/composite/TestdataCompositeSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/composite/TestdataCompositeSolution.java @@ -29,7 +29,7 @@ public static TestdataCompositeSolution generateSolution(int valueListSize, int } List otherValueList = new ArrayList<>(valueListSize); for (int i = 0; i < valueListSize; i++) { - TestdataValue value = new TestdataValue("Generated Value " + (valueListSize + i - 1)); + TestdataValue value = new TestdataValue("Generated Value " + (valueListSize + i)); otherValueList.add(value); } solution.setValueList(valueList); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java index 632a8732b8..c988bd6d25 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.testdomain.list.valuerange; +import java.util.ArrayList; import java.util.List; import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; @@ -16,6 +17,31 @@ public static SolutionDescriptor buildSolut TestdataListEntityProvidingEntity.class, TestdataListEntityProvidingValue.class); } + public static TestdataListEntityProvidingSolution generateSolution(int valueListSize, int entityListSize) { + var solution = new TestdataListEntityProvidingSolution(); + var valueList = new ArrayList(valueListSize); + for (var i = 0; i < valueListSize; i++) { + var value = new TestdataListEntityProvidingValue("Generated Value " + i); + valueList.add(value); + } + var entityList = new ArrayList(entityListSize); + var idx = 0; + for (var i = 0; i < entityListSize; i++) { + var expectedCount = Math.max(1, valueListSize / entityListSize); + var valueRange = new ArrayList(); + for (var j = 0; j < expectedCount; j++) { + if (idx >= valueListSize) { + break; + } + valueRange.add(valueList.get(idx++)); + } + var entity = new TestdataListEntityProvidingEntity("Generated Entity " + i, valueRange); + entityList.add(entity); + } + solution.setEntityList(entityList); + return solution; + } + public static TestdataListEntityProvidingSolution generateSolution() { var solution = new TestdataListEntityProvidingSolution(); var value1 = new TestdataListEntityProvidingValue("v1"); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/composite/TestdataListCompositeEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/composite/TestdataListCompositeEntityProvidingSolution.java index 3cc7a2c53a..e520402874 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/composite/TestdataListCompositeEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/composite/TestdataListCompositeEntityProvidingSolution.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.testdomain.list.valuerange.composite; +import java.util.ArrayList; import java.util.List; import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; @@ -17,6 +18,39 @@ public static SolutionDescriptor b TestdataListCompositeEntityProvidingEntity.class); } + public static TestdataListCompositeEntityProvidingSolution generateSolution(int valueListSize, int entityListSize) { + var solution = new TestdataListCompositeEntityProvidingSolution(); + var valueList = new ArrayList(valueListSize); + for (var i = 0; i < valueListSize; i++) { + var value = new TestdataListEntityProvidingValue("Generated Value " + i); + valueList.add(value); + } + var entityList = new ArrayList(entityListSize); + for (var i = 0; i < entityListSize; i++) { + var idx = 0; + var expectedCount = Math.max(1, valueListSize / 2); + var valueRange = new ArrayList(); + var secondValueRange = new ArrayList(); + for (var j = 0; j < expectedCount; j++) { + if (idx >= valueListSize) { + break; + } + valueRange.add(valueList.get(idx++)); + } + for (var j = 0; j < expectedCount; j++) { + if (idx >= valueListSize) { + break; + } + secondValueRange.add(valueList.get(idx++)); + } + var entity = new TestdataListCompositeEntityProvidingEntity("Generated Entity " + i, valueRange, + secondValueRange); + entityList.add(entity); + } + solution.setEntityList(entityList); + return solution; + } + public static TestdataListCompositeEntityProvidingSolution generateSolution() { var solution = new TestdataListCompositeEntityProvidingSolution(); var value1 = new TestdataListEntityProvidingValue("v1"); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/TestdataListUnassignedEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/TestdataListUnassignedEntityProvidingSolution.java index 9c09728f8d..98519bcef7 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/TestdataListUnassignedEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/TestdataListUnassignedEntityProvidingSolution.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.testdomain.list.valuerange.unassignedvar; +import java.util.ArrayList; import java.util.List; import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; @@ -18,6 +19,31 @@ public static SolutionDescriptor TestdataListUnassignedEntityProvidingEntity.class); } + public static TestdataListUnassignedEntityProvidingSolution generateSolution(int valueListSize, int entityListSize) { + var solution = new TestdataListUnassignedEntityProvidingSolution(); + var valueList = new ArrayList(valueListSize); + for (var i = 0; i < valueListSize; i++) { + var value = new TestdataValue("Generated Value " + i); + valueList.add(value); + } + var entityList = new ArrayList(entityListSize); + var idx = 0; + for (var i = 0; i < entityListSize; i++) { + var expectedCount = Math.max(1, valueListSize / entityListSize); + var valueRange = new ArrayList(); + for (var j = 0; j < expectedCount; j++) { + if (idx >= valueListSize) { + break; + } + valueRange.add(valueList.get(idx++)); + } + var entity = new TestdataListUnassignedEntityProvidingEntity("Generated Entity " + i, valueRange); + entityList.add(entity); + } + solution.setEntityList(entityList); + return solution; + } + public static TestdataListUnassignedEntityProvidingSolution generateSolution() { var solution = new TestdataListUnassignedEntityProvidingSolution(); var value1 = new TestdataValue("v1"); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/composite/TestdataListUnassignedCompositeEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/composite/TestdataListUnassignedCompositeEntityProvidingSolution.java index 53b6781201..9349fc9ea5 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/composite/TestdataListUnassignedCompositeEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/composite/TestdataListUnassignedCompositeEntityProvidingSolution.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.testdomain.list.valuerange.unassignedvar.composite; +import java.util.ArrayList; import java.util.List; import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; @@ -17,6 +18,40 @@ public static SolutionDescriptor(valueListSize); + for (var i = 0; i < valueListSize; i++) { + var value = new TestdataListEntityProvidingValue("Generated Value " + i); + valueList.add(value); + } + var entityList = new ArrayList(entityListSize); + for (var i = 0; i < entityListSize; i++) { + var idx = 0; + var expectedCount = Math.max(1, valueListSize / 2); + var valueRange = new ArrayList(); + var secondValueRange = new ArrayList(); + for (var j = 0; j < expectedCount; j++) { + if (idx >= valueListSize) { + break; + } + valueRange.add(valueList.get(idx++)); + } + for (var j = 0; j < expectedCount; j++) { + if (idx >= valueListSize) { + break; + } + secondValueRange.add(valueList.get(idx++)); + } + var entity = new TestdataListUnassignedCompositeEntityProvidingEntity("Generated Entity " + i, valueRange, + secondValueRange); + entityList.add(entity); + } + solution.setEntityList(entityList); + return solution; + } + public static TestdataListUnassignedCompositeEntityProvidingSolution generateSolution() { var solution = new TestdataListUnassignedCompositeEntityProvidingSolution(); var value1 = new TestdataListEntityProvidingValue("v1"); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/TestdataEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/TestdataEntityProvidingSolution.java index 2b899d5665..9611f4d9a1 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/TestdataEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/TestdataEntityProvidingSolution.java @@ -51,11 +51,19 @@ private static TestdataEntityProvidingSolution generateSolution(int valueListSiz valueList.add(value); } var entityList = new ArrayList(entityListSize); + var idx = 0; for (var i = 0; i < entityListSize; i++) { var expectedCount = Math.max(1, valueListSize / entityListSize); var valueRange = new ArrayList(); for (var j = 0; j < expectedCount; j++) { - valueRange.add(valueList.get((i * j) % valueListSize)); + if (initialized) { + valueRange.add(valueList.get((i * j) % valueListSize)); + } else { + if (idx >= valueListSize) { + break; + } + valueRange.add(valueList.get(idx++)); + } } var entity = new TestdataEntityProvidingEntity("Generated Entity " + i, valueRange); entity.setValue(initialized ? valueList.get(i % valueListSize) : null); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/composite/TestdataCompositeEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/composite/TestdataCompositeEntityProvidingSolution.java index 03213eb00a..7f87ecaffa 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/composite/TestdataCompositeEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/composite/TestdataCompositeEntityProvidingSolution.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.testdomain.valuerange.entityproviding.composite; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -22,6 +23,40 @@ public static SolutionDescriptor build TestdataCompositeEntityProvidingEntity.class); } + public static TestdataCompositeEntityProvidingSolution generateSolution(int valueListSize, + int entityListSize) { + var solution = new TestdataCompositeEntityProvidingSolution("Generated Solution 0"); + var valueList = new ArrayList(valueListSize); + for (var i = 0; i < valueListSize; i++) { + var value = new TestdataValue("Generated Value " + i); + valueList.add(value); + } + var entityList = new ArrayList(entityListSize); + for (var i = 0; i < entityListSize; i++) { + var idx = 0; + var expectedCount = Math.max(1, valueListSize / 2); + var valueRange = new ArrayList(); + var secondValueRange = new ArrayList(); + for (var j = 0; j < expectedCount; j++) { + if (idx >= valueListSize) { + break; + } + valueRange.add(valueList.get(idx++)); + } + for (var j = 0; j < expectedCount; j++) { + if (idx >= valueListSize) { + break; + } + secondValueRange.add(valueList.get(idx++)); + } + var entity = new TestdataCompositeEntityProvidingEntity("Generated Entity " + i, valueRange, + secondValueRange); + entityList.add(entity); + } + solution.setEntityList(entityList); + return solution; + } + public static TestdataCompositeEntityProvidingSolution generateSolution() { var solution = new TestdataCompositeEntityProvidingSolution("s1"); var value1 = new TestdataValue("v1"); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/TestdataAllowsUnassignedEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/TestdataAllowsUnassignedEntityProvidingSolution.java index 0ea66b58bc..4bdca6e6d9 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/TestdataAllowsUnassignedEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/TestdataAllowsUnassignedEntityProvidingSolution.java @@ -52,11 +52,19 @@ private static TestdataAllowsUnassignedEntityProvidingSolution generateSolution( valueList.add(value); } var entityList = new ArrayList(entityListSize); + var idx = 0; for (var i = 0; i < entityListSize; i++) { var expectedCount = Math.max(1, valueListSize / entityListSize); var valueRange = new ArrayList(); for (var j = 0; j < expectedCount; j++) { - valueRange.add(valueList.get((i * j) % valueListSize)); + if (initialized) { + valueRange.add(valueList.get((i * j) % valueListSize)); + } else { + if (idx >= valueListSize) { + break; + } + valueRange.add(valueList.get(idx++)); + } } var entity = new TestdataAllowsUnassignedEntityProvidingEntity("Generated Entity " + i, valueRange); entity.setValue(initialized ? valueList.get(i % valueListSize) : null); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/composite/TestdataAllowsUnassignedCompositeEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/composite/TestdataAllowsUnassignedCompositeEntityProvidingSolution.java index 4895e0fa85..c8ffc0c762 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/composite/TestdataAllowsUnassignedCompositeEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/composite/TestdataAllowsUnassignedCompositeEntityProvidingSolution.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.composite; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -22,6 +23,40 @@ public static SolutionDescriptor(valueListSize); + for (var i = 0; i < valueListSize; i++) { + var value = new TestdataValue("Generated Value " + i); + valueList.add(value); + } + var entityList = new ArrayList(entityListSize); + for (var i = 0; i < entityListSize; i++) { + var idx = 0; + var expectedCount = Math.max(1, valueListSize / 2); + var valueRange = new ArrayList(); + var secondValueRange = new ArrayList(); + for (var j = 0; j < expectedCount; j++) { + if (idx >= valueListSize) { + break; + } + valueRange.add(valueList.get(idx++)); + } + for (var j = 0; j < expectedCount; j++) { + if (idx >= valueListSize) { + break; + } + secondValueRange.add(valueList.get(idx++)); + } + var entity = new TestdataAllowsUnassignedCompositeEntityProvidingEntity("Generated Entity " + i, valueRange, + secondValueRange); + entityList.add(entity); + } + solution.setEntityList(entityList); + return solution; + } + public static TestdataAllowsUnassignedCompositeEntityProvidingSolution generateSolution() { var solution = new TestdataAllowsUnassignedCompositeEntityProvidingSolution("s1"); var value1 = new TestdataValue("v1"); diff --git a/core/src/test/java/ai/timefold/solver/core/testutil/PlannerAssert.java b/core/src/test/java/ai/timefold/solver/core/testutil/PlannerAssert.java index 51e7e1d736..6340a47eb3 100644 --- a/core/src/test/java/ai/timefold/solver/core/testutil/PlannerAssert.java +++ b/core/src/test/java/ai/timefold/solver/core/testutil/PlannerAssert.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.ListIterator; import java.util.NoSuchElementException; +import java.util.Objects; import ai.timefold.solver.core.impl.constructionheuristic.event.ConstructionHeuristicPhaseLifecycleListener; import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicPhaseScope; @@ -234,6 +235,28 @@ public static void assertCodesOfIterator(Iterator iterator, String... cod .containsExactly(codes); } + public static void assertNonNullCodesOfIterator(Iterator iterator, String... codes) { + assertThat(iterator).isNotNull(); + assertThat(iterator) + .toIterable() + .filteredOn(Objects::nonNull) + .map(PlannerAssert::codeIfNotNull) + .containsExactly(codes); + } + + public static void assertReversedNonNullCodesOfIterator(Iterator listIterator, String... codes) { + assertThat(listIterator).isNotNull(); + var i = codes.length - 1; + while (i >= 0) { + var next = listIterator.next(); + if (next == null) { + continue; + } + assertCode(codes[i], next); + i--; + } + } + public static void assertAllCodesOfIterator(Iterator iterator, String... codes) { assertCodesOfIterator(iterator, codes); assertThat(iterator).isExhausted(); From f212a67216de06f2323ec8435e7c3d11a2cd1f2c Mon Sep 17 00:00:00 2001 From: fred Date: Mon, 3 Nov 2025 15:40:57 -0300 Subject: [PATCH 06/13] feat: support value range sorting in `ValueSelectorFactory` --- .../FromEntityPropertyValueSelector.java | 25 +++-- ...ableFromSolutionPropertyValueSelector.java | 22 ++--- .../selector/value/ValueSelectorFactory.java | 98 +++++++++++-------- ...erableFromEntityPropertyValueSelector.java | 17 +++- .../score/director/ValueRangeManager.java | 10 ++ .../testdomain/list/TestdataListUtils.java | 2 +- 6 files changed, 112 insertions(+), 62 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java index 0335433f64..4c0d7eba01 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -22,13 +23,16 @@ public final class FromEntityPropertyValueSelector implements ValueSelector { private final ValueRangeDescriptor valueRangeDescriptor; + private final SelectionSorter selectionSorter; private final boolean randomSelection; private CountableValueRange countableValueRange; private InnerScoreDirector scoreDirector; - public FromEntityPropertyValueSelector(ValueRangeDescriptor valueRangeDescriptor, boolean randomSelection) { + public FromEntityPropertyValueSelector(ValueRangeDescriptor valueRangeDescriptor, + SelectionSorter selectionSorter, boolean randomSelection) { this.valueRangeDescriptor = valueRangeDescriptor; + this.selectionSorter = selectionSorter; this.randomSelection = randomSelection; } @@ -51,7 +55,7 @@ public void solvingEnded(SolverScope solverScope) { @Override public void phaseStarted(AbstractPhaseScope phaseScope) { super.phaseStarted(phaseScope); - this.countableValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor); + this.countableValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor, selectionSorter); } @Override @@ -63,6 +67,9 @@ public void phaseEnded(AbstractPhaseScope phaseScope) { // ************************************************************************ // Worker methods // ************************************************************************ + public SelectionSorter getSelectionSorter() { + return selectionSorter; + } @Override public GenuineVariableDescriptor getVariableDescriptor() { @@ -92,7 +99,7 @@ public long getSize(Object entity) { @Override public Iterator iterator(Object entity) { - var valueRange = scoreDirector.getValueRangeManager().getFromEntity(valueRangeDescriptor, entity); + var valueRange = scoreDirector.getValueRangeManager().getFromEntity(valueRangeDescriptor, entity, selectionSorter); if (!randomSelection) { return valueRange.createOriginalIterator(); } else { @@ -107,24 +114,22 @@ public Iterator endingIterator(Object entity) { // This logic aligns with the requirements for Nearby in the enterprise repository return countableValueRange.createOriginalIterator(); } else { - var valueRange = scoreDirector.getValueRangeManager().getFromEntity(valueRangeDescriptor, entity); + var valueRange = scoreDirector.getValueRangeManager().getFromEntity(valueRangeDescriptor, entity, selectionSorter); return valueRange.createOriginalIterator(); } } @Override public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) + if (!(o instanceof FromEntityPropertyValueSelector that)) return false; - FromEntityPropertyValueSelector that = (FromEntityPropertyValueSelector) o; - return randomSelection == that.randomSelection && Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor); + return randomSelection == that.randomSelection && Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor) + && Objects.equals(selectionSorter, that.selectionSorter); } @Override public int hashCode() { - return Objects.hash(valueRangeDescriptor, randomSelection); + return Objects.hash(valueRangeDescriptor, selectionSorter, randomSelection); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java index 810c7af3ee..7025a268bf 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; @@ -21,6 +22,7 @@ public final class IterableFromSolutionPropertyValueSelector implements IterableValueSelector { private final ValueRangeDescriptor valueRangeDescriptor; + private final SelectionSorter sorter; private final SelectionCacheType minimumCacheType; private final boolean randomSelection; private final boolean valueRangeMightContainEntity; @@ -30,8 +32,9 @@ public final class IterableFromSolutionPropertyValueSelector private boolean cachedEntityListIsDirty = false; public IterableFromSolutionPropertyValueSelector(ValueRangeDescriptor valueRangeDescriptor, - SelectionCacheType minimumCacheType, boolean randomSelection) { + SelectionSorter sorter, SelectionCacheType minimumCacheType, boolean randomSelection) { this.valueRangeDescriptor = valueRangeDescriptor; + this.sorter = sorter; this.minimumCacheType = minimumCacheType; this.randomSelection = randomSelection; valueRangeMightContainEntity = valueRangeDescriptor.mightContainEntity(); @@ -57,7 +60,7 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { super.phaseStarted(phaseScope); var scoreDirector = phaseScope.getScoreDirector(); cachedValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor, - scoreDirector.getWorkingSolution()); + scoreDirector.getWorkingSolution(), sorter); if (valueRangeMightContainEntity) { cachedEntityListRevision = scoreDirector.getWorkingEntityListRevision(); cachedEntityListIsDirty = false; @@ -74,7 +77,7 @@ public void stepStarted(AbstractStepScope stepScope) { cachedEntityListIsDirty = true; } else { cachedValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor, - scoreDirector.getWorkingSolution()); + scoreDirector.getWorkingSolution(), sorter); cachedEntityListRevision = scoreDirector.getWorkingEntityListRevision(); } } @@ -153,19 +156,16 @@ private void checkCachedEntityListIsDirty() { } @Override - public boolean equals(Object other) { - if (this == other) - return true; - if (other == null || getClass() != other.getClass()) + public boolean equals(Object o) { + if (!(o instanceof IterableFromSolutionPropertyValueSelector that)) return false; - var that = (IterableFromSolutionPropertyValueSelector) other; - return randomSelection == that.randomSelection && - Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor) && minimumCacheType == that.minimumCacheType; + return randomSelection == that.randomSelection && Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor) + && Objects.equals(sorter, that.sorter) && minimumCacheType == that.minimumCacheType; } @Override public int hashCode() { - return Objects.hash(valueRangeDescriptor, minimumCacheType, randomSelection); + return Objects.hash(valueRangeDescriptor, sorter, minimumCacheType, randomSelection); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java index 73aab920fa..9b97f8f34f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java @@ -122,10 +122,19 @@ public ValueSelector buildValueSelector(HeuristicConfigPolicy buildValueSelector(HeuristicConfigPolicy determineComparatorFactoryClas } } + private SelectionSorter determineSorter(GenuineVariableDescriptor variableDescriptor, + SelectionOrder resolvedSelectionOrder, ClassInstanceCache instanceCache) { + SelectionSorter sorter = null; + if (resolvedSelectionOrder == ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder.SORTED) { + var sorterManner = config.getSorterManner(); + var comparatorClass = determineComparatorClass(config); + var comparatorFactoryClass = determineComparatorFactoryClass(config); + if (sorterManner != null) { + if (!ValueSelectorConfig.hasSorter(sorterManner, variableDescriptor)) { + return null; + } + sorter = ValueSelectorConfig.determineSorter(sorterManner, variableDescriptor); + } else if (comparatorClass != null) { + Comparator sorterComparator = + instanceCache.newInstance(config, determineComparatorPropertyName(config), comparatorClass); + sorter = new ComparatorSelectionSorter<>(sorterComparator, + SelectionSorterOrder.resolve(config.getSorterOrder())); + } else if (comparatorFactoryClass != null) { + var comparatorFactory = instanceCache.newInstance(config, determineComparatorFactoryPropertyName(config), + comparatorFactoryClass); + sorter = new ComparatorFactorySelectionSorter<>(comparatorFactory, + SelectionSorterOrder.resolve(config.getSorterOrder())); + } else if (config.getSorterClass() != null) { + sorter = instanceCache.newInstance(config, "sorterClass", config.getSorterClass()); + } else { + throw new IllegalArgumentException(""" + The valueSelectorConfig (%s) with resolvedSelectionOrder (%s) needs \ + a sorterManner (%s) or a %s (%s) or a %s (%s) \ + or a sorterClass (%s).""" + .formatted(config, resolvedSelectionOrder, sorterManner, determineComparatorPropertyName(config), + comparatorClass, determineComparatorFactoryPropertyName(config), comparatorFactoryClass, + config.getSorterClass())); + } + } + return sorter; + } + private ValueSelector buildBaseValueSelector(GenuineVariableDescriptor variableDescriptor, - SelectionCacheType minimumCacheType, boolean randomSelection) { + SelectionSorter sorter, SelectionCacheType minimumCacheType, boolean randomSelection) { var valueRangeDescriptor = variableDescriptor.getValueRangeDescriptor(); // TODO minimumCacheType SOLVER is only a problem if the valueRange includes entities or custom weird cloning if (minimumCacheType == SelectionCacheType.SOLVER) { @@ -271,11 +319,13 @@ private ValueSelector buildBaseValueSelector(GenuineVariableDescripto + ") is not yet supported. Please use " + SelectionCacheType.PHASE + " instead."); } if (valueRangeDescriptor.canExtractValueRangeFromSolution()) { - return new IterableFromSolutionPropertyValueSelector<>(valueRangeDescriptor, minimumCacheType, randomSelection); + return new IterableFromSolutionPropertyValueSelector<>(valueRangeDescriptor, sorter, minimumCacheType, + randomSelection); } else { // TODO Do not allow PHASE cache on FromEntityPropertyValueSelector, except if the moveSelector is PHASE cached too. - var fromEntityPropertySelector = new FromEntityPropertyValueSelector<>(valueRangeDescriptor, randomSelection); - return new IterableFromEntityPropertyValueSelector<>(fromEntityPropertySelector, randomSelection); + var fromEntityPropertySelector = + new FromEntityPropertyValueSelector<>(valueRangeDescriptor, sorter, randomSelection); + return new IterableFromEntityPropertyValueSelector<>(fromEntityPropertySelector, minimumCacheType, randomSelection); } } @@ -372,37 +422,7 @@ private static void assertNotSorterClassAnd(ValueSelectorConfig config, String p protected ValueSelector applySorting(SelectionCacheType resolvedCacheType, SelectionOrder resolvedSelectionOrder, ValueSelector valueSelector, ClassInstanceCache instanceCache) { if (resolvedSelectionOrder == SelectionOrder.SORTED) { - SelectionSorter sorter; - var sorterManner = config.getSorterManner(); - var comparatorClass = determineComparatorClass(config); - var comparatorFactoryClass = determineComparatorFactoryClass(config); - if (sorterManner != null) { - var variableDescriptor = valueSelector.getVariableDescriptor(); - if (!ValueSelectorConfig.hasSorter(sorterManner, variableDescriptor)) { - return valueSelector; - } - sorter = ValueSelectorConfig.determineSorter(sorterManner, variableDescriptor); - } else if (comparatorClass != null) { - Comparator sorterComparator = - instanceCache.newInstance(config, determineComparatorPropertyName(config), comparatorClass); - sorter = new ComparatorSelectionSorter<>(sorterComparator, - SelectionSorterOrder.resolve(config.getSorterOrder())); - } else if (comparatorFactoryClass != null) { - var comparatorFactory = instanceCache.newInstance(config, determineComparatorFactoryPropertyName(config), - comparatorFactoryClass); - sorter = new ComparatorFactorySelectionSorter<>(comparatorFactory, - SelectionSorterOrder.resolve(config.getSorterOrder())); - } else if (config.getSorterClass() != null) { - sorter = instanceCache.newInstance(config, "sorterClass", config.getSorterClass()); - } else { - throw new IllegalArgumentException(""" - The valueSelectorConfig (%s) with resolvedSelectionOrder (%s) needs \ - a sorterManner (%s) or a %s (%s) or a %s (%s) \ - or a sorterClass (%s).""" - .formatted(config, resolvedSelectionOrder, sorterManner, determineComparatorPropertyName(config), - comparatorClass, determineComparatorFactoryPropertyName(config), comparatorFactoryClass, - config.getSorterClass())); - } + var sorter = determineSorter(valueSelector.getVariableDescriptor(), resolvedSelectionOrder, instanceCache); if (!valueSelector.getVariableDescriptor().canExtractValueRangeFromSolution() && resolvedCacheType == SelectionCacheType.STEP) { valueSelector = new FromEntitySortingValueSelector<>(valueSelector, resolvedCacheType, sorter); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java index 4ed4ef375d..efcb67758e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java @@ -2,6 +2,7 @@ import java.util.Iterator; +import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.FromEntityPropertyValueRangeDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector; @@ -28,13 +29,20 @@ public final class IterableFromEntityPropertyValueSelector extends Ab implements IterableValueSelector { private final FromEntityPropertyValueSelector childValueSelector; + private final SelectionCacheType minimumCacheType; private final boolean randomSelection; private final FromEntityPropertyValueRangeDescriptor valueRangeDescriptor; private InnerScoreDirector innerScoreDirector = null; public IterableFromEntityPropertyValueSelector(FromEntityPropertyValueSelector childValueSelector, boolean randomSelection) { + this(childValueSelector, SelectionCacheType.JUST_IN_TIME, randomSelection); + } + + public IterableFromEntityPropertyValueSelector(FromEntityPropertyValueSelector childValueSelector, + SelectionCacheType minimumCacheType, boolean randomSelection) { this.childValueSelector = childValueSelector; + this.minimumCacheType = minimumCacheType; this.randomSelection = randomSelection; this.valueRangeDescriptor = (FromEntityPropertyValueRangeDescriptor) childValueSelector .getVariableDescriptor().getValueRangeDescriptor(); @@ -73,6 +81,12 @@ public void phaseEnded(AbstractPhaseScope phaseScope) { // ************************************************************************ // Worker methods // ************************************************************************ + + @Override + public SelectionCacheType getCacheType() { + return minimumCacheType; + } + @Override public GenuineVariableDescriptor getVariableDescriptor() { return childValueSelector.getVariableDescriptor(); @@ -112,7 +126,8 @@ public long getSize() { @Override public Iterator iterator() { var valueRange = innerScoreDirector.getValueRangeManager() - .getFromSolution(valueRangeDescriptor, innerScoreDirector.getWorkingSolution()); + .getFromSolution(valueRangeDescriptor, innerScoreDirector.getWorkingSolution(), + childValueSelector.getSelectionSorter()); if (randomSelection) { return valueRange.createRandomIterator(workingRandom); } else { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java index 4c68dd1d09..e77588762d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java @@ -357,6 +357,16 @@ public CountableValueRange getFromSolution(ValueRangeDescriptor CountableValueRange getFromSolution(ValueRangeDescriptor valueRangeDescriptor, + @Nullable SelectionSorter sorter) { + if (cachedWorkingSolution == null) { + throw new IllegalStateException( + "Impossible state: value range (%s) requested before the working solution is known." + .formatted(valueRangeDescriptor)); + } + return getFromSolution(valueRangeDescriptor, cachedWorkingSolution, sorter); + } + @SuppressWarnings("unchecked") public CountableValueRange getFromSolution(ValueRangeDescriptor valueRangeDescriptor, Solution_ solution) { diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java index f1603b1660..99df7e80ef 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java @@ -314,7 +314,7 @@ public static ListVariableDescriptor getPin public static IterableFromEntityPropertyValueSelector getIterableFromEntityPropertyValueSelector(ValueRangeDescriptor valueRangeDescriptor, boolean randomSelection) { - var fromPropertySelector = new FromEntityPropertyValueSelector<>(valueRangeDescriptor, randomSelection); + var fromPropertySelector = new FromEntityPropertyValueSelector<>(valueRangeDescriptor, null, randomSelection); return new IterableFromEntityPropertyValueSelector<>(fromPropertySelector, randomSelection); } From 8d476b1de664839b491e052b191f1b9fc82091e0 Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 4 Nov 2025 10:27:54 -0300 Subject: [PATCH 07/13] chore: address comments --- .../api/domain/valuerange/ValueRange.java | 9 ------ .../AbstractCountableValueRange.java | 3 +- .../AbstractUncountableValueRange.java | 3 +- .../sort/SelectionSorterAdapter.java | 14 ++------- .../valuerange/sort/SortableValueRange.java | 18 ++++++++++++ .../ComparatorFactorySelectionSorter.java | 10 ++++--- .../decorator/ComparatorSelectionSorter.java | 10 ++++--- .../common/decorator/SelectionSetSorter.java | 28 ------------------ .../common/decorator/SelectionSorter.java | 17 ++++++++--- .../decorator/SortingEntitySelector.java | 3 +- .../move/decorator/SortingMoveSelector.java | 3 +- .../FromEntitySortingValueSelector.java | 3 +- .../value/decorator/SortingValueSelector.java | 3 +- .../score/director/ValueRangeManager.java | 18 ++++++++++-- .../selector/common/CodeAssertableSorter.java | 29 +++++++++++++++++++ .../selector/common/TestdataObjectSorter.java | 29 +++++++++++++++++++ .../ComparatorFactorySelectionSorterTest.java | 4 +-- .../ComparatorSelectionSorterTest.java | 4 +-- .../decorator/SortingEntitySelectorTest.java | 9 ++---- .../decorator/SortingMoveSelectorTest.java | 7 ++--- .../decorator/SortingValueSelectorTest.java | 10 ++----- 21 files changed, 143 insertions(+), 91 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SortableValueRange.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSetSorter.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/CodeAssertableSorter.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/TestdataObjectSorter.java diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRange.java b/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRange.java index 1e6c9c5989..9a39414678 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRange.java @@ -8,7 +8,6 @@ import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; -import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -46,14 +45,6 @@ public interface ValueRange { */ boolean contains(@Nullable T value); - /** - * The sorting operation copies the current value range and sorts it using the provided sorter. - * - * @param sorter never null, the value range sorter - * @return A new instance of the value range, with the data sorted. - */ - ValueRange sort(ValueRangeSorter sorter); - /** * Select in random order, but without shuffling the elements. * Each element might be selected multiple times. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractCountableValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractCountableValueRange.java index 473f5b28b0..d0de9f1703 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractCountableValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractCountableValueRange.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; import ai.timefold.solver.core.api.domain.valuerange.ValueRange; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeFactory; +import ai.timefold.solver.core.impl.domain.valuerange.sort.SortableValueRange; import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; import org.jspecify.annotations.NullMarked; @@ -17,7 +18,7 @@ * @see ValueRangeFactory */ @NullMarked -public abstract class AbstractCountableValueRange implements CountableValueRange { +public abstract class AbstractCountableValueRange implements CountableValueRange, SortableValueRange { /** * Certain optimizations can be applied if {@link Object#equals(Object)} can be relied upon diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractUncountableValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractUncountableValueRange.java index 8272888788..88c6c25160 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractUncountableValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/AbstractUncountableValueRange.java @@ -3,6 +3,7 @@ import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange; import ai.timefold.solver.core.api.domain.valuerange.ValueRange; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeFactory; +import ai.timefold.solver.core.impl.domain.valuerange.sort.SortableValueRange; import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; /** @@ -15,7 +16,7 @@ * Use {@link CountableValueRange} instead, and configure a step. */ @Deprecated(forRemoval = true, since = "1.1.0") -public abstract class AbstractUncountableValueRange implements ValueRange { +public abstract class AbstractUncountableValueRange implements ValueRange, SortableValueRange { @Override public ValueRange sort(ValueRangeSorter sorter) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java index 889947aa63..fbe98db015 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java @@ -1,12 +1,9 @@ package ai.timefold.solver.core.impl.domain.valuerange.sort; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Set; import java.util.SortedSet; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSetSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import org.jspecify.annotations.NullMarked; @@ -21,18 +18,11 @@ public static ValueRangeSorter of(Solution_ solution, Selectio @Override public List sort(List selectionList) { - var newList = new ArrayList<>(selectionList); - selectionSorter.sort(solution, newList); - return Collections.unmodifiableList(newList); + return selectionSorter.sort(solution, selectionList); } @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) public SortedSet sort(Set selectionSet) { - if (!(selectionSorter instanceof SelectionSetSorter selectionSetSorter)) { - throw new IllegalStateException( - "Impossible state: the sorting operation cannot be performed because the sorter does not support sorting collection sets."); - } - return selectionSetSorter.sort(solution, selectionSet); + return selectionSorter.sort(solution, selectionSet); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SortableValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SortableValueRange.java new file mode 100644 index 0000000000..33675282d8 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SortableValueRange.java @@ -0,0 +1,18 @@ +package ai.timefold.solver.core.impl.domain.valuerange.sort; + +import ai.timefold.solver.core.api.domain.valuerange.ValueRange; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +@FunctionalInterface +public interface SortableValueRange { + + /** + * The sorting operation copies the current value range and sorts it using the provided sorter. + * + * @param sorter never null, the value range sorter + * @return A new instance of the value range, with the data sorted. + */ + ValueRange sort(ValueRangeSorter sorter); +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java index 2f59692fb8..ea65818edb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.heuristic.selector.common.decorator; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -21,8 +22,7 @@ * @param the selection type */ @NullMarked -public final class ComparatorFactorySelectionSorter - implements SelectionSorter, SelectionSetSorter { +public final class ComparatorFactorySelectionSorter implements SelectionSorter { private final ComparatorFactory selectionComparatorFactory; private final SelectionSorterOrder selectionSorterOrder; @@ -41,9 +41,11 @@ private Comparator getAppliedComparator(Comparator comparator) { } @Override - public void sort(Solution_ solution, List selectionList) { + public List sort(Solution_ solution, List selectionList) { var appliedComparator = getAppliedComparator(selectionComparatorFactory.createComparator(solution)); - selectionList.sort(appliedComparator); + var sortedList = new ArrayList<>(selectionList); + sortedList.sort(appliedComparator); + return Collections.unmodifiableList(sortedList); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java index a056ab84b9..abfd04e88c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorter.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.heuristic.selector.common.decorator; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -20,8 +21,7 @@ * @param the selection type */ @NullMarked -public final class ComparatorSelectionSorter - implements SelectionSorter, SelectionSetSorter { +public final class ComparatorSelectionSorter implements SelectionSorter { private final Comparator appliedComparator; @@ -40,8 +40,10 @@ public ComparatorSelectionSorter(Comparator comparator, SelectionSorterOrder } @Override - public void sort(Solution_ solution, List selectionList) { - selectionList.sort(appliedComparator); + public List sort(Solution_ solution, List selectionList) { + var sortedList = new ArrayList<>(selectionList); + sortedList.sort(appliedComparator); + return Collections.unmodifiableList(sortedList); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSetSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSetSorter.java deleted file mode 100644 index 9260107b42..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSetSorter.java +++ /dev/null @@ -1,28 +0,0 @@ -package ai.timefold.solver.core.impl.heuristic.selector.common.decorator; - -import java.util.Set; -import java.util.SortedSet; - -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; - -import org.jspecify.annotations.NullMarked; - -/** - * Decides the order of a {@link Set} of selection values. - * - * @param the solution type, the class with the {@link PlanningSolution} annotation - * @param the selection type - */ -@NullMarked -@FunctionalInterface -public interface SelectionSetSorter { - - /** - * Creates a copy of the provided set and sort the data. - * - * @param solution never null, the current solution - * @param selectionSet never null, a {@link Set} of values that will be used as input for sorting. - */ - SortedSet sort(Solution_ solution, Set selectionSet); - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java index fb76aace58..81d7a3d137 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorter.java @@ -1,6 +1,8 @@ package ai.timefold.solver.core.impl.heuristic.selector.common.decorator; import java.util.List; +import java.util.Set; +import java.util.SortedSet; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; @@ -21,16 +23,23 @@ * @param the selection type */ @NullMarked -@FunctionalInterface public interface SelectionSorter { /** - * Apply an in-place sorting operation. - * + * Creates a copy of the provided list and sort the data. + * * @param solution never null, the current solution * @param selectionList never null, a {@link List} * of {@link PlanningEntity}, planningValue, {@link Move} or {@link Selector} that will be sorted. */ - void sort(Solution_ solution, List selectionList); + List sort(Solution_ solution, List selectionList); + + /** + * Creates a copy of the provided set and sort the data. + * + * @param solution never null, the current solution + * @param selectionSet never null, a {@link Set} of values that will be used as input for sorting. + */ + SortedSet sort(Solution_ solution, Set selectionSet); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java index f3c21da9d5..3d76e02278 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java @@ -60,7 +60,8 @@ public void constructCache(SolverScope solverScope) { return; } super.constructCache(solverScope); - sorter.sort(solverScope.getScoreDirector().getWorkingSolution(), cachedEntityList); + // We need to update the cachedEntityList since the sorter will copy the data and return a sorted list + cachedEntityList = sorter.sort(solverScope.getScoreDirector().getWorkingSolution(), cachedEntityList); logger.trace(" Sorted cachedEntityList: size ({}), entitySelector ({}).", cachedEntityList.size(), this); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelector.java index 5124baf609..495d36ce1a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelector.java @@ -25,7 +25,8 @@ public SortingMoveSelector(MoveSelector childMoveSelector, SelectionC @Override public void constructCache(SolverScope solverScope) { super.constructCache(solverScope); - sorter.sort(solverScope.getScoreDirector().getWorkingSolution(), cachedMoveList); + // We need to update the cachedMoveList since the sorter will copy the data and return a sorted list + cachedMoveList = sorter.sort(solverScope.getScoreDirector().getWorkingSolution(), cachedMoveList); logger.trace(" Sorted cachedMoveList: size ({}), moveSelector ({}).", cachedMoveList.size(), this); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java index d34b17a1e6..eb84c0e400 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java @@ -98,7 +98,8 @@ public Iterator iterator(Object entity) { childValueSelector.iterator(entity).forEachRemaining(cachedValueList::add); logger.trace(" Created cachedValueList: size ({}), valueSelector ({}).", cachedValueList.size(), this); - sorter.sort(scoreDirector.getWorkingSolution(), cachedValueList); + // We need to update the cachedValueList since the sorter will copy the data and return a sorted list + cachedValueList = sorter.sort(scoreDirector.getWorkingSolution(), cachedValueList); logger.trace(" Sorted cachedValueList: size ({}), valueSelector ({}).", cachedValueList.size(), this); return cachedValueList.iterator(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java index a9f01c324f..9ed64a4223 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java @@ -27,7 +27,8 @@ public SortingValueSelector(IterableValueSelector childValueSelector, @Override public void constructCache(SolverScope solverScope) { super.constructCache(solverScope); - sorter.sort(solverScope.getScoreDirector().getWorkingSolution(), cachedValueList); + // We need to update the cachedValueList since the sorter will copy the data and return a sorted list + cachedValueList = sorter.sort(solverScope.getScoreDirector().getWorkingSolution(), cachedValueList); logger.trace(" Sorted cachedValueList: size ({}), valueSelector ({}).", cachedValueList.size(), this); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java index e77588762d..27ff666ac4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java @@ -22,6 +22,7 @@ import ai.timefold.solver.core.impl.domain.valuerange.buildin.primdouble.DoubleValueRange; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; import ai.timefold.solver.core.impl.domain.valuerange.sort.SelectionSorterAdapter; +import ai.timefold.solver.core.impl.domain.valuerange.sort.SortableValueRange; import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; @@ -373,6 +374,7 @@ public CountableValueRange getFromSolution(ValueRangeDescriptor CountableValueRange getFromSolution(ValueRangeDescriptor valueRangeDescriptor, Solution_ solution, @Nullable SelectionSorter sorter) { var item = fromSolution[valueRangeDescriptor.getOrdinal()]; @@ -395,8 +397,13 @@ public CountableValueRange getFromSolution(ValueRangeDescriptor) valueRange.sort(sorterAdapter); + valueRange = (CountableValueRange) sortableValueRange.sort(sorterAdapter); } fromSolution[valueRangeDescriptor.getOrdinal()] = new CountableValueRangeItem<>(valueRange, sorter); } @@ -414,7 +421,7 @@ public CountableValueRange getFromEntity(ValueRangeDescriptor /** * @throws IllegalStateException if called before {@link #reset(Object)} is called */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) public CountableValueRange getFromEntity(ValueRangeDescriptor valueRangeDescriptor, Object entity, @Nullable SelectionSorter sorter) { if (cachedWorkingSolution == null) { @@ -445,8 +452,13 @@ public CountableValueRange getFromEntity(ValueRangeDescriptor valueRange = countableValueRange; } if (sorter != null) { + if (!(valueRange instanceof SortableValueRange sortableValueRange)) { + throw new IllegalStateException( + "Impossible state: value range (%s) on planning entity (%s) is not sortable." + .formatted(valueRangeDescriptor, entity)); + } var sorterAdapter = SelectionSorterAdapter.of(cachedWorkingSolution, sorter); - valueRange = (CountableValueRange) valueRange.sort(sorterAdapter); + valueRange = (CountableValueRange) sortableValueRange.sort(sorterAdapter); } valueRangeList[valueRangeDescriptor.getOrdinal()] = new CountableValueRangeItem<>(valueRange, sorter); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/CodeAssertableSorter.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/CodeAssertableSorter.java new file mode 100644 index 0000000000..f7fd47e323 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/CodeAssertableSorter.java @@ -0,0 +1,29 @@ +package ai.timefold.solver.core.impl.heuristic.selector.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; +import ai.timefold.solver.core.testutil.CodeAssertable; + +public class CodeAssertableSorter implements SelectionSorter { + + @Override + public List sort(Object solution, List selectionList) { + var sortedList = new ArrayList<>(selectionList); + Collections.sort(sortedList, Comparator.comparing(CodeAssertable::getCode)); + return sortedList; + } + + @Override + public SortedSet sort(Object solution, Set selectionSet) { + var sortedSet = new TreeSet(Comparator.comparing(CodeAssertable::getCode)); + sortedSet.addAll(selectionSet); + return sortedSet; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/TestdataObjectSorter.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/TestdataObjectSorter.java new file mode 100644 index 0000000000..c84bbc896b --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/TestdataObjectSorter.java @@ -0,0 +1,29 @@ +package ai.timefold.solver.core.impl.heuristic.selector.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; +import ai.timefold.solver.core.testdomain.TestdataObject; + +public class TestdataObjectSorter implements SelectionSorter { + + @Override + public List sort(Object solution, List selectionList) { + var sortedList = new ArrayList<>(selectionList); + Collections.sort(sortedList, Comparator.comparing(TestdataObject::getCode)); + return sortedList; + } + + @Override + public SortedSet sort(Object solution, Set selectionSet) { + var sortedSet = new TreeSet(Comparator.comparing(TestdataObject::getCode)); + sortedSet.addAll(selectionSet); + return sortedSet; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java index 42a05ab5ed..3fb8b9201b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java @@ -27,7 +27,7 @@ void sortAscending() { selectionList.add(new TestdataEntity("A")); selectionList.add(new TestdataEntity("D")); selectionList.add(new TestdataEntity("B")); - selectionSorter.sort(new TestdataSolution(), selectionList); + selectionList = selectionSorter.sort(new TestdataSolution(), selectionList); assertCodesOfIterator(selectionList.iterator(), "A", "B", "C", "D"); } @@ -43,7 +43,7 @@ void sortDescending() { selectionList.add(new TestdataEntity("A")); selectionList.add(new TestdataEntity("D")); selectionList.add(new TestdataEntity("B")); - selectionSorter.sort(new TestdataSolution(), selectionList); + selectionList = selectionSorter.sort(new TestdataSolution(), selectionList); assertCodesOfIterator(selectionList.iterator(), "D", "C", "B", "A"); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorterTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorterTest.java index f24d1de097..7a85f707af 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorterTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorSelectionSorterTest.java @@ -21,13 +21,13 @@ void sort() { Collections.addAll(arrayToSort, baseArray); ComparatorSelectionSorter selectionSorter = new ComparatorSelectionSorter<>( new TestComparator(), SelectionSorterOrder.ASCENDING); - selectionSorter.sort(null, arrayToSort); + arrayToSort = selectionSorter.sort(null, arrayToSort); assertThat(arrayToSort).isSortedAccordingTo(new TestComparator()); arrayToSort = new ArrayList<>(); Collections.addAll(arrayToSort, baseArray); selectionSorter = new ComparatorSelectionSorter<>(new TestComparator(), SelectionSorterOrder.DESCENDING); - selectionSorter.sort(null, arrayToSort); + arrayToSort = selectionSorter.sort(null, arrayToSort); assertThat(arrayToSort).isSortedAccordingTo(new TestComparator().reversed()); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java index 3862514873..8bf8f33544 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java @@ -10,10 +10,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.Comparator; - import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; +import ai.timefold.solver.core.impl.heuristic.selector.common.TestdataObjectSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; @@ -21,7 +20,6 @@ import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.testdomain.TestdataEntity; -import ai.timefold.solver.core.testdomain.TestdataObject; import ai.timefold.solver.core.testdomain.TestdataSolution; import org.junit.jupiter.api.Test; @@ -53,9 +51,8 @@ public void runCacheType(SelectionCacheType cacheType, int timesCalled) { new TestdataEntity("jan"), new TestdataEntity("feb"), new TestdataEntity("mar"), new TestdataEntity("apr"), new TestdataEntity("may"), new TestdataEntity("jun")); - SelectionSorter sorter = (scoreDirector, selectionList) -> selectionList - .sort(Comparator.comparing(TestdataObject::getCode)); - EntitySelector entitySelector = new SortingEntitySelector(childEntitySelector, cacheType, sorter); + EntitySelector entitySelector = + new SortingEntitySelector(childEntitySelector, cacheType, new TestdataObjectSorter()); SolverScope solverScope = mock(SolverScope.class); InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java index 1006ac40a4..3a09ec9666 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java @@ -22,7 +22,7 @@ import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.move.DummyMove; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.CodeAssertableSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; import ai.timefold.solver.core.impl.heuristic.selector.move.AbstractMoveSelectorFactory; import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; @@ -106,9 +106,8 @@ public void runCacheType(SelectionCacheType cacheType, int timesCalled) { new DummyMove("jan"), new DummyMove("feb"), new DummyMove("mar"), new DummyMove("apr"), new DummyMove("may"), new DummyMove("jun")); - SelectionSorter sorter = (scoreDirector, selectionList) -> selectionList - .sort(Comparator.comparing(DummyMove::getCode)); - MoveSelector moveSelector = new SortingMoveSelector(childMoveSelector, cacheType, sorter); + MoveSelector moveSelector = + new SortingMoveSelector(childMoveSelector, cacheType, new CodeAssertableSorter()); SolverScope solverScope = mock(SolverScope.class); InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java index fe900d8ace..05c1cfc5a8 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java @@ -8,18 +8,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.Comparator; - import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.TestdataObjectSorter; import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.testdomain.TestdataEntity; -import ai.timefold.solver.core.testdomain.TestdataObject; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; @@ -48,9 +45,8 @@ public void runOriginalSelection(SelectionCacheType cacheType, int timesCalled) new TestdataValue("jan"), new TestdataValue("feb"), new TestdataValue("mar"), new TestdataValue("apr"), new TestdataValue("may"), new TestdataValue("jun")); - SelectionSorter sorter = (scoreDirector, selectionList) -> selectionList - .sort(Comparator.comparing(TestdataObject::getCode)); - IterableValueSelector valueSelector = new SortingValueSelector(childValueSelector, cacheType, sorter); + IterableValueSelector valueSelector = + new SortingValueSelector(childValueSelector, cacheType, new TestdataObjectSorter()); SolverScope solverScope = mock(SolverScope.class); InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); From d776376774a87f18ecea692ab5068bb3d9e1e5a3 Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 4 Nov 2025 10:50:58 -0300 Subject: [PATCH 08/13] chore: address comments --- .../domain/valuerange/ValueRangeCache.java | 34 ++++----- .../FromEntityPropertyValueSelector.java | 5 +- .../selector/value/ValueSelectorFactory.java | 69 ++++++++++--------- .../score/director/ValueRangeManager.java | 2 - 4 files changed, 53 insertions(+), 57 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java index 9295635412..9d6b862fdd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/ValueRangeCache.java @@ -26,19 +26,19 @@ public final class ValueRangeCache private final List valuesWithFastRandomAccess; private final Set valuesWithFastLookup; - private final CacheType cacheType; + private final boolean trustedValues; - private ValueRangeCache(int size, Set emptyCacheSet, CacheType cacheType) { + private ValueRangeCache(int size, Set emptyCacheSet, boolean trustedValues) { this.valuesWithFastRandomAccess = new ArrayList<>(size); this.valuesWithFastLookup = emptyCacheSet; - this.cacheType = cacheType; + this.trustedValues = trustedValues; } - private ValueRangeCache(Collection collection, Set emptyCacheSet, CacheType cacheType) { + private ValueRangeCache(Collection collection, Set emptyCacheSet, boolean trustedValues) { this.valuesWithFastRandomAccess = new ArrayList<>(collection); this.valuesWithFastLookup = emptyCacheSet; this.valuesWithFastLookup.addAll(valuesWithFastRandomAccess); - this.cacheType = cacheType; + this.trustedValues = trustedValues; } public void add(@Nullable Value_ value) { @@ -83,10 +83,11 @@ public Iterator iterator(Random workingRandom) { */ public ValueRangeCache sort(ValueRangeSorter sorter) { var valuesWithFastRandomAccessSorted = sorter.sort(valuesWithFastRandomAccess); - return switch (cacheType) { - case USER_VALUES -> Builder.FOR_USER_VALUES.buildCache(valuesWithFastRandomAccessSorted); - case TRUSTED_VALUES -> Builder.FOR_TRUSTED_VALUES.buildCache(valuesWithFastRandomAccessSorted); - }; + if (trustedValues) { + return Builder.FOR_TRUSTED_VALUES.buildCache(valuesWithFastRandomAccessSorted); + } else { + return Builder.FOR_USER_VALUES.buildCache(valuesWithFastRandomAccessSorted); + } } public enum Builder { @@ -97,13 +98,12 @@ public enum Builder { FOR_USER_VALUES { @Override public ValueRangeCache buildCache(int size) { - return new ValueRangeCache<>(size, CollectionUtils.newIdentityHashSet(size), CacheType.USER_VALUES); + return new ValueRangeCache<>(size, CollectionUtils.newIdentityHashSet(size), false); } @Override public ValueRangeCache buildCache(Collection collection) { - return new ValueRangeCache<>(collection, CollectionUtils.newIdentityHashSet(collection.size()), - CacheType.USER_VALUES); + return new ValueRangeCache<>(collection, CollectionUtils.newIdentityHashSet(collection.size()), false); } }, @@ -118,13 +118,12 @@ public ValueRangeCache buildCache(Collection collection FOR_TRUSTED_VALUES { @Override public ValueRangeCache buildCache(int size) { - return new ValueRangeCache<>(size, CollectionUtils.newHashSet(size), CacheType.TRUSTED_VALUES); + return new ValueRangeCache<>(size, CollectionUtils.newHashSet(size), true); } @Override public ValueRangeCache buildCache(Collection collection) { - return new ValueRangeCache<>(collection, CollectionUtils.newHashSet(collection.size()), - CacheType.TRUSTED_VALUES); + return new ValueRangeCache<>(collection, CollectionUtils.newHashSet(collection.size()), true); } }; @@ -135,9 +134,4 @@ public ValueRangeCache buildCache(Collection collection } - private enum CacheType { - USER_VALUES, - TRUSTED_VALUES - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java index 4c0d7eba01..8d679cff9d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java @@ -123,8 +123,9 @@ public Iterator endingIterator(Object entity) { public boolean equals(Object o) { if (!(o instanceof FromEntityPropertyValueSelector that)) return false; - return randomSelection == that.randomSelection && Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor) - && Objects.equals(selectionSorter, that.selectionSorter); + return Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor) + && Objects.equals(selectionSorter, that.selectionSorter) + && randomSelection == that.randomSelection; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java index 9b97f8f34f..35558b40af 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.heuristic.selector.value; +import static ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder.SORTED; + import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -273,37 +275,38 @@ private static Class determineComparatorFactoryClas private SelectionSorter determineSorter(GenuineVariableDescriptor variableDescriptor, SelectionOrder resolvedSelectionOrder, ClassInstanceCache instanceCache) { - SelectionSorter sorter = null; - if (resolvedSelectionOrder == ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder.SORTED) { - var sorterManner = config.getSorterManner(); - var comparatorClass = determineComparatorClass(config); - var comparatorFactoryClass = determineComparatorFactoryClass(config); - if (sorterManner != null) { - if (!ValueSelectorConfig.hasSorter(sorterManner, variableDescriptor)) { - return null; - } - sorter = ValueSelectorConfig.determineSorter(sorterManner, variableDescriptor); - } else if (comparatorClass != null) { - Comparator sorterComparator = - instanceCache.newInstance(config, determineComparatorPropertyName(config), comparatorClass); - sorter = new ComparatorSelectionSorter<>(sorterComparator, - SelectionSorterOrder.resolve(config.getSorterOrder())); - } else if (comparatorFactoryClass != null) { - var comparatorFactory = instanceCache.newInstance(config, determineComparatorFactoryPropertyName(config), - comparatorFactoryClass); - sorter = new ComparatorFactorySelectionSorter<>(comparatorFactory, - SelectionSorterOrder.resolve(config.getSorterOrder())); - } else if (config.getSorterClass() != null) { - sorter = instanceCache.newInstance(config, "sorterClass", config.getSorterClass()); - } else { - throw new IllegalArgumentException(""" - The valueSelectorConfig (%s) with resolvedSelectionOrder (%s) needs \ - a sorterManner (%s) or a %s (%s) or a %s (%s) \ - or a sorterClass (%s).""" - .formatted(config, resolvedSelectionOrder, sorterManner, determineComparatorPropertyName(config), - comparatorClass, determineComparatorFactoryPropertyName(config), comparatorFactoryClass, - config.getSorterClass())); + if (resolvedSelectionOrder != SORTED) { + return null; + } + SelectionSorter sorter; + var sorterManner = config.getSorterManner(); + var comparatorClass = determineComparatorClass(config); + var comparatorFactoryClass = determineComparatorFactoryClass(config); + if (sorterManner != null) { + if (!ValueSelectorConfig.hasSorter(sorterManner, variableDescriptor)) { + return null; } + sorter = ValueSelectorConfig.determineSorter(sorterManner, variableDescriptor); + } else if (comparatorClass != null) { + Comparator sorterComparator = + instanceCache.newInstance(config, determineComparatorPropertyName(config), comparatorClass); + sorter = new ComparatorSelectionSorter<>(sorterComparator, + SelectionSorterOrder.resolve(config.getSorterOrder())); + } else if (comparatorFactoryClass != null) { + var comparatorFactory = instanceCache.newInstance(config, determineComparatorFactoryPropertyName(config), + comparatorFactoryClass); + sorter = new ComparatorFactorySelectionSorter<>(comparatorFactory, + SelectionSorterOrder.resolve(config.getSorterOrder())); + } else if (config.getSorterClass() != null) { + sorter = instanceCache.newInstance(config, "sorterClass", config.getSorterClass()); + } else { + throw new IllegalArgumentException(""" + The valueSelectorConfig (%s) with resolvedSelectionOrder (%s) needs \ + a sorterManner (%s) or a %s (%s) or a %s (%s) \ + or a sorterClass (%s).""" + .formatted(config, resolvedSelectionOrder, sorterManner, determineComparatorPropertyName(config), + comparatorClass, determineComparatorFactoryPropertyName(config), comparatorFactoryClass, + config.getSorterClass())); } return sorter; } @@ -372,14 +375,14 @@ protected void validateSorting(SelectionOrder resolvedSelectionOrder) { var sorterOrder = config.getSorterOrder(); var sorterClass = config.getSorterClass(); if ((sorterManner != null || comparatorClass != null || comparatorFactoryClass != null - || sorterOrder != null || sorterClass != null) && resolvedSelectionOrder != SelectionOrder.SORTED) { + || sorterOrder != null || sorterClass != null) && resolvedSelectionOrder != SORTED) { throw new IllegalArgumentException(""" The valueSelectorConfig (%s) with sorterManner (%s) \ and %s (%s) and %s (%s) and sorterOrder (%s) and sorterClass (%s) \ has a resolvedSelectionOrder (%s) that is not %s.""" .formatted(config, sorterManner, comparatorPropertyName, comparatorClass, comparatorFactoryPropertyName, comparatorFactoryClass, sorterOrder, sorterClass, resolvedSelectionOrder, - SelectionOrder.SORTED)); + SORTED)); } assertNotSorterMannerAnd(config, comparatorPropertyName, ValueSelectorFactory::determineComparatorClass); assertNotSorterMannerAnd(config, comparatorFactoryPropertyName, @@ -421,7 +424,7 @@ private static void assertNotSorterClassAnd(ValueSelectorConfig config, String p protected ValueSelector applySorting(SelectionCacheType resolvedCacheType, SelectionOrder resolvedSelectionOrder, ValueSelector valueSelector, ClassInstanceCache instanceCache) { - if (resolvedSelectionOrder == SelectionOrder.SORTED) { + if (resolvedSelectionOrder == SORTED) { var sorter = determineSorter(valueSelector.getVariableDescriptor(), resolvedSelectionOrder, instanceCache); if (!valueSelector.getVariableDescriptor().canExtractValueRangeFromSolution() && resolvedCacheType == SelectionCacheType.STEP) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java index 27ff666ac4..a2ef32f9a4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java @@ -381,7 +381,6 @@ public CountableValueRange getFromSolution(ValueRangeDescriptor) item.countableValueRange() : null; var valueRangeSorter = item != null ? item.sorter() : null; - // Avoid computeIfAbsent on the hot path; creates capturing lambda instances. // We read and sort the data again if needed if (valueRange == null || (sorter != null && !Objects.equals(valueRangeSorter, sorter))) { var extractedValueRange = valueRangeDescriptor. extractAllValues(Objects.requireNonNull(solution)); @@ -435,7 +434,6 @@ public CountableValueRange getFromEntity(ValueRangeDescriptor var item = valueRangeList[valueRangeDescriptor.getOrdinal()]; var valueRange = item != null ? (CountableValueRange) item.countableValueRange() : null; var valueRangeSorter = item != null ? item.sorter() : null; - // Avoid computeIfAbsent on the hot path; creates capturing lambda instances. // We read and sort the data again if needed if (valueRange == null || (sorter != null && !Objects.equals(valueRangeSorter, sorter))) { var extractedValueRange = From 43f59dea3e357c357e6bd8bd0240c056e1a447f8 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 5 Nov 2025 11:58:17 -0300 Subject: [PATCH 09/13] chore: address comments --- .../sort/SelectionSorterAdapter.java | 11 +- .../valuerange/sort/ValueRangeSorter.java | 6 + .../selector/common/ReachableValues.java | 89 ++++++--- .../FilteringEntityByEntitySelector.java | 2 +- .../FilteringEntityByValueSelector.java | 2 +- ...ableFromSolutionPropertyValueSelector.java | 18 +- .../selector/value/ValueSelectorFactory.java | 50 +---- .../FilteringValueRangeSelector.java | 22 ++- .../FromEntitySortingValueSelector.java | 134 ------------- ...erableFromEntityPropertyValueSelector.java | 4 + .../value/decorator/SortingValueSelector.java | 73 ------- .../score/director/ValueRangeManager.java | 64 ++++-- ...DefaultConstructionHeuristicPhaseTest.java | 30 +++ .../selector/common/ReachableValuesTest.java | 59 ++++++ .../selector/common/TestdataObjectSorter.java | 35 +++- .../decorator/SortingEntitySelectorTest.java | 3 +- .../list/ElementDestinationSelectorTest.java | 8 +- ...romSolutionPropertyValueSelectorTest.java} | 62 +++--- .../value/ValueSelectorFactoryTest.java | 64 +++--- .../FilteringValueRangeSelectorTest.java | 183 ++++++++++++++++++ ...leFromEntityPropertyValueSelectorTest.java | 121 ++++++++++++ .../testdomain/list/TestdataListUtils.java | 4 +- 22 files changed, 667 insertions(+), 377 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java rename core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/{decorator/SortingValueSelectorTest.java => IterableFromSolutionPropertyValueSelectorTest.java} (61%) create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelectorTest.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelectorTest.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java index fbe98db015..858f89da16 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java @@ -10,7 +10,7 @@ @NullMarked public record SelectionSorterAdapter(Solution_ solution, - SelectionSorter selectionSorter) implements ValueRangeSorter { + SelectionSorter innerSelectionSorter) implements ValueRangeSorter { public static ValueRangeSorter of(Solution_ solution, SelectionSorter selectionSorter) { return new SelectionSorterAdapter<>(solution, selectionSorter); @@ -18,11 +18,16 @@ public static ValueRangeSorter of(Solution_ solution, Selectio @Override public List sort(List selectionList) { - return selectionSorter.sort(solution, selectionList); + return innerSelectionSorter.sort(solution, selectionList); } @Override public SortedSet sort(Set selectionSet) { - return selectionSorter.sort(solution, selectionSet); + return innerSelectionSorter.sort(solution, selectionSet); + } + + @Override + public SelectionSorter getInnerSorter() { + return innerSelectionSorter; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java index da26aae9e7..ca5ae1216e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java @@ -4,6 +4,8 @@ import java.util.Set; import java.util.SortedSet; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; + import org.jspecify.annotations.NullMarked; /** @@ -28,4 +30,8 @@ public interface ValueRangeSorter { */ SortedSet sort(Set selectionSet); + /** + * @return the inner sorter class that will be used to sort the data. + */ + SelectionSorter getInnerSorter(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableValues.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableValues.java index 28cb9c0d77..93b6e10c89 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableValues.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableValues.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; @@ -10,6 +11,8 @@ import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.FromEntityPropertyValueRangeDescriptor; +import ai.timefold.solver.core.impl.domain.valuerange.sort.ValueRangeSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -21,22 +24,25 @@ * @see FromEntityPropertyValueRangeDescriptor */ @NullMarked -public final class ReachableValues { +public final class ReachableValues { - private final Map values; + private final Map> values; private final @Nullable Class valueClass; + private final @Nullable ValueRangeSorter valueRangeSorter; private final boolean acceptsNullValue; - private @Nullable ReachableItemValue firstCachedObject; - private @Nullable ReachableItemValue secondCachedObject; + private @Nullable ReachableItemValue firstCachedObject; + private @Nullable ReachableItemValue secondCachedObject; - public ReachableValues(Map values, Class valueClass, boolean acceptsNullValue) { + public ReachableValues(Map> values, @Nullable Class valueClass, + @Nullable ValueRangeSorter valueRangeSorter, boolean acceptsNullValue) { this.values = values; this.valueClass = valueClass; + this.valueRangeSorter = valueRangeSorter; this.acceptsNullValue = acceptsNullValue; } - private @Nullable ReachableItemValue fetchItemValue(Object value) { - ReachableItemValue selected = null; + private @Nullable ReachableItemValue fetchItemValue(V value) { + ReachableItemValue selected = null; if (firstCachedObject != null && firstCachedObject.value == value) { selected = firstCachedObject; } else if (secondCachedObject != null && secondCachedObject.value == value) { @@ -54,7 +60,7 @@ public ReachableValues(Map values, Class valueCla return selected; } - public List extractEntitiesAsList(Object value) { + public List extractEntitiesAsList(V value) { var itemValue = fetchItemValue(value); if (itemValue == null) { return Collections.emptyList(); @@ -62,11 +68,12 @@ public List extractEntitiesAsList(Object value) { return itemValue.randomAccessEntityList; } - public List extractValuesAsList(Object value) { + public List extractValuesAsList(V value) { var itemValue = fetchItemValue(value); if (itemValue == null) { return Collections.emptyList(); } + itemValue.checkSorting(valueRangeSorter); return itemValue.randomAccessValueList; } @@ -74,7 +81,7 @@ public int getSize() { return values.size(); } - public boolean isEntityReachable(@Nullable Object origin, @Nullable Object entity) { + public boolean isEntityReachable(@Nullable V origin, @Nullable E entity) { if (entity == null) { return true; } @@ -88,7 +95,7 @@ public boolean isEntityReachable(@Nullable Object origin, @Nullable Object entit return originItemValue.entityMap.containsKey(entity); } - public boolean isValueReachable(Object origin, @Nullable Object otherValue) { + public boolean isValueReachable(V origin, @Nullable V otherValue) { var originItemValue = fetchItemValue(Objects.requireNonNull(origin)); if (originItemValue == null) { return false; @@ -103,19 +110,34 @@ public boolean acceptsNullValue() { return acceptsNullValue; } - public boolean matchesValueClass(Object value) { + public boolean matchesValueClass(V value) { return valueClass != null && valueClass.isAssignableFrom(Objects.requireNonNull(value).getClass()); } + public @Nullable SelectionSorter getValueSelectionSorter() { + return valueRangeSorter != null ? valueRangeSorter.getInnerSorter() : null; + } + + public ReachableValues copy(@Nullable ValueRangeSorter valueRangeSorter) { + Map> newValues = ConfigUtils.isGenericTypeImmutable(valueClass) + ? new HashMap<>(values.size()) + : new IdentityHashMap<>(values.size()); + for (Map.Entry> entry : values.entrySet()) { + newValues.put(entry.getKey(), entry.getValue().copy()); + } + return new ReachableValues<>(newValues, valueClass, valueRangeSorter, acceptsNullValue); + } + @NullMarked - public static final class ReachableItemValue { - private final Object value; - private final Map entityMap; - private final Map valueMap; - private final List randomAccessEntityList; - private final List randomAccessValueList; - - public ReachableItemValue(Object value, int entityListSize, int valueListSize) { + public static final class ReachableItemValue { + private final V value; + private final Map entityMap; + private final Map valueMap; + private final List randomAccessEntityList; + private final List randomAccessValueList; + private boolean sorted = false; + + public ReachableItemValue(V value, int entityListSize, int valueListSize) { this.value = value; this.entityMap = new IdentityHashMap<>(entityListSize); this.randomAccessEntityList = new ArrayList<>(entityListSize); @@ -124,17 +146,40 @@ public ReachableItemValue(Object value, int entityListSize, int valueListSize) { this.randomAccessValueList = new ArrayList<>(valueListSize); } - public void addEntity(Object entity) { + private ReachableItemValue(V value, Map entityMap, Map valueMap, List randomAccessEntityList, + List randomAccessValueList) { + this.value = value; + this.entityMap = entityMap; + this.valueMap = valueMap; + this.randomAccessEntityList = randomAccessEntityList; + this.randomAccessValueList = randomAccessValueList; + } + + public void addEntity(E entity) { if (entityMap.put(entity, entity) == null) { randomAccessEntityList.add(entity); } } - public void addValue(Object value) { + public void addValue(V value) { if (valueMap.put(value, value) == null) { randomAccessValueList.add(value); } } + + private void checkSorting(@Nullable ValueRangeSorter valueRangeSorter) { + if (valueRangeSorter != null && !sorted) { + var sortedList = valueRangeSorter.sort(randomAccessValueList); + randomAccessValueList.clear(); + randomAccessValueList.addAll(sortedList); + sorted = true; + } + } + + public ReachableItemValue copy() { + return new ReachableItemValue<>(value, entityMap, valueMap, new ArrayList<>(randomAccessEntityList), + new ArrayList<>(randomAccessValueList)); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByEntitySelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByEntitySelector.java index e33fe7602b..4d56c08311 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByEntitySelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByEntitySelector.java @@ -72,7 +72,7 @@ public final class FilteringEntityByEntitySelector extends AbstractDe private Object replayedEntity; private BasicVariableDescriptor[] basicVariableDescriptors; private ValueRangeManager valueRangeManager; - private ReachableValues reachableValues; + private ReachableValues reachableValues; private List allEntities; public FilteringEntityByEntitySelector(EntitySelector childEntitySelector, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByValueSelector.java index 7cb10dd004..e720ee5e86 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByValueSelector.java @@ -69,7 +69,7 @@ public final class FilteringEntityByValueSelector extends AbstractDem private final boolean randomSelection; private Object replayedValue; - private ReachableValues reachableValues; + private ReachableValues reachableValues; private long entitiesSize; public FilteringEntityByValueSelector(EntitySelector childEntitySelector, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java index 7025a268bf..da0b59e110 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java @@ -22,7 +22,7 @@ public final class IterableFromSolutionPropertyValueSelector implements IterableValueSelector { private final ValueRangeDescriptor valueRangeDescriptor; - private final SelectionSorter sorter; + private final SelectionSorter selectionSorter; private final SelectionCacheType minimumCacheType; private final boolean randomSelection; private final boolean valueRangeMightContainEntity; @@ -32,9 +32,9 @@ public final class IterableFromSolutionPropertyValueSelector private boolean cachedEntityListIsDirty = false; public IterableFromSolutionPropertyValueSelector(ValueRangeDescriptor valueRangeDescriptor, - SelectionSorter sorter, SelectionCacheType minimumCacheType, boolean randomSelection) { + SelectionSorter selectionSorter, SelectionCacheType minimumCacheType, boolean randomSelection) { this.valueRangeDescriptor = valueRangeDescriptor; - this.sorter = sorter; + this.selectionSorter = selectionSorter; this.minimumCacheType = minimumCacheType; this.randomSelection = randomSelection; valueRangeMightContainEntity = valueRangeDescriptor.mightContainEntity(); @@ -51,6 +51,10 @@ public SelectionCacheType getCacheType() { return (intrinsicCacheType.compareTo(minimumCacheType) > 0) ? intrinsicCacheType : minimumCacheType; } + public SelectionSorter getSelectionSorter() { + return selectionSorter; + } + // ************************************************************************ // Cache lifecycle methods // ************************************************************************ @@ -60,7 +64,7 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { super.phaseStarted(phaseScope); var scoreDirector = phaseScope.getScoreDirector(); cachedValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor, - scoreDirector.getWorkingSolution(), sorter); + scoreDirector.getWorkingSolution(), selectionSorter); if (valueRangeMightContainEntity) { cachedEntityListRevision = scoreDirector.getWorkingEntityListRevision(); cachedEntityListIsDirty = false; @@ -77,7 +81,7 @@ public void stepStarted(AbstractStepScope stepScope) { cachedEntityListIsDirty = true; } else { cachedValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor, - scoreDirector.getWorkingSolution(), sorter); + scoreDirector.getWorkingSolution(), selectionSorter); cachedEntityListRevision = scoreDirector.getWorkingEntityListRevision(); } } @@ -160,12 +164,12 @@ public boolean equals(Object o) { if (!(o instanceof IterableFromSolutionPropertyValueSelector that)) return false; return randomSelection == that.randomSelection && Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor) - && Objects.equals(sorter, that.sorter) && minimumCacheType == that.minimumCacheType; + && Objects.equals(selectionSorter, that.selectionSorter) && minimumCacheType == that.minimumCacheType; } @Override public int hashCode() { - return Objects.hash(valueRangeDescriptor, sorter, minimumCacheType, randomSelection); + return Objects.hash(valueRangeDescriptor, selectionSorter, minimumCacheType, randomSelection); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java index 35558b40af..33c13afd4d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java @@ -31,14 +31,12 @@ import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.DowncastingValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.FilteringValueRangeSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.FilteringValueSelector; -import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.FromEntitySortingValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.InitializedValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.IterableFromEntityPropertyValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.ProbabilityValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.ReinitializeVariableValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.SelectedCountLimitValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.ShufflingValueSelector; -import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.SortingValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.UnassignedListValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.mimic.MimicRecordingValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.mimic.MimicReplayingValueSelector; @@ -125,16 +123,7 @@ public ValueSelector buildValueSelector(HeuristicConfigPolicy buildValueSelector(HeuristicConfigPolicy applySorting(SelectionCacheType resolvedCacheType, SelectionOrder resolvedSelectionOrder, - ValueSelector valueSelector, ClassInstanceCache instanceCache) { - if (resolvedSelectionOrder == SORTED) { - var sorter = determineSorter(valueSelector.getVariableDescriptor(), resolvedSelectionOrder, instanceCache); - if (!valueSelector.getVariableDescriptor().canExtractValueRangeFromSolution() - && resolvedCacheType == SelectionCacheType.STEP) { - valueSelector = new FromEntitySortingValueSelector<>(valueSelector, resolvedCacheType, sorter); - } else { - if (!(valueSelector instanceof IterableValueSelector)) { - throw new IllegalArgumentException("The valueSelectorConfig (" + config - + ") with resolvedCacheType (" + resolvedCacheType - + ") and resolvedSelectionOrder (" + resolvedSelectionOrder - + ") needs to be based on an " - + IterableValueSelector.class.getSimpleName() + " (" + valueSelector + ")." - + " Check your @" + ValueRangeProvider.class.getSimpleName() + " annotations."); - } - valueSelector = new SortingValueSelector<>((IterableValueSelector) valueSelector, - resolvedCacheType, sorter); - } - } - return valueSelector; - } - protected void validateProbability(SelectionOrder resolvedSelectionOrder) { if (config.getProbabilityWeightFactoryClass() != null && resolvedSelectionOrder != SelectionOrder.PROBABILISTIC) { @@ -594,10 +557,11 @@ private ValueSelector applyDowncasting(ValueSelector value return valueSelector; } - public static ValueSelector applyValueRangeFiltering( + public static ValueSelector applyValueRangeFiltering( HeuristicConfigPolicy configPolicy, ValueSelector valueSelector, - EntityDescriptor entityDescriptor, SelectionCacheType minimumCacheType, SelectionOrder selectionOrder, - boolean randomSelection, String entityValueRangeRecorderId, boolean assertBothSides) { + SelectionSorter selectionSorter, EntityDescriptor entityDescriptor, + SelectionCacheType minimumCacheType, SelectionOrder selectionOrder, boolean randomSelection, + String entityValueRangeRecorderId, boolean assertBothSides) { if (entityValueRangeRecorderId == null) { return valueSelector; } @@ -607,7 +571,7 @@ public static ValueSelector applyValueRangeFiltering( (IterableValueSelector) ValueSelectorFactory. create(valueSelectorConfig) .buildValueSelector(configPolicy, entityDescriptor, minimumCacheType, selectionOrder); return new FilteringValueRangeSelector<>((IterableValueSelector) valueSelector, replayingValueSelector, - randomSelection, assertBothSides); + selectionSorter, randomSelection, assertBothSides); } public enum ListValueFilteringType { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java index e67760ad15..94f74b5097 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java @@ -8,11 +8,13 @@ import java.util.Random; import java.util.function.Supplier; +import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector; import ai.timefold.solver.core.impl.heuristic.selector.common.ReachableValues; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.list.DestinationSelectorFactory; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.ListChangeMoveSelector; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.ListChangeMoveSelectorFactory; @@ -74,20 +76,22 @@ public final class FilteringValueRangeSelector extends AbstractDemand private final IterableValueSelector nonReplayingValueSelector; private final IterableValueSelector replayingValueSelector; + private final SelectionSorter selectionSorter; private final boolean randomSelection; private Object replayedValue = null; private long valuesSize; private ListVariableStateSupply listVariableStateSupply; - private ReachableValues reachableValues; + private ReachableValues reachableValues; private final boolean checkSourceAndDestination; public FilteringValueRangeSelector(IterableValueSelector nonReplayingValueSelector, - IterableValueSelector replayingValueSelector, boolean randomSelection, - boolean checkSourceAndDestination) { + IterableValueSelector replayingValueSelector, SelectionSorter selectionSorter, + boolean randomSelection, boolean checkSourceAndDestination) { this.nonReplayingValueSelector = nonReplayingValueSelector; this.replayingValueSelector = replayingValueSelector; + this.selectionSorter = selectionSorter; this.randomSelection = randomSelection; this.checkSourceAndDestination = checkSourceAndDestination; } @@ -111,7 +115,7 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { this.nonReplayingValueSelector.phaseStarted(phaseScope); this.replayingValueSelector.phaseStarted(phaseScope); this.reachableValues = phaseScope.getScoreDirector().getValueRangeManager() - .getReachableValues(listVariableStateSupply.getSourceVariableDescriptor()); + .getReachableValues(listVariableStateSupply.getSourceVariableDescriptor(), selectionSorter); valuesSize = reachableValues.getSize(); } @@ -131,6 +135,11 @@ public IterableValueSelector getChildValueSelector() { return nonReplayingValueSelector; } + @Override + public SelectionCacheType getCacheType() { + return nonReplayingValueSelector.getCacheType(); + } + @Override public GenuineVariableDescriptor getVariableDescriptor() { return nonReplayingValueSelector.getVariableDescriptor(); @@ -194,12 +203,13 @@ public Iterator endingIterator(Object entity) { public boolean equals(Object other) { return other instanceof FilteringValueRangeSelector that && Objects.equals(nonReplayingValueSelector, that.nonReplayingValueSelector) - && Objects.equals(replayingValueSelector, that.replayingValueSelector); + && Objects.equals(replayingValueSelector, that.replayingValueSelector) + && Objects.equals(selectionSorter, that.selectionSorter); } @Override public int hashCode() { - return Objects.hash(nonReplayingValueSelector, replayingValueSelector); + return Objects.hash(nonReplayingValueSelector, replayingValueSelector, selectionSorter); } @NullMarked diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java deleted file mode 100644 index eb84c0e400..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java +++ /dev/null @@ -1,134 +0,0 @@ -package ai.timefold.solver.core.impl.heuristic.selector.value.decorator; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; - -import ai.timefold.solver.core.api.score.director.ScoreDirector; -import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; -import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; -import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; -import ai.timefold.solver.core.impl.heuristic.selector.value.ValueSelector; -import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; - -public final class FromEntitySortingValueSelector - extends AbstractDemandEnabledSelector - implements ValueSelector { - - private final ValueSelector childValueSelector; - private final SelectionCacheType cacheType; - private final SelectionSorter sorter; - - private ScoreDirector scoreDirector = null; - - public FromEntitySortingValueSelector(ValueSelector childValueSelector, - SelectionCacheType cacheType, SelectionSorter sorter) { - this.childValueSelector = childValueSelector; - this.cacheType = cacheType; - this.sorter = sorter; - if (childValueSelector.isNeverEnding()) { - throw new IllegalStateException("The selector (" + this - + ") has a childValueSelector (" + childValueSelector - + ") with neverEnding (" + childValueSelector.isNeverEnding() + ")."); - } - if (cacheType != SelectionCacheType.STEP) { - throw new IllegalArgumentException("The selector (" + this - + ") does not support the cacheType (" + cacheType + ")."); - } - phaseLifecycleSupport.addEventListener(childValueSelector); - } - - public ValueSelector getChildValueSelector() { - return childValueSelector; - } - - @Override - public SelectionCacheType getCacheType() { - return cacheType; - } - - // ************************************************************************ - // Worker methods - // ************************************************************************ - - @Override - public void phaseStarted(AbstractPhaseScope phaseScope) { - super.phaseStarted(phaseScope); - scoreDirector = phaseScope.getScoreDirector(); - } - - @Override - public void phaseEnded(AbstractPhaseScope phaseScope) { - super.phaseEnded(phaseScope); - scoreDirector = null; - } - - @Override - public GenuineVariableDescriptor getVariableDescriptor() { - return childValueSelector.getVariableDescriptor(); - } - - @Override - public long getSize(Object entity) { - return childValueSelector.getSize(entity); - } - - @Override - public boolean isCountable() { - return true; - } - - @Override - public boolean isNeverEnding() { - return false; - } - - @Override - public Iterator iterator(Object entity) { - long childSize = childValueSelector.getSize(entity); - if (childSize > Integer.MAX_VALUE) { - throw new IllegalStateException("The selector (" + this - + ") has a childValueSelector (" + childValueSelector - + ") with childSize (" + childSize - + ") which is higher than Integer.MAX_VALUE."); - } - List cachedValueList = new ArrayList<>((int) childSize); - childValueSelector.iterator(entity).forEachRemaining(cachedValueList::add); - logger.trace(" Created cachedValueList: size ({}), valueSelector ({}).", - cachedValueList.size(), this); - // We need to update the cachedValueList since the sorter will copy the data and return a sorted list - cachedValueList = sorter.sort(scoreDirector.getWorkingSolution(), cachedValueList); - logger.trace(" Sorted cachedValueList: size ({}), valueSelector ({}).", - cachedValueList.size(), this); - return cachedValueList.iterator(); - } - - @Override - public Iterator endingIterator(Object entity) { - return iterator(entity); - } - - @Override - public boolean equals(Object other) { - if (this == other) - return true; - if (other == null || getClass() != other.getClass()) - return false; - var that = (FromEntitySortingValueSelector) other; - return Objects.equals(childValueSelector, that.childValueSelector) && cacheType == that.cacheType - && Objects.equals(sorter, that.sorter); - } - - @Override - public int hashCode() { - return Objects.hash(childValueSelector, cacheType, sorter); - } - - @Override - public String toString() { - return "Sorting(" + childValueSelector + ")"; - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java index efcb67758e..048161ca26 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java @@ -82,6 +82,10 @@ public void phaseEnded(AbstractPhaseScope phaseScope) { // Worker methods // ************************************************************************ + public FromEntityPropertyValueSelector getChildValueSelector() { + return childValueSelector; + } + @Override public SelectionCacheType getCacheType() { return minimumCacheType; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java deleted file mode 100644 index 9ed64a4223..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java +++ /dev/null @@ -1,73 +0,0 @@ -package ai.timefold.solver.core.impl.heuristic.selector.value.decorator; - -import java.util.Iterator; -import java.util.Objects; - -import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; -import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; - -public final class SortingValueSelector - extends AbstractCachingValueSelector - implements IterableValueSelector { - - protected final SelectionSorter sorter; - - public SortingValueSelector(IterableValueSelector childValueSelector, SelectionCacheType cacheType, - SelectionSorter sorter) { - super(childValueSelector, cacheType); - this.sorter = sorter; - } - - // ************************************************************************ - // Worker methods - // ************************************************************************ - - @Override - public void constructCache(SolverScope solverScope) { - super.constructCache(solverScope); - // We need to update the cachedValueList since the sorter will copy the data and return a sorted list - cachedValueList = sorter.sort(solverScope.getScoreDirector().getWorkingSolution(), cachedValueList); - logger.trace(" Sorted cachedValueList: size ({}), valueSelector ({}).", - cachedValueList.size(), this); - } - - @Override - public boolean isNeverEnding() { - return false; - } - - @Override - public Iterator iterator(Object entity) { - return iterator(); - } - - @Override - public Iterator iterator() { - return cachedValueList.iterator(); - } - - @Override - public boolean equals(Object other) { - if (this == other) - return true; - if (other == null || getClass() != other.getClass()) - return false; - if (!super.equals(other)) - return false; - SortingValueSelector that = (SortingValueSelector) other; - return Objects.equals(sorter, that.sorter); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), sorter); - } - - @Override - public String toString() { - return "Sorting(" + childValueSelector + ")"; - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java index a2ef32f9a4..208aeacfa1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java @@ -62,7 +62,7 @@ public final class ValueRangeManager { private final SolutionDescriptor solutionDescriptor; private final @Nullable CountableValueRangeItem[] fromSolution; - private final ReachableValues[] reachableValues; + private final ReachableValues[] reachableValues; private final Map[]> fromEntityMap = new IdentityHashMap<>(); @@ -381,8 +381,18 @@ public CountableValueRange getFromSolution(ValueRangeDescriptor) item.countableValueRange() : null; var valueRangeSorter = item != null ? item.sorter() : null; - // We read and sort the data again if needed + // The phase initialization logic can call operations like countOnSolution or countOnEntity, + // which do not consider sorting and initialize the value range without a sorter. + // Therefore, we return the range if there is no sorter or applied sorter is the same as the given sorter if (valueRange == null || (sorter != null && !Objects.equals(valueRangeSorter, sorter))) { + // We do not recalculate range if they have already been calculated + if (valueRange != null && valueRange instanceof SortableValueRange sortableValueRange) { + var newSortedValueRange = (CountableValueRange) sortableValueRange + .sort(SelectionSorterAdapter.of(solution, sorter)); + var newValueRange = new CountableValueRangeItem<>(newSortedValueRange, sorter); + fromSolution[valueRangeDescriptor.getOrdinal()] = newValueRange; + return newSortedValueRange; + } var extractedValueRange = valueRangeDescriptor. extractAllValues(Objects.requireNonNull(solution)); if (!(extractedValueRange instanceof CountableValueRange countableValueRange)) { throw new UnsupportedOperationException(""" @@ -434,8 +444,18 @@ public CountableValueRange getFromEntity(ValueRangeDescriptor var item = valueRangeList[valueRangeDescriptor.getOrdinal()]; var valueRange = item != null ? (CountableValueRange) item.countableValueRange() : null; var valueRangeSorter = item != null ? item.sorter() : null; - // We read and sort the data again if needed + // The phase initialization logic can call operations like countOnSolution or countOnEntity, + // which do not consider sorting and initialize the value range without a sorter. + // Therefore, we return the range if there is no sorter or applied sorter is the same as the given sorter if (valueRange == null || (sorter != null && !Objects.equals(valueRangeSorter, sorter))) { + // We do not recalculate range if they have already been calculated + if (valueRange != null && valueRange instanceof SortableValueRange sortableValueRange) { + var newSortedValueRange = (CountableValueRange) sortableValueRange + .sort(SelectionSorterAdapter.of(cachedWorkingSolution, sorter)); + var newValueRange = new CountableValueRangeItem<>(newSortedValueRange, sorter); + valueRangeList[valueRangeDescriptor.getOrdinal()] = newValueRange; + return newSortedValueRange; + } var extractedValueRange = valueRangeDescriptor. extractValuesFromEntity(cachedWorkingSolution, Objects.requireNonNull(entity)); if (!(extractedValueRange instanceof CountableValueRange countableValueRange)) { @@ -473,15 +493,28 @@ public long countOnEntity(ValueRangeDescriptor valueRangeDescriptor, .getSize(); } - public ReachableValues getReachableValues(GenuineVariableDescriptor variableDescriptor) { - var values = reachableValues[variableDescriptor.getValueRangeDescriptor().getOrdinal()]; - if (values != null) { + public ReachableValues getReachableValues(GenuineVariableDescriptor variableDescriptor) { + return getReachableValues(variableDescriptor, null); + } + + public ReachableValues getReachableValues(GenuineVariableDescriptor variableDescriptor, + @Nullable SelectionSorter sorter) { + var values = + (ReachableValues) reachableValues[variableDescriptor.getValueRangeDescriptor().getOrdinal()]; + // We return the value if there is no sorter or the applied sorter is the same as the given sorter + if (values != null && (sorter == null || Objects.equals(values.getValueSelectionSorter(), sorter))) { return values; } if (cachedWorkingSolution == null) { throw new IllegalStateException( "Impossible state: value reachability requested before the working solution is known."); } + // We do not recalculate all reachable values if they have already been calculated + if (values != null) { + var newValues = values.copy(SelectionSorterAdapter.of(cachedWorkingSolution, sorter)); + reachableValues[variableDescriptor.getValueRangeDescriptor().getOrdinal()] = newValues; + return newValues; + } var entityDescriptor = variableDescriptor.getEntityDescriptor(); var entityList = entityDescriptor.extractEntities(cachedWorkingSolution); var allValues = getFromSolution(variableDescriptor.getValueRangeDescriptor()); @@ -503,41 +536,42 @@ public ReachableValues getReachableValues(GenuineVariableDescriptor v valueClass = value.getClass(); break; } - Map reachableValuesMap = ConfigUtils.isGenericTypeImmutable(valueClass) + Map> reachableValuesMap = ConfigUtils.isGenericTypeImmutable(valueClass) ? new HashMap<>((int) valuesSize) : new IdentityHashMap<>((int) valuesSize); for (var entity : entityList) { var range = getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); for (var i = 0; i < range.getSize(); i++) { - var value = range.get(i); + var value = (V) range.get(i); if (value == null) { continue; } var item = initReachableMap(reachableValuesMap, value, entityList.size(), (int) valuesSize); - item.addEntity(entity); + item.addEntity((E) entity); for (int j = i + 1; j < range.getSize(); j++) { - var otherValue = range.get(j); + var otherValue = (V) range.get(j); if (otherValue == null) { continue; } item.addValue(otherValue); - var otherValueItem = - initReachableMap(reachableValuesMap, otherValue, entityList.size(), (int) valuesSize); + var otherValueItem = initReachableMap(reachableValuesMap, otherValue, entityList.size(), (int) valuesSize); otherValueItem.addValue(value); } } } - values = new ReachableValues(reachableValuesMap, valueClass, + values = new ReachableValues<>(reachableValuesMap, valueClass, + sorter != null ? SelectionSorterAdapter.of(cachedWorkingSolution, sorter) : null, variableDescriptor.getValueRangeDescriptor().acceptsNullInValueRange()); reachableValues[variableDescriptor.getValueRangeDescriptor().getOrdinal()] = values; return values; } - private static ReachableItemValue initReachableMap(Map reachableValuesMap, Object value, + private static ReachableItemValue initReachableMap(Map> reachableValuesMap, + V value, int entityListSize, int valueListSize) { var item = reachableValuesMap.get(value); if (item == null) { - item = new ReachableItemValue(value, entityListSize, valueListSize); + item = new ReachableItemValue<>(value, entityListSize, valueListSize); reachableValuesMap.put(value, item); } return item; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java index def92dd06f..a4858f9097 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java @@ -1170,6 +1170,36 @@ private static List generateListVariableEntityR var values = new ArrayList(); values.addAll(generateCommonConfiguration()); values.addAll(generateAdvancedListVariableConfiguration(SelectionCacheType.STEP)); + // Corner case where the value selector uses a range-filtering node and also sorts the data + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.DESCENDING)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig().withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + // Will create a range-filtering node and sort the data + .withValueSelectorConfig(new ValueSelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.STEP) + .withSorterManner(ValueSorterManner.DESCENDING)) + .withEntitySelectorConfig(new EntitySelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.STEP) + .withSorterManner(EntitySorterManner.DESCENDING))))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // Since we are starting from decreasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Both are sorted and the expected result won't be affected + true)); return values; } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableValuesTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableValuesTest.java index 315850e0b6..f84ba2679c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableValuesTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableValuesTest.java @@ -122,4 +122,63 @@ void testUnassignedReachableValues() { // Null value is not accepted because the setting allowUnassigned is false assertThat(reachableValues.isValueReachable(v1, null)).isTrue(); } + + @Test + void sortAscendingReachableValues() { + var v1 = new TestdataValue("V1"); + var v2 = new TestdataValue("V2"); + var v3 = new TestdataValue("V3"); + var v4 = new TestdataValue("V4"); + var v5 = new TestdataValue("V5"); + var a = new TestdataAllowsUnassignedEntityProvidingEntity("A", List.of(v3, v2, v1)); + var b = new TestdataAllowsUnassignedEntityProvidingEntity("B", List.of(v3, v2)); + var c = new TestdataAllowsUnassignedEntityProvidingEntity("C", List.of(v5, v4, v3, v2)); + var solution = new TestdataAllowsUnassignedEntityProvidingSolution(); + solution.setEntityList(List.of(a, b, c)); + + var scoreDirector = mockScoreDirector(TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor()); + scoreDirector.setWorkingSolution(solution); + + var solutionDescriptor = scoreDirector.getSolutionDescriptor(); + var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataAllowsUnassignedEntityProvidingEntity.class); + var reachableValues = scoreDirector.getValueRangeManager() + .getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0), + new TestdataObjectSorter()); + + assertThat(reachableValues.extractValuesAsList(v1)).containsExactlyInAnyOrder(v2, v3); + assertThat(reachableValues.extractValuesAsList(v2)).containsExactlyInAnyOrder(v1, v3, v4, v5); + assertThat(reachableValues.extractValuesAsList(v3)).containsExactlyInAnyOrder(v1, v2, v4, v5); + assertThat(reachableValues.extractValuesAsList(v4)).containsExactlyInAnyOrder(v2, v3, v5); + assertThat(reachableValues.extractValuesAsList(v5)).containsExactlyInAnyOrder(v2, v3, v4); + } + + @Test + void sortDescendingReachableValues() { + var v1 = new TestdataValue("V1"); + var v2 = new TestdataValue("V2"); + var v3 = new TestdataValue("V3"); + var v4 = new TestdataValue("V4"); + var v5 = new TestdataValue("V5"); + var a = new TestdataAllowsUnassignedEntityProvidingEntity("A", List.of(v2, v3, v1)); + var b = new TestdataAllowsUnassignedEntityProvidingEntity("B", List.of(v2, v3)); + var c = new TestdataAllowsUnassignedEntityProvidingEntity("C", List.of(v2, v3, v4, v5)); + var solution = new TestdataAllowsUnassignedEntityProvidingSolution(); + solution.setEntityList(List.of(a, b, c)); + + var scoreDirector = mockScoreDirector(TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor()); + scoreDirector.setWorkingSolution(solution); + + var solutionDescriptor = scoreDirector.getSolutionDescriptor(); + var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataAllowsUnassignedEntityProvidingEntity.class); + var reachableValues = scoreDirector.getValueRangeManager() + .getReachableValues(entityDescriptor.getGenuineVariableDescriptorList().get(0), + new TestdataObjectSorter(false)); + + assertThat(reachableValues.extractValuesAsList(v1)).containsExactlyInAnyOrder(v3, v2); + assertThat(reachableValues.extractValuesAsList(v1)).containsExactlyInAnyOrder(v3, v2); + assertThat(reachableValues.extractValuesAsList(v2)).containsExactlyInAnyOrder(v5, v4, v3, v1); + assertThat(reachableValues.extractValuesAsList(v3)).containsExactlyInAnyOrder(v5, v4, v2, v1); + assertThat(reachableValues.extractValuesAsList(v4)).containsExactlyInAnyOrder(v5, v3, v2); + assertThat(reachableValues.extractValuesAsList(v5)).containsExactlyInAnyOrder(v4, v3, v2); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/TestdataObjectSorter.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/TestdataObjectSorter.java index c84bbc896b..d91eceb824 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/TestdataObjectSorter.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/TestdataObjectSorter.java @@ -11,19 +11,38 @@ import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.testdomain.TestdataObject; -public class TestdataObjectSorter implements SelectionSorter { +public class TestdataObjectSorter implements SelectionSorter { + + private final boolean ascending; + + public TestdataObjectSorter() { + this(true); + } + + public TestdataObjectSorter(boolean ascending) { + this.ascending = ascending; + } @Override - public List sort(Object solution, List selectionList) { + public List sort(S solution, List selectionList) { var sortedList = new ArrayList<>(selectionList); - Collections.sort(sortedList, Comparator.comparing(TestdataObject::getCode)); - return sortedList; + var comparator = Comparator.comparing(TestdataObject::getCode); + if (!ascending) { + comparator = comparator.reversed(); + } + var updatedList = new ArrayList<>(sortedList.stream().map(v -> (TestdataObject) v).toList()); + Collections.sort(updatedList, comparator); + return (List) updatedList; } @Override - public SortedSet sort(Object solution, Set selectionSet) { - var sortedSet = new TreeSet(Comparator.comparing(TestdataObject::getCode)); - sortedSet.addAll(selectionSet); - return sortedSet; + public SortedSet sort(S solution, Set selectionSet) { + var comparator = Comparator.comparing(TestdataObject::getCode); + if (!ascending) { + comparator = comparator.reversed(); + } + var sortedSet = new TreeSet<>(comparator); + sortedSet.addAll(selectionSet.stream().map(v -> (TestdataObject) v).toList()); + return (SortedSet) sortedSet; } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java index 8bf8f33544..5c5505e181 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelectorTest.java @@ -52,7 +52,8 @@ public void runCacheType(SelectionCacheType cacheType, int timesCalled) { new TestdataEntity("apr"), new TestdataEntity("may"), new TestdataEntity("jun")); EntitySelector entitySelector = - new SortingEntitySelector(childEntitySelector, cacheType, new TestdataObjectSorter()); + new SortingEntitySelector(childEntitySelector, cacheType, + new TestdataObjectSorter()); SolverScope solverScope = mock(SolverScope.class); InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java index 99e862665f..4250265896 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java @@ -248,7 +248,7 @@ void randomWithEntityValueRange() { var filteringValueRangeSelector = mockIterableFromEntityPropertyValueSelector(valueSelector, true); var replayinValueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v3); checkEntityValueRange(new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, null, true, false), scoreDirector, new TestRandom(1, 1, 1), "C[0]"); // select A for V1 and random pos A[2] @@ -263,7 +263,7 @@ void randomWithEntityValueRange() { // Cause the value iterator return no value at the second call doReturn(List.of(v1).iterator(), Collections.emptyIterator()).when(valueSelector).iterator(); checkEntityValueRange(new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, null, true, false), scoreDirector, new TestRandom(3, 0, 0, 0), "A[2]"); // select B for V1 and random pos B[1] @@ -277,7 +277,7 @@ void randomWithEntityValueRange() { // Cause the value iterator return no value at the second call doReturn(List.of(v2).iterator(), Collections.emptyIterator()).when(valueSelector).iterator(); checkEntityValueRange(new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, null, true, false), scoreDirector, new TestRandom(3, 1, 1, 0), "B[1]"); // select C for V5 and first unpinned pos C[1] @@ -290,7 +290,7 @@ void randomWithEntityValueRange() { // Cause the value iterator return no value at the second call doReturn(List.of(v5).iterator(), Collections.emptyIterator()).when(valueSelector).iterator(); checkEntityValueRange(new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, null, true, false), scoreDirector, new TestRandom(3, 1, 0), "C[1]"); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelectorTest.java similarity index 61% rename from core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelectorTest.java index 05c1cfc5a8..9ef5133c75 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelectorTest.java @@ -1,20 +1,18 @@ -package ai.timefold.solver.core.impl.heuristic.selector.value.decorator; +package ai.timefold.solver.core.impl.heuristic.selector.value; import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfValueSelector; -import static ai.timefold.solver.core.testutil.PlannerAssert.verifyPhaseLifecycle; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.List; + import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; -import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; import ai.timefold.solver.core.impl.heuristic.selector.common.TestdataObjectSorter; -import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.score.director.ValueRangeManager; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -22,49 +20,51 @@ import org.junit.jupiter.api.Test; -class SortingValueSelectorTest { +class IterableFromSolutionPropertyValueSelectorTest { @Test void originalSelectionCacheTypeSolver() { - runOriginalSelection(SelectionCacheType.SOLVER, 1); + runOriginalSelection(SelectionCacheType.SOLVER); } @Test void originalSelectionCacheTypePhase() { - runOriginalSelection(SelectionCacheType.PHASE, 2); + runOriginalSelection(SelectionCacheType.PHASE); } @Test void originalSelectionCacheTypeStep() { - runOriginalSelection(SelectionCacheType.STEP, 5); + runOriginalSelection(SelectionCacheType.STEP); } - public void runOriginalSelection(SelectionCacheType cacheType, int timesCalled) { - IterableValueSelector childValueSelector = SelectorTestUtils.mockIterableValueSelector( - TestdataEntity.class, "value", - new TestdataValue("jan"), new TestdataValue("feb"), new TestdataValue("mar"), - new TestdataValue("apr"), new TestdataValue("may"), new TestdataValue("jun")); - - IterableValueSelector valueSelector = - new SortingValueSelector(childValueSelector, cacheType, new TestdataObjectSorter()); + public void runOriginalSelection(SelectionCacheType cacheType) { + var valueRangeDescriptor = TestdataEntity.buildVariableDescriptorForValue(); + var valueSelector = new IterableFromSolutionPropertyValueSelector(valueRangeDescriptor.getValueRangeDescriptor(), + new TestdataObjectSorter(), cacheType, false); - SolverScope solverScope = mock(SolverScope.class); + var solution = new TestdataSolution(); + solution.setValueList(List.of(new TestdataValue("jan"), new TestdataValue("feb"), new TestdataValue("mar"), + new TestdataValue("apr"), new TestdataValue("may"), new TestdataValue("jun"))); + var solverScope = mock(SolverScope.class); InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); doReturn(scoreDirector).when(solverScope).getScoreDirector(); - doReturn(new TestdataSolution()).when(scoreDirector).getWorkingSolution(); + doReturn(solution).when(scoreDirector).getWorkingSolution(); + doReturn(new ValueRangeManager<>(TestdataSolution.buildSolutionDescriptor())).when(scoreDirector) + .getValueRangeManager(); valueSelector.solvingStarted(solverScope); - AbstractPhaseScope phaseScopeA = mock(AbstractPhaseScope.class); + var phaseScopeA = mock(AbstractPhaseScope.class); when(phaseScopeA.getSolverScope()).thenReturn(solverScope); + when(phaseScopeA.getScoreDirector()).thenReturn(scoreDirector); valueSelector.phaseStarted(phaseScopeA); - AbstractStepScope stepScopeA1 = mock(AbstractStepScope.class); + var stepScopeA1 = mock(AbstractStepScope.class); when(stepScopeA1.getPhaseScope()).thenReturn(phaseScopeA); valueSelector.stepStarted(stepScopeA1); assertAllCodesOfValueSelector(valueSelector, "apr", "feb", "jan", "jun", "mar", "may"); valueSelector.stepEnded(stepScopeA1); - AbstractStepScope stepScopeA2 = mock(AbstractStepScope.class); + var stepScopeA2 = mock(AbstractStepScope.class); when(stepScopeA2.getPhaseScope()).thenReturn(phaseScopeA); valueSelector.stepStarted(stepScopeA2); assertAllCodesOfValueSelector(valueSelector, "apr", "feb", "jan", "jun", "mar", "may"); @@ -72,23 +72,26 @@ public void runOriginalSelection(SelectionCacheType cacheType, int timesCalled) valueSelector.phaseEnded(phaseScopeA); - AbstractPhaseScope phaseScopeB = mock(AbstractPhaseScope.class); + var phaseScopeB = mock(AbstractPhaseScope.class); + when(phaseScopeB.getSolverScope()).thenReturn(solverScope); when(phaseScopeB.getSolverScope()).thenReturn(solverScope); + when(phaseScopeB.getScoreDirector()).thenReturn(scoreDirector); + valueSelector.phaseStarted(phaseScopeB); - AbstractStepScope stepScopeB1 = mock(AbstractStepScope.class); + var stepScopeB1 = mock(AbstractStepScope.class); when(stepScopeB1.getPhaseScope()).thenReturn(phaseScopeB); valueSelector.stepStarted(stepScopeB1); assertAllCodesOfValueSelector(valueSelector, "apr", "feb", "jan", "jun", "mar", "may"); valueSelector.stepEnded(stepScopeB1); - AbstractStepScope stepScopeB2 = mock(AbstractStepScope.class); + var stepScopeB2 = mock(AbstractStepScope.class); when(stepScopeB2.getPhaseScope()).thenReturn(phaseScopeB); valueSelector.stepStarted(stepScopeB2); assertAllCodesOfValueSelector(valueSelector, "apr", "feb", "jan", "jun", "mar", "may"); valueSelector.stepEnded(stepScopeB2); - AbstractStepScope stepScopeB3 = mock(AbstractStepScope.class); + var stepScopeB3 = mock(AbstractStepScope.class); when(stepScopeB3.getPhaseScope()).thenReturn(phaseScopeB); valueSelector.stepStarted(stepScopeB3); assertAllCodesOfValueSelector(valueSelector, "apr", "feb", "jan", "jun", "mar", "may"); @@ -97,10 +100,5 @@ public void runOriginalSelection(SelectionCacheType cacheType, int timesCalled) valueSelector.phaseEnded(phaseScopeB); valueSelector.solvingEnded(solverScope); - - verifyPhaseLifecycle(childValueSelector, 1, 2, 5); - verify(childValueSelector, times(timesCalled)).iterator(); - verify(childValueSelector, times(timesCalled)).getSize(); } - } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactoryTest.java index 0b8c2ddfbb..d21f67077f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactoryTest.java @@ -9,6 +9,7 @@ import java.util.Comparator; import java.util.Iterator; +import java.util.List; import java.util.stream.Stream; import ai.timefold.solver.core.api.score.director.ScoreDirector; @@ -17,6 +18,8 @@ import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; +import ai.timefold.solver.core.impl.domain.valuerange.descriptor.FromEntityPropertyValueRangeDescriptor; +import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; @@ -25,9 +28,9 @@ import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.AssignedListValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.FilteringValueSelector; +import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.IterableFromEntityPropertyValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.ProbabilityValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.ShufflingValueSelector; -import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.SortingValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.UnassignedListValueSelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; @@ -211,55 +214,66 @@ void applyProbability_withSelectionProbabilityWeightFactory() { @Test void applySorting_withSorterComparatorClass() { ValueSelectorConfig valueSelectorConfig = new ValueSelectorConfig() + .withCacheType(SelectionCacheType.PHASE) .withSorterComparatorClass(DummyValueComparator.class); - applySorting(valueSelectorConfig); + applySorting(valueSelectorConfig, true); + applySorting(valueSelectorConfig, false); } @Test void applySorting_withComparatorClass() { ValueSelectorConfig valueSelectorConfig = new ValueSelectorConfig() + .withCacheType(SelectionCacheType.PHASE) .withComparatorClass(DummyValueComparator.class); - applySorting(valueSelectorConfig); + applySorting(valueSelectorConfig, true); + applySorting(valueSelectorConfig, false); } @Test void applySorting_withSorterWeightFactoryClass() { ValueSelectorConfig valueSelectorConfig = new ValueSelectorConfig() + .withCacheType(SelectionCacheType.PHASE) .withSorterWeightFactoryClass(DummySelectionComparatorFactory.class); - applySorting(valueSelectorConfig); + applySorting(valueSelectorConfig, true); + applySorting(valueSelectorConfig, false); } @Test void applySorting_withComparatorFactoryClass() { ValueSelectorConfig valueSelectorConfig = new ValueSelectorConfig() + .withCacheType(SelectionCacheType.PHASE) .withComparatorFactoryClass(DummySelectionComparatorFactory.class); - applySorting(valueSelectorConfig); + applySorting(valueSelectorConfig, true); + applySorting(valueSelectorConfig, false); } - private void applySorting(ValueSelectorConfig valueSelectorConfig) { + private void applySorting(ValueSelectorConfig valueSelectorConfig, boolean canExtractValueRangeFromSolution) { ValueSelectorFactory valueSelectorFactory = ValueSelectorFactory.create(valueSelectorConfig); valueSelectorFactory.validateSorting(SelectionOrder.SORTED); - - ValueSelector baseValueSelector = mock(IterableValueSelector.class); + EntityDescriptor entityDescriptor = mock(EntityDescriptor.class); GenuineVariableDescriptor variableDescriptor = mock(GenuineVariableDescriptor.class); - when(baseValueSelector.getVariableDescriptor()).thenReturn(variableDescriptor); - when(variableDescriptor.canExtractValueRangeFromSolution()).thenReturn(true); - - ValueSelector resultingValueSelector = - valueSelectorFactory.applySorting(SelectionCacheType.PHASE, SelectionOrder.SORTED, baseValueSelector, - ClassInstanceCache.create()); - assertThat(resultingValueSelector).isExactlyInstanceOf(SortingValueSelector.class); - } - - @Test - void applySortingFailsFast_withoutAnySorter() { - ValueSelectorFactory valueSelectorFactory = ValueSelectorFactory.create(new ValueSelectorConfig()); - ValueSelector baseValueSelector = mock(ValueSelector.class); - assertThatIllegalArgumentException().isThrownBy( - () -> valueSelectorFactory.applySorting(SelectionCacheType.PHASE, SelectionOrder.SORTED, baseValueSelector, - ClassInstanceCache.create())) - .withMessageContaining("needs a sorterManner"); + when(entityDescriptor.getGenuineVariableDescriptorList()).thenReturn(List.of(variableDescriptor)); + ValueRangeDescriptor valueRangeDescriptor = mock(FromEntityPropertyValueRangeDescriptor.class); + when(variableDescriptor.getValueRangeDescriptor()).thenReturn(valueRangeDescriptor); + when(valueRangeDescriptor.getVariableDescriptor()).thenReturn(variableDescriptor); + when(valueRangeDescriptor.canExtractValueRangeFromSolution()).thenReturn(canExtractValueRangeFromSolution); + + if (canExtractValueRangeFromSolution) { + IterableFromSolutionPropertyValueSelector baseValueSelector = + (IterableFromSolutionPropertyValueSelector) valueSelectorFactory.buildValueSelector( + buildHeuristicConfigPolicy(), + entityDescriptor, SelectionCacheType.PHASE, SelectionOrder.SORTED); + assertThat(baseValueSelector.getSelectionSorter()).isNotNull(); + assertThat(baseValueSelector.getCacheType()).isEqualTo(SelectionCacheType.PHASE); + } else { + IterableFromEntityPropertyValueSelector baseValueSelector = + (IterableFromEntityPropertyValueSelector) valueSelectorFactory.buildValueSelector( + buildHeuristicConfigPolicy(), + entityDescriptor, SelectionCacheType.PHASE, SelectionOrder.SORTED); + assertThat(baseValueSelector.getChildValueSelector().getSelectionSorter()).isNotNull(); + assertThat(baseValueSelector.getCacheType()).isEqualTo(SelectionCacheType.PHASE); + } } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelectorTest.java new file mode 100644 index 0000000000..33c8f868c7 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelectorTest.java @@ -0,0 +1,183 @@ +package ai.timefold.solver.core.impl.heuristic.selector.value.decorator; + +import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfValueSelector; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.heuristic.selector.common.TestdataObjectSorter; +import ai.timefold.solver.core.impl.heuristic.selector.value.FromEntityPropertyValueSelector; +import ai.timefold.solver.core.impl.heuristic.selector.value.mimic.ManualValueMimicRecorder; +import ai.timefold.solver.core.impl.heuristic.selector.value.mimic.MimicReplayingValueSelector; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.score.director.ValueRangeManager; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution; +import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue; + +import org.junit.jupiter.api.Test; + +class FilteringValueRangeSelectorTest { + + @Test + void originalSelectionCacheTypeSolver() { + runOriginalSelection(SelectionCacheType.SOLVER); + } + + @Test + void originalSelectionCacheTypePhase() { + runOriginalSelection(SelectionCacheType.PHASE); + } + + @Test + void originalSelectionCacheTypeStep() { + runOriginalSelection(SelectionCacheType.STEP); + } + + public void runOriginalSelection(SelectionCacheType cacheType) { + var valueRangeDescriptor = TestdataListEntityProvidingEntity.buildVariableDescriptorForValueList(); + var fromEntityPropertySelector = + new FromEntityPropertyValueSelector<>( + valueRangeDescriptor.getValueRangeDescriptor(), new TestdataObjectSorter<>(), false); + var iterableValueSelector = new IterableFromEntityPropertyValueSelector<>(fromEntityPropertySelector, cacheType, false); + var mimicRecorder = new ManualValueMimicRecorder<>(iterableValueSelector); + var replayingValueSelector = new MimicReplayingValueSelector<>(mimicRecorder); + var valueSelector = new FilteringValueRangeSelector<>(iterableValueSelector, replayingValueSelector, + new TestdataObjectSorter<>(), false, false); + + var solution = new TestdataListEntityProvidingSolution(); + var jan = new TestdataListEntityProvidingValue("jan"); + var feb = new TestdataListEntityProvidingValue("feb"); + var mar = new TestdataListEntityProvidingValue("mar"); + var apr = new TestdataListEntityProvidingValue("apr"); + var may = new TestdataListEntityProvidingValue("may"); + var jun = new TestdataListEntityProvidingValue("jun"); + var firstEntity = new TestdataListEntityProvidingEntity("e1", List.of(jan, feb, mar)); + var secondEntity = new TestdataListEntityProvidingEntity("e2", List.of(apr, may, jun)); + solution.setEntityList(List.of(firstEntity, secondEntity)); + + var solverScope = mock(SolverScope.class); + InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); + doReturn(scoreDirector).when(solverScope).getScoreDirector(); + doReturn(solution).when(scoreDirector).getWorkingSolution(); + doReturn(ValueRangeManager.of(TestdataListEntityProvidingSolution.buildSolutionDescriptor(), solution)) + .when(scoreDirector) + .getValueRangeManager(); + var listVariableSupply = mock(ListVariableStateSupply.class); + doReturn(listVariableSupply).when(scoreDirector).getListVariableStateSupply(any()); + doReturn(TestdataListEntityProvidingEntity.buildVariableDescriptorForValueList()).when(listVariableSupply) + .getSourceVariableDescriptor(); + valueSelector.solvingStarted(solverScope); + + var phaseScopeA = mock(AbstractPhaseScope.class); + when(phaseScopeA.getSolverScope()).thenReturn(solverScope); + when(phaseScopeA.getScoreDirector()).thenReturn(scoreDirector); + valueSelector.phaseStarted(phaseScopeA); + + var stepScopeA1 = mock(AbstractStepScope.class); + when(stepScopeA1.getPhaseScope()).thenReturn(phaseScopeA); + valueSelector.stepStarted(stepScopeA1); + mimicRecorder.setRecordedValue(jan); + assertAllCodesOfValueSelector(valueSelector, 6, "feb", "mar"); + mimicRecorder.setRecordedValue(feb); + assertAllCodesOfValueSelector(valueSelector, 6, "jan", "mar"); + mimicRecorder.setRecordedValue(mar); + assertAllCodesOfValueSelector(valueSelector, 6, "feb", "jan"); + mimicRecorder.setRecordedValue(apr); + assertAllCodesOfValueSelector(valueSelector, 6, "jun", "may"); + mimicRecorder.setRecordedValue(may); + assertAllCodesOfValueSelector(valueSelector, 6, "apr", "jun"); + mimicRecorder.setRecordedValue(jun); + assertAllCodesOfValueSelector(valueSelector, 6, "apr", "may"); + valueSelector.stepEnded(stepScopeA1); + + var stepScopeA2 = mock(AbstractStepScope.class); + when(stepScopeA2.getPhaseScope()).thenReturn(phaseScopeA); + valueSelector.stepStarted(stepScopeA2); + mimicRecorder.setRecordedValue(jan); + assertAllCodesOfValueSelector(valueSelector, 6, "feb", "mar"); + mimicRecorder.setRecordedValue(feb); + assertAllCodesOfValueSelector(valueSelector, 6, "jan", "mar"); + mimicRecorder.setRecordedValue(mar); + assertAllCodesOfValueSelector(valueSelector, 6, "feb", "jan"); + mimicRecorder.setRecordedValue(apr); + assertAllCodesOfValueSelector(valueSelector, 6, "jun", "may"); + mimicRecorder.setRecordedValue(may); + assertAllCodesOfValueSelector(valueSelector, 6, "apr", "jun"); + mimicRecorder.setRecordedValue(jun); + assertAllCodesOfValueSelector(valueSelector, 6, "apr", "may"); + valueSelector.stepEnded(stepScopeA2); + + valueSelector.phaseEnded(phaseScopeA); + + var phaseScopeB = mock(AbstractPhaseScope.class); + when(phaseScopeB.getSolverScope()).thenReturn(solverScope); + when(phaseScopeB.getSolverScope()).thenReturn(solverScope); + when(phaseScopeB.getScoreDirector()).thenReturn(scoreDirector); + + valueSelector.phaseStarted(phaseScopeB); + + var stepScopeB1 = mock(AbstractStepScope.class); + when(stepScopeB1.getPhaseScope()).thenReturn(phaseScopeB); + valueSelector.stepStarted(stepScopeB1); + mimicRecorder.setRecordedValue(jan); + assertAllCodesOfValueSelector(valueSelector, 6, "feb", "mar"); + mimicRecorder.setRecordedValue(feb); + assertAllCodesOfValueSelector(valueSelector, 6, "jan", "mar"); + mimicRecorder.setRecordedValue(mar); + assertAllCodesOfValueSelector(valueSelector, 6, "feb", "jan"); + mimicRecorder.setRecordedValue(apr); + assertAllCodesOfValueSelector(valueSelector, 6, "jun", "may"); + mimicRecorder.setRecordedValue(may); + assertAllCodesOfValueSelector(valueSelector, 6, "apr", "jun"); + mimicRecorder.setRecordedValue(jun); + assertAllCodesOfValueSelector(valueSelector, 6, "apr", "may"); + valueSelector.stepEnded(stepScopeB1); + + var stepScopeB2 = mock(AbstractStepScope.class); + when(stepScopeB2.getPhaseScope()).thenReturn(phaseScopeB); + valueSelector.stepStarted(stepScopeB2); + mimicRecorder.setRecordedValue(jan); + assertAllCodesOfValueSelector(valueSelector, 6, "feb", "mar"); + mimicRecorder.setRecordedValue(feb); + assertAllCodesOfValueSelector(valueSelector, 6, "jan", "mar"); + mimicRecorder.setRecordedValue(mar); + assertAllCodesOfValueSelector(valueSelector, 6, "feb", "jan"); + mimicRecorder.setRecordedValue(apr); + assertAllCodesOfValueSelector(valueSelector, 6, "jun", "may"); + mimicRecorder.setRecordedValue(may); + assertAllCodesOfValueSelector(valueSelector, 6, "apr", "jun"); + mimicRecorder.setRecordedValue(jun); + assertAllCodesOfValueSelector(valueSelector, 6, "apr", "may"); + valueSelector.stepEnded(stepScopeB2); + + var stepScopeB3 = mock(AbstractStepScope.class); + when(stepScopeB3.getPhaseScope()).thenReturn(phaseScopeB); + valueSelector.stepStarted(stepScopeB3); + mimicRecorder.setRecordedValue(jan); + assertAllCodesOfValueSelector(valueSelector, 6, "feb", "mar"); + mimicRecorder.setRecordedValue(feb); + assertAllCodesOfValueSelector(valueSelector, 6, "jan", "mar"); + mimicRecorder.setRecordedValue(mar); + assertAllCodesOfValueSelector(valueSelector, 6, "feb", "jan"); + mimicRecorder.setRecordedValue(apr); + assertAllCodesOfValueSelector(valueSelector, 6, "jun", "may"); + mimicRecorder.setRecordedValue(may); + assertAllCodesOfValueSelector(valueSelector, 6, "apr", "jun"); + mimicRecorder.setRecordedValue(jun); + assertAllCodesOfValueSelector(valueSelector, 6, "apr", "may"); + valueSelector.stepEnded(stepScopeB3); + + valueSelector.phaseEnded(phaseScopeB); + + valueSelector.solvingEnded(solverScope); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelectorTest.java new file mode 100644 index 0000000000..3310a5bb88 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelectorTest.java @@ -0,0 +1,121 @@ +package ai.timefold.solver.core.impl.heuristic.selector.value.decorator; + +import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfIterator; +import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfValueSelector; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; +import ai.timefold.solver.core.impl.heuristic.selector.common.TestdataObjectSorter; +import ai.timefold.solver.core.impl.heuristic.selector.value.FromEntityPropertyValueSelector; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.score.director.ValueRangeManager; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.testdomain.TestdataValue; +import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingSolution; + +import org.junit.jupiter.api.Test; + +class IterableFromEntityPropertyValueSelectorTest { + + @Test + void originalSelectionCacheTypeSolver() { + runOriginalSelection(SelectionCacheType.SOLVER); + } + + @Test + void originalSelectionCacheTypePhase() { + runOriginalSelection(SelectionCacheType.PHASE); + } + + @Test + void originalSelectionCacheTypeStep() { + runOriginalSelection(SelectionCacheType.STEP); + } + + public void runOriginalSelection(SelectionCacheType cacheType) { + var valueRangeDescriptor = TestdataEntityProvidingEntity.buildVariableDescriptorForValue(); + var fromEntityPropertySelector = + new FromEntityPropertyValueSelector<>( + valueRangeDescriptor.getValueRangeDescriptor(), new TestdataObjectSorter<>(), false); + var valueSelector = new IterableFromEntityPropertyValueSelector<>(fromEntityPropertySelector, cacheType, false); + + var solution = new TestdataEntityProvidingSolution(); + var firstEntity = new TestdataEntityProvidingEntity(); + firstEntity.setValueRange(List.of(new TestdataValue("jan"), new TestdataValue("feb"), new TestdataValue("mar"))); + var secondEntity = new TestdataEntityProvidingEntity(); + secondEntity.setValueRange(List.of(new TestdataValue("apr"), new TestdataValue("may"), new TestdataValue("jun"))); + solution.setEntityList(List.of(firstEntity, secondEntity)); + var solverScope = mock(SolverScope.class); + InnerScoreDirector scoreDirector = mock(InnerScoreDirector.class); + doReturn(scoreDirector).when(solverScope).getScoreDirector(); + doReturn(solution).when(scoreDirector).getWorkingSolution(); + doReturn(ValueRangeManager.of(TestdataEntityProvidingSolution.buildSolutionDescriptor(), solution)).when(scoreDirector) + .getValueRangeManager(); + valueSelector.solvingStarted(solverScope); + + var phaseScopeA = mock(AbstractPhaseScope.class); + when(phaseScopeA.getSolverScope()).thenReturn(solverScope); + when(phaseScopeA.getScoreDirector()).thenReturn(scoreDirector); + valueSelector.phaseStarted(phaseScopeA); + + var stepScopeA1 = mock(AbstractStepScope.class); + when(stepScopeA1.getPhaseScope()).thenReturn(phaseScopeA); + valueSelector.stepStarted(stepScopeA1); + assertAllCodesOfValueSelector(valueSelector, "apr", "feb", "jan", "jun", "mar", "may"); + assertAllCodesOfIterator(valueSelector.iterator(firstEntity), "feb", "jan", "mar"); + assertAllCodesOfIterator(valueSelector.iterator(secondEntity), "apr", "jun", "may"); + valueSelector.stepEnded(stepScopeA1); + + var stepScopeA2 = mock(AbstractStepScope.class); + when(stepScopeA2.getPhaseScope()).thenReturn(phaseScopeA); + valueSelector.stepStarted(stepScopeA2); + assertAllCodesOfValueSelector(valueSelector, "apr", "feb", "jan", "jun", "mar", "may"); + assertAllCodesOfIterator(valueSelector.iterator(firstEntity), "feb", "jan", "mar"); + assertAllCodesOfIterator(valueSelector.iterator(secondEntity), "apr", "jun", "may"); + valueSelector.stepEnded(stepScopeA2); + + valueSelector.phaseEnded(phaseScopeA); + + var phaseScopeB = mock(AbstractPhaseScope.class); + when(phaseScopeB.getSolverScope()).thenReturn(solverScope); + when(phaseScopeB.getSolverScope()).thenReturn(solverScope); + when(phaseScopeB.getScoreDirector()).thenReturn(scoreDirector); + + valueSelector.phaseStarted(phaseScopeB); + + var stepScopeB1 = mock(AbstractStepScope.class); + when(stepScopeB1.getPhaseScope()).thenReturn(phaseScopeB); + valueSelector.stepStarted(stepScopeB1); + assertAllCodesOfValueSelector(valueSelector, "apr", "feb", "jan", "jun", "mar", "may"); + assertAllCodesOfIterator(valueSelector.iterator(firstEntity), "feb", "jan", "mar"); + assertAllCodesOfIterator(valueSelector.iterator(secondEntity), "apr", "jun", "may"); + valueSelector.stepEnded(stepScopeB1); + + var stepScopeB2 = mock(AbstractStepScope.class); + when(stepScopeB2.getPhaseScope()).thenReturn(phaseScopeB); + valueSelector.stepStarted(stepScopeB2); + assertAllCodesOfValueSelector(valueSelector, "apr", "feb", "jan", "jun", "mar", "may"); + assertAllCodesOfIterator(valueSelector.iterator(firstEntity), "feb", "jan", "mar"); + assertAllCodesOfIterator(valueSelector.iterator(secondEntity), "apr", "jun", "may"); + valueSelector.stepEnded(stepScopeB2); + + var stepScopeB3 = mock(AbstractStepScope.class); + when(stepScopeB3.getPhaseScope()).thenReturn(phaseScopeB); + valueSelector.stepStarted(stepScopeB3); + assertAllCodesOfValueSelector(valueSelector, "apr", "feb", "jan", "jun", "mar", "may"); + assertAllCodesOfIterator(valueSelector.iterator(firstEntity), "feb", "jan", "mar"); + assertAllCodesOfIterator(valueSelector.iterator(secondEntity), "apr", "jun", "may"); + valueSelector.stepEnded(stepScopeB3); + + valueSelector.phaseEnded(phaseScopeB); + + valueSelector.solvingEnded(solverScope); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java index 99df7e80ef..6b599139fe 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java @@ -340,10 +340,10 @@ public static ListVariableDescriptor getPin var iterableEntityPropertyValueSelector = mockIterableFromEntityPropertyValueSelector(nonReplaying, randomSelection); // Ensure OptimizedRandomFilteringValueRangeIterator is created for random iterators - return new FilteringValueRangeSelector<>(iterableEntityPropertyValueSelector, replayingValueSelector, + return new FilteringValueRangeSelector<>(iterableEntityPropertyValueSelector, replayingValueSelector, null, randomSelection, assertBothSides); } else { - return new FilteringValueRangeSelector<>(nonReplaying, replayingValueSelector, randomSelection, + return new FilteringValueRangeSelector<>(nonReplaying, replayingValueSelector, null, randomSelection, assertBothSides); } } From b32015e51e436d6cd658a49a5b30dc7f75c230ec Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 5 Nov 2025 13:22:16 -0300 Subject: [PATCH 10/13] chore: address sonar --- .../FilteringEntityByEntitySelector.java | 7 ++++--- .../FilteringEntityByValueSelector.java | 16 +++++++++------- .../decorator/FilteringValueRangeSelector.java | 17 ++++++++++------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByEntitySelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByEntitySelector.java index 4d56c08311..019d8ddd68 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByEntitySelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByEntitySelector.java @@ -72,7 +72,7 @@ public final class FilteringEntityByEntitySelector extends AbstractDe private Object replayedEntity; private BasicVariableDescriptor[] basicVariableDescriptors; private ValueRangeManager valueRangeManager; - private ReachableValues reachableValues; + private ReachableValues reachableValues; private List allEntities; public FilteringEntityByEntitySelector(EntitySelector childEntitySelector, @@ -419,7 +419,7 @@ private static class SingleVariableRandomFilteringValueRangeIterator private final Iterator allEntitiesIterator; private final BasicVariableDescriptor basicVariableDescriptor; - private final ReachableValues reachableValues; + private final ReachableValues reachableValues; private final Random workingRandom; private final int maxBailoutSize; private Object currentReplayedEntity = null; @@ -429,7 +429,8 @@ private static class SingleVariableRandomFilteringValueRangeIterator private SingleVariableRandomFilteringValueRangeIterator(Supplier upcomingEntitySupplier, Iterator allEntitiesIterator, BasicVariableDescriptor[] basicVariableDescriptors, - ValueRangeManager valueRangeManager, ReachableValues reachableValues, Random workingRandom, + ValueRangeManager valueRangeManager, ReachableValues reachableValues, + Random workingRandom, int maxBailoutSize) { super(upcomingEntitySupplier, basicVariableDescriptors, valueRangeManager); this.allEntitiesIterator = allEntitiesIterator; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByValueSelector.java index e720ee5e86..a502a9d159 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByValueSelector.java @@ -69,7 +69,7 @@ public final class FilteringEntityByValueSelector extends AbstractDem private final boolean randomSelection; private Object replayedValue; - private ReachableValues reachableValues; + private ReachableValues reachableValues; private long entitiesSize; public FilteringEntityByValueSelector(EntitySelector childEntitySelector, @@ -187,10 +187,11 @@ public int hashCode() { private static class OriginalFilteringValueRangeIterator extends UpcomingSelectionIterator { private final Supplier upcomingValueSupplier; - private final ReachableValues reachableValues; + private final ReachableValues reachableValues; private Iterator valueIterator; - private OriginalFilteringValueRangeIterator(Supplier upcomingValueSupplier, ReachableValues reachableValues) { + private OriginalFilteringValueRangeIterator(Supplier upcomingValueSupplier, + ReachableValues reachableValues) { this.reachableValues = Objects.requireNonNull(reachableValues); this.upcomingValueSupplier = Objects.requireNonNull(upcomingValueSupplier); } @@ -222,11 +223,11 @@ private static class OriginalFilteringValueRangeListIterator extends UpcomingSel private final Supplier upcomingValueSupplier; private final ListIterator entityIterator; - private final ReachableValues reachableValues; + private final ReachableValues reachableValues; private Object replayedValue; private OriginalFilteringValueRangeListIterator(Supplier upcomingValueSupplier, - ListIterator entityIterator, ReachableValues reachableValues) { + ListIterator entityIterator, ReachableValues reachableValues) { this.upcomingValueSupplier = upcomingValueSupplier; this.entityIterator = entityIterator; this.reachableValues = reachableValues; @@ -283,12 +284,13 @@ protected Object createPreviousSelection() { private static class RandomFilteringValueRangeIterator implements Iterator { private final Supplier upcomingValueSupplier; - private final ReachableValues reachableValues; + private final ReachableValues reachableValues; private final Random workingRandom; private Object currentUpcomingValue; private List entityList; - private RandomFilteringValueRangeIterator(Supplier upcomingValueSupplier, ReachableValues reachableValues, + private RandomFilteringValueRangeIterator(Supplier upcomingValueSupplier, + ReachableValues reachableValues, Random workingRandom) { this.upcomingValueSupplier = upcomingValueSupplier; this.reachableValues = Objects.requireNonNull(reachableValues); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java index 94f74b5097..42d26bf8d8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java @@ -76,13 +76,13 @@ public final class FilteringValueRangeSelector extends AbstractDemand private final IterableValueSelector nonReplayingValueSelector; private final IterableValueSelector replayingValueSelector; - private final SelectionSorter selectionSorter; + private final SelectionSorter selectionSorter; private final boolean randomSelection; private Object replayedValue = null; private long valuesSize; private ListVariableStateSupply listVariableStateSupply; - private ReachableValues reachableValues; + private ReachableValues reachableValues; private final boolean checkSourceAndDestination; @@ -91,7 +91,7 @@ public FilteringValueRangeSelector(IterableValueSelector nonReplaying boolean randomSelection, boolean checkSourceAndDestination) { this.nonReplayingValueSelector = nonReplayingValueSelector; this.replayingValueSelector = replayingValueSelector; - this.selectionSorter = selectionSorter; + this.selectionSorter = (SelectionSorter) selectionSorter; this.randomSelection = randomSelection; this.checkSourceAndDestination = checkSourceAndDestination; } @@ -216,7 +216,7 @@ public int hashCode() { private abstract class AbstractFilteringValueRangeIterator implements Iterator { private final Supplier upcomingValueSupplier; private final ListVariableStateSupply listVariableStateSupply; - private final ReachableValues reachableValues; + private final ReachableValues reachableValues; private final boolean checkSourceAndDestination; private boolean initialized = false; private boolean hasData = false; @@ -227,7 +227,8 @@ private abstract class AbstractFilteringValueRangeIterator implements Iterator currentUpcomingList; - AbstractFilteringValueRangeIterator(Supplier upcomingValueSupplier, ReachableValues reachableValues, + AbstractFilteringValueRangeIterator(Supplier upcomingValueSupplier, + ReachableValues reachableValues, ListVariableStateSupply listVariableStateSupply, boolean checkSourceAndDestination) { this.upcomingValueSupplier = upcomingValueSupplier; this.reachableValues = Objects.requireNonNull(reachableValues); @@ -321,7 +322,8 @@ private class OriginalFilteringValueRangeIterator extends AbstractFilteringValue private Iterator reachableValueIterator; private Object selected = null; - private OriginalFilteringValueRangeIterator(Supplier upcomingValueSupplier, ReachableValues reachableValues, + private OriginalFilteringValueRangeIterator(Supplier upcomingValueSupplier, + ReachableValues reachableValues, ListVariableStateSupply listVariableStateSupply, boolean checkSourceAndDestination) { super(upcomingValueSupplier, reachableValues, listVariableStateSupply, checkSourceAndDestination); } @@ -371,7 +373,8 @@ private class RandomFilteringValueRangeIterator extends AbstractFilteringValueRa private Object replayedValue; private List reachableValueList = null; - private RandomFilteringValueRangeIterator(Supplier upcomingValueSupplier, ReachableValues reachableValues, + private RandomFilteringValueRangeIterator(Supplier upcomingValueSupplier, + ReachableValues reachableValues, ListVariableStateSupply listVariableStateSupply, Random workingRandom, boolean checkSourceAndDestination) { super(upcomingValueSupplier, reachableValues, listVariableStateSupply, checkSourceAndDestination); From a5334f0c4310293c1ea5efe72aa4707f86cc61ca Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 5 Nov 2025 14:21:43 -0300 Subject: [PATCH 11/13] chore: improve value selector contract --- .../value/FromEntityPropertyValueSelector.java | 2 ++ .../IterableFromSolutionPropertyValueSelector.java | 1 + .../heuristic/selector/value/ValueSelector.java | 13 +++++++++++++ .../selector/value/ValueSelectorFactory.java | 11 +++++------ .../decorator/FilteringValueRangeSelector.java | 11 ++++++++--- .../IterableFromEntityPropertyValueSelector.java | 6 ++++++ .../list/ElementDestinationSelectorTest.java | 8 ++++---- .../decorator/FilteringValueRangeSelectorTest.java | 2 +- .../core/testdomain/list/TestdataListUtils.java | 4 ++-- 9 files changed, 42 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java index 8d679cff9d..fa607f7eff 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java @@ -67,6 +67,8 @@ public void phaseEnded(AbstractPhaseScope phaseScope) { // ************************************************************************ // Worker methods // ************************************************************************ + + @Override public SelectionSorter getSelectionSorter() { return selectionSorter; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java index da0b59e110..a2f42df2e4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java @@ -51,6 +51,7 @@ public SelectionCacheType getCacheType() { return (intrinsicCacheType.compareTo(minimumCacheType) > 0) ? intrinsicCacheType : minimumCacheType; } + @Override public SelectionSorter getSelectionSorter() { return selectionSorter; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelector.java index 6264a04e33..72db0bc39a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelector.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector; import ai.timefold.solver.core.impl.heuristic.selector.IterableSelector; import ai.timefold.solver.core.impl.heuristic.selector.Selector; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; /** * Selects values from the {@link ValueRangeProvider} for a {@link PlanningVariable} annotated property. @@ -51,4 +52,16 @@ public interface ValueSelector extends Selector { */ Iterator endingIterator(Object entity); + /** + * Returns the selection sorter applied to the node. + * By default, it returns null and must be overridden by the child class if necessary. + * + * @return the selection sorter. + * + * @param the sorter value type. + */ + default SelectionSorter getSelectionSorter() { + return null; + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java index 33c13afd4d..e1931444a4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java @@ -136,7 +136,7 @@ public ValueSelector buildValueSelector(HeuristicConfigPolicy applyDowncasting(ValueSelector value return valueSelector; } - public static ValueSelector applyValueRangeFiltering( + public static ValueSelector applyValueRangeFiltering( HeuristicConfigPolicy configPolicy, ValueSelector valueSelector, - SelectionSorter selectionSorter, EntityDescriptor entityDescriptor, - SelectionCacheType minimumCacheType, SelectionOrder selectionOrder, boolean randomSelection, - String entityValueRangeRecorderId, boolean assertBothSides) { + EntityDescriptor entityDescriptor, SelectionCacheType minimumCacheType, SelectionOrder selectionOrder, + boolean randomSelection, String entityValueRangeRecorderId, boolean assertBothSides) { if (entityValueRangeRecorderId == null) { return valueSelector; } @@ -571,7 +570,7 @@ public static ValueSelector applyValueRangeFiltering( (IterableValueSelector) ValueSelectorFactory. create(valueSelectorConfig) .buildValueSelector(configPolicy, entityDescriptor, minimumCacheType, selectionOrder); return new FilteringValueRangeSelector<>((IterableValueSelector) valueSelector, replayingValueSelector, - selectionSorter, randomSelection, assertBothSides); + randomSelection, assertBothSides); } public enum ListValueFilteringType { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java index 42d26bf8d8..81802447b7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java @@ -87,11 +87,11 @@ public final class FilteringValueRangeSelector extends AbstractDemand private final boolean checkSourceAndDestination; public FilteringValueRangeSelector(IterableValueSelector nonReplayingValueSelector, - IterableValueSelector replayingValueSelector, SelectionSorter selectionSorter, - boolean randomSelection, boolean checkSourceAndDestination) { + IterableValueSelector replayingValueSelector, boolean randomSelection, + boolean checkSourceAndDestination) { this.nonReplayingValueSelector = nonReplayingValueSelector; this.replayingValueSelector = replayingValueSelector; - this.selectionSorter = (SelectionSorter) selectionSorter; + this.selectionSorter = nonReplayingValueSelector.getSelectionSorter(); this.randomSelection = randomSelection; this.checkSourceAndDestination = checkSourceAndDestination; } @@ -135,6 +135,11 @@ public IterableValueSelector getChildValueSelector() { return nonReplayingValueSelector; } + @Override + public SelectionSorter getSelectionSorter() { + return nonReplayingValueSelector.getSelectionSorter(); + } + @Override public SelectionCacheType getCacheType() { return nonReplayingValueSelector.getCacheType(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java index 048161ca26..1d8278fb62 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/IterableFromEntityPropertyValueSelector.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.impl.domain.valuerange.descriptor.FromEntityPropertyValueRangeDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.value.FromEntityPropertyValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; @@ -86,6 +87,11 @@ public FromEntityPropertyValueSelector getChildValueSelector() { return childValueSelector; } + @Override + public SelectionSorter getSelectionSorter() { + return childValueSelector.getSelectionSorter(); + } + @Override public SelectionCacheType getCacheType() { return minimumCacheType; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java index 4250265896..99e862665f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java @@ -248,7 +248,7 @@ void randomWithEntityValueRange() { var filteringValueRangeSelector = mockIterableFromEntityPropertyValueSelector(valueSelector, true); var replayinValueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v3); checkEntityValueRange(new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, null, true, false), + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), scoreDirector, new TestRandom(1, 1, 1), "C[0]"); // select A for V1 and random pos A[2] @@ -263,7 +263,7 @@ void randomWithEntityValueRange() { // Cause the value iterator return no value at the second call doReturn(List.of(v1).iterator(), Collections.emptyIterator()).when(valueSelector).iterator(); checkEntityValueRange(new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, null, true, false), + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), scoreDirector, new TestRandom(3, 0, 0, 0), "A[2]"); // select B for V1 and random pos B[1] @@ -277,7 +277,7 @@ void randomWithEntityValueRange() { // Cause the value iterator return no value at the second call doReturn(List.of(v2).iterator(), Collections.emptyIterator()).when(valueSelector).iterator(); checkEntityValueRange(new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, null, true, false), + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), scoreDirector, new TestRandom(3, 1, 1, 0), "B[1]"); // select C for V5 and first unpinned pos C[1] @@ -290,7 +290,7 @@ void randomWithEntityValueRange() { // Cause the value iterator return no value at the second call doReturn(List.of(v5).iterator(), Collections.emptyIterator()).when(valueSelector).iterator(); checkEntityValueRange(new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, null, true, false), + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), scoreDirector, new TestRandom(3, 1, 0), "C[1]"); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelectorTest.java index 33c8f868c7..55316b02ae 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelectorTest.java @@ -51,7 +51,7 @@ public void runOriginalSelection(SelectionCacheType cacheType) { var mimicRecorder = new ManualValueMimicRecorder<>(iterableValueSelector); var replayingValueSelector = new MimicReplayingValueSelector<>(mimicRecorder); var valueSelector = new FilteringValueRangeSelector<>(iterableValueSelector, replayingValueSelector, - new TestdataObjectSorter<>(), false, false); + false, false); var solution = new TestdataListEntityProvidingSolution(); var jan = new TestdataListEntityProvidingValue("jan"); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java index 6b599139fe..99df7e80ef 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java @@ -340,10 +340,10 @@ public static ListVariableDescriptor getPin var iterableEntityPropertyValueSelector = mockIterableFromEntityPropertyValueSelector(nonReplaying, randomSelection); // Ensure OptimizedRandomFilteringValueRangeIterator is created for random iterators - return new FilteringValueRangeSelector<>(iterableEntityPropertyValueSelector, replayingValueSelector, null, + return new FilteringValueRangeSelector<>(iterableEntityPropertyValueSelector, replayingValueSelector, randomSelection, assertBothSides); } else { - return new FilteringValueRangeSelector<>(nonReplaying, replayingValueSelector, null, randomSelection, + return new FilteringValueRangeSelector<>(nonReplaying, replayingValueSelector, randomSelection, assertBothSides); } } From c8c2bd8f51e41e432f2c54dfd7840ca37532ff56 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 5 Nov 2025 16:15:12 -0300 Subject: [PATCH 12/13] chore: new fail-fast --- .../heuristic/selector/value/ValueSelectorFactory.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java index e1931444a4..37585415e5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java @@ -127,6 +127,13 @@ public ValueSelector buildValueSelector(HeuristicConfigPolicy Date: Wed, 5 Nov 2025 16:39:39 -0300 Subject: [PATCH 13/13] chore: add new test --- .../selector/value/ValueSelectorFactory.java | 7 ---- ...DefaultConstructionHeuristicPhaseTest.java | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java index 37585415e5..e1931444a4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java @@ -127,13 +127,6 @@ public ValueSelector buildValueSelector(HeuristicConfigPolicy PlannerTestUtils.solve(solverConfig, solution)) + .hasMessageContaining( + "The nearbySelectorConfig") + .hasMessageContaining( + "Maybe remove difficultyComparatorClass or difficultyWeightFactoryClass from your @PlanningEntity annotation.") + .hasMessageContaining( + "Maybe remove strengthComparatorClass or strengthWeightFactoryClass from your @PlanningVariable annotation.") + .hasMessageContaining( + "Maybe disable nearby selection."); + } + @Test void failMixedModelDefaultConfiguration() { var solverConfig = PlannerTestUtils