Skip to content

Commit 5aacd1b

Browse files
committed
feat: maximum function over connected ranges
1 parent 41c117e commit 5aacd1b

File tree

6 files changed

+227
-0
lines changed

6 files changed

+227
-0
lines changed

core/src/main/java/ai/timefold/solver/core/api/score/stream/common/ConnectedRange.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package ai.timefold.solver.core.api.score.stream.common;
22

3+
import java.util.Collection;
4+
import java.util.function.ToIntBiFunction;
5+
import java.util.function.ToIntFunction;
6+
37
import org.jspecify.annotations.NonNull;
48

59
/**
@@ -46,6 +50,26 @@ public interface ConnectedRange<Range_, Point_ extends Comparable<Point_>, Diffe
4650
*/
4751
int getMaximumOverlap();
4852

53+
/**
54+
* Get the maximum sum of a function amongst distinct ranges of overlapping values
55+
* amongst all points contained by this {@link ConnectedRange}.
56+
*
57+
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
58+
* for any point contained by this {@link ConnectedRange}.
59+
*/
60+
int getMaximumValue(ToIntFunction<? super Range_> functionSupplier);
61+
62+
/**
63+
* Get the maximum sum of a function amongst distinct ranges of overlapping values
64+
* amongst all points contained by this {@link ConnectedRange}. This method allows you to use
65+
* a function that takes all active ranges as an input. Use {@link ::getMaximumValue} if possible
66+
* for efficiency.
67+
*
68+
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
69+
* for any point contained by this {@link ConnectedRange}.
70+
*/
71+
int getMaximumValueForDistinctRanges(ToIntBiFunction<Collection<? super Range_>, Difference_> functionSupplier);
72+
4973
/**
5074
* Get the length of this {@link ConnectedRange}.
5175
*

core/src/main/java/ai/timefold/solver/core/api/score/stream/common/ConnectedRangeChain.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package ai.timefold.solver.core.api.score.stream.common;
22

3+
import java.util.Collection;
4+
import java.util.function.ToIntBiFunction;
5+
import java.util.function.ToIntFunction;
6+
37
import org.jspecify.annotations.NonNull;
48

59
/**
@@ -24,4 +28,24 @@ public interface ConnectedRangeChain<Range_, Point_ extends Comparable<Point_>,
2428
*/
2529
@NonNull
2630
Iterable<RangeGap<Point_, Difference_>> getGaps();
31+
32+
/**
33+
* Get the maximum sum of a function amongst distinct ranges of overlapping values
34+
* amongst all points contained by this {@link ConnectedRange}.
35+
*
36+
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
37+
* for any point contained by this {@link ConnectedRange}.
38+
*/
39+
int getMaximumValue(ToIntFunction<? super Range_> functionSupplier);
40+
41+
/**
42+
* Get the maximum sum of a function amongst distinct ranges of overlapping values
43+
* amongst all points contained by this {@link ConnectedRange}. This method allows you to use
44+
* a function that takes all active ranges as an input. Use {@link ::getMaximumValue} if possible
45+
* for efficiency.
46+
*
47+
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
48+
* for any point contained by this {@link ConnectedRange}.
49+
*/
50+
int getMaximumValueForDistinctRanges(ToIntBiFunction<Collection<? super Range_>, Difference_> functionSupplier);
2751
}

core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeChainImpl.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package ai.timefold.solver.core.impl.score.stream.collector.connected_ranges;
22

3+
import java.util.Collection;
34
import java.util.NavigableMap;
45
import java.util.NavigableSet;
56
import java.util.Objects;
67
import java.util.TreeMap;
78
import java.util.function.BiFunction;
9+
import java.util.function.ToIntBiFunction;
10+
import java.util.function.ToIntFunction;
811

912
import ai.timefold.solver.core.api.score.stream.common.ConnectedRange;
1013
import ai.timefold.solver.core.api.score.stream.common.ConnectedRangeChain;
@@ -251,6 +254,38 @@ void removeRange(Range<Range_, Point_> range) {
251254
return (Iterable) startSplitPointToNextGap.values();
252255
}
253256

257+
/**
258+
* Get the maximum sum of a function amongst distinct ranges of overlapping values
259+
* amongst all points contained by this {@link ConnectedRangeChain}.
260+
*
261+
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
262+
* for any point contained by this {@link ConnectedRangeChain}.
263+
*/
264+
public int getMaximumValue(ToIntFunction<? super Range_> functionSupplier) {
265+
var max = 0;
266+
for (ConnectedRange<Range_, Point_, Difference_> range : getConnectedRanges()) {
267+
max = Math.max(range.getMaximumValue(functionSupplier), max);
268+
}
269+
return max;
270+
}
271+
272+
/**
273+
* Get the maximum sum of a function amongst distinct ranges of overlapping values
274+
* amongst all points contained by this {@link ConnectedRangeChain}. This method allows you to use
275+
* a function that takes all active ranges as an input. Use {@link ::getMaximumValue} if possible
276+
* for efficiency.
277+
*
278+
* @return get the maximum sum of a function amongst distinct ranges of overlapping values
279+
* for any point contained by this {@link ConnectedRangeChain}.
280+
*/
281+
public int getMaximumValueForDistinctRanges(ToIntBiFunction<Collection<? super Range_>, Difference_> functionSupplier) {
282+
var max = 0;
283+
for (ConnectedRange<Range_, Point_, Difference_> range : getConnectedRanges()) {
284+
max = Math.max(range.getMaximumValueForDistinctRanges(functionSupplier), max);
285+
}
286+
return max;
287+
}
288+
254289
@Override
255290
public boolean equals(Object o) {
256291
if (this == o)

core/src/main/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeImpl.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package ai.timefold.solver.core.impl.score.stream.collector.connected_ranges;
22

3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
import java.util.Collections;
36
import java.util.Iterator;
47
import java.util.NavigableSet;
58
import java.util.Objects;
69
import java.util.function.BiFunction;
10+
import java.util.function.ToIntBiFunction;
11+
import java.util.function.ToIntFunction;
712

813
import ai.timefold.solver.core.api.score.stream.common.ConnectedRange;
914

@@ -134,6 +139,43 @@ public int getMaximumOverlap() {
134139
return maximumOverlap;
135140
}
136141

142+
@Override
143+
public int getMaximumValue(ToIntFunction<? super Range_> functionSupplier) {
144+
var current = startSplitPoint;
145+
var activeRangeCount = 0;
146+
var maxValue = 0;
147+
var activeValue = 0;
148+
do {
149+
activeRangeCount += current.rangesStartingAtSplitPointSet.size() - current.rangesEndingAtSplitPointSet.size();
150+
activeValue += current.rangesStartingAtSplitPointSet.stream().map(Range::getValue).mapToInt(functionSupplier).sum();
151+
activeValue -= current.rangesEndingAtSplitPointSet.stream().map(Range::getValue).mapToInt(functionSupplier).sum();
152+
maxValue = Math.max(maxValue, activeValue);
153+
current = splitPointSet.higher(current);
154+
} while (activeRangeCount > 0 && current != null);
155+
return maxValue;
156+
}
157+
158+
@Override
159+
public int getMaximumValueForDistinctRanges(ToIntBiFunction<Collection<? super Range_>, Difference_> functionSupplier) {
160+
var current = startSplitPoint;
161+
var activeValue = 0;
162+
var activeRanges = new ArrayList<>();
163+
var next = splitPointSet.higher(current);
164+
while (next != null) {
165+
activeRanges.addAll(current.rangesStartingAtSplitPointSet);
166+
activeRanges.removeAll(current.rangesEndingAtSplitPointSet);
167+
activeValue = Math.max(
168+
functionSupplier.applyAsInt(
169+
Collections.unmodifiableList(activeRanges),
170+
differenceFunction.apply(current.splitPoint, next.splitPoint)),
171+
activeValue);
172+
current = next;
173+
next = splitPointSet.higher(current);
174+
}
175+
;
176+
return activeValue;
177+
}
178+
137179
@Override
138180
public @NonNull Point_ getStart() {
139181
return startSplitPoint.splitPoint;

core/src/test/java/ai/timefold/solver/core/impl/score/stream/collector/connected_ranges/ConnectedRangeTrackerTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44

55
import java.util.ArrayList;
6+
import java.util.Collection;
67
import java.util.HashMap;
78
import java.util.List;
89
import java.util.Map;
@@ -67,6 +68,10 @@ private ConnectedRangeTracker<TestRange, Integer, Integer> getIntegerConnectedRa
6768
return new ConnectedRangeTracker<>(TestRange::getStart, TestRange::getEnd, (a, b) -> b - a);
6869
}
6970

71+
static int rangeMaxFunction(Collection<? super TestRange> ranges, int length) {
72+
return 100 * ranges.size() / length;
73+
}
74+
7075
@Test
7176
void testNonConsecutiveRanges() {
7277
ConnectedRangeTracker<TestRange, Integer, Integer> tree = getIntegerConnectedRangeTracker();
@@ -96,6 +101,10 @@ void testNonConsecutiveRanges() {
96101
assertThat(connectedRangeList.get(2).getMinimumOverlap()).isEqualTo(1);
97102
assertThat(connectedRangeList.get(2).getMaximumOverlap()).isEqualTo(1);
98103

104+
assertThat(connectedRangeList.get(2).getMaximumValue(i -> 1)).isEqualTo(1);
105+
assertThat(connectedRangeList.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
106+
.isEqualTo(50);
107+
99108
verifyGaps(tree);
100109
}
101110

@@ -116,6 +125,14 @@ void testConsecutiveRanges() {
116125
assertThat(connectedRangeList.get(0)).containsExactly(new TestRange(0, 2), new TestRange(2, 4), new TestRange(4, 7));
117126
assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(1);
118127
assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(1);
128+
assertThat(connectedRangeList.get(0).getMaximumValue(i -> 1)).isEqualTo(1);
129+
assertThat(connectedRangeList.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
130+
.isEqualTo(50);
131+
132+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(1);
133+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
134+
.isEqualTo(50);
135+
119136
verifyGaps(tree);
120137
}
121138

@@ -135,9 +152,20 @@ void testDuplicateRanges() {
135152
assertThat(connectedRangeList.get(0)).containsExactly(a.getValue(), a.getValue());
136153
assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(2);
137154
assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(2);
155+
assertThat(connectedRangeList.get(0).getMaximumValue(i -> 1)).isEqualTo(2);
156+
assertThat(connectedRangeList.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
157+
.isEqualTo(100);
138158
assertThat(connectedRangeList.get(1)).containsExactly(b.getValue());
139159
assertThat(connectedRangeList.get(1).getMinimumOverlap()).isEqualTo(1);
140160
assertThat(connectedRangeList.get(1).getMaximumOverlap()).isEqualTo(1);
161+
assertThat(connectedRangeList.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
162+
assertThat(connectedRangeList.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
163+
.isEqualTo(33);
164+
165+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(2);
166+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
167+
.isEqualTo(100);
168+
141169
verifyGaps(tree);
142170
}
143171

@@ -221,16 +249,29 @@ void testOverlappingRange() {
221249
assertThat(connectedRanges.get(0).hasOverlap()).isTrue();
222250
assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1);
223251
assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(2);
252+
assertThat(connectedRanges.get(0).getMaximumValue(i -> 1)).isEqualTo(2);
253+
assertThat(connectedRanges.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
254+
.isEqualTo(200);
224255

225256
assertThat(connectedRanges.get(1)).containsExactly(d.getValue());
226257
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
227258
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
228259
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
260+
assertThat(connectedRanges.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
261+
assertThat(connectedRanges.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
262+
.isEqualTo(100);
229263

230264
assertThat(connectedRanges.get(2)).containsExactly(e.getValue(), removedTestRange2);
231265
assertThat(connectedRanges.get(2).hasOverlap()).isTrue();
232266
assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(2);
233267
assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(2);
268+
assertThat(connectedRanges.get(2).getMaximumValue(i -> 1)).isEqualTo(2);
269+
assertThat(connectedRanges.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
270+
.isEqualTo(100);
271+
272+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(2);
273+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
274+
.isEqualTo(200);
234275

235276
verifyGaps(tree);
236277

@@ -247,16 +288,29 @@ void testOverlappingRange() {
247288
assertThat(connectedRanges.get(0).hasOverlap()).isFalse();
248289
assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1);
249290
assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1);
291+
assertThat(connectedRanges.get(0).getMaximumValue(i -> 1)).isEqualTo(1);
292+
assertThat(connectedRanges.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
293+
.isEqualTo(100);
250294

251295
assertThat(connectedRanges.get(1)).containsExactly(d.getValue());
252296
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
253297
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
254298
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
299+
assertThat(connectedRanges.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
300+
assertThat(connectedRanges.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
301+
.isEqualTo(100);
255302

256303
assertThat(connectedRanges.get(2)).containsExactly(e.getValue(), removedTestRange2);
257304
assertThat(connectedRanges.get(2).hasOverlap()).isTrue();
258305
assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(2);
259306
assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(2);
307+
assertThat(connectedRanges.get(2).getMaximumValue(i -> 1)).isEqualTo(2);
308+
assertThat(connectedRanges.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
309+
.isEqualTo(100);
310+
311+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(2);
312+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
313+
.isEqualTo(100);
260314

261315
verifyGaps(tree);
262316

@@ -272,16 +326,29 @@ void testOverlappingRange() {
272326
assertThat(connectedRanges.get(0).hasOverlap()).isFalse();
273327
assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1);
274328
assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1);
329+
assertThat(connectedRanges.get(0).getMaximumValue(i -> 1)).isEqualTo(1);
330+
assertThat(connectedRanges.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
331+
.isEqualTo(100);
275332

276333
assertThat(connectedRanges.get(1)).containsExactly(d.getValue());
277334
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
278335
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
279336
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
337+
assertThat(connectedRanges.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
338+
assertThat(connectedRanges.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
339+
.isEqualTo(100);
280340

281341
assertThat(connectedRanges.get(2)).containsExactly(e.getValue());
282342
assertThat(connectedRanges.get(2).hasOverlap()).isFalse();
283343
assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(1);
284344
assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(1);
345+
assertThat(connectedRanges.get(2).getMaximumValue(i -> 1)).isEqualTo(1);
346+
assertThat(connectedRanges.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
347+
.isEqualTo(50);
348+
349+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(1);
350+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
351+
.isEqualTo(100);
285352

286353
verifyGaps(tree);
287354
Range<TestRange, Integer> g = tree.getRange(new TestRange(6, 7));
@@ -298,6 +365,10 @@ void testOverlappingRange() {
298365
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
299366
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
300367
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
368+
369+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(1);
370+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
371+
.isEqualTo(100);
301372
}
302373

303374
void verifyGaps(ConnectedRangeTracker<TestRange, Integer, Integer> tree) {

docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,37 @@ Java::
10661066
----
10671067
====
10681068

1069+
In the event that data about distinct ranges of overlapping values is needed beyond the total count, `ConnectedRange` provides methods to calculate a max sum
1070+
of an integer function over each distinct arrangement of overlapping entities:
1071+
1072+
[tabs]
1073+
====
1074+
Java::
1075+
+
1076+
[source,java,options="nowrap"]
1077+
----
1078+
.groupBy(Job::getEquipment,
1079+
ConstraintCollectors.toConnectedRanges(
1080+
Job::getStart,
1081+
Job::getEnd
1082+
)
1083+
).expand(
1084+
connectedRangeChain -> connectedRangeChain.getMaximumValueForDistinctRanges(
1085+
(activeJobList, duration) -> {
1086+
var required = activeJobList.stream().mapToInt(Job::getCapacityRequired).sum()
1087+
var capacity = equipment.getCapacity() - (activeJobList.size() - 1); // Lose a little capacity per additional job
1088+
return Math.max(0, capacity - required);
1089+
}
1090+
)
1091+
)
1092+
.filter((connectedRangeChain, amountOverCapacity) -> amountOverCapacity > 0)
1093+
.penalize((connectedRangeChain, amountOverCapacity) -> amountOverCapacity)
1094+
)
1095+
)
1096+
----
1097+
====
1098+
1099+
10691100
[#collectorsLoadBalance]
10701101
==== Load balancing collectors
10711102

0 commit comments

Comments
 (0)