Skip to content

Commit c0bfd3a

Browse files
committed
Finishing touches
1 parent 10e309b commit c0bfd3a

File tree

3 files changed

+46
-38
lines changed

3 files changed

+46
-38
lines changed

core/src/main/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListSwapMoveDefinition.java

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import java.util.Objects;
44
import java.util.function.Function;
55

6-
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
76
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;
87
import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningSolutionMetaModel;
98
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
@@ -16,27 +15,26 @@
1615
import ai.timefold.solver.core.preview.api.move.SolutionView;
1716

1817
import org.jspecify.annotations.NullMarked;
18+
import org.jspecify.annotations.Nullable;
1919

2020
@NullMarked
2121
public class ListSwapMoveDefinition<Solution_, Entity_, Value_>
2222
implements MoveDefinition<Solution_> {
2323

2424
private final PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel;
25-
private final Function<Entity_, Comparable> planningIdGetter;
25+
private final @Nullable Function<Entity_, Comparable> planningIdGetter;
2626

2727
public ListSwapMoveDefinition(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel) {
2828
this.variableMetaModel = Objects.requireNonNull(variableMetaModel);
2929
this.planningIdGetter = getPlanningIdGetter(variableMetaModel.entity().type());
3030
}
3131

32-
private <A> Function<A, Comparable> getPlanningIdGetter(Class<A> sourceClass) {
32+
private <A> @Nullable Function<A, Comparable> getPlanningIdGetter(Class<A> sourceClass) {
3333
SolutionDescriptor<Solution_> solutionDescriptor =
3434
((DefaultPlanningSolutionMetaModel<Solution_>) variableMetaModel.entity().solution()).solutionDescriptor();
3535
MemberAccessor planningIdMemberAccessor = solutionDescriptor.getPlanningIdAccessor(sourceClass);
3636
if (planningIdMemberAccessor == null) {
37-
throw new IllegalArgumentException(
38-
"The fromClass (%s) has no member with a @%s annotation, so the pairs cannot be made unique ([A,B] vs [B,A])."
39-
.formatted(sourceClass, PlanningId.class.getSimpleName()));
37+
return null;
4038
}
4139
return planningIdMemberAccessor.getGetterFunction();
4240
}
@@ -47,17 +45,23 @@ public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactor
4745
.filter((solutionView, value) -> solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList)
4846
.map((solutionView, value) -> new FullElementPosition<>(value,
4947
solutionView.getPositionOf(variableMetaModel, value).ensureAssigned(), planningIdGetter));
50-
// TODO this requires everything that is ever swapped to implement @PlanningID; likely not acceptable
51-
return moveStreamFactory.pick(assignedValueStream)
52-
.pick(assignedValueStream,
53-
EnumeratingJoiners.lessThan(a -> a),
54-
EnumeratingJoiners.filtering(this::isValidSwap))
55-
.asMove((solutionView, leftPosition, rightPosition) -> Moves.swap(leftPosition.elementPosition,
56-
rightPosition.elementPosition, variableMetaModel));
48+
if (planningIdGetter == null) { // If the user hasn't defined a planning ID, we will follow a slower path.
49+
return moveStreamFactory.pick(assignedValueStream)
50+
.pick(assignedValueStream,
51+
EnumeratingJoiners.filtering(this::isValidSwap))
52+
.asMove((solutionView, leftPosition, rightPosition) -> Moves.swap(leftPosition.elementPosition,
53+
rightPosition.elementPosition, variableMetaModel));
54+
} else {
55+
return moveStreamFactory.pick(assignedValueStream)
56+
.pick(assignedValueStream,
57+
EnumeratingJoiners.lessThan(a -> a),
58+
EnumeratingJoiners.filtering(this::isValidSwap))
59+
.asMove((solutionView, leftPosition, rightPosition) -> Moves.swap(leftPosition.elementPosition,
60+
rightPosition.elementPosition, variableMetaModel));
61+
}
5762
}
5863

59-
private boolean isValidSwap(SolutionView<Solution_> solutionView,
60-
FullElementPosition<Entity_, Value_> leftPosition,
64+
private boolean isValidSwap(SolutionView<Solution_> solutionView, FullElementPosition<Entity_, Value_> leftPosition,
6165
FullElementPosition<Entity_, Value_> rightPosition) {
6266
if (Objects.equals(leftPosition, rightPosition)) {
6367
return false;
@@ -68,15 +72,9 @@ private boolean isValidSwap(SolutionView<Solution_> solutionView,
6872

6973
@NullMarked
7074
private record FullElementPosition<Entity_, Value_>(Value_ value, PositionInList elementPosition,
71-
Function<Entity_, Comparable> planningIdGetter) implements Comparable<FullElementPosition<Entity_, Value_>> {
72-
73-
public static <Solution_, Entity_, Value_> FullElementPosition<Entity_, Value_> of(
74-
PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
75-
SolutionView<Solution_> solutionView, Value_ value,
76-
Function<Entity_, Comparable> planningIdGetter) {
77-
var assignedElement = solutionView.getPositionOf(variableMetaModel, value).ensureAssigned();
78-
return new FullElementPosition<>(value, assignedElement, planningIdGetter);
79-
}
75+
@Nullable Function<Entity_, Comparable> planningIdGetter)
76+
implements
77+
Comparable<FullElementPosition<Entity_, Value_>> {
8078

8179
public Entity_ entity() {
8280
return elementPosition.entity();
@@ -88,6 +86,9 @@ public int index() {
8886

8987
@Override
9088
public int compareTo(FullElementPosition<Entity_, Value_> o) {
89+
if (planningIdGetter == null) { // The code will not get here if the getter is null.
90+
throw new IllegalStateException("Impossible state: The planningIdGetter is null, cannot compare entities.");
91+
}
9192
var entityComparison = planningIdGetter.apply(this.entity()).compareTo(planningIdGetter.apply(o.entity()));
9293
if (entityComparison != 0) {
9394
return entityComparison;

core/src/main/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/SwapMoveDefinition.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import java.util.function.Function;
66
import java.util.stream.Stream;
77

8-
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
98
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;
109
import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningSolutionMetaModel;
1110
import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel;
@@ -21,6 +20,7 @@
2120
import ai.timefold.solver.core.preview.api.move.SolutionView;
2221

2322
import org.jspecify.annotations.NullMarked;
23+
import org.jspecify.annotations.Nullable;
2424

2525
@NullMarked
2626
public class SwapMoveDefinition<Solution_, Entity_>
@@ -66,23 +66,29 @@ public SwapMoveDefinition(List<PlanningVariableMetaModel<Solution_, Entity_, Obj
6666
public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactory) {
6767
var entityType = entityMetaModel.type();
6868
var entityStream = moveStreamFactory.forEach(entityType, false);
69-
// TODO this requires everything that is ever swapped to implement @PlanningID; likely not acceptable
70-
return moveStreamFactory.pick(entityStream)
71-
.pick(entityStream,
72-
buildLessThanId(entityType),
73-
EnumeratingJoiners.filtering(this::isValidSwap))
74-
.asMove((solutionView, leftEntity, rightEntity) -> Moves.swap(leftEntity, rightEntity,
75-
variableMetaModelList.toArray(new PlanningVariableMetaModel[0])));
69+
var lessThanIdJoiner = buildLessThanId(entityType);
70+
if (lessThanIdJoiner == null) { // If the user hasn't defined a planning ID, we will follow a slower path.
71+
return moveStreamFactory.pick(entityStream)
72+
.pick(entityStream,
73+
EnumeratingJoiners.filtering(this::isValidSwap))
74+
.asMove((solutionView, leftEntity, rightEntity) -> Moves.swap(leftEntity, rightEntity,
75+
variableMetaModelList.toArray(new PlanningVariableMetaModel[0])));
76+
} else {
77+
return moveStreamFactory.pick(entityStream)
78+
.pick(entityStream,
79+
buildLessThanId(entityType),
80+
EnumeratingJoiners.filtering(this::isValidSwap))
81+
.asMove((solutionView, leftEntity, rightEntity) -> Moves.swap(leftEntity, rightEntity,
82+
variableMetaModelList.toArray(new PlanningVariableMetaModel[0])));
83+
}
7684
}
7785

78-
private <A> DefaultBiEnumeratingJoiner<A, A> buildLessThanId(Class<A> sourceClass) {
86+
private <A> @Nullable DefaultBiEnumeratingJoiner<A, A> buildLessThanId(Class<A> sourceClass) {
7987
SolutionDescriptor<Solution_> solutionDescriptor =
8088
((DefaultPlanningSolutionMetaModel<Solution_>) entityMetaModel.solution()).solutionDescriptor();
8189
MemberAccessor planningIdMemberAccessor = solutionDescriptor.getPlanningIdAccessor(sourceClass);
8290
if (planningIdMemberAccessor == null) {
83-
throw new IllegalArgumentException(
84-
"The fromClass (%s) has no member with a @%s annotation, so the pairs cannot be made unique ([A,B] vs [B,A])."
85-
.formatted(sourceClass, PlanningId.class.getSimpleName()));
91+
return null;
8692
}
8793
Function<A, Comparable> planningIdGetter = planningIdMemberAccessor.getGetterFunction();
8894
return (DefaultBiEnumeratingJoiner<A, A>) EnumeratingJoiners.lessThan(planningIdGetter);

core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/bi/JoinBiEnumeratingStream.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import ai.timefold.solver.core.impl.neighborhood.stream.enumerating.joiner.DefaultBiEnumeratingJoiner;
1818

1919
import org.jspecify.annotations.NullMarked;
20+
import org.jspecify.annotations.Nullable;
2021

2122
@NullMarked
2223
public final class JoinBiEnumeratingStream<Solution_, A, B> extends AbstractBiEnumeratingStream<Solution_, A, B>
@@ -25,11 +26,11 @@ public final class JoinBiEnumeratingStream<Solution_, A, B> extends AbstractBiEn
2526
private final ForeBridgeUniEnumeratingStream<Solution_, A> leftParent;
2627
private final ForeBridgeUniEnumeratingStream<Solution_, B> rightParent;
2728
private final DefaultBiEnumeratingJoiner<A, B> joiner;
28-
private final BiEnumeratingPredicate<Solution_, A, B> filtering;
29+
private final @Nullable BiEnumeratingPredicate<Solution_, A, B> filtering;
2930

3031
public JoinBiEnumeratingStream(EnumeratingStreamFactory<Solution_> enumeratingStreamFactory,
3132
ForeBridgeUniEnumeratingStream<Solution_, A> leftParent, ForeBridgeUniEnumeratingStream<Solution_, B> rightParent,
32-
DefaultBiEnumeratingJoiner<A, B> joiner, BiEnumeratingPredicate<Solution_, A, B> filtering) {
33+
DefaultBiEnumeratingJoiner<A, B> joiner, @Nullable BiEnumeratingPredicate<Solution_, A, B> filtering) {
3334
super(enumeratingStreamFactory);
3435
this.leftParent = leftParent;
3536
this.rightParent = rightParent;

0 commit comments

Comments
 (0)