Skip to content

Commit bd1be4b

Browse files
authored
fix: restore support for null values in sortable value ranges (#2238)
1 parent 7ee4fa4 commit bd1be4b

7 files changed

Lines changed: 303 additions & 5 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/NullAllowingValueRange.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public boolean contains(T value) {
5353

5454
@Override
5555
public ValueRange<T> sort(ValueRangeSorter<T> sorter) {
56-
return childValueRange.sort(sorter);
56+
return new NullAllowingValueRange<>(childValueRange.sort(sorter));
5757
}
5858

5959
@Override

core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
import java.util.List;
1313
import java.util.Objects;
1414

15+
import ai.timefold.solver.core.api.score.HardSoftScore;
1516
import ai.timefold.solver.core.api.score.SimpleScore;
1617
import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator;
18+
import ai.timefold.solver.core.api.score.stream.Constraint;
19+
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
20+
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
1721
import ai.timefold.solver.core.api.solver.SolutionManager;
1822
import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig;
1923
import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicType;
@@ -31,6 +35,8 @@
3135
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig;
3236
import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig;
3337
import ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner;
38+
import ai.timefold.solver.core.config.solver.SolverConfig;
39+
import ai.timefold.solver.core.config.solver.termination.TerminationConfig;
3440
import ai.timefold.solver.core.testdomain.TestdataEntity;
3541
import ai.timefold.solver.core.testdomain.TestdataSolution;
3642
import ai.timefold.solver.core.testdomain.TestdataValue;
@@ -55,6 +61,8 @@
5561
import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListEntity;
5662
import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListSolution;
5763
import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListValue;
64+
import ai.timefold.solver.core.testdomain.list.unassignedvar.sort.TestdataAllowsUnassignedListSortableEntity;
65+
import ai.timefold.solver.core.testdomain.list.unassignedvar.sort.TestdataAllowsUnassignedListSortableSolution;
5866
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity;
5967
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingScoreCalculator;
6068
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution;
@@ -82,6 +90,8 @@
8290
import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedEasyScoreCalculator;
8391
import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedEntity;
8492
import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedSolution;
93+
import ai.timefold.solver.core.testdomain.unassignedvar.sort.TestdataAllowsUnassignedSortableEntity;
94+
import ai.timefold.solver.core.testdomain.unassignedvar.sort.TestdataAllowsUnassignedSortableSolution;
8595
import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingEntity;
8696
import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingScoreCalculator;
8797
import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingSolution;
@@ -1328,6 +1338,32 @@ void solveValueFactorySorting(ConstructionHeuristicTestConfig phaseConfig) {
13281338
}
13291339
}
13301340

1341+
@Test
1342+
void penalizeBasicVariable() {
1343+
var solverConfig = new SolverConfig()
1344+
.withSolutionClass(TestdataAllowsUnassignedSortableSolution.class)
1345+
.withEntityClasses(TestdataAllowsUnassignedSortableEntity.class)
1346+
.withConstraintProviderClass(PenalizeAssignedConstraintProvider.class)
1347+
.withPhases(new ConstructionHeuristicPhaseConfig()
1348+
.withTerminationConfig(new TerminationConfig().withStepCountLimit(3)));
1349+
var problem = TestdataAllowsUnassignedSortableSolution.generateSolution(1, 1, false);
1350+
var solution = PlannerTestUtils.solve(solverConfig, problem);
1351+
assertThat(solution.getEntityList().getFirst().getValue()).isNull();
1352+
}
1353+
1354+
@Test
1355+
void penalizeListVariable() {
1356+
var solverConfig = new SolverConfig()
1357+
.withSolutionClass(TestdataAllowsUnassignedListSortableSolution.class)
1358+
.withEntityClasses(TestdataAllowsUnassignedListSortableEntity.class)
1359+
.withConstraintProviderClass(ListPenalizeAssignedConstraintProvider.class)
1360+
.withPhases(new ConstructionHeuristicPhaseConfig()
1361+
.withTerminationConfig(new TerminationConfig().withStepCountLimit(3)));
1362+
var problem = TestdataAllowsUnassignedListSortableSolution.generateSolution(1, 1, false);
1363+
var solution = PlannerTestUtils.solve(solverConfig, problem);
1364+
assertThat(solution.getEntityList().getFirst().getValueList()).isEmpty();
1365+
}
1366+
13311367
@Test
13321368
void failConstructionHeuristicEntityRange() {
13331369
var solverConfig =
@@ -1496,4 +1532,33 @@ private record ConstructionHeuristicTestConfig(ConstructionHeuristicPhaseConfig
14961532
boolean shuffle) {
14971533

14981534
}
1535+
1536+
public static class PenalizeAssignedConstraintProvider implements ConstraintProvider {
1537+
1538+
@Override
1539+
public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) {
1540+
return new Constraint[] { penalizeAssigned(constraintFactory) };
1541+
}
1542+
1543+
Constraint penalizeAssigned(ConstraintFactory constraintFactory) {
1544+
return constraintFactory.forEach(TestdataAllowsUnassignedSortableEntity.class)
1545+
.penalize(HardSoftScore.ONE_HARD)
1546+
.asConstraint("penalize assigned");
1547+
}
1548+
}
1549+
1550+
public static class ListPenalizeAssignedConstraintProvider implements ConstraintProvider {
1551+
1552+
@Override
1553+
public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) {
1554+
return new Constraint[] { penalizeAssigned(constraintFactory) };
1555+
}
1556+
1557+
Constraint penalizeAssigned(ConstraintFactory constraintFactory) {
1558+
return constraintFactory.forEach(TestdataAllowsUnassignedListSortableEntity.class)
1559+
.filter(entity -> !entity.getValueList().isEmpty())
1560+
.penalize(HardSoftScore.ONE_HARD)
1561+
.asConstraint("penalize assigned");
1562+
}
1563+
}
14991564
}

core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/NullAllowingValueRangeTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,16 @@ void sort() {
104104
new ComparatorFactorySelectionSorter<>(solution -> integerComparator, SelectionSorterOrder.DESCENDING));
105105
assertAllElementsOfIterator(new NullAllowingValueRange<>(
106106
(new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(ascComparatorSorter).createOriginalIterator(),
107-
-15, -1, 0, 1, 25);
107+
null, -15, -1, 0, 1, 25);
108108
assertAllElementsOfIterator(new NullAllowingValueRange<>(
109109
(new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(ascComparatorFactorySorter)
110-
.createOriginalIterator(), -15, -1, 0, 1, 25);
110+
.createOriginalIterator(), null, -15, -1, 0, 1, 25);
111111
assertAllElementsOfIterator(new NullAllowingValueRange<>(
112112
(new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(descComparatorSorter).createOriginalIterator(),
113-
25, 1, 0, -1, -15);
113+
null, 25, 1, 0, -1, -15);
114114
assertAllElementsOfIterator(new NullAllowingValueRange<>(
115115
(new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(descComparatorFactorySorter)
116-
.createOriginalIterator(), 25, 1, 0, -1, -15);
116+
.createOriginalIterator(), null, 25, 1, 0, -1, -15);
117117
}
118118

119119
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package ai.timefold.solver.core.testdomain.list.unassignedvar.sort;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
7+
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
8+
import ai.timefold.solver.core.testdomain.TestdataObject;
9+
import ai.timefold.solver.core.testdomain.common.TestSortableObjectComparator;
10+
import ai.timefold.solver.core.testdomain.common.TestdataSortableValue;
11+
12+
@PlanningEntity
13+
public class TestdataAllowsUnassignedListSortableEntity extends TestdataObject {
14+
15+
@PlanningListVariable(allowsUnassignedValues = true, valueRangeProviderRefs = "valueRange",
16+
comparatorClass = TestSortableObjectComparator.class)
17+
private List<TestdataSortableValue> valueList;
18+
19+
public TestdataAllowsUnassignedListSortableEntity() {
20+
}
21+
22+
public TestdataAllowsUnassignedListSortableEntity(String code) {
23+
super(code);
24+
this.valueList = new ArrayList<>();
25+
}
26+
27+
public List<TestdataSortableValue> getValueList() {
28+
return valueList;
29+
}
30+
31+
public void setValueList(List<TestdataSortableValue> valueList) {
32+
this.valueList = valueList;
33+
}
34+
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package ai.timefold.solver.core.testdomain.list.unassignedvar.sort;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.Random;
7+
import java.util.stream.IntStream;
8+
9+
import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
10+
import ai.timefold.solver.core.api.domain.solution.PlanningScore;
11+
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
12+
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
13+
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
14+
import ai.timefold.solver.core.api.score.HardSoftScore;
15+
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
16+
import ai.timefold.solver.core.testdomain.common.TestdataSortableValue;
17+
18+
@PlanningSolution
19+
public class TestdataAllowsUnassignedListSortableSolution {
20+
21+
public static SolutionDescriptor<TestdataAllowsUnassignedListSortableSolution> buildSolutionDescriptor() {
22+
return SolutionDescriptor.buildSolutionDescriptor(
23+
TestdataAllowsUnassignedListSortableSolution.class,
24+
TestdataAllowsUnassignedListSortableEntity.class,
25+
TestdataSortableValue.class);
26+
}
27+
28+
public static TestdataAllowsUnassignedListSortableSolution generateSolution(int valueCount, int entityCount,
29+
boolean shuffle) {
30+
var entityList = new ArrayList<>(IntStream.range(0, entityCount)
31+
.mapToObj(i -> new TestdataAllowsUnassignedListSortableEntity("Generated Entity " + i))
32+
.toList());
33+
var valueList = new ArrayList<>(IntStream.range(0, valueCount)
34+
.mapToObj(i -> new TestdataSortableValue("Generated Value " + i, i))
35+
.toList());
36+
if (shuffle) {
37+
var random = new Random(0);
38+
Collections.shuffle(entityList, random);
39+
Collections.shuffle(valueList, random);
40+
}
41+
TestdataAllowsUnassignedListSortableSolution solution = new TestdataAllowsUnassignedListSortableSolution();
42+
solution.setValueList(valueList);
43+
solution.setEntityList(entityList);
44+
return solution;
45+
}
46+
47+
private List<TestdataSortableValue> valueList;
48+
private List<TestdataAllowsUnassignedListSortableEntity> entityList;
49+
private HardSoftScore score;
50+
51+
@ValueRangeProvider(id = "valueRange")
52+
@ProblemFactCollectionProperty
53+
public List<TestdataSortableValue> getValueList() {
54+
return valueList;
55+
}
56+
57+
public void setValueList(List<TestdataSortableValue> valueList) {
58+
this.valueList = valueList;
59+
}
60+
61+
@PlanningEntityCollectionProperty
62+
public List<TestdataAllowsUnassignedListSortableEntity> getEntityList() {
63+
return entityList;
64+
}
65+
66+
public void setEntityList(List<TestdataAllowsUnassignedListSortableEntity> entityList) {
67+
this.entityList = entityList;
68+
}
69+
70+
@PlanningScore
71+
public HardSoftScore getScore() {
72+
return score;
73+
}
74+
75+
public void setScore(HardSoftScore score) {
76+
this.score = score;
77+
}
78+
79+
public void removeEntity(TestdataAllowsUnassignedListSortableEntity entity) {
80+
this.entityList = entityList.stream()
81+
.filter(e -> e != entity)
82+
.toList();
83+
}
84+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package ai.timefold.solver.core.testdomain.unassignedvar.sort;
2+
3+
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
4+
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
5+
import ai.timefold.solver.core.testdomain.TestdataObject;
6+
import ai.timefold.solver.core.testdomain.common.TestSortableObjectComparator;
7+
import ai.timefold.solver.core.testdomain.common.TestdataSortableValue;
8+
9+
@PlanningEntity
10+
public class TestdataAllowsUnassignedSortableEntity extends TestdataObject {
11+
12+
@PlanningVariable(allowsUnassigned = true, valueRangeProviderRefs = "valueRange",
13+
comparatorClass = TestSortableObjectComparator.class)
14+
private TestdataSortableValue value;
15+
16+
public TestdataAllowsUnassignedSortableEntity() {
17+
}
18+
19+
public TestdataAllowsUnassignedSortableEntity(String code) {
20+
super(code);
21+
}
22+
23+
public TestdataSortableValue getValue() {
24+
return value;
25+
}
26+
27+
public void setValue(TestdataSortableValue value) {
28+
this.value = value;
29+
}
30+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package ai.timefold.solver.core.testdomain.unassignedvar.sort;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.Random;
7+
import java.util.stream.IntStream;
8+
9+
import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
10+
import ai.timefold.solver.core.api.domain.solution.PlanningScore;
11+
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
12+
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
13+
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
14+
import ai.timefold.solver.core.api.score.HardSoftScore;
15+
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
16+
import ai.timefold.solver.core.testdomain.common.TestdataSortableValue;
17+
18+
@PlanningSolution
19+
public class TestdataAllowsUnassignedSortableSolution {
20+
21+
public static SolutionDescriptor<TestdataAllowsUnassignedSortableSolution> buildSolutionDescriptor() {
22+
return SolutionDescriptor.buildSolutionDescriptor(
23+
TestdataAllowsUnassignedSortableSolution.class,
24+
TestdataAllowsUnassignedSortableEntity.class,
25+
TestdataSortableValue.class);
26+
}
27+
28+
public static TestdataAllowsUnassignedSortableSolution generateSolution(int valueCount, int entityCount,
29+
boolean shuffle) {
30+
var entityList = new ArrayList<>(IntStream.range(0, entityCount)
31+
.mapToObj(i -> new TestdataAllowsUnassignedSortableEntity("Generated Entity " + i))
32+
.toList());
33+
var valueList = new ArrayList<>(IntStream.range(0, valueCount)
34+
.mapToObj(i -> new TestdataSortableValue("Generated Value " + i, i))
35+
.toList());
36+
if (shuffle) {
37+
var random = new Random(0);
38+
Collections.shuffle(entityList, random);
39+
Collections.shuffle(valueList, random);
40+
}
41+
TestdataAllowsUnassignedSortableSolution solution = new TestdataAllowsUnassignedSortableSolution();
42+
solution.setValueList(valueList);
43+
solution.setEntityList(entityList);
44+
return solution;
45+
}
46+
47+
private List<TestdataSortableValue> valueList;
48+
private List<TestdataAllowsUnassignedSortableEntity> entityList;
49+
private HardSoftScore score;
50+
51+
@ValueRangeProvider(id = "valueRange")
52+
@ProblemFactCollectionProperty
53+
public List<TestdataSortableValue> getValueList() {
54+
return valueList;
55+
}
56+
57+
public void setValueList(List<TestdataSortableValue> valueList) {
58+
this.valueList = valueList;
59+
}
60+
61+
@PlanningEntityCollectionProperty
62+
public List<TestdataAllowsUnassignedSortableEntity> getEntityList() {
63+
return entityList;
64+
}
65+
66+
public void setEntityList(List<TestdataAllowsUnassignedSortableEntity> entityList) {
67+
this.entityList = entityList;
68+
}
69+
70+
@PlanningScore
71+
public HardSoftScore getScore() {
72+
return score;
73+
}
74+
75+
public void setScore(HardSoftScore score) {
76+
this.score = score;
77+
}
78+
79+
public void removeEntity(TestdataAllowsUnassignedSortableEntity entity) {
80+
this.entityList = entityList.stream()
81+
.filter(e -> e != entity)
82+
.toList();
83+
}
84+
}

0 commit comments

Comments
 (0)