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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.stream.Stream;

import ai.timefold.solver.core.impl.score.ScoreUtil;
import ai.timefold.solver.core.impl.score.definition.BendableScoreDefinition;
Expand All @@ -22,18 +21,30 @@
* @see Score
*/
@NullMarked
public record BendableBigDecimalScore(BigDecimal[] hardScores,
public record BendableBigDecimalScore(long structuralScore, BigDecimal[] hardScores,
Comment thread
triceo marked this conversation as resolved.
BigDecimal[] softScores) implements IBendableScore<BendableBigDecimalScore> {

public BendableBigDecimalScore(BigDecimal[] hardScores,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's deprecate these convenience constructors to make it clear that the canonical constructor is still the way to go.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about that; the solver sets the structural score, not score calculation, so for the vast majority, if not all, cases, users will do Score(0L, ...).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the user need to create a score? Should they? IMO no.
I'd even go as far as to write in the Javadoc that the constructor is not considered public API, as the user has no need to create Score instances.

BigDecimal[] softScores) {
this(0L, hardScores, softScores);
}

public static BendableBigDecimalScore parseScore(String scoreString) {
var scoreTokens = ScoreUtil.parseBendableScoreTokens(BendableBigDecimalScore.class, scoreString);
var hardScores = new BigDecimal[scoreTokens[0].length];
long structuralScore = 0L;
if (scoreTokens[0] != null && scoreTokens[0].length > 0) {
structuralScore = ScoreUtil.parseLevelAsLong(BendableBigDecimalScore.class, scoreString, scoreTokens[0][0]);
}
var hardScores = new BigDecimal[scoreTokens[1].length];
for (var i = 0; i < hardScores.length; i++) {
hardScores[i] = ScoreUtil.parseLevelAsBigDecimal(BendableBigDecimalScore.class, scoreString, scoreTokens[0][i]);
hardScores[i] = ScoreUtil.parseLevelAsBigDecimal(BendableBigDecimalScore.class, scoreString, scoreTokens[1][i]);
}
var softScores = new BigDecimal[scoreTokens[1].length];
var softScores = new BigDecimal[scoreTokens[2].length];
for (var i = 0; i < softScores.length; i++) {
softScores[i] = ScoreUtil.parseLevelAsBigDecimal(BendableBigDecimalScore.class, scoreString, scoreTokens[1][i]);
softScores[i] = ScoreUtil.parseLevelAsBigDecimal(BendableBigDecimalScore.class, scoreString, scoreTokens[2][i]);
}
if (structuralScore != 0L) {
return new BendableBigDecimalScore(structuralScore, hardScores, softScores);
}
return of(hardScores, softScores);
}
Expand Down Expand Up @@ -134,6 +145,9 @@ public BigDecimal hardOrSoftScore(int index) {

@Override
public boolean isFeasible() {
if (structuralScore < 0) {
return false;
}
for (var hardScore : hardScores) {
if (hardScore.compareTo(BigDecimal.ZERO) < 0) {
return false;
Expand Down Expand Up @@ -270,6 +284,9 @@ public Number[] toLevelNumbers() {
@Override
public boolean equals(Object o) {
if (o instanceof BendableBigDecimalScore other) {
if (structuralScore != other.structuralScore) {
return false;
}
if (hardLevelsSize() != other.hardLevelsSize()
|| softLevelsSize() != other.softLevelsSize()) {
return false;
Expand All @@ -291,16 +308,22 @@ public boolean equals(Object o) {

@Override
public int hashCode() {
var scoreHashCodes = Stream.concat(Arrays.stream(hardScores), Arrays.stream(softScores))
.map(BigDecimal::stripTrailingZeros)
.mapToInt(BigDecimal::hashCode)
.toArray();
return Arrays.hashCode(scoreHashCodes);
var hash = Long.hashCode(structuralScore);
for (var hardScore : hardScores) {
hash = 31 * hash + hardScore.stripTrailingZeros().hashCode();
}
for (var softScore : softScores) {
hash = 31 * hash + softScore.stripTrailingZeros().hashCode();
}
return hash;
}

@Override
public int compareTo(BendableBigDecimalScore other) {
validateCompatible(other);
if (structuralScore != other.structuralScore) {
return Long.compare(structuralScore, other.structuralScore);
}
for (var i = 0; i < hardScores.length; i++) {
var hardScoreComparison = hardScores[i].compareTo(other.hardScore(i));
if (hardScoreComparison != 0) {
Expand All @@ -323,7 +346,10 @@ public String toShortString() {

@Override
public String toString() {
var s = new StringBuilder(((hardScores.length + softScores.length) * 4) + 7);
var s = new StringBuilder(((hardScores.length + softScores.length) * 4) + 15);
if (structuralScore < 0) {
s.append("%dstructural/".formatted(structuralScore));
}
s.append("[");
var first = true;
for (var hardScore : hardScores) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package ai.timefold.solver.core.api.score;

import java.util.Arrays;
import java.util.Objects;

import ai.timefold.solver.core.impl.score.ScoreUtil;
import ai.timefold.solver.core.impl.score.definition.BendableScoreDefinition;
Expand All @@ -20,17 +19,29 @@
* @see Score
*/
@NullMarked
public record BendableScore(long[] hardScores, long[] softScores) implements IBendableScore<BendableScore> {
public record BendableScore(long structuralScore, long[] hardScores,
long[] softScores) implements IBendableScore<BendableScore> {

public BendableScore(long[] hardScores, long[] softScores) {
this(0L, hardScores, softScores);
}

public static BendableScore parseScore(String scoreString) {
var scoreTokens = ScoreUtil.parseBendableScoreTokens(BendableScore.class, scoreString);
var hardScores = new long[scoreTokens[0].length];
long structuralScore = 0L;
if (scoreTokens[0] != null && scoreTokens[0].length > 0) {
structuralScore = ScoreUtil.parseLevelAsLong(BendableScore.class, scoreString, scoreTokens[0][0]);
}
var hardScores = new long[scoreTokens[1].length];
for (var i = 0; i < hardScores.length; i++) {
hardScores[i] = ScoreUtil.parseLevelAsLong(BendableScore.class, scoreString, scoreTokens[0][i]);
hardScores[i] = ScoreUtil.parseLevelAsLong(BendableScore.class, scoreString, scoreTokens[1][i]);
}
var softScores = new long[scoreTokens[1].length];
var softScores = new long[scoreTokens[2].length];
for (var i = 0; i < softScores.length; i++) {
softScores[i] = ScoreUtil.parseLevelAsLong(BendableScore.class, scoreString, scoreTokens[1][i]);
softScores[i] = ScoreUtil.parseLevelAsLong(BendableScore.class, scoreString, scoreTokens[2][i]);
}
if (structuralScore != 0L) {
return new BendableScore(structuralScore, hardScores, softScores);
}
return of(hardScores, softScores);
}
Expand Down Expand Up @@ -123,6 +134,9 @@ public long hardOrSoftScore(int index) {

@Override
public boolean isFeasible() {
if (structuralScore < 0) {
return false;
}
for (var hardScore : hardScores) {
if (hardScore < 0) {
return false;
Expand Down Expand Up @@ -249,6 +263,9 @@ public Number[] toLevelNumbers() {
@Override
public boolean equals(Object o) {
if (o instanceof BendableScore other) {
if (structuralScore != other.structuralScore) {
return false;
}
if (hardLevelsSize() != other.hardLevelsSize()
|| softLevelsSize() != other.softLevelsSize()) {
return false;
Expand All @@ -270,12 +287,18 @@ public boolean equals(Object o) {

@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(hardScores), Arrays.hashCode(softScores));
var hash = Long.hashCode(structuralScore);
hash = 31 * hash + Arrays.hashCode(hardScores);
hash = 31 * hash + Arrays.hashCode(softScores);
return hash;
}

@Override
public int compareTo(BendableScore other) {
validateCompatible(other);
if (structuralScore != other.structuralScore) {
return Long.compare(structuralScore, other.structuralScore);
}
for (var i = 0; i < hardScores.length; i++) {
if (hardScores[i] != other.hardScore(i)) {
return Long.compare(hardScores[i], other.hardScore(i));
Expand All @@ -296,7 +319,10 @@ public String toShortString() {

@Override
public String toString() {
var s = new StringBuilder(((hardScores.length + softScores.length) * 4) + 7);
var s = new StringBuilder(((hardScores.length + softScores.length) * 4) + 15);
if (structuralScore < 0) {
s.append("%dstructural/".formatted(structuralScore));
}
s.append("[");
var first = true;
for (var hardScore : hardScores) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* @see Score
*/
@NullMarked
public record HardMediumSoftBigDecimalScore(BigDecimal hardScore, BigDecimal mediumScore,
public record HardMediumSoftBigDecimalScore(long structuralScore, BigDecimal hardScore, BigDecimal mediumScore,
BigDecimal softScore) implements Score<HardMediumSoftBigDecimalScore> {

public static final HardMediumSoftBigDecimalScore ZERO = new HardMediumSoftBigDecimalScore(BigDecimal.ZERO,
Expand All @@ -45,13 +45,28 @@ public record HardMediumSoftBigDecimalScore(BigDecimal hardScore, BigDecimal med
private static final HardMediumSoftBigDecimalScore MINUS_ONE_SOFT =
new HardMediumSoftBigDecimalScore(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ONE.negate());

public HardMediumSoftBigDecimalScore(BigDecimal hardScore, BigDecimal mediumScore,
BigDecimal softScore) {
this(0L, hardScore, mediumScore, softScore);
}

public static HardMediumSoftBigDecimalScore parseScore(String scoreString) {
var scoreTokens = ScoreUtil.parseScoreTokens(HardMediumSoftBigDecimalScore.class, scoreString,
HARD_LABEL, MEDIUM_LABEL, SOFT_LABEL);
var hardScore = ScoreUtil.parseLevelAsBigDecimal(HardMediumSoftBigDecimalScore.class, scoreString, scoreTokens[0]);
var mediumScore = ScoreUtil.parseLevelAsBigDecimal(HardMediumSoftBigDecimalScore.class, scoreString, scoreTokens[1]);
var softScore = ScoreUtil.parseLevelAsBigDecimal(HardMediumSoftBigDecimalScore.class, scoreString, scoreTokens[2]);
return of(hardScore, mediumScore, softScore);
if (scoreTokens.length == 3) {
var hardScore = ScoreUtil.parseLevelAsBigDecimal(HardMediumSoftBigDecimalScore.class, scoreString, scoreTokens[0]);
var mediumScore =
ScoreUtil.parseLevelAsBigDecimal(HardMediumSoftBigDecimalScore.class, scoreString, scoreTokens[1]);
var softScore = ScoreUtil.parseLevelAsBigDecimal(HardMediumSoftBigDecimalScore.class, scoreString, scoreTokens[2]);
return of(hardScore, mediumScore, softScore);
} else {
var structuralScore = ScoreUtil.parseLevelAsLong(HardMediumSoftBigDecimalScore.class, scoreString, scoreTokens[0]);
var hardScore = ScoreUtil.parseLevelAsBigDecimal(HardMediumSoftBigDecimalScore.class, scoreString, scoreTokens[1]);
var mediumScore =
ScoreUtil.parseLevelAsBigDecimal(HardMediumSoftBigDecimalScore.class, scoreString, scoreTokens[2]);
var softScore = ScoreUtil.parseLevelAsBigDecimal(HardMediumSoftBigDecimalScore.class, scoreString, scoreTokens[3]);
return new HardMediumSoftBigDecimalScore(structuralScore, hardScore, mediumScore, softScore);
}
}

public static HardMediumSoftBigDecimalScore of(BigDecimal hardScore, BigDecimal mediumScore,
Expand Down Expand Up @@ -118,7 +133,7 @@ public static HardMediumSoftBigDecimalScore ofSoft(BigDecimal softScore) {
*/
@Override
public boolean isFeasible() {
return hardScore.compareTo(BigDecimal.ZERO) >= 0;
return structuralScore >= 0 && hardScore.compareTo(BigDecimal.ZERO) >= 0;
}

@Override
Expand Down Expand Up @@ -183,8 +198,9 @@ public Number[] toLevelNumbers() {

@Override
public boolean equals(Object o) {
if (o instanceof HardMediumSoftBigDecimalScore(var otherHardScore, var otherMediumScore, var otherSoftScore)) {
return hardScore.stripTrailingZeros().equals(otherHardScore.stripTrailingZeros())
if (o instanceof HardMediumSoftBigDecimalScore(var otherStructuralScore, var otherHardScore, var otherMediumScore, var otherSoftScore)) {
return structuralScore == otherStructuralScore
&& hardScore.stripTrailingZeros().equals(otherHardScore.stripTrailingZeros())
&& mediumScore.stripTrailingZeros().equals(otherMediumScore.stripTrailingZeros())
&& softScore.stripTrailingZeros().equals(otherSoftScore.stripTrailingZeros());
}
Expand All @@ -193,11 +209,15 @@ public boolean equals(Object o) {

@Override
public int hashCode() {
return Objects.hash(hardScore.stripTrailingZeros(), mediumScore.stripTrailingZeros(), softScore.stripTrailingZeros());
return Objects.hash(structuralScore, hardScore.stripTrailingZeros(), mediumScore.stripTrailingZeros(),
softScore.stripTrailingZeros());
}

@Override
public int compareTo(HardMediumSoftBigDecimalScore other) {
if (structuralScore != other.structuralScore) {
return Long.compare(structuralScore, other.structuralScore);
}
var hardScoreComparison = hardScore.compareTo(other.hardScore());
if (hardScoreComparison != 0) {
return hardScoreComparison;
Expand All @@ -218,7 +238,10 @@ public String toShortString() {

@Override
public String toString() {
return hardScore + HARD_LABEL + "/" + mediumScore + MEDIUM_LABEL + "/" + softScore + SOFT_LABEL;
return (structuralScore < 0)
? "%dstructural/%s%s/%s%s/%s%s".formatted(structuralScore, hardScore, HARD_LABEL, mediumScore, MEDIUM_LABEL,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have String constants for hard/medium/soft, let's introduce a constant for "structural" as well.

softScore, SOFT_LABEL)
: "%s%s/%s%s/%s%s".formatted(hardScore, HARD_LABEL, mediumScore, MEDIUM_LABEL, softScore, SOFT_LABEL);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* @see Score
*/
@NullMarked
public record HardMediumSoftScore(long hardScore, long mediumScore,
public record HardMediumSoftScore(long structuralScore, long hardScore, long mediumScore,
long softScore) implements Score<HardMediumSoftScore> {

public static final HardMediumSoftScore ZERO = new HardMediumSoftScore(0L, 0L, 0L);
Expand All @@ -31,13 +31,25 @@ public record HardMediumSoftScore(long hardScore, long mediumScore,
public static final HardMediumSoftScore ONE_SOFT = new HardMediumSoftScore(0L, 0L, 1L);
private static final HardMediumSoftScore MINUS_ONE_SOFT = new HardMediumSoftScore(0L, 0L, -1L);

public HardMediumSoftScore(long hardScore, long mediumScore, long softScore) {
this(0L, hardScore, mediumScore, softScore);
}

public static HardMediumSoftScore parseScore(String scoreString) {
var scoreTokens = ScoreUtil.parseScoreTokens(HardMediumSoftScore.class, scoreString,
HARD_LABEL, MEDIUM_LABEL, SOFT_LABEL);
var hardScore = ScoreUtil.parseLevelAsLong(HardMediumSoftScore.class, scoreString, scoreTokens[0]);
var mediumScore = ScoreUtil.parseLevelAsLong(HardMediumSoftScore.class, scoreString, scoreTokens[1]);
var softScore = ScoreUtil.parseLevelAsLong(HardMediumSoftScore.class, scoreString, scoreTokens[2]);
return of(hardScore, mediumScore, softScore);
if (scoreTokens.length == 3) {
var hardScore = ScoreUtil.parseLevelAsLong(HardMediumSoftScore.class, scoreString, scoreTokens[0]);
var mediumScore = ScoreUtil.parseLevelAsLong(HardMediumSoftScore.class, scoreString, scoreTokens[1]);
var softScore = ScoreUtil.parseLevelAsLong(HardMediumSoftScore.class, scoreString, scoreTokens[2]);
return of(hardScore, mediumScore, softScore);
} else {
var structuralScore = ScoreUtil.parseLevelAsLong(HardMediumSoftScore.class, scoreString, scoreTokens[0]);
var hardScore = ScoreUtil.parseLevelAsLong(HardMediumSoftScore.class, scoreString, scoreTokens[1]);
var mediumScore = ScoreUtil.parseLevelAsLong(HardMediumSoftScore.class, scoreString, scoreTokens[2]);
var softScore = ScoreUtil.parseLevelAsLong(HardMediumSoftScore.class, scoreString, scoreTokens[3]);
return new HardMediumSoftScore(structuralScore, hardScore, mediumScore, softScore);
}
}

public static HardMediumSoftScore of(long hardScore, long mediumScore, long softScore) {
Expand Down Expand Up @@ -103,7 +115,7 @@ public static HardMediumSoftScore ofSoft(long softScore) {
*/
@Override
public boolean isFeasible() {
return hardScore >= 0L;
return structuralScore >= 0 && hardScore >= 0L;
}

@Override
Expand Down Expand Up @@ -158,8 +170,9 @@ public Number[] toLevelNumbers() {

@Override
public boolean equals(Object o) {
if (o instanceof HardMediumSoftScore(var otherHardScore, var otherMediumScore, var otherSoftScore)) {
return hardScore == otherHardScore
if (o instanceof HardMediumSoftScore(var otherStructuralScore, var otherHardScore, var otherMediumScore, var otherSoftScore)) {
return structuralScore == otherStructuralScore
&& hardScore == otherHardScore
&& mediumScore == otherMediumScore
&& softScore == otherSoftScore;
}
Expand All @@ -168,7 +181,9 @@ public boolean equals(Object o) {

@Override
public int compareTo(HardMediumSoftScore other) {
if (hardScore != other.hardScore()) {
if (structuralScore != other.structuralScore()) {
return Long.compare(structuralScore, other.structuralScore());
} else if (hardScore != other.hardScore()) {
return Long.compare(hardScore, other.hardScore());
} else if (mediumScore != other.mediumScore()) {
return Long.compare(mediumScore, other.mediumScore());
Expand All @@ -184,7 +199,10 @@ public String toShortString() {

@Override
public String toString() {
return hardScore + HARD_LABEL + "/" + mediumScore + MEDIUM_LABEL + "/" + softScore + SOFT_LABEL;
return (structuralScore < 0)
? "%dstructural/%d%s/%d%s/%d%s".formatted(structuralScore, hardScore, HARD_LABEL, mediumScore, MEDIUM_LABEL,
softScore, SOFT_LABEL)
: "%d%s/%d%s/%d%s".formatted(hardScore, HARD_LABEL, mediumScore, MEDIUM_LABEL, softScore, SOFT_LABEL);
}

}
Loading