11package ai .timefold .solver .core .impl .neighborhood .maybeapi .move ;
22
33import java .util .Objects ;
4+ import java .util .function .Function ;
45
6+ import ai .timefold .solver .core .api .domain .lookup .PlanningId ;
7+ import ai .timefold .solver .core .impl .domain .common .accessor .MemberAccessor ;
8+ import ai .timefold .solver .core .impl .domain .solution .descriptor .DefaultPlanningSolutionMetaModel ;
9+ import ai .timefold .solver .core .impl .domain .solution .descriptor .SolutionDescriptor ;
510import ai .timefold .solver .core .impl .neighborhood .maybeapi .MoveDefinition ;
611import ai .timefold .solver .core .impl .neighborhood .maybeapi .MoveStream ;
712import ai .timefold .solver .core .impl .neighborhood .maybeapi .MoveStreamFactory ;
@@ -17,46 +22,59 @@ public class ListSwapMoveDefinition<Solution_, Entity_, Value_>
1722 implements MoveDefinition <Solution_ > {
1823
1924 private final PlanningListVariableMetaModel <Solution_ , Entity_ , Value_ > variableMetaModel ;
25+ private final Function <Entity_ , Comparable > planningIdGetter ;
2026
2127 public ListSwapMoveDefinition (PlanningListVariableMetaModel <Solution_ , Entity_ , Value_ > variableMetaModel ) {
2228 this .variableMetaModel = Objects .requireNonNull (variableMetaModel );
29+ this .planningIdGetter = getPlanningIdGetter (variableMetaModel .entity ().type ());
30+ }
31+
32+ private <A > Function <A , Comparable > getPlanningIdGetter (Class <A > sourceClass ) {
33+ SolutionDescriptor <Solution_ > solutionDescriptor =
34+ ((DefaultPlanningSolutionMetaModel <Solution_ >) variableMetaModel .entity ().solution ()).solutionDescriptor ();
35+ MemberAccessor planningIdMemberAccessor = solutionDescriptor .getPlanningIdAccessor (sourceClass );
36+ 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 ()));
40+ }
41+ return planningIdMemberAccessor .getGetterFunction ();
2342 }
2443
2544 @ Override
2645 public MoveStream <Solution_ > build (MoveStreamFactory <Solution_ > moveStreamFactory ) {
2746 var assignedValueStream = moveStreamFactory .forEach (variableMetaModel .type (), false )
28- .filter ((solutionView ,
29- value ) -> solutionView .getPositionOf (variableMetaModel , value ) instanceof PositionInList );
30- var validAssignedValuePairStream = assignedValueStream .join (assignedValueStream ,
31- EnumeratingJoiners .filtering ((SolutionView <Solution_ > solutionView , Value_ leftValue ,
32- Value_ rightValue ) -> !Objects .equals (leftValue , rightValue )));
33- // Ensure unique pairs; without demanding PlanningId, this becomes tricky.
34- // Convert values to their locations in list.
35- var validAssignedValueUniquePairStream =
36- validAssignedValuePairStream
37- .map ((solutionView , leftValue , rightValue ) -> new UniquePair <>(leftValue , rightValue ))
38- .distinct ()
39- .map ((solutionView , pair ) -> FullElementPosition .of (variableMetaModel , solutionView , pair .first ()),
40- (solutionView , pair ) -> FullElementPosition .of (variableMetaModel , solutionView , pair .second ()));
41- // Eliminate pairs that cannot be swapped due to value range restrictions.
42- var result = validAssignedValueUniquePairStream
43- .filter ((solutionView , leftPosition , rightPosition ) -> solutionView .isValueInRange (variableMetaModel ,
44- rightPosition .entity (), leftPosition .value ())
45- && solutionView .isValueInRange (variableMetaModel , leftPosition .entity (), rightPosition .value ()));
46- // Finally pick the moves.
47- return moveStreamFactory .pick (result )
47+ .filter ((solutionView , value ) -> solutionView .getPositionOf (variableMetaModel , value ) instanceof PositionInList )
48+ .map ((solutionView , value ) -> new FullElementPosition <>(value ,
49+ solutionView .getPositionOf (variableMetaModel , value ).ensureAssigned (), planningIdGetter ));
50+ return moveStreamFactory .pick (assignedValueStream )
51+ .pick (assignedValueStream ,
52+ EnumeratingJoiners .lessThan (a -> a ),
53+ EnumeratingJoiners .filtering (this ::isValidSwap ))
4854 .asMove ((solutionView , leftPosition , rightPosition ) -> Moves .swap (leftPosition .elementPosition ,
4955 rightPosition .elementPosition , variableMetaModel ));
5056 }
5157
58+ private boolean isValidSwap (SolutionView <Solution_ > solutionView ,
59+ FullElementPosition <Entity_ , Value_ > leftPosition ,
60+ FullElementPosition <Entity_ , Value_ > rightPosition ) {
61+ if (Objects .equals (leftPosition , rightPosition )) {
62+ return false ;
63+ }
64+ return solutionView .isValueInRange (variableMetaModel , rightPosition .entity (), leftPosition .value ())
65+ && solutionView .isValueInRange (variableMetaModel , leftPosition .entity (), rightPosition .value ());
66+ }
67+
5268 @ NullMarked
53- private record FullElementPosition <Entity_ , Value_ >(Value_ value , PositionInList elementPosition ) {
69+ private record FullElementPosition <Entity_ , Value_ >(Value_ value , PositionInList elementPosition ,
70+ Function <Entity_ , Comparable > planningIdGetter ) implements Comparable <FullElementPosition <Entity_ , Value_ >> {
5471
5572 public static <Solution_ , Entity_ , Value_ > FullElementPosition <Entity_ , Value_ > of (
5673 PlanningListVariableMetaModel <Solution_ , Entity_ , Value_ > variableMetaModel ,
57- SolutionView <Solution_ > solutionView , Value_ value ) {
74+ SolutionView <Solution_ > solutionView , Value_ value ,
75+ Function <Entity_ , Comparable > planningIdGetter ) {
5876 var assignedElement = solutionView .getPositionOf (variableMetaModel , value ).ensureAssigned ();
59- return new FullElementPosition <>(value , assignedElement );
77+ return new FullElementPosition <>(value , assignedElement , planningIdGetter );
6078 }
6179
6280 public Entity_ entity () {
@@ -67,6 +85,15 @@ public int index() {
6785 return elementPosition .index ();
6886 }
6987
88+ @ Override
89+ public int compareTo (FullElementPosition <Entity_ , Value_ > o ) {
90+ var entityComparison = planningIdGetter .apply (this .entity ()).compareTo (planningIdGetter .apply (o .entity ()));
91+ if (entityComparison != 0 ) {
92+ return entityComparison ;
93+ }
94+ return Integer .compare (this .index (), o .index ());
95+ }
96+
7097 }
7198
7299}
0 commit comments