Skip to content

Commit fe481bf

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

File tree

6 files changed

+215
-0
lines changed

6 files changed

+215
-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
@@ -2,6 +2,10 @@
22

33
import org.jspecify.annotations.NonNull;
44

5+
import java.util.Collection;
6+
import java.util.function.ToIntBiFunction;
7+
import java.util.function.ToIntFunction;
8+
59
/**
610
* Contains info regarding {@link ConnectedRange}s and {@link RangeGap}s for a collection of ranges.
711
*
@@ -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: 36 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,39 @@ void removeRange(Range<Range_, Point_> range) {
251254
return (Iterable) startSplitPointToNextGap.values();
252255
}
253256

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

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
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;
9+
610
import java.util.function.BiFunction;
11+
import java.util.function.ToIntBiFunction;
12+
import java.util.function.ToIntFunction;
713

814
import ai.timefold.solver.core.api.score.stream.common.ConnectedRange;
915

@@ -134,6 +140,43 @@ public int getMaximumOverlap() {
134140
return maximumOverlap;
135141
}
136142

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

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

Lines changed: 57 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,13 @@ 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)).isEqualTo(50);
134+
119135
verifyGaps(tree);
120136
}
121137

@@ -135,9 +151,19 @@ void testDuplicateRanges() {
135151
assertThat(connectedRangeList.get(0)).containsExactly(a.getValue(), a.getValue());
136152
assertThat(connectedRangeList.get(0).getMinimumOverlap()).isEqualTo(2);
137153
assertThat(connectedRangeList.get(0).getMaximumOverlap()).isEqualTo(2);
154+
assertThat(connectedRangeList.get(0).getMaximumValue(i -> 1)).isEqualTo(2);
155+
assertThat(connectedRangeList.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
156+
.isEqualTo(100);
138157
assertThat(connectedRangeList.get(1)).containsExactly(b.getValue());
139158
assertThat(connectedRangeList.get(1).getMinimumOverlap()).isEqualTo(1);
140159
assertThat(connectedRangeList.get(1).getMaximumOverlap()).isEqualTo(1);
160+
assertThat(connectedRangeList.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
161+
assertThat(connectedRangeList.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction))
162+
.isEqualTo(33);
163+
164+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(2);
165+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
166+
141167
verifyGaps(tree);
142168
}
143169

@@ -221,16 +247,25 @@ void testOverlappingRange() {
221247
assertThat(connectedRanges.get(0).hasOverlap()).isTrue();
222248
assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1);
223249
assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(2);
250+
assertThat(connectedRanges.get(0).getMaximumValue(i -> 1)).isEqualTo(2);
251+
assertThat(connectedRanges.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(200);
224252

225253
assertThat(connectedRanges.get(1)).containsExactly(d.getValue());
226254
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
227255
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
228256
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
257+
assertThat(connectedRanges.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
258+
assertThat(connectedRanges.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
229259

230260
assertThat(connectedRanges.get(2)).containsExactly(e.getValue(), removedTestRange2);
231261
assertThat(connectedRanges.get(2).hasOverlap()).isTrue();
232262
assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(2);
233263
assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(2);
264+
assertThat(connectedRanges.get(2).getMaximumValue(i -> 1)).isEqualTo(2);
265+
assertThat(connectedRanges.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
266+
267+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(2);
268+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(200);
234269

235270
verifyGaps(tree);
236271

@@ -247,16 +282,26 @@ void testOverlappingRange() {
247282
assertThat(connectedRanges.get(0).hasOverlap()).isFalse();
248283
assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1);
249284
assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1);
285+
assertThat(connectedRanges.get(0).getMaximumValue(i -> 1)).isEqualTo(1);
286+
assertThat(connectedRanges.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
250287

251288
assertThat(connectedRanges.get(1)).containsExactly(d.getValue());
252289
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
253290
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
254291
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
292+
assertThat(connectedRanges.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
293+
assertThat(connectedRanges.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
255294

256295
assertThat(connectedRanges.get(2)).containsExactly(e.getValue(), removedTestRange2);
257296
assertThat(connectedRanges.get(2).hasOverlap()).isTrue();
258297
assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(2);
259298
assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(2);
299+
assertThat(connectedRanges.get(2).getMaximumValue(i -> 1)).isEqualTo(2);
300+
assertThat(connectedRanges.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
301+
302+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(2);
303+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
304+
260305

261306
verifyGaps(tree);
262307

@@ -272,16 +317,25 @@ void testOverlappingRange() {
272317
assertThat(connectedRanges.get(0).hasOverlap()).isFalse();
273318
assertThat(connectedRanges.get(0).getMinimumOverlap()).isEqualTo(1);
274319
assertThat(connectedRanges.get(0).getMaximumOverlap()).isEqualTo(1);
320+
assertThat(connectedRanges.get(0).getMaximumValue(i -> 1)).isEqualTo(1);
321+
assertThat(connectedRanges.get(0).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
275322

276323
assertThat(connectedRanges.get(1)).containsExactly(d.getValue());
277324
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
278325
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
279326
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
327+
assertThat(connectedRanges.get(1).getMaximumValue(i -> 1)).isEqualTo(1);
328+
assertThat(connectedRanges.get(1).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
280329

281330
assertThat(connectedRanges.get(2)).containsExactly(e.getValue());
282331
assertThat(connectedRanges.get(2).hasOverlap()).isFalse();
283332
assertThat(connectedRanges.get(2).getMinimumOverlap()).isEqualTo(1);
284333
assertThat(connectedRanges.get(2).getMaximumOverlap()).isEqualTo(1);
334+
assertThat(connectedRanges.get(2).getMaximumValue(i -> 1)).isEqualTo(1);
335+
assertThat(connectedRanges.get(2).getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(50);
336+
337+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(1);
338+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
285339

286340
verifyGaps(tree);
287341
Range<TestRange, Integer> g = tree.getRange(new TestRange(6, 7));
@@ -298,6 +352,9 @@ void testOverlappingRange() {
298352
assertThat(connectedRanges.get(1).hasOverlap()).isFalse();
299353
assertThat(connectedRanges.get(1).getMinimumOverlap()).isEqualTo(1);
300354
assertThat(connectedRanges.get(1).getMaximumOverlap()).isEqualTo(1);
355+
356+
assertThat(tree.getConnectedRangeChain().getMaximumValue(i -> 1)).isEqualTo(1);
357+
assertThat(tree.getConnectedRangeChain().getMaximumValueForDistinctRanges(ConnectedRangeTrackerTest::rangeMaxFunction)).isEqualTo(100);
301358
}
302359

303360
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)