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..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,10 @@ 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; /** * Abstract superclass for {@link CountableValueRange} (and therefore {@link ValueRange}). @@ -13,7 +17,8 @@ * @see ValueRange * @see ValueRangeFactory */ -public abstract class AbstractCountableValueRange implements CountableValueRange { +@NullMarked +public abstract class AbstractCountableValueRange implements CountableValueRange, SortableValueRange { /** * Certain optimizations can be applied if {@link Object#equals(Object)} can be relied upon @@ -33,4 +38,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..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,8 @@ 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; /** * Abstract superclass for {@link ValueRange} that is not a {@link CountableValueRange}). @@ -14,6 +16,10 @@ * 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) { + 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..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 @@ -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 boolean trustedValues; - private ValueRangeCache(int size, Set emptyCacheSet) { + private ValueRangeCache(int size, Set emptyCacheSet, boolean trustedValues) { this.valuesWithFastRandomAccess = new ArrayList<>(size); this.valuesWithFastLookup = emptyCacheSet; + this.trustedValues = trustedValues; } - private ValueRangeCache(Collection collection, Set emptyCacheSet) { + private ValueRangeCache(Collection collection, Set emptyCacheSet, boolean trustedValues) { this.valuesWithFastRandomAccess = new ArrayList<>(collection); this.valuesWithFastLookup = emptyCacheSet; this.valuesWithFastLookup.addAll(valuesWithFastRandomAccess); + this.trustedValues = trustedValues; } 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 = sorter.sort(valuesWithFastRandomAccess); + if (trustedValues) { + return Builder.FOR_TRUSTED_VALUES.buildCache(valuesWithFastRandomAccessSorted); + } else { + return Builder.FOR_USER_VALUES.buildCache(valuesWithFastRandomAccessSorted); + } + } + public enum Builder { /** @@ -80,12 +98,12 @@ 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), false); } @Override public ValueRangeCache buildCache(Collection collection) { - return new ValueRangeCache<>(collection, CollectionUtils.newIdentityHashSet(collection.size())); + return new ValueRangeCache<>(collection, CollectionUtils.newIdentityHashSet(collection.size()), false); } }, @@ -100,12 +118,12 @@ 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), true); } @Override public ValueRangeCache buildCache(Collection collection) { - return new ValueRangeCache<>(collection, CollectionUtils.newHashSet(collection.size())); + return new ValueRangeCache<>(collection, CollectionUtils.newHashSet(collection.size()), true); } }; 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..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 @@ -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 ai.timefold.solver.core.impl.heuristic.selector.common.iterator.CachedListRandomIterator; import org.jspecify.annotations.NullMarked; @@ -55,6 +57,12 @@ public boolean contains(@Nullable T value) { return cache.contains(value); } + @Override + public ValueRange sort(ValueRangeSorter sorter) { + var sortedList = sorter.sort(list); + return new ListValueRange<>(sortedList, 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..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 @@ -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,6 +37,11 @@ public CompositeCountableValueRange(List cache, boolean isValueImmutable) { + this.cache = cache; this.isValueImmutable = isValueImmutable; } @@ -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..858f89da16 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/SelectionSorterAdapter.java @@ -0,0 +1,33 @@ +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.SelectionSorter; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record SelectionSorterAdapter(Solution_ solution, + SelectionSorter innerSelectionSorter) implements ValueRangeSorter { + + public static ValueRangeSorter of(Solution_ solution, SelectionSorter selectionSorter) { + return new SelectionSorterAdapter<>(solution, selectionSorter); + } + + @Override + public List sort(List selectionList) { + return innerSelectionSorter.sort(solution, selectionList); + } + + @Override + public SortedSet sort(Set 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/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/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..ca5ae1216e --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/sort/ValueRangeSorter.java @@ -0,0 +1,37 @@ +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.SelectionSorter; + +import org.jspecify.annotations.NullMarked; + +/** + * Basic contract for sorting a range of elements. + * + * @param the value range type + */ +@NullMarked +public interface ValueRangeSorter { + + /** + * Creates a copy of the provided list and sort the data. + * + * @param selectionList never null, a {@link List} of values that will be used as input for sorting. + */ + List 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); + + /** + * @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/common/decorator/ComparatorFactorySelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java index 0a7483c674..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,21 +1,27 @@ 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; 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.api.score.director.ScoreDirector; 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 */ +@NullMarked public final class ComparatorFactorySelectionSorter implements SelectionSorter { private final ComparatorFactory selectionComparatorFactory; @@ -35,10 +41,18 @@ private Comparator getAppliedComparator(Comparator comparator) { } @Override - public void sort(ScoreDirector scoreDirector, List selectionList) { - var appliedComparator = - getAppliedComparator(selectionComparatorFactory.createComparator(scoreDirector.getWorkingSolution())); - selectionList.sort(appliedComparator); + public List sort(Solution_ solution, List selectionList) { + var appliedComparator = getAppliedComparator(selectionComparatorFactory.createComparator(solution)); + var sortedList = new ArrayList<>(selectionList); + sortedList.sort(appliedComparator); + return Collections.unmodifiableList(sortedList); + } + + @Override + public SortedSet sort(Solution_ solution, Set selectionSet) { + var treeSet = new TreeSet<>(getAppliedComparator(selectionComparatorFactory.createComparator(solution))); + treeSet.addAll(selectionSet); + return Collections.unmodifiableSortedSet(treeSet); } @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 7c4305c6f3..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,20 +1,26 @@ 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; 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.api.score.director.ScoreDirector; 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 */ +@NullMarked public final class ComparatorSelectionSorter implements SelectionSorter { private final Comparator appliedComparator; @@ -34,8 +40,17 @@ public ComparatorSelectionSorter(Comparator comparator, SelectionSorterOrder } @Override - public void sort(ScoreDirector scoreDirector, 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 + public SortedSet sort(Solution_ solution, Set selectionSet) { + var treeSet = new TreeSet<>(appliedComparator); + treeSet.addAll(selectionSet); + return Collections.unmodifiableSortedSet(treeSet); } @Override 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..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,13 +1,16 @@ 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; -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 +22,24 @@ * @param the solution type, the class with the {@link PlanningSolution} annotation * @param the selection type */ -@FunctionalInterface +@NullMarked public interface SelectionSorter { /** - * @param scoreDirector never null, the {@link ScoreDirector} - * which has the {@link ScoreDirector#getWorkingSolution()} to which the selections belong or apply to + * 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} + * of {@link PlanningEntity}, planningValue, {@link Move} or {@link Selector} that will be sorted. + */ + 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. */ - void sort(ScoreDirector scoreDirector, List selectionList); + SortedSet sort(Solution_ solution, Set selectionSet); } 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..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 7cb10dd004..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/entity/decorator/SortingEntitySelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java index 7467f25f8b..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(), 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 b9a30af845..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(), 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/FromEntityPropertyValueSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java index 0335433f64..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 @@ -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 @@ -64,6 +68,11 @@ public void phaseEnded(AbstractPhaseScope phaseScope) { // Worker methods // ************************************************************************ + @Override + public SelectionSorter getSelectionSorter() { + return selectionSorter; + } + @Override public GenuineVariableDescriptor getVariableDescriptor() { return valueRangeDescriptor.getVariableDescriptor(); @@ -92,7 +101,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 +116,23 @@ 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 Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor) + && Objects.equals(selectionSorter, that.selectionSorter) + && randomSelection == that.randomSelection; } @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..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 @@ -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 selectionSorter; 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 selectionSorter, SelectionCacheType minimumCacheType, boolean randomSelection) { this.valueRangeDescriptor = valueRangeDescriptor; + this.selectionSorter = selectionSorter; this.minimumCacheType = minimumCacheType; this.randomSelection = randomSelection; valueRangeMightContainEntity = valueRangeDescriptor.mightContainEntity(); @@ -48,6 +51,11 @@ public SelectionCacheType getCacheType() { return (intrinsicCacheType.compareTo(minimumCacheType) > 0) ? intrinsicCacheType : minimumCacheType; } + @Override + public SelectionSorter getSelectionSorter() { + return selectionSorter; + } + // ************************************************************************ // Cache lifecycle methods // ************************************************************************ @@ -57,7 +65,7 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { super.phaseStarted(phaseScope); var scoreDirector = phaseScope.getScoreDirector(); cachedValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor, - scoreDirector.getWorkingSolution()); + scoreDirector.getWorkingSolution(), selectionSorter); if (valueRangeMightContainEntity) { cachedEntityListRevision = scoreDirector.getWorkingEntityListRevision(); cachedEntityListIsDirty = false; @@ -74,7 +82,7 @@ public void stepStarted(AbstractStepScope stepScope) { cachedEntityListIsDirty = true; } else { cachedValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor, - scoreDirector.getWorkingSolution()); + scoreDirector.getWorkingSolution(), selectionSorter); cachedEntityListRevision = scoreDirector.getWorkingEntityListRevision(); } } @@ -153,19 +161,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(selectionSorter, that.selectionSorter) && minimumCacheType == that.minimumCacheType; } @Override public int hashCode() { - return Objects.hash(valueRangeDescriptor, 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/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 73aab920fa..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 @@ -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; @@ -29,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; @@ -122,10 +122,10 @@ public ValueSelector buildValueSelector(HeuristicConfigPolicy buildValueSelector(HeuristicConfigPolicy determineComparatorFactoryClas } } + private SelectionSorter determineSorter(GenuineVariableDescriptor variableDescriptor, + SelectionOrder resolvedSelectionOrder, ClassInstanceCache instanceCache) { + 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; + } + 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 +308,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); } } @@ -322,14 +361,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, @@ -369,59 +408,6 @@ 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())); - } - 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) { 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..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 @@ -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,12 +76,13 @@ 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; @@ -88,6 +91,7 @@ public FilteringValueRangeSelector(IterableValueSelector nonReplaying boolean checkSourceAndDestination) { this.nonReplayingValueSelector = nonReplayingValueSelector; this.replayingValueSelector = replayingValueSelector; + this.selectionSorter = nonReplayingValueSelector.getSelectionSorter(); 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,16 @@ public IterableValueSelector getChildValueSelector() { return nonReplayingValueSelector; } + @Override + public SelectionSorter getSelectionSorter() { + return nonReplayingValueSelector.getSelectionSorter(); + } + + @Override + public SelectionCacheType getCacheType() { + return nonReplayingValueSelector.getCacheType(); + } + @Override public GenuineVariableDescriptor getVariableDescriptor() { return nonReplayingValueSelector.getVariableDescriptor(); @@ -194,19 +208,20 @@ 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 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; @@ -217,7 +232,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); @@ -311,7 +327,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); } @@ -361,7 +378,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); 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 844ecd5e0f..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FromEntitySortingValueSelector.java +++ /dev/null @@ -1,133 +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); - sorter.sort(scoreDirector, 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 4ed4ef375d..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 @@ -2,9 +2,11 @@ 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; +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; @@ -28,13 +30,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 +82,21 @@ public void phaseEnded(AbstractPhaseScope phaseScope) { // ************************************************************************ // Worker methods // ************************************************************************ + + public FromEntityPropertyValueSelector getChildValueSelector() { + return childValueSelector; + } + + @Override + public SelectionSorter getSelectionSorter() { + return childValueSelector.getSelectionSorter(); + } + + @Override + public SelectionCacheType getCacheType() { + return minimumCacheType; + } + @Override public GenuineVariableDescriptor getVariableDescriptor() { return childValueSelector.getVariableDescriptor(); @@ -112,7 +136,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/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 beaae10d4a..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/SortingValueSelector.java +++ /dev/null @@ -1,72 +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); - sorter.sort(solverScope.getScoreDirector(), 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 1b63dcffad..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 @@ -21,20 +21,21 @@ 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.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; 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 +60,10 @@ @NullMarked public final class ValueRangeManager { - private final Logger logger = LoggerFactory.getLogger(getClass()); - private final SolutionDescriptor solutionDescriptor; - private final CountableValueRange[] fromSolution; - private final ReachableValues[] reachableValues; - private final Map[]> fromEntityMap = + private final @Nullable CountableValueRangeItem[] fromSolution; + private final ReachableValues[] reachableValues; + private final Map[]> fromEntityMap = new IdentityHashMap<>(); private @Nullable Solution_ cachedWorkingSolution = null; @@ -86,7 +85,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()]; } @@ -359,11 +358,41 @@ 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) { - var valueRange = fromSolution[valueRangeDescriptor.getOrdinal()]; - if (valueRange == null) { // Avoid computeIfAbsent on the hot path; creates capturing lambda instances. + return getFromSolution(valueRangeDescriptor, solution, null); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + 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; + // 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(""" @@ -376,9 +405,18 @@ public CountableValueRange getFromSolution(ValueRangeDescriptor) sortableValueRange.sort(sorterAdapter); + } + fromSolution[valueRangeDescriptor.getOrdinal()] = new CountableValueRangeItem<>(valueRange, sorter); } - return (CountableValueRange) valueRange; + return valueRange; } /** @@ -386,6 +424,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", "rawtypes" }) + 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 +440,22 @@ 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; + // 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)) { @@ -409,9 +469,18 @@ public CountableValueRange getFromEntity(ValueRangeDescriptor } else { valueRange = countableValueRange; } - valueRangeList[valueRangeDescriptor.getOrdinal()] = valueRange; + 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) sortableValueRange.sort(sorterAdapter); + } + valueRangeList[valueRangeDescriptor.getOrdinal()] = new CountableValueRangeItem<>(valueRange, sorter); } - return (CountableValueRange) valueRange; + return valueRange; } public long countOnSolution(ValueRangeDescriptor valueRangeDescriptor, Solution_ solution) { @@ -424,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()); @@ -454,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; @@ -506,4 +589,9 @@ public void reset(@Nullable Solution_ workingSolution) { } } + private record CountableValueRangeItem(CountableValueRange countableValueRange, + @Nullable SelectionSorter sorter) { + + } + } 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..dfcc9c6217 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 @@ -22,6 +22,7 @@ import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedValuePlacerConfig; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder; +import ai.timefold.solver.core.config.heuristic.selector.common.nearby.NearbySelectionConfig; import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner; import ai.timefold.solver.core.config.heuristic.selector.list.DestinationSelectorConfig; @@ -35,6 +36,7 @@ import ai.timefold.solver.core.testdomain.common.DummyHardSoftEasyScoreCalculator; import ai.timefold.solver.core.testdomain.common.TestdataObjectSortableDescendingComparator; import ai.timefold.solver.core.testdomain.common.TestdataObjectSortableDescendingFactory; +import ai.timefold.solver.core.testdomain.list.TestDistanceMeter; import ai.timefold.solver.core.testdomain.list.TestdataListEntity; import ai.timefold.solver.core.testdomain.list.TestdataListSolution; import ai.timefold.solver.core.testdomain.list.TestdataListValue; @@ -1170,6 +1172,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; } @@ -1594,6 +1626,39 @@ void failConstructionHeuristicBothProperties() { } } + @Test + void failConstructionHeuristicBothNearbyAndSorting() { + // Two comparator properties + var solverConfig = PlannerTestUtils + .buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class) + .withEasyScoreCalculatorClass(TestdataListSolutionEasyScoreCalculator.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedEntityPlacerConfig() + .withEntitySelectorConfig(new EntitySelectorConfig().withId("sortedEntitySelector")) + .withMoveSelectorConfigs(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig( + new EntitySelectorConfig().withMimicSelectorRef("sortedEntitySelector")) + .withValueSelectorConfig(new ValueSelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withComparatorFactoryClass( + TestdataObjectSortableDescendingFactory.class) + .withNearbySelectionConfig(new NearbySelectionConfig() + .withOriginValueSelectorConfig(new ValueSelectorConfig() + .withMimicSelectorRef("sortedEntitySelector")) + .withNearbyDistanceMeterClass(TestDistanceMeter.class)))))); + var solution = new TestdataListSolution(); + assertThatCode(() -> 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 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 { } } 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/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 new file mode 100644 index 0000000000..d91eceb824 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/TestdataObjectSorter.java @@ -0,0 +1,48 @@ +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 { + + private final boolean ascending; + + public TestdataObjectSorter() { + this(true); + } + + public TestdataObjectSorter(boolean ascending) { + this.ascending = ascending; + } + + @Override + public List sort(S solution, List selectionList) { + var sortedList = new ArrayList<>(selectionList); + 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(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/common/decorator/ComparatorFactorySelectionSorterTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java index 3ff74694ab..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 @@ -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); + 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); + 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 a1a0c949ef..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 @@ -4,22 +4,22 @@ 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; 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; 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 org.junit.jupiter.api.Test; @@ -51,11 +51,14 @@ 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); + 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..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 @@ -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; @@ -21,12 +22,13 @@ 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; 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; @@ -104,11 +106,13 @@ 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); + 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/IterableFromSolutionPropertyValueSelectorTest.java similarity index 56% 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 5ce2feb59c..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,69 +1,70 @@ -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.Comparator; +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.decorator.SelectionSorter; -import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; +import ai.timefold.solver.core.impl.heuristic.selector.common.TestdataObjectSorter; 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.TestdataObject; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; 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")); - - SelectionSorter sorter = (scoreDirector, selectionList) -> selectionList - .sort(Comparator.comparing(TestdataObject::getCode)); - IterableValueSelector valueSelector = new SortingValueSelector(childValueSelector, cacheType, sorter); - - SolverScope solverScope = mock(SolverScope.class); + public void runOriginalSelection(SelectionCacheType cacheType) { + var valueRangeDescriptor = TestdataEntity.buildVariableDescriptorForValue(); + var valueSelector = new IterableFromSolutionPropertyValueSelector(valueRangeDescriptor.getValueRangeDescriptor(), + new TestdataObjectSorter(), cacheType, false); + + 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(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"); @@ -71,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"); @@ -96,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..55316b02ae --- /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, + 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/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/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); } 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();