33import java .util .Objects ;
44import java .util .function .Function ;
55
6- import ai .timefold .solver .core .api .domain .lookup .PlanningId ;
76import ai .timefold .solver .core .impl .domain .common .accessor .MemberAccessor ;
87import ai .timefold .solver .core .impl .domain .solution .descriptor .DefaultPlanningSolutionMetaModel ;
98import ai .timefold .solver .core .impl .domain .solution .descriptor .SolutionDescriptor ;
1615import ai .timefold .solver .core .preview .api .move .SolutionView ;
1716
1817import org .jspecify .annotations .NullMarked ;
18+ import org .jspecify .annotations .Nullable ;
1919
2020@ NullMarked
2121public 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 ;
0 commit comments