diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java b/core/core-impl/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java index fb7e764b5c..ad763cd541 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java @@ -8,9 +8,11 @@ import java.util.stream.Stream; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; import ai.timefold.solver.core.api.solver.SolutionManager; +import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; import ai.timefold.solver.core.impl.util.CollectionUtils; /** @@ -19,6 +21,7 @@ * * @param * @param constraintRef never null + * @param weight never null * @param score never null * @param matches null if analysis not available; * empty if constraint has no matches, but still non-zero constraint weight; @@ -26,25 +29,41 @@ * This is a {@link List} to simplify access to individual elements, * but it contains no duplicates just like {@link HashSet} wouldn't. */ -public record ConstraintAnalysis>(ConstraintRef constraintRef, Score_ score, - List> matches) { +public record ConstraintAnalysis>(ConstraintRef constraintRef, Score_ weight, + Score_ score, List> matches) { - static > ConstraintAnalysis of(ConstraintRef constraintRef, Score_ score) { - return new ConstraintAnalysis<>(constraintRef, score, null); + static > ConstraintAnalysis of(ConstraintRef constraintRef, Score_ constraintWeight, + Score_ score) { + return new ConstraintAnalysis<>(constraintRef, constraintWeight, score, null); } public ConstraintAnalysis { + Objects.requireNonNull(constraintRef); + if (weight == null) { + /* + * Only possible in ConstraintMatchAwareIncrementalScoreCalculator and/or tests. + * Easy doesn't support constraint analysis at all. + * CS always provides constraint weights. + */ + throw new IllegalArgumentException(""" + The constraint weight must be non-null. + Maybe use a non-deprecated %s constructor in your %s implementation? + """ + .stripTrailing() + .formatted(DefaultConstraintMatchTotal.class.getSimpleName(), + ConstraintMatchAwareIncrementalScoreCalculator.class.getSimpleName())); + } Objects.requireNonNull(score); } ConstraintAnalysis negate() { if (matches == null) { - return ConstraintAnalysis.of(constraintRef, score.negate()); + return ConstraintAnalysis.of(constraintRef, weight.negate(), score.negate()); } else { var negatedMatchAnalyses = matches.stream() .map(MatchAnalysis::negate) .toList(); - return new ConstraintAnalysis<>(constraintRef, score.negate(), negatedMatchAnalyses); + return new ConstraintAnalysis<>(constraintRef, weight.negate(), score.negate(), negatedMatchAnalyses); } } @@ -71,9 +90,10 @@ static > ConstraintAnalysis diff( .formatted(constraintAnalysis, otherConstraintAnalysis, constraintRef)); } // Compute the diff. + var constraintWeightDifference = constraintAnalysis.weight().subtract(otherConstraintAnalysis.weight()); var scoreDifference = constraintAnalysis.score().subtract(otherConstraintAnalysis.score()); if (matchAnalyses == null) { - return ConstraintAnalysis.of(constraintRef, scoreDifference); + return ConstraintAnalysis.of(constraintRef, constraintWeightDifference, scoreDifference); } var matchAnalysisMap = mapMatchesToJustifications(matchAnalyses); var otherMatchAnalysisMap = mapMatchesToJustifications(otherMatchAnalyses); @@ -99,7 +119,7 @@ static > ConstraintAnalysis diff( } }) .collect(Collectors.toList()); - return new ConstraintAnalysis<>(constraintRef, scoreDifference, result); + return new ConstraintAnalysis<>(constraintRef, constraintWeightDifference, scoreDifference, result); } private static > Map> @@ -121,9 +141,11 @@ static > ConstraintAnalysis diff( @Override public String toString() { if (matches == null) { - return "(" + score + ", no match analysis)"; + return "(%s at %s, no matches)" + .formatted(score, weight); } else { - return "(" + score + ", " + matches.size() + " matches)"; + return "(%s at %s, %s matches)" + .formatted(score, weight, matches.size()); } } } diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java b/core/core-impl/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java index c40ecad2c6..566a8a0140 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java @@ -1,6 +1,8 @@ package ai.timefold.solver.core.api.score.analysis; import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.TreeMap; @@ -38,8 +40,13 @@ * * @param score never null * @param constraintMap never null; - * constraints will be present even if they have no matches, unless their weight is zero; + * for each constraint identified by its {@link Constraint#getConstraintRef()}, + * the {@link ConstraintAnalysis} that describes the impact of that constraint on the overall score. + * Constraints are present even if they have no matches, unless their weight is zero; * zero-weight constraints are not present. + * Entries in the map have a stable iteration order; items are ordered first by {@link ConstraintAnalysis#weight()}, + * then by {@link ConstraintAnalysis#constraintRef()}. + * * @param */ public record ScoreAnalysis>(Score_ score, @@ -52,17 +59,17 @@ public record ScoreAnalysis>(Score_ score, throw new IllegalArgumentException("The constraintMap must not be empty."); } // Ensure consistent order and no external interference. - constraintMap = Collections.unmodifiableMap(new TreeMap<>(constraintMap)); - } - - /** - * For each constraint identified by its {@link Constraint#getConstraintRef()} id}, - * the {@link ConstraintAnalysis} that describes the impact of that constraint on the overall score. - * - * @return never null, unmodifiable - */ - public Map> constraintMap() { - return constraintMap; + var comparator = Comparator., Score_> comparing(ConstraintAnalysis::weight) + .reversed() + .thenComparing(ConstraintAnalysis::constraintRef); + constraintMap = Collections.unmodifiableMap(constraintMap.values() + .stream() + .sorted(comparator) + .collect(Collectors.toMap( + ConstraintAnalysis::constraintRef, + Function.identity(), + (constraintAnalysis, otherConstraintAnalysis) -> constraintAnalysis, + LinkedHashMap::new))); } /** @@ -127,8 +134,4 @@ public ScoreAnalysis diff(ScoreAnalysis other) { return new ScoreAnalysis<>(score.subtract(other.score()), result); } - @Override - public String toString() { - return "(" + score + ", " + constraintMap + ")"; - } } diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java index b37fa6c0b6..ce015b8116 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java @@ -38,6 +38,11 @@ public DefaultConstraintMatchTotal(String constraintPackage, String constraintNa this(ConstraintRef.of(constraintPackage, constraintName)); } + /** + * + * @deprecated Prefer {@link #DefaultConstraintMatchTotal(ConstraintRef, Score_)}. + */ + @Deprecated(forRemoval = true, since = "1.5.0") public DefaultConstraintMatchTotal(ConstraintRef constraintRef) { this.constraintRef = requireNonNull(constraintRef); this.constraintWeight = null; diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index 528774e7c8..17413e4f91 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -62,10 +62,12 @@ static > ConstraintAnalysis getConstraintAn return new MatchAnalysis<>(constraintMatchTotal.getConstraintRef(), score, entry.getKey()); }) .toList(); - return new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), constraintMatchTotal.getScore(), + return new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), constraintMatchTotal.getConstraintWeight(), + constraintMatchTotal.getScore(), matchAnalyses); } else { - return new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), constraintMatchTotal.getScore(), null); + return new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), constraintMatchTotal.getConstraintWeight(), + constraintMatchTotal.getScore(), null); } } diff --git a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/TestdataIncrementalScoreCalculator.java b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/TestdataIncrementalScoreCalculator.java index 1f95ea89a2..6781d685d3 100644 --- a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/TestdataIncrementalScoreCalculator.java +++ b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/TestdataIncrementalScoreCalculator.java @@ -28,7 +28,7 @@ public class TestdataIncrementalScoreCalculator public void resetWorkingSolution(TestdataSolution workingSolution) { score = 0; constraintMatchTotal = new DefaultConstraintMatchTotal<>( - ConstraintRef.of("ai.timefold.solver.core.impl.testdata.domain", "testConstraint")); + ConstraintRef.of("ai.timefold.solver.core.impl.testdata.domain", "testConstraint"), SimpleScore.ONE); indictmentMap = new HashMap<>(); for (TestdataEntity left : workingSolution.getEntityList()) { TestdataValue value = left.getValue(); diff --git a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/chained/shadow/TestdataShadowingChainedIncrementalScoreCalculator.java b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/chained/shadow/TestdataShadowingChainedIncrementalScoreCalculator.java index 8a6827a040..d7bedd8016 100644 --- a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/chained/shadow/TestdataShadowingChainedIncrementalScoreCalculator.java +++ b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/chained/shadow/TestdataShadowingChainedIncrementalScoreCalculator.java @@ -28,7 +28,8 @@ public class TestdataShadowingChainedIncrementalScoreCalculator public void resetWorkingSolution(TestdataShadowingChainedSolution workingSolution) { score = 0; constraintMatchTotal = new DefaultConstraintMatchTotal<>( - ConstraintRef.of("ai.timefold.solver.core.impl.testdata.domain.chained.shadow", "testConstraint")); + ConstraintRef.of("ai.timefold.solver.core.impl.testdata.domain.chained.shadow", "testConstraint"), + SimpleScore.ONE); indictmentMap = new HashMap<>(); for (TestdataShadowingChainedEntity left : workingSolution.getChainedEntityList()) { String code = left.getCode(); diff --git a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/list/shadow_history/TestdataListWithShadowHistoryIncrementalScoreCalculator.java b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/list/shadow_history/TestdataListWithShadowHistoryIncrementalScoreCalculator.java index 47e7a99481..e0bf5230d3 100644 --- a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/list/shadow_history/TestdataListWithShadowHistoryIncrementalScoreCalculator.java +++ b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/list/shadow_history/TestdataListWithShadowHistoryIncrementalScoreCalculator.java @@ -28,7 +28,8 @@ public class TestdataListWithShadowHistoryIncrementalScoreCalculator public void resetWorkingSolution(TestdataListSolutionWithShadowHistory workingSolution) { score = 0; constraintMatchTotal = new DefaultConstraintMatchTotal<>( - ConstraintRef.of("ai.timefold.solver.core.impl.testdata.domain.chained.shadow", "testConstraint")); + ConstraintRef.of("ai.timefold.solver.core.impl.testdata.domain.chained.shadow", "testConstraint"), + SimpleScore.ONE); indictmentMap = new HashMap<>(); for (TestdataListEntityWithShadowHistory left : workingSolution.getEntityList()) { String code = left.getCode(); diff --git a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/nullable/TestdataNullableIncrementalScoreCalculator.java b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/nullable/TestdataNullableIncrementalScoreCalculator.java index 2fdf057115..0bb3ac0f83 100644 --- a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/nullable/TestdataNullableIncrementalScoreCalculator.java +++ b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/nullable/TestdataNullableIncrementalScoreCalculator.java @@ -29,7 +29,7 @@ public final class TestdataNullableIncrementalScoreCalculator public void resetWorkingSolution(TestdataNullableSolution workingSolution) { score = 0; constraintMatchTotal = new DefaultConstraintMatchTotal<>( - ConstraintRef.of("ai.timefold.solver.core.impl.testdata.domain.shadow", "testConstraint")); + ConstraintRef.of("ai.timefold.solver.core.impl.testdata.domain.shadow", "testConstraint"), SimpleScore.ONE); indictmentMap = new HashMap<>(); for (TestdataNullableEntity left : workingSolution.getEntityList()) { TestdataValue value = left.getValue(); diff --git a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/shadow/TestdataShadowedIncrementalScoreCalculator.java b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/shadow/TestdataShadowedIncrementalScoreCalculator.java index dd02288147..b4b6c776bd 100644 --- a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/shadow/TestdataShadowedIncrementalScoreCalculator.java +++ b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/shadow/TestdataShadowedIncrementalScoreCalculator.java @@ -29,7 +29,8 @@ public class TestdataShadowedIncrementalScoreCalculator public void resetWorkingSolution(TestdataShadowedSolution workingSolution) { score = 0; constraintMatchTotal = new DefaultConstraintMatchTotal<>( - ConstraintRef.of("ai.timefold.solver.core.impl.testdata.domain.shadow", "testConstraint")); + ConstraintRef.of("ai.timefold.solver.core.impl.testdata.domain.shadow", "testConstraint"), + SimpleScore.ONE); indictmentMap = new HashMap<>(); for (TestdataShadowedEntity left : workingSolution.getEntityList()) { TestdataValue value = left.getValue(); diff --git a/examples/src/main/java/ai/timefold/solver/examples/machinereassignment/optional/score/MachineReassignmentIncrementalScoreCalculator.java b/examples/src/main/java/ai/timefold/solver/examples/machinereassignment/optional/score/MachineReassignmentIncrementalScoreCalculator.java index 3bcf66dacc..6dbc2c763a 100644 --- a/examples/src/main/java/ai/timefold/solver/examples/machinereassignment/optional/score/MachineReassignmentIncrementalScoreCalculator.java +++ b/examples/src/main/java/ai/timefold/solver/examples/machinereassignment/optional/score/MachineReassignmentIncrementalScoreCalculator.java @@ -440,23 +440,26 @@ public void resetWorkingSolution(MachineReassignment workingSolution, boolean co @Override public Collection> getConstraintMatchTotals() { DefaultConstraintMatchTotal maximumCapacityMatchTotal = - new DefaultConstraintMatchTotal<>(ConstraintRef.of(CONSTRAINT_PACKAGE, MrConstraints.MAXIMUM_CAPACITY)); + getConstraintMatchTotal(MrConstraints.MAXIMUM_CAPACITY, HardSoftLongScore.ONE_HARD); DefaultConstraintMatchTotal serviceConflictMatchTotal = - new DefaultConstraintMatchTotal<>(ConstraintRef.of(CONSTRAINT_PACKAGE, MrConstraints.SERVICE_CONFLICT)); + getConstraintMatchTotal(MrConstraints.SERVICE_CONFLICT, HardSoftLongScore.ONE_HARD); DefaultConstraintMatchTotal serviceLocationSpreadMatchTotal = - new DefaultConstraintMatchTotal<>(ConstraintRef.of(CONSTRAINT_PACKAGE, MrConstraints.SERVICE_LOCATION_SPREAD)); + getConstraintMatchTotal(MrConstraints.SERVICE_LOCATION_SPREAD, HardSoftLongScore.ONE_HARD); DefaultConstraintMatchTotal serviceDependencyMatchTotal = - new DefaultConstraintMatchTotal<>(ConstraintRef.of(CONSTRAINT_PACKAGE, MrConstraints.SERVICE_DEPENDENCY)); + getConstraintMatchTotal(MrConstraints.SERVICE_DEPENDENCY, HardSoftLongScore.ONE_HARD); DefaultConstraintMatchTotal loadCostMatchTotal = - new DefaultConstraintMatchTotal<>(ConstraintRef.of(CONSTRAINT_PACKAGE, MrConstraints.LOAD_COST)); + getConstraintMatchTotal(MrConstraints.LOAD_COST, HardSoftLongScore.ONE_SOFT); DefaultConstraintMatchTotal balanceCostMatchTotal = - new DefaultConstraintMatchTotal<>(ConstraintRef.of(CONSTRAINT_PACKAGE, MrConstraints.BALANCE_COST)); + getConstraintMatchTotal(MrConstraints.BALANCE_COST, HardSoftLongScore.ONE_SOFT); DefaultConstraintMatchTotal processMoveCostMatchTotal = - new DefaultConstraintMatchTotal<>(ConstraintRef.of(CONSTRAINT_PACKAGE, MrConstraints.PROCESS_MOVE_COST)); + getConstraintMatchTotal(MrConstraints.PROCESS_MOVE_COST, + HardSoftLongScore.ofSoft(globalPenaltyInfo.getProcessMoveCostWeight())); DefaultConstraintMatchTotal serviceMoveCostMatchTotal = - new DefaultConstraintMatchTotal<>(ConstraintRef.of(CONSTRAINT_PACKAGE, MrConstraints.SERVICE_MOVE_COST)); + getConstraintMatchTotal(MrConstraints.SERVICE_MOVE_COST, + HardSoftLongScore.ofSoft(globalPenaltyInfo.getServiceMoveCostWeight())); DefaultConstraintMatchTotal machineMoveCostMatchTotal = - new DefaultConstraintMatchTotal<>(ConstraintRef.of(CONSTRAINT_PACKAGE, MrConstraints.MACHINE_MOVE_COST)); + getConstraintMatchTotal(MrConstraints.MACHINE_MOVE_COST, + HardSoftLongScore.ofSoft(globalPenaltyInfo.getMachineMoveCostWeight())); for (MrServiceScorePart serviceScorePart : serviceScorePartMap.values()) { MrService service = serviceScorePart.service; @@ -540,6 +543,11 @@ public Collection> getConstraintMatchTot return constraintMatchTotalList; } + private static DefaultConstraintMatchTotal getConstraintMatchTotal(String constraintName, + HardSoftLongScore constraintWeight) { + return new DefaultConstraintMatchTotal<>(ConstraintRef.of(CONSTRAINT_PACKAGE, constraintName), constraintWeight); + } + @Override public Map> getIndictmentMap() { return null; // Calculate it non-incrementally from getConstraintMatchTotals() diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java index 414ce40ed1..f679dad803 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java @@ -35,11 +35,13 @@ public final ScoreAnalysis deserialize(JsonParser p, DeserializationCont var constraintPackage = constraintNode.get("package").asText(); var constraintName = constraintNode.get("name").asText(); var constraintRef = ConstraintRef.of(constraintPackage, constraintName); + var constraintWeight = parseScore(constraintNode.get("weight").asText()); var constraintScore = parseScore(constraintNode.get("score").asText()); var matchScoreList = new ArrayList>(); JsonNode matchesNode = constraintNode.get("matches"); if (matchesNode == null) { - constraintAnalysisList.put(constraintRef, new ConstraintAnalysis<>(constraintRef, constraintScore, null)); + constraintAnalysisList.put(constraintRef, + new ConstraintAnalysis<>(constraintRef, constraintWeight, constraintScore, null)); } else { constraintNode.get("matches").forEach(matchNode -> { var matchScore = parseScore(matchNode.get("score").asText()); @@ -48,7 +50,7 @@ public final ScoreAnalysis deserialize(JsonParser p, DeserializationCont matchScoreList.add(new MatchAnalysis<>(constraintRef, matchScore, justification)); }); constraintAnalysisList.put(constraintRef, - new ConstraintAnalysis<>(constraintRef, constraintScore, matchScoreList)); + new ConstraintAnalysis<>(constraintRef, constraintWeight, constraintScore, matchScoreList)); } }); return new ScoreAnalysis<>(score, constraintAnalysisList); diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java index 61aee8e201..1264a39291 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java @@ -25,6 +25,7 @@ public void serialize(ScoreAnalysis value, JsonGenerator gen, Serializer Map constraintAnalysisMap = new LinkedHashMap<>(); constraintAnalysisMap.put("package", constraintRef.packageName()); constraintAnalysisMap.put("name", constraintRef.constraintName()); + constraintAnalysisMap.put("weight", constraintAnalysis.weight().toString()); constraintAnalysisMap.put("score", constraintAnalysis.score().toString()); if (constraintAnalysis.matches() != null) { List> matchAnalysis = new ArrayList<>(constraintAnalysis.matches().size()); diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java index daa5637076..5a16c3be90 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java @@ -63,26 +63,31 @@ void scoreAnalysisWithoutMatches() throws JsonProcessingException { objectMapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); objectMapper.registerModule(TimefoldJacksonModule.createModule()); - var constraintRef1 = ConstraintRef.of("package1", "constraint1"); - var constraintRef2 = ConstraintRef.of("package2", "constraint2"); - var constraintAnalysis1 = new ConstraintAnalysis<>(constraintRef1, HardSoftScore.ofHard(1), null); - var constraintAnalysis2 = new ConstraintAnalysis<>(constraintRef2, HardSoftScore.ofSoft(2), null); + var constraintRef1 = ConstraintRef.of("packageB", "constraint1"); + var constraintRef2 = ConstraintRef.of("packageA", "constraint2"); + var constraintAnalysis1 = + new ConstraintAnalysis<>(constraintRef1, HardSoftScore.ofSoft(1), HardSoftScore.ofSoft(2), null); + var constraintAnalysis2 = + new ConstraintAnalysis<>(constraintRef2, HardSoftScore.ofHard(1), HardSoftScore.ofHard(1), null); var originalScoreAnalysis = new ScoreAnalysis<>(HardSoftScore.of(1, 2), Map.of(constraintRef1, constraintAnalysis1, constraintRef2, constraintAnalysis2)); + // Hardest constraints first, package name second. var serialized = objectMapper.writeValueAsString(originalScoreAnalysis); Assertions.assertThat(serialized) .isEqualToIgnoringWhitespace(""" { "score" : "1hard/2soft", "constraints" : [ { - "package" : "package1", - "name" : "constraint1", + "package" : "packageA", + "name" : "constraint2", + "weight" : "1hard/0soft", "score" : "1hard/0soft" }, { - "package" : "package2", - "name" : "constraint2", + "package" : "packageB", + "name" : "constraint1", + "weight" : "0hard/1soft", "score" : "0hard/2soft" } ] }"""); @@ -109,9 +114,11 @@ void scoreAnalysisWithMatches() throws JsonProcessingException { var matchAnalysis4 = new MatchAnalysis<>(constraintRef2, HardSoftScore.ofSoft(3), DefaultConstraintJustification.of(HardSoftScore.ofSoft(3), "A", "C")); var constraintAnalysis1 = - new ConstraintAnalysis<>(constraintRef1, HardSoftScore.ofHard(2), List.of(matchAnalysis1, matchAnalysis2)); + new ConstraintAnalysis<>(constraintRef1, HardSoftScore.ofHard(1), HardSoftScore.ofHard(2), + List.of(matchAnalysis1, matchAnalysis2)); var constraintAnalysis2 = - new ConstraintAnalysis<>(constraintRef2, HardSoftScore.ofSoft(4), List.of(matchAnalysis3, matchAnalysis4)); + new ConstraintAnalysis<>(constraintRef2, HardSoftScore.ofSoft(1), HardSoftScore.ofSoft(4), + List.of(matchAnalysis3, matchAnalysis4)); var originalScoreAnalysis = new ScoreAnalysis<>(HardSoftScore.of(2, 4), Map.of(constraintRef1, constraintAnalysis1, constraintRef2, constraintAnalysis2)); @@ -124,6 +131,7 @@ void scoreAnalysisWithMatches() throws JsonProcessingException { "constraints" : [ { "package" : "package1", "name" : "constraint1", + "weight" : "1hard/0soft", "score" : "2hard/0soft", "matches" : [ { "score" : "1hard/0soft", @@ -135,6 +143,7 @@ void scoreAnalysisWithMatches() throws JsonProcessingException { }, { "package" : "package2", "name" : "constraint2", + "weight" : "0hard/1soft", "score" : "0hard/4soft", "matches" : [ { "score" : "0hard/1soft",