diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupply.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupply.java index cbd7f111106..c2b3221cda8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupply.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ExternalizedListVariableStateSupply.java @@ -56,6 +56,24 @@ public void externalize(NextElementShadowVariableDescriptor shadowVar nextExternalized = true; } + @Override + public int getIndexOrFail(Object planningValue) { + var index = listVariableState.getIndex(planningValue); + if (index < 0) { + throw new IllegalStateException("The element (%s) is not assigned to any list variable."); + } + return index; + } + + @Override + public int getIndexOrElse(Object planningValue, int defaultValue) { + var index = listVariableState.getIndex(planningValue); + if (index < 0) { + return defaultValue; + } + return index; + } + @Override public void resetWorkingSolution(InnerScoreDirector scoreDirector) { workingSolution = scoreDirector.getWorkingSolution(); @@ -106,11 +124,6 @@ public ElementPosition getElementPosition(Object planningValue) { return listVariableState.getElementPosition(planningValue); } - @Override - public @Nullable Integer getIndex(Object planningValue) { - return listVariableState.getIndex(planningValue); - } - @Override public @Nullable Object getInverseSingleton(Object planningValue) { return listVariableState.getInverseSingleton(planningValue); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableState.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableState.java index d6987b4e980..ff39260de0c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableState.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableState.java @@ -228,7 +228,7 @@ private ChangeType processElementPosition(Object entity, Object element, int ind return ChangeType.BOTH; } var oldIndex = getIndex(element); - if (oldIndex == null) { // Technically impossible, but we handle it anyway. + if (oldIndex < 0) { // Technically impossible, but we handle it anyway. return ChangeType.BOTH; } return comparePositions(entity, oldEntity, index, oldIndex); @@ -261,15 +261,16 @@ public ElementPosition getElementPosition(Object planningValue) { } } - public Integer getIndex(Object planningValue) { + public int getIndex(Object planningValue) { if (externalizedIndexProcessor == null) { var position = elementPositionMap.get(planningValue); if (position == null) { - return null; + return -1; } return position.getIndex(); } - return externalizedIndexProcessor.getIndex(planningValue); + var indexOrNull = externalizedIndexProcessor.getIndex(planningValue); + return indexOrNull == null ? -1 : indexOrNull; } public Object getInverseSingleton(Object planningValue) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupply.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupply.java index b0434323dc7..6a594a20075 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupply.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ListVariableStateSupply.java @@ -1,6 +1,10 @@ package ai.timefold.solver.core.impl.domain.variable; +import ai.timefold.solver.core.api.domain.variable.IndexShadowVariable; +import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable; +import ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; +import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.listener.SourcedListVariableListener; @@ -23,10 +27,8 @@ * it means that there is a field on an entity holding the value of the shadow variable. * In this case, we will attempt to use that value. * Otherwise, we will keep an internal track of all the possible shadow variables - * ({@link ai.timefold.solver.core.api.domain.variable.IndexShadowVariable}, - * {@link ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable}, - * {@link ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable}, - * {@link ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable}), + * ({@link IndexShadowVariable}, {@link InverseRelationShadowVariable}, + * {@link PreviousElementShadowVariable}, {@link NextElementShadowVariable}), * and use values from this internal representation. * * @param @@ -50,26 +52,20 @@ public interface ListVariableStateSupply * Get {@code planningValue}'s index in the {@link PlanningListVariable list variable} it is an element of. * * @param planningValue never null - * @return {@code planningValue}'s index in the list variable it is an element of or {@code null} if the value is unassigned + * @return {@code planningValue}'s index in the list variable it is an element of + * @throws IllegalStateException if the value is unassigned */ - @Nullable - Integer getIndex(Object planningValue); - - default int getIndexOrFail(Object planningValue) { - var index = getIndex(planningValue); - if (index == null) { - throw new IllegalStateException("The element (%s) is not assigned to any list variable."); - } - return index; - } - - default int getIndexOrElse(Object planningValue, int defaultValue) { - var index = getIndex(planningValue); - if (index == null) { - return defaultValue; - } - return index; - } + int getIndexOrFail(Object planningValue); + + /** + * Get {@code planningValue}'s index in the {@link PlanningListVariable list variable} it is an element of. + * + * @param planningValue never null + * @param defaultValue the value to return if {@code planningValue} is unassigned + * @return {@code planningValue}'s index in the list variable it is an element of or {@code defaultValue} if the value is + * unassigned + */ + int getIndexOrElse(Object planningValue, int defaultValue); /** * If entity1.varA = x then the inverse of x is entity1. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/DelegatingListVariableStateSupply.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/DelegatingListVariableStateSupply.java index 8cc7a7d95f4..e9ca4de4521 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/DelegatingListVariableStateSupply.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/DelegatingListVariableStateSupply.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.kopt; -import java.util.function.Function; +import java.util.function.ToIntFunction; import ai.timefold.solver.core.impl.domain.variable.IndexShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.ListElementsChangeEvent; @@ -17,7 +17,9 @@ @NullMarked record DelegatingListVariableStateSupply(ListVariableStateSupply delegate, - Function indexFunction) implements ListVariableStateSupply { + ToIntFunction indexFunction) + implements + ListVariableStateSupply { @Override public void externalize(IndexShadowVariableDescriptor shadowVariableDescriptor) { @@ -40,8 +42,21 @@ public void externalize(NextElementShadowVariableDescriptor shadowVar } @Override - public @Nullable Integer getIndex(Object planningValue) { - return indexFunction.apply(planningValue); + public int getIndexOrFail(Object planningValue) { + var index = indexFunction.applyAsInt(planningValue); + if (index < 0) { + throw new IllegalStateException("The element (%s) is not assigned to any list variable."); + } + return index; + } + + @Override + public int getIndexOrElse(Object planningValue, int defaultValue) { + var index = indexFunction.applyAsInt(planningValue); + if (index < 0) { + return defaultValue; + } + return index; } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveIteratorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveIteratorTest.java index 9ff1d0ab49c..fec36eb2fa3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveIteratorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveIteratorTest.java @@ -146,7 +146,7 @@ private KOptMoveInfo setupValidOddSequentialKOptMove(KOptListMoveIteratorMockDat when(mocks.listVariableStateSupply.getElementPosition(entityList.get(i))) .thenReturn(ElementPosition.of(entity, i)); when(mocks.listVariableStateSupply.getInverseSingleton(entityList.get(i))).thenReturn(entity); - when(mocks.listVariableStateSupply.getIndex(entityList.get(i))).thenReturn(i); + when(mocks.listVariableStateSupply.getIndexOrFail(entityList.get(i))).thenReturn(i); when(mocks.listVariableStateSupply.getSourceVariableDescriptor()).thenReturn(mocks.listVariableDescriptor); } when(mocks.listVariableDescriptor.getListSize(entity)).thenReturn(entityList.size()); @@ -251,7 +251,7 @@ private KOptMoveInfo setupValidNonsequential4OptMove(KOptListMoveIteratorMockDat when(mocks.listVariableStateSupply.getElementPosition(entityList.get(i))) .thenReturn(ElementPosition.of(entity, i)); when(mocks.listVariableStateSupply.getInverseSingleton(entityList.get(i))).thenReturn(entity); - when(mocks.listVariableStateSupply.getIndex(entityList.get(i))).thenReturn(i); + when(mocks.listVariableStateSupply.getIndexOrFail(entityList.get(i))).thenReturn(i); when(mocks.listVariableStateSupply.getSourceVariableDescriptor()).thenReturn(mocks.listVariableDescriptor); } when(mocks.listVariableDescriptor.getListSize(entity)).thenReturn(entityList.size()); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptUtilsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptUtilsTest.java index 4ec3567b0d0..f5ee1f5286b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptUtilsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptUtilsTest.java @@ -260,8 +260,8 @@ private static KOptDescriptor fromRemovedAndAddedEdges( return new KOptDescriptor<>(tourArray, incl, item -> originalTour.get((originalTour.indexOf(item) + 1) % originalTour.size()), - getBetweenPredicate( - new DelegatingListVariableStateSupply<>(mock(ListVariableStateSupply.class), originalTour::indexOf))); + getBetweenPredicate(new DelegatingListVariableStateSupply(mock(ListVariableStateSupply.class), + originalTour::indexOf))); } private static int identityIndexOf(List sourceList, TestdataListValue query) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/SelectorBasedKOptListMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/SelectorBasedKOptListMoveTest.java index 616c3c69b2c..911eab85fa6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/SelectorBasedKOptListMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/SelectorBasedKOptListMoveTest.java @@ -718,7 +718,7 @@ private static Function getSuccessorFunction(ListVariableD return node -> { var entity = listVariableStateSupply.getInverseSingleton(node); var valueList = (List) listVariableDescriptor.getValue(entity); - var index = listVariableStateSupply.getIndex(node); + var index = listVariableStateSupply.getIndexOrFail(node); if (index == valueList.size() - 1) { var firstUnpinnedIndex = listVariableDescriptor.getFirstUnpinnedIndex(entity); return valueList.get(firstUnpinnedIndex);