Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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 @@
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 @@
"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 = (long) distance * 8L; // Prevents unlikely yet possible overflow.

Check warning on line 325 in core/src/main/java/ai/timefold/solver/core/impl/move/MoveDirector.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unnecessary cast to "long".

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZzcDqLa-jsa3wBjT3f1&open=AZzcDqLa-jsa3wBjT3f1&pullRequest=2180
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 @@
}

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 @@ -177,6 +177,31 @@ <Entity_, Value_> void changeVariable(PlanningVariableMetaModel<Solution_, Entit
<Entity_, Value_> Value_ moveValueBetweenLists(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
Entity_ sourceEntity, int sourceIndex, Entity_ destinationEntity, int destinationIndex);

/**
* Replaces a value from one entity's {@link PlanningListVariable planning list variable} with another.
* The value is removed from the replacementIndex, shifting all later values to the left.
* The value is then added at the sourceIndex, replacing the pre-existing value and unassigning it.
* This means that the replacement list will be one item shorter after the move.
*
* @param variableMetaModel Describes the variable to be changed.
* @param sourceEntity The entity in which the value will be replaced.
* @param sourceIndex The index in the source entity's list variable which contains the value to be replaced;
* Acceptable values range from zero to one less than list size.
* @param replacementEntity The entity from which the value will be taken.
* @param replacementIndex The index in the replacementEntity's list variable which contains the value to be moved;
* Acceptable values range from zero to one less than list size.
* All values at or after the index are shifted to the left.
* @return the value that was replaced
* @throws IndexOutOfBoundsException if either index is out of bounds
* @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(
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 Down
Loading
Loading