Skip to content

Commit e54c4d7

Browse files
committed
First working test
1 parent 86f7f7e commit e54c4d7

File tree

9 files changed

+520
-54
lines changed

9 files changed

+520
-54
lines changed

core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ public final <Stream_ extends AbstractDataStream<Solution_>> Stream_ shareAndAdd
2929
}
3030

3131
protected boolean guaranteesDistinct() {
32-
return true; // Default implementation, can be overridden by subclasses.
32+
if (parent != null) {
33+
// It is generally safe to take this from the parent; if the stream disagrees, it may override.
34+
return parent.guaranteesDistinct();
35+
} else { // Streams need to explicitly opt-in by overriding this method.
36+
return false;
37+
}
3338
}
3439

3540
// ************************************************************************

core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListAssignMove.java

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move;
22

3-
import java.util.Collection;
4-
import java.util.List;
5-
import java.util.Objects;
6-
73
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel;
84
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
95
import ai.timefold.solver.core.preview.api.move.Move;
106
import ai.timefold.solver.core.preview.api.move.MutableSolutionView;
117
import ai.timefold.solver.core.preview.api.move.Rebaser;
8+
import org.jspecify.annotations.NullMarked;
129

13-
import org.jspecify.annotations.NonNull;
10+
import java.util.Collection;
11+
import java.util.List;
12+
import java.util.Objects;
1413

14+
@NullMarked
1515
public final class ListAssignMove<Solution_, Entity_, Value_> extends AbstractMove<Solution_> {
1616

1717
private final PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel;
@@ -31,23 +31,23 @@ public ListAssignMove(PlanningListVariableMetaModel<Solution_, Entity_, Value_>
3131
}
3232

3333
@Override
34-
public void execute(@NonNull MutableSolutionView<Solution_> mutableSolutionView) {
34+
public void execute(MutableSolutionView<Solution_> mutableSolutionView) {
3535
mutableSolutionView.assignValue(variableMetaModel, planningValue, destinationEntity, destinationIndex);
3636
}
3737

3838
@Override
39-
public @NonNull Move<Solution_> rebase(@NonNull Rebaser rebaser) {
40-
return new ListAssignMove<>(variableMetaModel, rebaser.rebase(planningValue),
41-
rebaser.rebase(destinationEntity), destinationIndex);
39+
public Move<Solution_> rebase(Rebaser rebaser) {
40+
return new ListAssignMove<>(variableMetaModel, Objects.requireNonNull(rebaser.rebase(planningValue)),
41+
Objects.requireNonNull(rebaser.rebase(destinationEntity)), destinationIndex);
4242
}
4343

4444
@Override
45-
public @NonNull Collection<?> extractPlanningEntities() {
45+
public Collection<Entity_> extractPlanningEntities() {
4646
return List.of(destinationEntity);
4747
}
4848

4949
@Override
50-
public @NonNull Collection<?> extractPlanningValues() {
50+
public Collection<Value_> extractPlanningValues() {
5151
return List.of(planningValue);
5252
}
5353

@@ -56,8 +56,20 @@ public void execute(@NonNull MutableSolutionView<Solution_> mutableSolutionView)
5656
return List.of(variableMetaModel);
5757
}
5858

59+
public Value_ getPlanningValue() {
60+
return planningValue;
61+
}
62+
63+
public Entity_ getDestinationEntity() {
64+
return destinationEntity;
65+
}
66+
67+
public int getDestinationIndex() {
68+
return destinationIndex;
69+
}
70+
5971
@Override
60-
public @NonNull String toString() {
72+
public String toString() {
6173
return String.format("%s {null -> %s[%d]}", planningValue, destinationEntity, destinationIndex);
6274
}
6375
}

core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
77
import ai.timefold.solver.core.preview.api.move.MutableSolutionView;
88
import ai.timefold.solver.core.preview.api.move.Rebaser;
9-
import org.jspecify.annotations.NonNull;
9+
import org.jspecify.annotations.NullMarked;
10+
import org.jspecify.annotations.Nullable;
1011

1112
import java.util.Collection;
1213
import java.util.Collections;
@@ -22,6 +23,7 @@
2223
*
2324
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
2425
*/
26+
@NullMarked
2527
public final class ListChangeMove<Solution_, Entity_, Value_> extends AbstractMove<Solution_> {
2628

2729
private final PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel;
@@ -30,7 +32,7 @@ public final class ListChangeMove<Solution_, Entity_, Value_> extends AbstractMo
3032
private final Entity_ destinationEntity;
3133
private final int destinationIndex;
3234

33-
private Value_ planningValue;
35+
private @Nullable Value_ planningValue;
3436

3537
/**
3638
* The move removes a planning value element from {@code sourceEntity.listVariable[sourceIndex]}
@@ -118,7 +120,7 @@ private Value_ getMovedValue() {
118120
// ************************************************************************
119121

120122
@Override
121-
public void execute(@NonNull MutableSolutionView<Solution_> solutionView) {
123+
public void execute(MutableSolutionView<Solution_> solutionView) {
122124
if (sourceEntity == destinationEntity) {
123125
planningValue = solutionView.swapValues(variableMetaModel, sourceEntity, sourceIndex, destinationIndex);
124126
} else {
@@ -128,14 +130,14 @@ public void execute(@NonNull MutableSolutionView<Solution_> solutionView) {
128130
}
129131

130132
@Override
131-
public @NonNull ListChangeMove<Solution_, Entity_, Value_> rebase(@NonNull Rebaser rebaser) {
133+
public ListChangeMove<Solution_, Entity_, Value_> rebase(Rebaser rebaser) {
132134
return new ListChangeMove<>(variableMetaModel,
133-
rebaser.rebase(sourceEntity), sourceIndex,
134-
rebaser.rebase(destinationEntity), destinationIndex);
135+
Objects.requireNonNull(rebaser.rebase(sourceEntity)), sourceIndex,
136+
Objects.requireNonNull(rebaser.rebase(destinationEntity)), destinationIndex);
135137
}
136138

137139
@Override
138-
public @NonNull Collection<Object> extractPlanningEntities() {
140+
public Collection<Entity_> extractPlanningEntities() {
139141
if (sourceEntity == destinationEntity) {
140142
return Collections.singleton(sourceEntity);
141143
} else {
@@ -144,23 +146,40 @@ public void execute(@NonNull MutableSolutionView<Solution_> solutionView) {
144146
}
145147

146148
@Override
147-
public @NonNull Collection<Object> extractPlanningValues() {
148-
return Collections.singleton(planningValue);
149+
public Collection<Value_> extractPlanningValues() {
150+
return Collections.singleton(getMovedValue());
149151
}
150152

151153
@Override
152154
protected List<VariableMetaModel<Solution_, ?, ?>> getVariableMetaModels() {
153155
return List.of(variableMetaModel);
154156
}
155157

158+
public Entity_ getSourceEntity() {
159+
return sourceEntity;
160+
}
161+
162+
public int getSourceIndex() {
163+
return sourceIndex;
164+
}
165+
166+
public Entity_ getDestinationEntity() {
167+
return destinationEntity;
168+
}
169+
170+
public int getDestinationIndex() {
171+
return destinationIndex;
172+
}
173+
156174
@Override
157175
public boolean equals(Object o) {
158176
if (this == o)
159177
return true;
160178
if (!(o instanceof ListChangeMove<?, ?, ?> that))
161179
return false;
162180
return sourceIndex == that.sourceIndex && destinationIndex == that.destinationIndex
163-
&& Objects.equals(variableMetaModel, that.variableMetaModel) && Objects.equals(sourceEntity, that.sourceEntity)
181+
&& Objects.equals(variableMetaModel, that.variableMetaModel)
182+
&& Objects.equals(sourceEntity, that.sourceEntity)
164183
&& Objects.equals(destinationEntity, that.destinationEntity);
165184
}
166185

@@ -170,7 +189,7 @@ public int hashCode() {
170189
}
171190

172191
@Override
173-
public @NonNull String toString() {
192+
public String toString() {
174193
return String.format("%s {%s[%d] -> %s[%d]}",
175194
getMovedValue(), sourceEntity, sourceIndex, destinationEntity, destinationIndex);
176195
}

core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
import ai.timefold.solver.core.preview.api.move.Move;
66
import ai.timefold.solver.core.preview.api.move.MutableSolutionView;
77
import ai.timefold.solver.core.preview.api.move.Rebaser;
8-
import org.jspecify.annotations.NonNull;
8+
import org.jspecify.annotations.NullMarked;
99

1010
import java.util.Collection;
1111
import java.util.Collections;
1212
import java.util.List;
1313
import java.util.Objects;
1414

15+
@NullMarked
1516
public final class ListUnassignMove<Solution_, Entity_, Value_> extends AbstractMove<Solution_> {
1617

1718
private final PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel;
@@ -31,23 +32,23 @@ public ListUnassignMove(PlanningListVariableMetaModel<Solution_, Entity_, Value_
3132
}
3233

3334
@Override
34-
public void execute(@NonNull MutableSolutionView<Solution_> solutionView) {
35+
public void execute(MutableSolutionView<Solution_> solutionView) {
3536
solutionView.unassignValue(variableMetaModel, movedValue, sourceEntity, sourceIndex);
3637
}
3738

3839
@Override
39-
public @NonNull Move<Solution_> rebase(@NonNull Rebaser rebaser) {
40-
return new ListUnassignMove<>(variableMetaModel, rebaser.rebase(movedValue), rebaser.rebase(sourceEntity),
41-
sourceIndex);
40+
public Move<Solution_> rebase(Rebaser rebaser) {
41+
return new ListUnassignMove<>(variableMetaModel, Objects.requireNonNull(rebaser.rebase(movedValue)),
42+
Objects.requireNonNull(rebaser.rebase(sourceEntity)), sourceIndex);
4243
}
4344

4445
@Override
45-
public @NonNull Collection<?> extractPlanningEntities() {
46+
public Collection<Entity_> extractPlanningEntities() {
4647
return Collections.singleton(sourceEntity);
4748
}
4849

4950
@Override
50-
public @NonNull Collection<?> extractPlanningValues() {
51+
public Collection<Value_> extractPlanningValues() {
5152
return Collections.singleton(movedValue);
5253
}
5354

@@ -56,6 +57,14 @@ public void execute(@NonNull MutableSolutionView<Solution_> solutionView) {
5657
return List.of(variableMetaModel);
5758
}
5859

60+
public Entity_ getSourceEntity() {
61+
return sourceEntity;
62+
}
63+
64+
public int getSourceIndex() {
65+
return sourceIndex;
66+
}
67+
5968
@Override
6069
public boolean equals(Object o) {
6170
if (this == o)
@@ -72,7 +81,7 @@ public int hashCode() {
7281
}
7382

7483
@Override
75-
public @NonNull String toString() {
84+
public String toString() {
7685
return String.format("%s {%s[%d] -> null}", movedValue, sourceEntity, sourceIndex);
7786
}
7887
}

core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@ public ListChangeMoveProvider(PlanningListVariableMetaModel<Solution_, Entity_,
3636
};
3737
this.noChangeDetectionFilter = (solutionView, value, targetPosition) -> {
3838
var currentPosition = solutionView.getPositionOf(variableMetaModel, Objects.requireNonNull(value));
39-
if (currentPosition.equals(targetPosition)) { // The target position is the same as the current position.
40-
return false;
41-
}
4239
if (!(currentPosition instanceof PositionInList currentPositionInList)) {
43-
// The current position is unassigned, so we can assign the value.
44-
return true;
40+
// The current position is unassigned, which is acceptable if we can assign it to a list.
41+
return targetPosition instanceof PositionInList;
4542
}
4643
if (!(targetPosition instanceof PositionInList targetPositionInList)) {
47-
// The target position is unassigned, so we can unassign the value.
44+
// The target position is unassigned, which is only acceptable if we can unassign the value.
4845
return true;
4946
}
47+
if (Objects.equals(currentPositionInList, targetPositionInList)) {
48+
return false; // No change in position, so no need to create a move.
49+
}
5050
if (currentPositionInList.entity() != targetPositionInList.entity()) {
5151
return true; // Different entities, so we can freely change the position.
5252
}
@@ -76,8 +76,10 @@ public MoveProducer<Solution_> apply(MoveStreamFactory<Solution_> moveStreamFact
7676
.map((solutionView, entity, value) -> {
7777
if (entity == null) { // This will trigger unassignment of the value.
7878
return ElementPosition.unassigned();
79-
} else if (value == null) { // This will trigger assignment of the value at the end of the list.
80-
return ElementPosition.of(entity, solutionView.countValues(variableMetaModel, entity));
79+
}
80+
var valueCount = solutionView.countValues(variableMetaModel, entity);
81+
if (value == null || valueCount == 0) { // This will trigger assignment of the value at the end of the list.
82+
return ElementPosition.of(entity, valueCount);
8183
} else { // This will trigger assignment of the value immediately before this value.
8284
return solutionView.getPositionOf(variableMetaModel, value);
8385
}

core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package ai.timefold.solver.core.preview.api.domain.metamodel;
22

3-
import java.util.List;
4-
53
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
64
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
75
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
8-
96
import org.jspecify.annotations.NullMarked;
107

8+
import java.util.List;
9+
1110
/**
1211
* Represents the meta-model of an entity.
1312
* Gives access to the entity's variable meta-models.
@@ -131,6 +130,15 @@ default boolean hasVariable(String variableName) {
131130
return false;
132131
}
133132

133+
/**
134+
* As defined by {@link #genuineVariable()} ()},
135+
* but only succeeds if the variable is a {@link PlanningVariable basic planning variable}.
136+
*/
137+
@SuppressWarnings("unchecked")
138+
default <Value_> PlanningVariableMetaModel<Solution_, Entity_, Value_> planningVariable() {
139+
return (PlanningVariableMetaModel<Solution_, Entity_, Value_>) genuineVariable();
140+
}
141+
134142
/**
135143
* As defined by {@link #variable(String)},
136144
* but only succeeds if the variable is a {@link PlanningVariable basic planning variable}.
@@ -140,6 +148,15 @@ default <Value_> PlanningVariableMetaModel<Solution_, Entity_, Value_> planningV
140148
return (PlanningVariableMetaModel<Solution_, Entity_, Value_>) variable(variableName);
141149
}
142150

151+
/**
152+
* As defined by {@link #genuineVariable()},
153+
* but only succeeds if the variable is a {@link PlanningListVariable planning list variable}.
154+
*/
155+
@SuppressWarnings("unchecked")
156+
default <Value_> PlanningListVariableMetaModel<Solution_, Entity_, Value_> planningListVariable() {
157+
return (PlanningListVariableMetaModel<Solution_, Entity_, Value_>) genuineVariable();
158+
}
159+
143160
/**
144161
* As defined by {@link #variable(String)},
145162
* but only succeeds if the variable is a {@link PlanningListVariable planning list variable}.

core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
class ChangeMoveProviderTest {
3838

3939
@Test
40-
void fromSolutionBasicVariable() {
40+
void fromSolution() {
4141
var solutionDescriptor = TestdataSolution.buildSolutionDescriptor();
4242
var variableMetaModel = solutionDescriptor.getMetaModel()
4343
.entity(TestdataEntity.class)
@@ -100,7 +100,7 @@ void fromSolutionBasicVariable() {
100100
}
101101

102102
@Test
103-
void fromSolutionBasicVariableIncompleteValueRange() {
103+
void fromSolutionIncompleteValueRange() {
104104
var solutionDescriptor = TestdataIncompleteValueRangeSolution.buildSolutionDescriptor();
105105
var variableMetaModel = solutionDescriptor.getMetaModel()
106106
.entity(TestdataIncompleteValueRangeEntity.class)
@@ -168,7 +168,7 @@ void fromSolutionBasicVariableIncompleteValueRange() {
168168
}
169169

170170
@Test
171-
void fromEntityBasicVariable() {
171+
void fromEntity() {
172172
var solutionDescriptor = TestdataEntityProvidingSolution.buildSolutionDescriptor();
173173
var variableMetaModel = solutionDescriptor.getMetaModel()
174174
.entity(TestdataEntityProvidingEntity.class)
@@ -209,7 +209,7 @@ void fromEntityBasicVariable() {
209209
}
210210

211211
@Test
212-
void fromEntityBasicVariableAllowsUnassigned() {
212+
void fromEntityAllowsUnassigned() {
213213
var solutionDescriptor = TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor();
214214
var variableMetaModel = solutionDescriptor.getMetaModel()
215215
.entity(TestdataAllowsUnassignedEntityProvidingEntity.class)
@@ -271,7 +271,7 @@ void fromEntityBasicVariableAllowsUnassigned() {
271271
}
272272

273273
@Test
274-
void fromSolutionBasicVariableAllowsUnassigned() {
274+
void fromSolutionAllowsUnassigned() {
275275
var solutionDescriptor = TestdataAllowsUnassignedSolution.buildSolutionDescriptor();
276276
var variableMetaModel = solutionDescriptor.getMetaModel()
277277
.entity(TestdataAllowsUnassignedEntity.class)

0 commit comments

Comments
 (0)