14
14
import ai .timefold .solver .core .preview .api .domain .metamodel .PlanningListVariableMetaModel ;
15
15
import ai .timefold .solver .core .preview .api .domain .metamodel .PositionInList ;
16
16
import ai .timefold .solver .core .preview .api .domain .metamodel .UnassignedElement ;
17
+ import ai .timefold .solver .core .preview .api .move .SolutionView ;
17
18
18
19
import org .jspecify .annotations .NullMarked ;
19
20
21
+ /**
22
+ * For each unassigned value, creates a move to assign it to some position of some list variable.
23
+ * For each assigned value that is not pinned, creates:
24
+ *
25
+ * <ul>
26
+ * <li>A move to unassign it.</li>
27
+ * <li>A move to reassign it to another position if assigned.</li>
28
+ * </ul>
29
+ *
30
+ * To assign or reassign a value, creates:
31
+ *
32
+ * <ul>
33
+ * <li>A move for every unpinned value in every entity's list variable to assign the value before that position.</li>
34
+ * <li>A move for every entity to assign it to the last position in the list variable.</li>
35
+ * </ul>
36
+ *
37
+ * This is a generic move provider that works with any list variable;
38
+ * user-defined change move providers needn't be this complex, as they understand the specifics of the domain.
39
+ */
20
40
@ NullMarked
21
41
public class ListChangeMoveProvider <Solution_ , Entity_ , Value_ >
22
42
implements MoveProvider <Solution_ > {
23
43
24
44
private final PlanningListVariableMetaModel <Solution_ , Entity_ , Value_ > variableMetaModel ;
25
45
private final BiDataFilter <Solution_ , Entity_ , Value_ > isValueInListFilter ;
26
- private final BiDataFilter <Solution_ , Value_ , ElementPosition > validChangeFilter ;
27
46
28
47
public ListChangeMoveProvider (PlanningListVariableMetaModel <Solution_ , Entity_ , Value_ > variableMetaModel ) {
29
48
this .variableMetaModel = Objects .requireNonNull (variableMetaModel );
@@ -36,51 +55,23 @@ public ListChangeMoveProvider(PlanningListVariableMetaModel<Solution_, Entity_,
36
55
}
37
56
return solution .isValueInRange (variableMetaModel , entity , value );
38
57
};
39
- this .validChangeFilter = (solutionView , value , targetPosition ) -> {
40
- var currentPosition = solutionView .getPositionOf (variableMetaModel , value );
41
- if (currentPosition .equals (targetPosition )) { // No change needed.
42
- return false ;
43
- }
44
- if (currentPosition instanceof UnassignedElement ) {
45
- // Only assign the value if the target entity will accept it.
46
- var targetPositionInList = targetPosition .ensureAssigned ();
47
- return solutionView .isValueInRange (variableMetaModel , targetPositionInList .entity (), value );
48
- } else {
49
- if (!(targetPosition instanceof PositionInList targetPositionInList )) { // Unassigning a value.
50
- return true ;
51
- }
52
- var currentPositionInList = currentPosition .ensureAssigned ();
53
- if (currentPositionInList .entity () == targetPositionInList .entity ()) {
54
- var valueCount = solutionView .countValues (variableMetaModel , currentPositionInList .entity ());
55
- if (valueCount == 1 ) {
56
- return false ; // The value is already in the list, and it is the only one.
57
- } else if (targetPositionInList .index () == valueCount ) {
58
- // The value is already in the list, and we are trying to move it past the end of the list.
59
- return false ;
60
- } else { // Same list, same position; ignore.
61
- return currentPositionInList .index () != targetPositionInList .index ();
62
- }
63
- } else { // We can move freely between entities, assuming the target entity accepts the value.
64
- return solutionView .isValueInRange (variableMetaModel , targetPositionInList .entity (), value );
65
- }
66
- }
67
- };
68
58
}
69
59
70
60
@ Override
71
61
public MoveProducer <Solution_ > apply (MoveStreamFactory <Solution_ > moveStreamFactory ) {
72
- // For each unassigned value, we need to create a move to assign it to some position of some list variable.
73
- // For each assigned value that is not pinned, we need to create:
74
- // - A move to unassign it.
75
- // - A move to reassign it to another position if assigned.
76
- // To assign or reassign a value, we need to create:
77
- // - A move for every unpinned value in every entity's list variable to assign the value before that position.
78
- // - A move for every entity to assign it to the last position in the list variable.
62
+ // Stream with unpinned entities;
63
+ // includes null if the variable allows unassigned values.
79
64
var unpinnedEntities =
80
65
moveStreamFactory .enumerate (variableMetaModel .entity ().type (), variableMetaModel .allowsUnassignedValues ());
66
+ // Stream with unpinned values, which are assigned to any list variable;
67
+ // always includes null so that we can later create a position at the end of the list,
68
+ // i.e. with no value after it.
81
69
var unpinnedValues = moveStreamFactory .enumerate (variableMetaModel .type (), true )
82
70
.filter ((solutionView , value ) -> value == null
83
71
|| solutionView .getPositionOf (variableMetaModel , value ) instanceof PositionInList );
72
+ // Joins the two previous streams to create pairs of (entity, value),
73
+ // eliminating values which do not match that entity's value range.
74
+ // It maps these pairs to expected target positions in that entity's list variable.
84
75
var entityValuePairs = unpinnedEntities .join (unpinnedValues , DataJoiners .filtering (isValueInListFilter ))
85
76
.map ((solutionView , entity , value ) -> {
86
77
if (entity == null ) { // Null entity means we need to unassign the value.
@@ -94,8 +85,12 @@ public MoveProducer<Solution_> apply(MoveStreamFactory<Solution_> moveStreamFact
94
85
}
95
86
})
96
87
.distinct ();
88
+ // Finally the stream of these positions is joined with the stream of all existing values,
89
+ // filtering out those which would not result in a valid move.
97
90
var dataStream = moveStreamFactory .enumerate (variableMetaModel .type (), false )
98
- .join (entityValuePairs , DataJoiners .filtering (validChangeFilter ));
91
+ .join (entityValuePairs , DataJoiners .filtering (this ::isValidChange ));
92
+ // When picking from this stream, we decide what kind of move we need to create,
93
+ // based on whether the value is assigned or unassigned.
99
94
return moveStreamFactory .pick (dataStream )
100
95
.asMove ((solutionView , value , targetPosition ) -> {
101
96
var currentPosition = solutionView .getPositionOf (variableMetaModel , Objects .requireNonNull (value ));
@@ -115,4 +110,36 @@ public MoveProducer<Solution_> apply(MoveStreamFactory<Solution_> moveStreamFact
115
110
});
116
111
}
117
112
113
+ private boolean isValidChange (SolutionView <Solution_ > solutionView , Value_ value , ElementPosition targetPosition ) {
114
+ var currentPosition = solutionView .getPositionOf (variableMetaModel , value );
115
+ if (currentPosition .equals (targetPosition )) { // No change needed.
116
+ return false ;
117
+ }
118
+
119
+ if (currentPosition instanceof UnassignedElement ) { // Only assign the value if the target entity will accept it.
120
+ var targetPositionInList = targetPosition .ensureAssigned ();
121
+ return solutionView .isValueInRange (variableMetaModel , targetPositionInList .entity (), value );
122
+ }
123
+
124
+ if (!(targetPosition instanceof PositionInList targetPositionInList )) { // Unassigning a value.
125
+ return true ;
126
+ }
127
+
128
+ var currentPositionInList = currentPosition .ensureAssigned ();
129
+ if (currentPositionInList .entity () == targetPositionInList .entity ()) { // The value is already in the list.
130
+
131
+ var valueCount = solutionView .countValues (variableMetaModel , currentPositionInList .entity ());
132
+ if (valueCount == 1 ) { // The value is the only value in the list; no change.
133
+ return false ;
134
+ } else if (targetPositionInList .index () == valueCount ) { // Trying to move the value past the end of the list.
135
+ return false ;
136
+ } else { // Same list, same position; ignore.
137
+ return currentPositionInList .index () != targetPositionInList .index ();
138
+ }
139
+ }
140
+
141
+ // We can move freely between entities, assuming the target entity accepts the value.
142
+ return solutionView .isValueInRange (variableMetaModel , targetPositionInList .entity (), value );
143
+ }
144
+
118
145
}
0 commit comments