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
25 changes: 13 additions & 12 deletions CONSTITUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ Key words per [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119.html): **MUST**

**Documentation**:

1. **Public API** (MUST): All public classes/interfaces/methods MUST have Javadoc with `@param`, `@return`, `@throws` (omit NPE for null params; omit if present on referenced overload). `@since` optional.
2. **User-facing** (MUST):
1. **Public API**: All public classes/interfaces/methods MUST have Javadoc with `@param`, `@return`, `@throws` (omit NPE for null params; omit if present on referenced overload). `@since` optional.
2. **User-facing**:
- New features → user guide;
- Config changes → reference docs;
- Breaking changes → migration guides.
3. **Examples** (SHOULD): Docs SHOULD include code examples; quickstart examples SHOULD be added externally.
4. **Implementation** (SHOULD): Complex classes SHOULD have class-level Javadoc; non-obvious details SHOULD have comments.
3. **Examples**: Docs SHOULD include code examples; quickstart examples SHOULD be added externally.
4. **Implementation**: Complex classes SHOULD have class-level Javadoc; non-obvious details SHOULD have comments.
5. **Formatting**: Any form of documentation (being source code comments, Javadoc, or user documentation) must follow [Semantic Line Breaks](https://sembr.org).

### II. Consistent Terminology

Expand Down Expand Up @@ -88,11 +89,10 @@ Performance/stress tests are in other repositories; not applicable here.
**Style**:
1. Newlines: formatter preserves them; use sparingly to separate logical blocks
2. Field access order in methods SHOULD match declaration order
3. Imports MUST be used; no fully qualified names in source (exception: disambiguating same simple name)
4. `var` preferred over diamond; diamond MUST be used when `var` not used and type is inferable
3. `var` preferred over diamond; diamond MUST be used when `var` not used and type is inferable
- ✅ `var list = new ArrayList<String>();`; ✅ `List<String> list = new ArrayList<>();`
- ❌ `var list = new ArrayList<>();` (compiler error); ❌ `List<String> list = new ArrayList<String>();`
5. Asterisk imports MUST NOT be used
4. Asterisk imports MUST NOT be used

**SonarCloud Quality Gates** (MUST):
- Reliability and Maintainability grades MUST be B or better; PRs worsening below B fail CI
Expand Down Expand Up @@ -178,11 +178,12 @@ MUST use:

## Package Structure and API Stability

| Package type | Stability |
|---|---|
| `*.api.*` | 100% backwards compatible; breaking only in major versions |
| `*.config.*` | 100% backwards compatible; breaking only in major versions |
| All others | No guarantees |
| Package type | Stability |
|-------------------|-------------------------------------------------------------------------------|
| `*.api.*` | 100% backwards compatible; breaking only in major versions |
| `*.preview.api.*` | Best-effort backwards compatibility; may break in minor versions if necessary |
| `*.config.*` | 100% backwards compatible; breaking only in major versions |
| All others | No guarantees |

**Versioning**: MAJOR = breaking API/config change; MINOR = new backwards-compat feature; PATCH = bug fix.

Expand Down
1 change: 1 addition & 0 deletions build/build-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@
<file>eclipse.importorder</file>
</importOrder>
<removeUnusedImports/>
<shortenFullyQualifiedTypes />
<replaceRegex> <!-- Wildcard imports will break the build. -->
<name>Remove wildcard imports</name>
<searchRegex>import\s+[^\*\s]+\*;(\r\n|\r|\n)</searchRegex>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ai.timefold.solver.core.api.domain.common;

import java.util.Objects;

import ai.timefold.solver.core.api.solver.change.ProblemChange;
import ai.timefold.solver.core.preview.api.move.Move;

Expand Down Expand Up @@ -29,4 +31,12 @@ public interface Lookup {
*/
<T> @Nullable T lookUpWorkingObject(@Nullable T problemFactOrPlanningEntity);

/**
* As defined by {@link #lookUpWorkingObject(Object)},
* but does not accept null arguments and cannot return null.
*/
default <T> T lookUpNonNullWorkingObject(T problemFactOrPlanningEntity) {
return lookUpWorkingObject(Objects.requireNonNull(problemFactOrPlanningEntity));
}
Comment on lines +38 to +40

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Iterator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.random.RandomGenerator;

import ai.timefold.solver.core.impl.util.ElementAwareArrayList;
Expand Down Expand Up @@ -30,12 +31,21 @@
@NullMarked
public sealed interface RetiringRandomIterator<T extends @Nullable Object>
extends Iterator<T>
permits DefaultRetiringRandomIterator {
permits DefaultRetiringRandomIterator, RetiringRandomIterator.MappingRetiringRandomIterator {

static <T extends @Nullable Object> RetiringRandomIterator<T> of(ElementAwareArrayList<T> list, RandomGenerator random) {
return new DefaultRetiringRandomIterator<>(list, random);
}

/**
* Adapts an iterator of one type to another, without changing which element retirement targets:
* {@link #retire()} on the result still retires whatever the delegate itself last handed out.
*/
static <S extends @Nullable Object, T extends @Nullable Object> RetiringRandomIterator<T> mapping(
RetiringRandomIterator<S> delegate, Function<S, T> mapper) {
return new MappingRetiringRandomIterator<>(delegate, mapper);
}

/**
* Returns whether there are any elements left to pick from.
* Only turns {@code false} once every element has been retired,
Expand Down Expand Up @@ -76,4 +86,32 @@ Maybe use hasNext() and next() with your own stop condition instead."""
.formatted(this));
}

/**
* Adapts a {@link RetiringRandomIterator} of one type to another,
* by mapping each element through a function,
* without changing which element is retired:
* {@link #retire()} still retires whatever the delegate last handed out,
* keyed by the delegate's own identity, not by the mapped value.
*/
record MappingRetiringRandomIterator<S extends @Nullable Object, T extends @Nullable Object>(
RetiringRandomIterator<S> delegate,
Function<S, T> mapper) implements RetiringRandomIterator<T> {

@Override
public boolean hasNext() {
return delegate.hasNext();
}

@Override
public T next() {
return mapper.apply(delegate.next());
}

@Override
public void retire() {
delegate.retire();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ private static void writeDeepCloneInstructions(ClonerDescriptor clonerDescriptor
*/
private static void writeDeepCloneInstructions(ClonerDescriptor clonerDescriptor,
ClonerMethodDescriptor clonerMethodDescriptor,
Class<?> deeplyClonedFieldClass, java.lang.reflect.Type type, Var toClone,
Class<?> deeplyClonedFieldClass, Type type, Var toClone,
Var cloneResultHolder) {
BlockCreator blockCreator = clonerMethodDescriptor.blockCreator;

Expand Down Expand Up @@ -593,7 +593,7 @@ private static void writeDeepCloneSolutionInstructions(
**/
private static void writeDeepCloneCollectionInstructions(ClonerDescriptor clonerDescriptor,
ClonerMethodDescriptor clonerMethodDescriptor,
Class<?> deeplyClonedFieldClass, java.lang.reflect.Type type, Var toClone,
Class<?> deeplyClonedFieldClass, Type type, Var toClone,
Var cloneResultHolder) {
var blockCreator = clonerMethodDescriptor.blockCreator;

Expand All @@ -607,7 +607,7 @@ private static void writeDeepCloneCollectionInstructions(ClonerDescriptor cloner
blockCreator.localVar(toClone.name() + "$Iterator", blockCreator.withCollection(toClone).iterator());
blockCreator.while_(condition -> condition.yield(condition.withIterator(iterator).hasNext()), whileLoopBlock -> {
Class<?> elementClass;
java.lang.reflect.Type elementClassType;
Type elementClassType;
if (type instanceof ParameterizedType parameterizedType) {
// Assume Collection follow Collection<T> convention of first type argument = element class
elementClassType = parameterizedType.getActualTypeArguments()[0];
Expand Down Expand Up @@ -688,7 +688,7 @@ private static void checkCastAndAssign(BlockCreator blockCreator, Class<?> deepl
**/
private static void writeDeepCloneMapInstructions(ClonerDescriptor clonerDescriptor,
ClonerMethodDescriptor clonerMethodDescriptor,
Class<?> deeplyClonedFieldClass, java.lang.reflect.Type type, Var toClone,
Class<?> deeplyClonedFieldClass, Type type, Var toClone,
Var cloneResultHolder) {
var blockCreator = clonerMethodDescriptor.blockCreator;

Expand All @@ -705,8 +705,8 @@ private static void writeDeepCloneMapInstructions(ClonerDescriptor clonerDescrip
blockCreator.while_(condition -> condition.yield(condition.withIterator(iterator).hasNext()), whileLoopBlock -> {
Class<?> keyClass;
Class<?> elementClass;
java.lang.reflect.Type keyType;
java.lang.reflect.Type elementClassType;
Type keyType;
Type elementClassType;
if (type instanceof ParameterizedType parameterizedType) {
// Assume Map follow Map<K,V> convention of second type argument = value class
keyType = parameterizedType.getActualTypeArguments()[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineEntityMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningEntityMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel;
Expand All @@ -20,6 +22,11 @@
public final class DefaultGenuineEntityMetaModel<Solution_, Entity_>
implements GenuineEntityMetaModel<Solution_, Entity_>, InnerPlanningEntityMetaModel<Solution_, Entity_> {

static final Comparator<PlanningEntityMetaModel<?, ?>> ENTITY_META_MODEL_COMPARATOR =
Comparator.comparingInt(
(PlanningEntityMetaModel<?, ?> entityMetaModel) -> ((InnerPlanningEntityMetaModel<?, ?>) entityMetaModel)
.entityDescriptor().getOrdinal());

private final EntityDescriptor<Solution_> entityDescriptor;
private final PlanningSolutionMetaModel<Solution_> solution;
private final Class<Entity_> type;
Expand Down Expand Up @@ -59,7 +66,7 @@ public <Value_> GenuineVariableMetaModel<Solution_, Entity_, Value_> genuineVari
return switch (genuineVariables.size()) {
case 0 -> throw new IllegalStateException("The entity class (%s) has no genuine variables."
.formatted(type().getCanonicalName()));
case 1 -> (GenuineVariableMetaModel<Solution_, Entity_, Value_>) genuineVariables.get(0);
case 1 -> (GenuineVariableMetaModel<Solution_, Entity_, Value_>) genuineVariables.getFirst();
default -> throw new IllegalStateException("The entity class (%s) has multiple genuine variables (%s)."
.formatted(type().getCanonicalName(), genuineVariables));
};
Expand Down Expand Up @@ -182,6 +189,11 @@ public void addVariable(VariableMetaModel<Solution_, Entity_, ?> variable) {
variables.add(variable);
}

@Override
public int compareTo(PlanningEntityMetaModel<Solution_, Entity_> other) {
return ENTITY_META_MODEL_COMPARATOR.compare(this, other);
}

@Override
public String toString() {
return "Genuine entity (%s) with variables (%s)"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package ai.timefold.solver.core.impl.domain.solution.descriptor;

import static ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel.VARIABLE_META_MODEL_COMPARATOR;

import java.util.Objects;

import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningEntityMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineEntityMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;

import org.jspecify.annotations.NullMarked;

@NullMarked
public record DefaultPlanningListVariableMetaModel<Solution_, Entity_, Value_>(
PlanningEntityMetaModel<Solution_, Entity_> entity,
GenuineEntityMetaModel<Solution_, Entity_> entity,
ListVariableDescriptor<Solution_> variableDescriptor)
implements
PlanningListVariableMetaModel<Solution_, Entity_, Value_>,
Expand All @@ -32,11 +35,15 @@ public boolean allowsUnassignedValues() {
return variableDescriptor.allowsUnassignedValues();
}

@Override
public boolean isValueRangeOnSolution() {
return variableDescriptor.canExtractValueRangeFromSolution();
}

@Override
public boolean equals(Object o) {
// Do not use entity in equality checks;
// If an entity is subclassed, that subclass will have it
// own distinct VariableMetaModel
// If an entity is subclassed, that subclass will have it own distinct VariableMetaModel
if (o instanceof DefaultPlanningListVariableMetaModel<?, ?, ?> that) {
return Objects.equals(variableDescriptor, that.variableDescriptor);
}
Expand All @@ -48,6 +55,11 @@ public int hashCode() {
return Objects.hash(variableDescriptor);
}

@Override
public int compareTo(VariableMetaModel<Solution_, Entity_, Value_> other) {
return VARIABLE_META_MODEL_COMPARATOR.compare(this, other);
}

@Override
public String toString() {
return "Genuine List Variable '%s %s.%s' (allowsUnassignedValues: %b)"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
package ai.timefold.solver.core.impl.domain.solution.descriptor;

import java.util.Comparator;
import java.util.Objects;

import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningEntityMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineEntityMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;

import org.jspecify.annotations.NullMarked;

@NullMarked
public record DefaultPlanningVariableMetaModel<Solution_, Entity_, Value_>(
PlanningEntityMetaModel<Solution_, Entity_> entity,
GenuineEntityMetaModel<Solution_, Entity_> entity,
BasicVariableDescriptor<Solution_> variableDescriptor)
implements
PlanningVariableMetaModel<Solution_, Entity_, Value_>,
InnerGenuineVariableMetaModel<Solution_> {

static final Comparator<VariableMetaModel<?, ?, ?>> VARIABLE_META_MODEL_COMPARATOR =
Comparator.comparing((VariableMetaModel<?, ?, ?> variableMetaModel) -> variableMetaModel.entity())

Check warning on line 22 in core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningVariableMetaModel.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this lambda with method reference 'VariableMetaModel::entity'.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AaAtos2TvKLTf8kMy_Hj&open=AaAtos2TvKLTf8kMy_Hj&pullRequest=2610
.thenComparingInt(
(VariableMetaModel<?, ?, ?> variableMetaModel) -> ((InnerVariableMetaModel<?>) variableMetaModel)
.variableDescriptor().getOrdinal());

@SuppressWarnings("unchecked")
@Override
public Class<Value_> type() {
Expand All @@ -32,11 +40,16 @@
return variableDescriptor.allowsUnassigned();
}

@Override
public boolean isValueRangeOnSolution() {
return variableDescriptor.canExtractValueRangeFromSolution();
}

@Override
public boolean equals(Object o) {
// Do not use entity in equality checks;
// If an entity is subclassed, that subclass will have it
// own distinct VariableMetaModel
// If an entity is subclassed,
// that subclass will have it own distinct VariableMetaModel
if (o instanceof DefaultPlanningVariableMetaModel<?, ?, ?> that) {
return Objects.equals(variableDescriptor, that.variableDescriptor);
}
Expand All @@ -48,6 +61,11 @@
return Objects.hash(variableDescriptor);
}

@Override
public int compareTo(VariableMetaModel<Solution_, Entity_, Value_> other) {
return VARIABLE_META_MODEL_COMPARATOR.compare(this, other);
}

@Override
public String toString() {
return "Genuine Variable '%s %s.%s' (allowsUnassigned: %b)"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package ai.timefold.solver.core.impl.domain.solution.descriptor;

import static ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultGenuineEntityMetaModel.ENTITY_META_MODEL_COMPARATOR;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningEntityMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.ShadowEntityMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.ShadowVariableMetaModel;
Expand Down Expand Up @@ -61,6 +64,11 @@ public void addVariable(VariableMetaModel<Solution_, Entity_, ?> variable) {
variables.add(shadowVariable);
}

@Override
public int compareTo(PlanningEntityMetaModel<Solution_, Entity_> other) {
return ENTITY_META_MODEL_COMPARATOR.compare(this, other);
}

@Override
public String toString() {
return "Shadow entity (%s) with shadow variables (%s)"
Expand Down
Loading
Loading