Skip to content

Commit 687252d

Browse files
davidmandlenick-someone
authored andcommitted
Add RangeMap#merge, analogous to Map#merge.
Rollback of 8000dc9 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=258637808
1 parent 278fffd commit 687252d

File tree

4 files changed

+295
-0
lines changed

4 files changed

+295
-0
lines changed

guava-tests/test/com/google/common/collect/TreeRangeMapTest.java

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.Map;
2929
import java.util.Map.Entry;
3030
import java.util.NoSuchElementException;
31+
import java.util.function.BiFunction;
3132
import junit.framework.Test;
3233
import junit.framework.TestCase;
3334
import junit.framework.TestSuite;
@@ -525,6 +526,156 @@ public void testPutCoalescingComplex() {
525526
rangeMap.asMapOfRanges());
526527
}
527528

529+
public void testMergeOntoRangeOverlappingLowerBound() {
530+
// {[0..2): 1}
531+
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
532+
rangeMap.put(Range.closedOpen(0, 2), 1);
533+
534+
rangeMap.merge(Range.closedOpen(1, 3), 2, Integer::sum);
535+
536+
// {[0..1): 1, [1..2): 3, [2, 3): 2}
537+
assertEquals(
538+
new ImmutableMap.Builder<>()
539+
.put(Range.closedOpen(0, 1), 1)
540+
.put(Range.closedOpen(1, 2), 3)
541+
.put(Range.closedOpen(2, 3), 2)
542+
.build(),
543+
rangeMap.asMapOfRanges());
544+
}
545+
546+
public void testMergeOntoRangeOverlappingUpperBound() {
547+
// {[1..3): 1}
548+
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
549+
rangeMap.put(Range.closedOpen(1, 3), 1);
550+
551+
rangeMap.merge(Range.closedOpen(0, 2), 2, Integer::sum);
552+
553+
// {[0..1): 2, [1..2): 3, [2, 3): 1}
554+
assertEquals(
555+
new ImmutableMap.Builder<>()
556+
.put(Range.closedOpen(0, 1), 2)
557+
.put(Range.closedOpen(1, 2), 3)
558+
.put(Range.closedOpen(2, 3), 1)
559+
.build(),
560+
rangeMap.asMapOfRanges());
561+
}
562+
563+
public void testMergeOntoIdenticalRange() {
564+
// {[0..1): 1}
565+
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
566+
rangeMap.put(Range.closedOpen(0, 1), 1);
567+
568+
rangeMap.merge(Range.closedOpen(0, 1), 2, Integer::sum);
569+
570+
// {[0..1): 3}
571+
assertEquals(ImmutableMap.of(Range.closedOpen(0, 1), 3), rangeMap.asMapOfRanges());
572+
}
573+
574+
public void testMergeOntoSuperRange() {
575+
// {[0..3): 1}
576+
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
577+
rangeMap.put(Range.closedOpen(0, 3), 1);
578+
579+
rangeMap.merge(Range.closedOpen(1, 2), 2, Integer::sum);
580+
581+
// {[0..1): 1, [1..2): 3, [2..3): 1}
582+
assertEquals(
583+
new ImmutableMap.Builder<>()
584+
.put(Range.closedOpen(0, 1), 1)
585+
.put(Range.closedOpen(1, 2), 3)
586+
.put(Range.closedOpen(2, 3), 1)
587+
.build(),
588+
rangeMap.asMapOfRanges());
589+
}
590+
591+
public void testMergeOntoSubRange() {
592+
// {[1..2): 1}
593+
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
594+
rangeMap.put(Range.closedOpen(1, 2), 1);
595+
596+
rangeMap.merge(Range.closedOpen(0, 3), 2, Integer::sum);
597+
598+
// {[0..1): 2, [1..2): 3, [2..3): 2}
599+
assertEquals(
600+
new ImmutableMap.Builder<>()
601+
.put(Range.closedOpen(0, 1), 2)
602+
.put(Range.closedOpen(1, 2), 3)
603+
.put(Range.closedOpen(2, 3), 2)
604+
.build(),
605+
rangeMap.asMapOfRanges());
606+
}
607+
608+
public void testMergeOntoDisconnectedRanges() {
609+
// {[0..1): 1, [2, 3): 2}
610+
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
611+
rangeMap.put(Range.closedOpen(0, 1), 1);
612+
rangeMap.put(Range.closedOpen(2, 3), 2);
613+
614+
rangeMap.merge(Range.closedOpen(0, 3), 3, Integer::sum);
615+
616+
// {[0..1): 4, [1..2): 3, [2..3): 5}
617+
assertEquals(
618+
new ImmutableMap.Builder<>()
619+
.put(Range.closedOpen(0, 1), 4)
620+
.put(Range.closedOpen(1, 2), 3)
621+
.put(Range.closedOpen(2, 3), 5)
622+
.build(),
623+
rangeMap.asMapOfRanges());
624+
}
625+
626+
public void testMergeNullValue() {
627+
// {[1..2): 1, [3, 4): 2}
628+
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
629+
rangeMap.put(Range.closedOpen(1, 2), 1);
630+
rangeMap.put(Range.closedOpen(3, 4), 2);
631+
632+
rangeMap.merge(Range.closedOpen(0, 5), null, (v1, v2) -> v1 + 1);
633+
634+
// {[1..2): 2, [3..4): 3}
635+
assertEquals(
636+
new ImmutableMap.Builder<>()
637+
.put(Range.closedOpen(1, 2), 2)
638+
.put(Range.closedOpen(3, 4), 3)
639+
.build(),
640+
rangeMap.asMapOfRanges());
641+
}
642+
643+
public void testMergeWithRemappingFunctionReturningNullValue() {
644+
// {[1..2): 1, [3, 4): 2}
645+
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
646+
rangeMap.put(Range.closedOpen(1, 2), 1);
647+
rangeMap.put(Range.closedOpen(3, 4), 2);
648+
649+
rangeMap.merge(Range.closedOpen(0, 5), 3, (v1, v2) -> null);
650+
651+
// {[0..1): 3, [2..3): 3, [4, 5): 3}
652+
assertEquals(
653+
new ImmutableMap.Builder<>()
654+
.put(Range.closedOpen(0, 1), 3)
655+
.put(Range.closedOpen(2, 3), 3)
656+
.put(Range.closedOpen(4, 5), 3)
657+
.build(),
658+
rangeMap.asMapOfRanges());
659+
}
660+
661+
public void testMergeAllRangeTriples() {
662+
for (Range<Integer> range1 : RANGES) {
663+
for (Range<Integer> range2 : RANGES) {
664+
for (Range<Integer> range3 : RANGES) {
665+
Map<Integer, Integer> model = Maps.newHashMap();
666+
mergeModel(model, range1, 1, Integer::sum);
667+
mergeModel(model, range2, 2, Integer::sum);
668+
mergeModel(model, range3, 3, Integer::sum);
669+
RangeMap<Integer, Integer> test = TreeRangeMap.create();
670+
test.merge(range1, 1, Integer::sum);
671+
test.merge(range2, 2, Integer::sum);
672+
test.merge(range3, 3, Integer::sum);
673+
verify(model, test);
674+
}
675+
}
676+
}
677+
}
678+
528679
public void testSubRangeMapExhaustive() {
529680
for (Range<Integer> range1 : RANGES) {
530681
for (Range<Integer> range2 : RANGES) {
@@ -724,4 +875,16 @@ private static void removeModel(Map<Integer, Integer> model, Range<Integer> rang
724875
}
725876
}
726877
}
878+
879+
private static void mergeModel(
880+
Map<Integer, Integer> model,
881+
Range<Integer> range,
882+
int value,
883+
BiFunction<? super Integer, ? super Integer, ? extends Integer> remappingFunction) {
884+
for (int i = MIN_BOUND - 1; i <= MAX_BOUND + 1; i++) {
885+
if (range.contains(i)) {
886+
model.merge(i, value, remappingFunction);
887+
}
888+
}
889+
}
727890
}

guava/src/com/google/common/collect/ImmutableRangeMap.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.Map;
3030
import java.util.Map.Entry;
3131
import java.util.NoSuchElementException;
32+
import java.util.function.BiFunction;
3233
import java.util.function.Function;
3334
import java.util.stream.Collector;
3435
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -271,6 +272,21 @@ public void remove(Range<K> range) {
271272
throw new UnsupportedOperationException();
272273
}
273274

275+
/**
276+
* Guaranteed to throw an exception and leave the {@code RangeMap} unmodified.
277+
*
278+
* @throws UnsupportedOperationException always
279+
* @deprecated Unsupported operation.
280+
*/
281+
@Deprecated
282+
@Override
283+
public void merge(
284+
Range<K> range,
285+
@Nullable V value,
286+
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
287+
throw new UnsupportedOperationException();
288+
}
289+
274290
@Override
275291
public ImmutableMap<Range<K>, V> asMapOfRanges() {
276292
if (ranges.isEmpty()) {

guava/src/com/google/common/collect/RangeMap.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Map;
2323
import java.util.Map.Entry;
2424
import java.util.NoSuchElementException;
25+
import java.util.function.BiFunction;
2526
import org.checkerframework.checker.nullness.qual.Nullable;
2627

2728
/**
@@ -107,6 +108,25 @@ public interface RangeMap<K extends Comparable, V> {
107108
*/
108109
void remove(Range<K> range);
109110

111+
/**
112+
* Merges a value into the map over a range by applying a remapping function.
113+
*
114+
* <p>If any parts of the range are already present in this range map, those parts are mapped to
115+
* new values by applying the remapping function. Any parts of the range not already present in
116+
* this range map are mapped to the specified value, unless the value is {@code null}.
117+
*
118+
* <p>Any existing map entry spanning either range boundary may be split at the boundary, even if
119+
* the merge does not affect its value.
120+
*
121+
* <p>For example, if {@code rangeMap} had one entry {@code [1, 5] => 3} then {@code
122+
* rangeMap.merge(Range.closed(0,2), 3, Math::max)} could yield a range map with the entries
123+
* {@code [0, 1) => 3, [1, 2] => 3, (2, 5] => 3}.
124+
*/
125+
void merge(
126+
Range<K> range,
127+
@Nullable V value,
128+
BiFunction<? super V, ? super V, ? extends V> remappingFunction);
129+
110130
/**
111131
* Returns a view of this range map as an unmodifiable {@code Map<Range<K>, V>}. Modifications to
112132
* this range map are guaranteed to read through to the returned {@code Map}.

guava/src/com/google/common/collect/TreeRangeMap.java

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.NavigableMap;
3838
import java.util.NoSuchElementException;
3939
import java.util.Set;
40+
import java.util.function.BiFunction;
4041
import org.checkerframework.checker.nullness.qual.Nullable;
4142

4243
/**
@@ -239,6 +240,80 @@ public void remove(Range<K> rangeToRemove) {
239240
entriesByLowerBound.subMap(rangeToRemove.lowerBound, rangeToRemove.upperBound).clear();
240241
}
241242

243+
private void split(Cut<K> cut) {
244+
/*
245+
* The comments for this method will use | to indicate the cut point and ( ) to indicate the
246+
* bounds of ranges in the range map.
247+
*/
248+
Entry<Cut<K>, RangeMapEntry<K, V>> mapEntryToSplit = entriesByLowerBound.lowerEntry(cut);
249+
if (mapEntryToSplit == null) {
250+
return;
251+
}
252+
// we know ( |
253+
RangeMapEntry<K, V> rangeMapEntry = mapEntryToSplit.getValue();
254+
if (rangeMapEntry.getUpperBound().compareTo(cut) <= 0) {
255+
return;
256+
}
257+
// we know ( | )
258+
putRangeMapEntry(rangeMapEntry.getLowerBound(), cut, rangeMapEntry.getValue());
259+
putRangeMapEntry(cut, rangeMapEntry.getUpperBound(), rangeMapEntry.getValue());
260+
}
261+
262+
@Override
263+
public void merge(
264+
Range<K> range,
265+
@Nullable V value,
266+
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
267+
checkNotNull(range);
268+
checkNotNull(remappingFunction);
269+
270+
if (range.isEmpty()) {
271+
return;
272+
}
273+
split(range.lowerBound);
274+
split(range.upperBound);
275+
276+
// Due to the splitting of any entries spanning the range bounds, we know that any entry with a
277+
// lower bound in the merge range is entirely contained by the merge range.
278+
Set<Entry<Cut<K>, RangeMapEntry<K, V>>> entriesInMergeRange =
279+
entriesByLowerBound.subMap(range.lowerBound, range.upperBound).entrySet();
280+
281+
// Create entries mapping any unmapped ranges in the merge range to the specified value.
282+
ImmutableMap.Builder<Cut<K>, RangeMapEntry<K, V>> gaps = ImmutableMap.builder();
283+
if (value != null) {
284+
final Iterator<Entry<Cut<K>, RangeMapEntry<K, V>>> backingItr =
285+
entriesInMergeRange.iterator();
286+
Cut<K> lowerBound = range.lowerBound;
287+
while (backingItr.hasNext()) {
288+
RangeMapEntry<K, V> entry = backingItr.next().getValue();
289+
Cut<K> upperBound = entry.getLowerBound();
290+
if (!lowerBound.equals(upperBound)) {
291+
gaps.put(lowerBound, new RangeMapEntry<K, V>(lowerBound, upperBound, value));
292+
}
293+
lowerBound = entry.getUpperBound();
294+
}
295+
if (!lowerBound.equals(range.upperBound)) {
296+
gaps.put(lowerBound, new RangeMapEntry<K, V>(lowerBound, range.upperBound, value));
297+
}
298+
}
299+
300+
// Remap all existing entries in the merge range.
301+
final Iterator<Entry<Cut<K>, RangeMapEntry<K, V>>> backingItr = entriesInMergeRange.iterator();
302+
while (backingItr.hasNext()) {
303+
Entry<Cut<K>, RangeMapEntry<K, V>> entry = backingItr.next();
304+
V newValue = remappingFunction.apply(entry.getValue().getValue(), value);
305+
if (newValue == null) {
306+
backingItr.remove();
307+
} else {
308+
entry.setValue(
309+
new RangeMapEntry<K, V>(
310+
entry.getValue().getLowerBound(), entry.getValue().getUpperBound(), newValue));
311+
}
312+
}
313+
314+
entriesByLowerBound.putAll(gaps.build());
315+
}
316+
242317
@Override
243318
public Map<Range<K>, V> asMapOfRanges() {
244319
return new AsMapOfRanges(entriesByLowerBound.values());
@@ -347,6 +422,14 @@ public void remove(Range range) {
347422
checkNotNull(range);
348423
}
349424

425+
@Override
426+
@SuppressWarnings("rawtypes") // necessary for static EMPTY_SUB_RANGE_MAP instance
427+
public void merge(Range range, @Nullable Object value, BiFunction remappingFunction) {
428+
checkNotNull(range);
429+
throw new IllegalArgumentException(
430+
"Cannot merge range " + range + " into an empty subRangeMap");
431+
}
432+
350433
@Override
351434
public Map<Range, Object> asMapOfRanges() {
352435
return Collections.emptyMap();
@@ -461,6 +544,19 @@ public void remove(Range<K> range) {
461544
}
462545
}
463546

547+
@Override
548+
public void merge(
549+
Range<K> range,
550+
@Nullable V value,
551+
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
552+
checkArgument(
553+
subRange.encloses(range),
554+
"Cannot merge range %s into a subRangeMap(%s)",
555+
range,
556+
subRange);
557+
TreeRangeMap.this.merge(range, value, remappingFunction);
558+
}
559+
464560
@Override
465561
public RangeMap<K, V> subRangeMap(Range<K> range) {
466562
if (!range.isConnected(subRange)) {

0 commit comments

Comments
 (0)