11package ai .timefold .solver .core .impl .move ;
22
3+ import java .util .ArrayList ;
4+ import java .util .Collections ;
35import java .util .List ;
46import java .util .Objects ;
57import java .util .function .BiFunction ;
@@ -197,6 +199,29 @@ public final <Entity_, Value_> Value_ moveValueBetweenLists(
197199 return element ;
198200 }
199201
202+ @ Override
203+ public <Entity_ , Value_ > Value_ replaceValue (PlanningListVariableMetaModel <Solution_ , Entity_ , Value_ > variableMetaModel ,
204+ Entity_ sourceEntity , int sourceIndex , Entity_ destinationEntity , int destinationIndex ) {
205+ if (destinationEntity == sourceEntity ) {
206+ return replaceValue (variableMetaModel , sourceEntity , sourceIndex , destinationIndex );
207+ }
208+
209+ var variableDescriptor = extractVariableDescriptor (variableMetaModel );
210+ var toReplace = (Value_ ) variableDescriptor .getElement (destinationEntity , destinationIndex );
211+ externalScoreDirector .beforeListVariableElementUnassigned (variableDescriptor , toReplace );
212+ externalScoreDirector .beforeListVariableChanged (variableDescriptor , destinationEntity , destinationIndex ,
213+ destinationIndex + 1 );
214+ externalScoreDirector .beforeListVariableChanged (variableDescriptor , sourceEntity , sourceIndex , sourceIndex + 1 );
215+ var toMove = variableDescriptor .removeElement (sourceEntity , sourceIndex );
216+ variableDescriptor .setElement (destinationEntity , destinationIndex , toMove );
217+ externalScoreDirector .afterListVariableChanged (variableDescriptor , sourceEntity , sourceIndex , sourceIndex );
218+ externalScoreDirector .afterListVariableChanged (variableDescriptor , destinationEntity , destinationIndex ,
219+ destinationIndex + 1 );
220+ externalScoreDirector .afterListVariableElementUnassigned (variableDescriptor , toReplace );
221+ externalScoreDirector .triggerVariableListeners ();
222+ return toReplace ;
223+ }
224+
200225 @ SuppressWarnings ("unchecked" )
201226 @ Override
202227 public final <Entity_ , Value_ > Value_ moveValueInList (
@@ -207,32 +232,104 @@ public final <Entity_, Value_> Value_ moveValueInList(
207232 "When moving values in the same list, sourceIndex (%d) and destinationIndex (%d) must be different."
208233 .formatted (sourceIndex , destinationIndex ));
209234 } else if (sourceIndex < 0 || destinationIndex < 0 ) {
210- throw new IllegalArgumentException (
211- "The sourceIndex (%d) and destinationIndex (%d) must both be >= 0."
212- .formatted (sourceIndex , destinationIndex ));
235+ throw new IndexOutOfBoundsException ("The sourceIndex (%d) and destinationIndex (%d) must both be >= 0."
236+ .formatted (sourceIndex , destinationIndex ));
213237 }
214238
215- var fromIndex = Math .min (sourceIndex , destinationIndex );
216- var toIndex = Math .max (sourceIndex , destinationIndex ) + 1 ;
217-
218239 var variableDescriptor = extractVariableDescriptor (variableMetaModel );
219240 var list = variableDescriptor .getValue (sourceEntity );
220241 var listSize = list .size ();
221242 if (sourceIndex >= listSize ) {
222- throw new IllegalArgumentException (
223- "The sourceIndex (%d) must be less than the list size (%d)." .formatted (sourceIndex , listSize ));
224- } else if (destinationIndex > listSize ) { // destinationIndex == listSize is allowed (append to the end of the list)
225- throw new IllegalArgumentException (
226- "The destinationIndex (%d) must be less than or equal to the list size (%d)."
227- .formatted (destinationIndex , listSize ));
243+ throw new IndexOutOfBoundsException ("The sourceIndex (%d) must be less than the list size (%d)."
244+ .formatted (sourceIndex , listSize ));
245+ } else if (destinationIndex >= listSize ) {
246+ throw new IndexOutOfBoundsException ("The destinationIndex (%d) must be less than the list size (%d)."
247+ .formatted (destinationIndex , listSize ));
228248 }
229249
250+ var fromIndex = Math .min (sourceIndex , destinationIndex );
251+ var toIndex = Math .max (sourceIndex , destinationIndex ) + 1 ;
230252 externalScoreDirector .beforeListVariableChanged (variableDescriptor , sourceEntity , fromIndex , toIndex );
231- var element = (Value_ ) list .remove (sourceIndex );
232- list .add (destinationIndex , element );
253+ moveInList (list , sourceIndex , destinationIndex );
233254 externalScoreDirector .afterListVariableChanged (variableDescriptor , sourceEntity , fromIndex , toIndex );
234255 externalScoreDirector .triggerVariableListeners ();
235- return element ;
256+ return (Value_ ) list .get (destinationIndex );
257+ }
258+
259+ @ Override
260+ public <Entity_ , Value_ > Value_ replaceValue (
261+ PlanningListVariableMetaModel <Solution_ , Entity_ , Value_ > variableMetaModel , Entity_ entity , int sourceIndex ,
262+ int destinationIndex ) {
263+ if (sourceIndex == destinationIndex ) {
264+ throw new IllegalArgumentException (
265+ "When replacing values in the same list, sourceIndex (%d) and destinationIndex (%d) must be different."
266+ .formatted (sourceIndex , destinationIndex ));
267+ } else if (sourceIndex < 0 || destinationIndex < 0 ) {
268+ throw new IndexOutOfBoundsException ("The sourceIndex (%d) and destinationIndex (%d) must both be >= 0."
269+ .formatted (sourceIndex , destinationIndex ));
270+ }
271+
272+ var variableDescriptor = extractVariableDescriptor (variableMetaModel );
273+ var fromIndex = Math .min (sourceIndex , destinationIndex );
274+ var toIndex = Math .max (sourceIndex , destinationIndex ) + 1 ;
275+ var list = variableDescriptor .getValue (entity );
276+ var toReplace = (Value_ ) list .get (destinationIndex );
277+ externalScoreDirector .beforeListVariableElementUnassigned (variableDescriptor , toReplace );
278+ externalScoreDirector .beforeListVariableChanged (variableDescriptor , entity , fromIndex , toIndex );
279+ if (destinationIndex > sourceIndex ) {
280+ // Remove from sourceIndex after setting the destination to preserve index validity.
281+ list .set (destinationIndex , list .get (sourceIndex ));
282+ list .remove (sourceIndex );
283+ } else {
284+ list .set (destinationIndex , list .remove (sourceIndex ));
285+ }
286+ externalScoreDirector .afterListVariableChanged (variableDescriptor , entity , fromIndex , toIndex - 1 );
287+ externalScoreDirector .afterListVariableElementUnassigned (variableDescriptor , toReplace );
288+ externalScoreDirector .triggerVariableListeners ();
289+ return toReplace ;
290+ }
291+
292+ /**
293+ * Moves the element at index {@code from} to index {@code to} in a list,
294+ * choosing the faster of two strategies based on the move's distance and position within the list.
295+ *
296+ * <p>
297+ * <b>Strategy selection</b> (lo = min(from, to), d = |from − to|):
298+ * <ul>
299+ * <li>Use {@code Collections.rotate} when {@code d * 8 < n − lo}
300+ * (distance is small relative to the remaining tail).</li>
301+ * <li>Use {@code remove + add} otherwise.</li>
302+ * </ul>
303+ *
304+ * <p>
305+ * <b>Why position matters</b>: {@code remove+add} shifts {@code (n−1−from) + (n−1−to)} elements in total.
306+ * When one endpoint is near the tail, one of those copies is nearly free,
307+ * making {@code remove+add} cheap even for large lists.
308+ * {@code rotate} always pays for the full sublist span,
309+ * so it only wins when that span is short relative to what {@code removeAdd} would have to copy.
310+ *
311+ * <p>
312+ * The threshold constant 8 was determined empirically by benchmarking on HotSpot
313+ * with a microbenchmark that performed moves of varying distances and positions within lists of varying sizes.
314+ *
315+ * @param list the list to mutate; assumes {@link ArrayList}
316+ * @param from index of the element to move
317+ * @param to index the element should occupy after the move
318+ */
319+ private static <T > void moveInList (List <T > list , int from , int to ) {
320+ var distance = Math .abs (from - to );
321+ if (distance == 1 ) {
322+ Collections .swap (list , from , to );
323+ return ;
324+ }
325+ var distanceTimesEight = distance * 8L ; // Long prevents unlikely yet possible overflow.
326+ var lowerIndex = Math .min (from , to );
327+ var tailLength = list .size () - lowerIndex ;
328+ if (distanceTimesEight < tailLength ) {
329+ Collections .rotate (list .subList (lowerIndex , lowerIndex + distance + 1 ), from < to ? -1 : 1 );
330+ } else {
331+ list .add (to , list .remove (from ));
332+ }
236333 }
237334
238335 @ Override
@@ -276,15 +373,11 @@ public <Entity_, Value_> void swapValuesInList(PlanningListVariableMetaModel<Sol
276373 }
277374
278375 var variableDescriptor = extractVariableDescriptor (variableMetaModel );
279- var leftElement = variableDescriptor .getElement (entity , leftIndex );
280- var rightElement = variableDescriptor .getElement (entity , rightIndex );
281-
282376 var fromIndex = Math .min (leftIndex , rightIndex );
283377 var toIndex = Math .max (leftIndex , rightIndex ) + 1 ;
284378 externalScoreDirector .beforeListVariableChanged (variableDescriptor , entity , fromIndex , toIndex );
285379 var list = variableDescriptor .getValue (entity );
286- list .set (leftIndex , rightElement );
287- list .set (rightIndex , leftElement );
380+ Collections .swap (list , leftIndex , rightIndex );
288381 externalScoreDirector .afterListVariableChanged (variableDescriptor , entity , fromIndex , toIndex );
289382 externalScoreDirector .triggerVariableListeners ();
290383 }
0 commit comments