Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ai.timefold.solver.core.impl.move;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
Expand Down Expand Up @@ -197,6 +199,32 @@ public final <Entity_, Value_> Value_ moveValueBetweenLists(
return element;
}

@Override
public <Entity_, Value_> Value_ replaceValueBetweenLists(
PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel, Entity_ sourceEntity, int sourceIndex,
Entity_ replacementEntity, int replacementIndex) {
if (sourceEntity == replacementEntity) {
throw new IllegalArgumentException(
"Source entity (%s) and replacement entity (%s) must be different when replacing values between lists."
.formatted(sourceEntity, replacementEntity));
}

var variableDescriptor = extractVariableDescriptor(variableMetaModel);
var toReplace = (Value_) variableDescriptor.getElement(sourceEntity, sourceIndex);
externalScoreDirector.beforeListVariableElementUnassigned(variableDescriptor, toReplace);
externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex + 1);
externalScoreDirector.beforeListVariableChanged(variableDescriptor, replacementEntity, replacementIndex,
replacementIndex + 1);
var toMove = variableDescriptor.removeElement(replacementEntity, replacementIndex);
variableDescriptor.setElement(sourceEntity, sourceIndex, toMove);
externalScoreDirector.afterListVariableChanged(variableDescriptor, replacementEntity, replacementIndex,
replacementIndex);
externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex + 1);
externalScoreDirector.afterListVariableElementUnassigned(variableDescriptor, toReplace);
externalScoreDirector.triggerVariableListeners();
return toReplace;
}

@SuppressWarnings("unchecked")
@Override
public final <Entity_, Value_> Value_ moveValueInList(
Expand All @@ -207,32 +235,101 @@ public final <Entity_, Value_> Value_ moveValueInList(
"When moving values in the same list, sourceIndex (%d) and destinationIndex (%d) must be different."
.formatted(sourceIndex, destinationIndex));
} else if (sourceIndex < 0 || destinationIndex < 0) {
throw new IllegalArgumentException(
"The sourceIndex (%d) and destinationIndex (%d) must both be >= 0."
.formatted(sourceIndex, destinationIndex));
throw new IndexOutOfBoundsException("The sourceIndex (%d) and destinationIndex (%d) must both be >= 0."
.formatted(sourceIndex, destinationIndex));
}

var fromIndex = Math.min(sourceIndex, destinationIndex);
var toIndex = Math.max(sourceIndex, destinationIndex) + 1;

var variableDescriptor = extractVariableDescriptor(variableMetaModel);
var list = variableDescriptor.getValue(sourceEntity);
var listSize = list.size();
if (sourceIndex >= listSize) {
throw new IllegalArgumentException(
"The sourceIndex (%d) must be less than the list size (%d).".formatted(sourceIndex, listSize));
} else if (destinationIndex > listSize) { // destinationIndex == listSize is allowed (append to the end of the list)
throw new IllegalArgumentException(
"The destinationIndex (%d) must be less than or equal to the list size (%d)."
.formatted(destinationIndex, listSize));
throw new IndexOutOfBoundsException("The sourceIndex (%d) must be less than the list size (%d)."
.formatted(sourceIndex, listSize));
} else if (destinationIndex >= listSize) {
throw new IndexOutOfBoundsException("The destinationIndex (%d) must be less than the list size (%d)."
.formatted(destinationIndex, listSize));
}

var fromIndex = Math.min(sourceIndex, destinationIndex);
var toIndex = Math.max(sourceIndex, destinationIndex) + 1;
externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex);
var element = (Value_) list.remove(sourceIndex);
list.add(destinationIndex, element);
moveInList(list, sourceIndex, destinationIndex);
externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex);
externalScoreDirector.triggerVariableListeners();
return element;
return (Value_) list.get(destinationIndex);
}

@Override
public <Entity_, Value_> Value_ replaceValueInList(
PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel, Entity_ sourceEntity, int sourceIndex,
int replacementIndex) {
if (sourceIndex == replacementIndex) {
throw new IllegalArgumentException(
"When replacing values in the same list, sourceIndex (%d) and replacementIndex (%d) must be different."
.formatted(sourceIndex, replacementIndex));
} else if (sourceIndex < 0 || replacementIndex < 0) {
throw new IndexOutOfBoundsException("The sourceIndex (%d) and replacementIndex (%d) must both be >= 0."
.formatted(sourceIndex, replacementIndex));
}

var variableDescriptor = extractVariableDescriptor(variableMetaModel);
var fromIndex = Math.min(sourceIndex, replacementIndex);
var toIndex = Math.max(sourceIndex, replacementIndex) + 1;
var list = variableDescriptor.getValue(sourceEntity);
var toReplace = (Value_) list.get(replacementIndex);
externalScoreDirector.beforeListVariableElementUnassigned(variableDescriptor, toReplace);
externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex);
list.set(replacementIndex, list.get(sourceIndex));
// Remove from sourceIndex after setting the replacement to preserve index validity,
// as the replacement may occur after the source position.
list.remove(sourceIndex);
externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex - 1);
externalScoreDirector.afterListVariableElementUnassigned(variableDescriptor, toReplace);
externalScoreDirector.triggerVariableListeners();
return toReplace;
}

/**
* Moves the element at index {@code from} to index {@code to} in a list,
* choosing the faster of two strategies based on the move's distance and position within the list.
*
* <p>
* <b>Strategy selection</b> (lo = min(from, to), d = |from − to|):
* <ul>
* <li>Use {@code Collections.rotate} when {@code d * 8 < n − lo}
* (distance is small relative to the remaining tail).</li>
* <li>Use {@code remove + add} otherwise.</li>
* </ul>
*
* <p>
* <b>Why position matters</b>: {@code remove+add} shifts {@code (n−1−from) + (n−1−to)} elements in total.
* When one endpoint is near the tail, one of those copies is nearly free,
* making {@code remove+add} cheap even for large lists.
* {@code rotate} always pays for the full sublist span,
* so it only wins when that span is short relative to what {@code removeAdd} would have to copy.
*
* <p>
* The threshold constant 8 was determined empirically by benchmarking on HotSpot
* with a microbenchmark that performed moves of varying distances and positions within lists of varying sizes.
*
* @param list the list to mutate; assumes {@link ArrayList}
* @param from index of the element to move
* @param to index the element should occupy after the move
*/
private static <T> void moveInList(List<T> list, int from, int to) {
var distance = Math.abs(from - to);
if (distance == 1) {
Collections.swap(list, from, to);
return;
}
var distanceTimesEight = distance * 8L; // Long prevents unlikely yet possible overflow.
var lowerIndex = Math.min(from, to);
var tailLength = list.size() - lowerIndex;
if (distanceTimesEight < tailLength) {
Collections.rotate(list.subList(lowerIndex, lowerIndex + distance + 1), from < to ? -1 : 1);
} else {
list.add(to, list.remove(from));
}
}

@Override
Expand Down Expand Up @@ -276,15 +373,11 @@ public <Entity_, Value_> void swapValuesInList(PlanningListVariableMetaModel<Sol
}

var variableDescriptor = extractVariableDescriptor(variableMetaModel);
var leftElement = variableDescriptor.getElement(entity, leftIndex);
var rightElement = variableDescriptor.getElement(entity, rightIndex);

var fromIndex = Math.min(leftIndex, rightIndex);
var toIndex = Math.max(leftIndex, rightIndex) + 1;
externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, fromIndex, toIndex);
var list = variableDescriptor.getValue(entity);
list.set(leftIndex, rightElement);
list.set(rightIndex, leftElement);
Collections.swap(list, leftIndex, rightIndex);
externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex);
externalScoreDirector.triggerVariableListeners();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ <Entity_, Value_> void unassignValue(PlanningListVariableMetaModel<Solution_, En
* Acceptable values range from zero to one less than list size.
* All values after the index are shifted to the left.
* @return the removed value
* @throws IndexOutOfBoundsException if the index is out of bounds
*/
<Entity_, Value_> Value_ unassignValue(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
Entity_ entity, int index);
Expand Down Expand Up @@ -171,12 +170,38 @@ <Entity_, Value_> void changeVariable(PlanningVariableMetaModel<Solution_, Entit
* All values at or after the index are shifted to the right.
* To append to the end of the list, use the list size as index.
* @return the value that was moved
* @throws IndexOutOfBoundsException if either index is out of bounds
* @throws IllegalArgumentException if sourceEntity == destinationEntity
*/
<Entity_, Value_> Value_ moveValueBetweenLists(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
Entity_ sourceEntity, int sourceIndex, Entity_ destinationEntity, int destinationIndex);

/**
* Replaces a value in one entity's {@link PlanningListVariable planning list variable} with a value taken from another.
* The value is removed from {@code replacementEntity} at {@code replacementIndex}, shifting all later values to the left.
* The removed value is then assigned to {@code sourceEntity} at {@code sourceIndex},
* overwriting the pre-existing value and unassigning it.
* This means that the replacementEntity's list will be one item shorter after the move,
* while the sourceEntity's list size remains unchanged.
*
* @param variableMetaModel Describes the variable to be changed.
* @param sourceEntity The entity in which the value at {@code sourceIndex} will be replaced (overwritten).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: I would expect destinationEntity and destinationIndex instead of sourceEntity and sourceIndex, but might be just my personal preference.

* @param sourceIndex The index in the source entity's list variable whose current value will be overwritten;
* Acceptable values range from zero to one less than the source list size.
* @param replacementEntity The entity from which the replacement value will be taken and removed.
* @param replacementIndex The index in the replacementEntity's list variable which contains the value to be moved and
* removed;
* Acceptable values range from zero to one less than the replacement list size.
* All values at or after the index are shifted to the left.
* @return the value that was replaced
* @throws IllegalArgumentException if sourceEntity == replacementEntity;
* use {@link #replaceValueInList(PlanningListVariableMetaModel, Object, int, int)} instead.
* @see #moveValueBetweenLists(PlanningListVariableMetaModel, Object, int, Object, int) Similar operation that moves the
* value to the destination without removing the pre-existing value.
*/
<Entity_, Value_> Value_ replaceValueBetweenLists(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike the name replaceValueBetweenLists; sounds like a SwapMove, when it really is drop value from target list, move value from source list to target list. Maybe something like replaceAndDrop to make it clear one value will be unassigned.

PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel, Entity_ sourceEntity, int sourceIndex,
Entity_ replacementEntity, int replacementIndex);

/**
* As defined by {@link #moveValueBetweenLists(PlanningListVariableMetaModel, Object, int, Object, int)},
* but using {@link PositionInList} to specify the source and destination positions.
Expand Down Expand Up @@ -205,14 +230,39 @@ default <Entity_, Value_> Value_ moveValueBetweenLists(
* Acceptable values range from zero to one less than list size.
* All values at or after the index are shifted to the right.
* @return the value that was moved
* @throws IndexOutOfBoundsException if either index is out of bounds
* @throws IllegalArgumentException if sourceIndex == destinationIndex
* @see #replaceValueInList(PlanningListVariableMetaModel, Object, int, int) Similar operation that replaces the value at
* the destination index instead.
* @see #shiftValue(PlanningListVariableMetaModel, Object, int, int) Equivalent operation using offset calculation instead
* of index arithmetics.
*/
<Entity_, Value_> Value_ moveValueInList(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
Entity_ sourceEntity, int sourceIndex, int destinationIndex);

/**
* Moves a value within one entity's {@link PlanningListVariable planning list variable}.
* Behaves as if the value is first put in the replacementIndex,
* and then removed from the sourceIndex, shifting all later values to the left.
* The value previously at the replacementIndex is unassigned.
*
* @param variableMetaModel Describes the variable to be changed.
* @param sourceEntity The entity whose variable value is to be moved.
* @param sourceIndex The index in the source entity's list variable which contains the value to be moved;
* Acceptable values range from zero to one less than list size.
* All values after the index are shifted to the left.
* @param replacementIndex The index in the source entity's list variable to which the value will be moved;
* Acceptable values range from zero to one less than list size.
* The value previously at this index is unassigned.
* @return the value that was replaced
* @throws IllegalArgumentException if sourceIndex == replacementIndex
* @see #moveValueInList(PlanningListVariableMetaModel, Object, int, int) Similar operation that moves the value to the
* destination index instead.
* @see #shiftValue(PlanningListVariableMetaModel, Object, int, int) Equivalent operation using offset calculation instead
* of index arithmetic.
*/
<Entity_, Value_> Value_ replaceValueInList(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
Entity_ sourceEntity, int sourceIndex, int replacementIndex);

/**
* Moves a value within one entity's {@link PlanningListVariable planning list variable},
* by the given offset.
Expand All @@ -225,7 +275,6 @@ <Entity_, Value_> Value_ moveValueInList(PlanningListVariableMetaModel<Solution_
* The offset must not be zero.
* The offset must not move the value out of bounds.
* @return the value that was moved
* @throws IndexOutOfBoundsException if either index is out of bounds
* @throws IllegalArgumentException if sourceIndex == destinationIndex
* @see #moveValueInList(PlanningListVariableMetaModel, Object, int, int) Equivalent operation using index arithmetics
* instead of offset calculation.
Expand All @@ -252,7 +301,6 @@ default <Entity_, Value_> Value_ shiftValue(PlanningListVariableMetaModel<Soluti
* @param rightEntity The second entity whose variable value is to be swapped.
* @param rightIndex The index in the right entity's list variable which contains the other value to be swapped;
* Acceptable values range from zero to one less than list size.
* @throws IndexOutOfBoundsException if either index is out of bounds
* @throws IllegalArgumentException if leftEntity == rightEntity while leftIndex == rightIndex
*/
<Entity_, Value_> void swapValuesBetweenLists(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
Expand All @@ -267,7 +315,6 @@ <Entity_, Value_> void swapValuesBetweenLists(PlanningListVariableMetaModel<Solu
* Acceptable values range from zero to one less than list size.
* @param rightIndex The index in the entity's list variable which contains the other value to be swapped;
* Acceptable values range from zero to one less than list size.
* @throws IndexOutOfBoundsException if either index is out of bounds
* @throws IllegalArgumentException if leftIndex == rightIndex
*/
<Entity_, Value_> void swapValuesInList(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
Expand Down
Loading
Loading