Skip to content

Commit 9c56885

Browse files
committed
chore: make it clear that we rely on entity equality
1 parent 59ad57a commit 9c56885

7 files changed

Lines changed: 45 additions & 16 deletions

File tree

core/src/main/java/ai/timefold/solver/core/api/domain/common/PlanningId.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import java.lang.annotation.Retention;
88
import java.lang.annotation.Target;
9+
import java.util.UUID;
910

1011
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
1112
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
@@ -23,7 +24,7 @@
2324
* {@link ValueRangeProvider planning value} class or any {@link ProblemFactCollectionProperty problem fact} class.
2425
* <p>
2526
* The return type can be any {@link Comparable} type which overrides {@link Object#equals(Object)} and
26-
* {@link Object#hashCode()}, and is usually {@link Long} or {@link String}.
27+
* {@link Object#hashCode()}, and is usually {@link Long}, {@link UUID} or {@link String}.
2728
* It must never return a null instance.
2829
*/
2930
@Target({ METHOD, FIELD })

core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningEntity.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.Comparator;
99

1010
import ai.timefold.solver.core.api.domain.common.ComparatorFactory;
11+
import ai.timefold.solver.core.api.domain.common.PlanningId;
1112
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
1213
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
1314

@@ -27,6 +28,22 @@
2728
* it is not a planning entity and the solver will fail fast.
2829
*
2930
* <p>
31+
* Planning entities have special requirements on {@link #equals(Object)}:
32+
*
33+
* <ul>
34+
* <li>If two different entities are equal as defined by {@link #equals(Object)},
35+
* they must represent the same entity.
36+
* The solver will treat them as one entity.</li>
37+
* <li>Entity {@link #equals(Object)} and {@link #hashCode()} must not depend on any planning variable.
38+
* The return value of these methods must not change when any planning variable changes.
39+
* It is recommended for entities to implement a unique {@link PlanningId},
40+
* and use that for equality comparisons.</li>
41+
* </ul>
42+
*
43+
* Failing to follow these requirements will cause undefined behavior in the solver,
44+
* ranging from explicit fail fasts to silently producing incorrect results.
45+
*
46+
* <p>
3047
* The class should have a public no-arg constructor, so it can be cloned
3148
* (unless the {@link PlanningSolution#solutionCloner()} is specified).
3249
*/

core/src/main/java/ai/timefold/solver/core/api/domain/solution/PlanningEntityCollectionProperty.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@
99
import java.util.Collection;
1010
import java.util.LinkedHashSet;
1111
import java.util.List;
12-
import java.util.SortedSet;
12+
import java.util.SequencedCollection;
1313

1414
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
1515

1616
/**
1717
* Specifies that a property (or a field) on a {@link PlanningSolution} class is a {@link Collection} of planning entities.
1818
* <p>
19-
* Every element in the planning entity collection should have the {@link PlanningEntity} annotation.
19+
* Every element in the planning entity collection must have the {@link PlanningEntity} annotation.
2020
* Every element in the planning entity collection will be registered with the solver.
2121
* <p>
2222
* For solver reproducibility, the collection must have a deterministic, stable iteration order.
23-
* It is recommended to use a {@link List}, {@link LinkedHashSet} or {@link SortedSet}.
23+
* It must not contain duplicates, as defined by {@link #equals(Object)}.
24+
* It is recommended to use any {@link SequencedCollection}, such as {@link List} or {@link LinkedHashSet}.
25+
*
26+
* @see PlanningEntityProperty
2427
*/
2528
@Target({ METHOD, FIELD })
2629
@Retention(RUNTIME)

core/src/main/java/ai/timefold/solver/core/api/domain/solution/PlanningEntityProperty.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
/**
1313
* Specifies that a property (or a field) on a {@link PlanningSolution} class is a planning entity.
1414
* <p>
15-
* The planning entity should have the {@link PlanningEntity} annotation.
15+
* The planning entity must have the {@link PlanningEntity} annotation.
1616
* The planning entity will be registered with the solver.
1717
*/
1818
@Target({ METHOD, FIELD })

core/src/main/java/ai/timefold/solver/core/api/domain/solution/ProblemFactCollectionProperty.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import java.util.Collection;
1010
import java.util.LinkedHashSet;
1111
import java.util.List;
12-
import java.util.SortedSet;
12+
import java.util.SequencedCollection;
1313

1414
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
1515
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
@@ -26,7 +26,8 @@
2626
* they are automatically available as facts for {@link ConstraintFactory#forEach(Class)}.
2727
* <p>
2828
* For solver reproducibility, the collection must have a deterministic, stable iteration order.
29-
* It is recommended to use a {@link List}, {@link LinkedHashSet} or {@link SortedSet}.
29+
* It must not contain duplicates (as defined by {@link #equals(Object)}.
30+
* It is recommended to use any {@link SequencedCollection}, such as {@link List} or {@link LinkedHashSet}.
3031
*
3132
* @see ProblemFactProperty
3233
*/

core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package ai.timefold.solver.core.impl.bavet.uni;
22

3-
import java.util.IdentityHashMap;
3+
import java.util.HashMap;
44
import java.util.Map;
55

66
import ai.timefold.solver.core.impl.bavet.common.AbstractNode;
@@ -32,7 +32,7 @@ public abstract sealed class AbstractForEachUniNode<A>
3232
private final Class<A> forEachClass;
3333
private final int outputStoreSize;
3434
private final StaticPropagationQueue<UniTuple<A>> propagationQueue;
35-
protected final Map<A, UniTuple<A>> tupleMap = new IdentityHashMap<>(1000);
35+
protected final Map<A, UniTuple<A>> tupleMap = new HashMap<>(1000);
3636

3737
protected AbstractForEachUniNode(Class<A> forEachClass, TupleLifecycle<UniTuple<A>> nextNodesTupleLifecycle,
3838
int outputStoreSize) {

docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ Alternatively, you can sometimes also introduce <<cachedProblemFact,_a cached pr
224224
For some functionality
225225
(such as xref:using-timefold-solver/running-the-solver.adoc#multithreadedIncrementalSolving[multi-threaded incremental solving]
226226
and xref:responding-to-change/responding-to-change.adoc#realTimePlanning[real-time planning]),
227-
Timefold Solver needs to map problem facts and planning entities to an ID.
227+
Timefold Solver needs to map problem facts and planning entities to a unique ID.
228228
Timefold Solver uses that ID to _rebase_ a move from one thread's solution state to another's.
229229

230230
To enable such functionality, specify the `@PlanningId` annotation on the identification field or getter method,
@@ -253,8 +253,8 @@ A `@PlanningId` property must be:
253253
* Unique for that specific class
254254
** It does not need to be unique across different problem fact classes
255255
(unless in that rare case that those classes are mixed in the same value range or planning entity collection).
256-
* An instance of a type that implements `Object.hashCode()` and `Object.equals()`. See the Javadoc on the https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Object.html[`java.lang.Object` class] for details.
257-
** It's recommended to use the type `Integer`, `int`, `Long`, `long`, `String` or `UUID`.
256+
* An instance of a https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Comparable.html[`Comparable` class].
257+
** It's recommended to use types such as `Long`, `String` or `UUID`.
258258
* Never `null` by the time `Solver.solve()` is called.
259259
260260
@@ -358,11 +358,18 @@ Instead, calculate the free time in the score constraints (or as a <<shadowVaria
358358
359359
If historic data needs to be considered too, then create a problem fact to hold the total of the historic assignments up to, but __not including__, the planning window (so that it does not change when a planning entity changes) and let the score constraints take it into account.
360360
361-
==== Keep planning entity `hashCode()` implementations constant
361+
==== Keep planning entity `equals()` and `hashCode()` implementations constant
362362
363-
Planning entity `hashCode()` implementations must remain constant.
364-
Therefore, entity `hashCode()` implementations must not depend on any planning variables, as these change during solving.
365-
Pay special attention when using data structures with auto-generated `hashCode()` as entities,
363+
Planning entity equality implementations must remain constant:
364+
365+
- If two entities are equal at the start of solving, they must remain equal during solving.
366+
- If two entities are not equal at the start of solving, they must remain not equal during solving.
367+
- If two entities are equal, they are considered to be the same entity.
368+
369+
From this, it follows that entity `equals()` and `hashCode()` implementations must not depend on any planning variables, as these change during solving.
370+
You can use a <<planningId,`@PlanningId`>> to distinguish planning entities from each other.
371+
372+
Pay special attention when using data structures with auto-generated `equals()` and `hashCode()`,
366373
such as Kotlin data classes or Lombok's `@EqualsAndHashCode`.
367374
368375
==== Planning entity implementations must be mutable

0 commit comments

Comments
 (0)