Skip to content

Commit

Permalink
Add RangeMap#merge, analogous to Map#merge.
Browse files Browse the repository at this point in the history
Rollback of 8000dc9

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=258637808
  • Loading branch information
davidmandle authored and nick-someone committed Jul 24, 2019
1 parent 278fffd commit 687252d
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 0 deletions.
163 changes: 163 additions & 0 deletions guava-tests/test/com/google/common/collect/TreeRangeMapTest.java
Expand Up @@ -28,6 +28,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.function.BiFunction;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
Expand Down Expand Up @@ -525,6 +526,156 @@ public void testPutCoalescingComplex() {
rangeMap.asMapOfRanges());
}

public void testMergeOntoRangeOverlappingLowerBound() {
// {[0..2): 1}
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closedOpen(0, 2), 1);

rangeMap.merge(Range.closedOpen(1, 3), 2, Integer::sum);

// {[0..1): 1, [1..2): 3, [2, 3): 2}
assertEquals(
new ImmutableMap.Builder<>()
.put(Range.closedOpen(0, 1), 1)
.put(Range.closedOpen(1, 2), 3)
.put(Range.closedOpen(2, 3), 2)
.build(),
rangeMap.asMapOfRanges());
}

public void testMergeOntoRangeOverlappingUpperBound() {
// {[1..3): 1}
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closedOpen(1, 3), 1);

rangeMap.merge(Range.closedOpen(0, 2), 2, Integer::sum);

// {[0..1): 2, [1..2): 3, [2, 3): 1}
assertEquals(
new ImmutableMap.Builder<>()
.put(Range.closedOpen(0, 1), 2)
.put(Range.closedOpen(1, 2), 3)
.put(Range.closedOpen(2, 3), 1)
.build(),
rangeMap.asMapOfRanges());
}

public void testMergeOntoIdenticalRange() {
// {[0..1): 1}
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closedOpen(0, 1), 1);

rangeMap.merge(Range.closedOpen(0, 1), 2, Integer::sum);

// {[0..1): 3}
assertEquals(ImmutableMap.of(Range.closedOpen(0, 1), 3), rangeMap.asMapOfRanges());
}

public void testMergeOntoSuperRange() {
// {[0..3): 1}
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closedOpen(0, 3), 1);

rangeMap.merge(Range.closedOpen(1, 2), 2, Integer::sum);

// {[0..1): 1, [1..2): 3, [2..3): 1}
assertEquals(
new ImmutableMap.Builder<>()
.put(Range.closedOpen(0, 1), 1)
.put(Range.closedOpen(1, 2), 3)
.put(Range.closedOpen(2, 3), 1)
.build(),
rangeMap.asMapOfRanges());
}

public void testMergeOntoSubRange() {
// {[1..2): 1}
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closedOpen(1, 2), 1);

rangeMap.merge(Range.closedOpen(0, 3), 2, Integer::sum);

// {[0..1): 2, [1..2): 3, [2..3): 2}
assertEquals(
new ImmutableMap.Builder<>()
.put(Range.closedOpen(0, 1), 2)
.put(Range.closedOpen(1, 2), 3)
.put(Range.closedOpen(2, 3), 2)
.build(),
rangeMap.asMapOfRanges());
}

public void testMergeOntoDisconnectedRanges() {
// {[0..1): 1, [2, 3): 2}
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closedOpen(0, 1), 1);
rangeMap.put(Range.closedOpen(2, 3), 2);

rangeMap.merge(Range.closedOpen(0, 3), 3, Integer::sum);

// {[0..1): 4, [1..2): 3, [2..3): 5}
assertEquals(
new ImmutableMap.Builder<>()
.put(Range.closedOpen(0, 1), 4)
.put(Range.closedOpen(1, 2), 3)
.put(Range.closedOpen(2, 3), 5)
.build(),
rangeMap.asMapOfRanges());
}

public void testMergeNullValue() {
// {[1..2): 1, [3, 4): 2}
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closedOpen(1, 2), 1);
rangeMap.put(Range.closedOpen(3, 4), 2);

rangeMap.merge(Range.closedOpen(0, 5), null, (v1, v2) -> v1 + 1);

// {[1..2): 2, [3..4): 3}
assertEquals(
new ImmutableMap.Builder<>()
.put(Range.closedOpen(1, 2), 2)
.put(Range.closedOpen(3, 4), 3)
.build(),
rangeMap.asMapOfRanges());
}

public void testMergeWithRemappingFunctionReturningNullValue() {
// {[1..2): 1, [3, 4): 2}
RangeMap<Integer, Integer> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closedOpen(1, 2), 1);
rangeMap.put(Range.closedOpen(3, 4), 2);

rangeMap.merge(Range.closedOpen(0, 5), 3, (v1, v2) -> null);

// {[0..1): 3, [2..3): 3, [4, 5): 3}
assertEquals(
new ImmutableMap.Builder<>()
.put(Range.closedOpen(0, 1), 3)
.put(Range.closedOpen(2, 3), 3)
.put(Range.closedOpen(4, 5), 3)
.build(),
rangeMap.asMapOfRanges());
}

public void testMergeAllRangeTriples() {
for (Range<Integer> range1 : RANGES) {
for (Range<Integer> range2 : RANGES) {
for (Range<Integer> range3 : RANGES) {
Map<Integer, Integer> model = Maps.newHashMap();
mergeModel(model, range1, 1, Integer::sum);
mergeModel(model, range2, 2, Integer::sum);
mergeModel(model, range3, 3, Integer::sum);
RangeMap<Integer, Integer> test = TreeRangeMap.create();
test.merge(range1, 1, Integer::sum);
test.merge(range2, 2, Integer::sum);
test.merge(range3, 3, Integer::sum);
verify(model, test);
}
}
}
}

public void testSubRangeMapExhaustive() {
for (Range<Integer> range1 : RANGES) {
for (Range<Integer> range2 : RANGES) {
Expand Down Expand Up @@ -724,4 +875,16 @@ private static void removeModel(Map<Integer, Integer> model, Range<Integer> rang
}
}
}

private static void mergeModel(
Map<Integer, Integer> model,
Range<Integer> range,
int value,
BiFunction<? super Integer, ? super Integer, ? extends Integer> remappingFunction) {
for (int i = MIN_BOUND - 1; i <= MAX_BOUND + 1; i++) {
if (range.contains(i)) {
model.merge(i, value, remappingFunction);
}
}
}
}
16 changes: 16 additions & 0 deletions guava/src/com/google/common/collect/ImmutableRangeMap.java
Expand Up @@ -29,6 +29,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collector;
import org.checkerframework.checker.nullness.qual.Nullable;
Expand Down Expand Up @@ -271,6 +272,21 @@ public void remove(Range<K> range) {
throw new UnsupportedOperationException();
}

/**
* Guaranteed to throw an exception and leave the {@code RangeMap} unmodified.
*
* @throws UnsupportedOperationException always
* @deprecated Unsupported operation.
*/
@Deprecated
@Override
public void merge(
Range<K> range,
@Nullable V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
throw new UnsupportedOperationException();
}

@Override
public ImmutableMap<Range<K>, V> asMapOfRanges() {
if (ranges.isEmpty()) {
Expand Down
20 changes: 20 additions & 0 deletions guava/src/com/google/common/collect/RangeMap.java
Expand Up @@ -22,6 +22,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.function.BiFunction;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
Expand Down Expand Up @@ -107,6 +108,25 @@ public interface RangeMap<K extends Comparable, V> {
*/
void remove(Range<K> range);

/**
* Merges a value into the map over a range by applying a remapping function.
*
* <p>If any parts of the range are already present in this range map, those parts are mapped to
* new values by applying the remapping function. Any parts of the range not already present in
* this range map are mapped to the specified value, unless the value is {@code null}.
*
* <p>Any existing map entry spanning either range boundary may be split at the boundary, even if
* the merge does not affect its value.
*
* <p>For example, if {@code rangeMap} had one entry {@code [1, 5] => 3} then {@code
* rangeMap.merge(Range.closed(0,2), 3, Math::max)} could yield a range map with the entries
* {@code [0, 1) => 3, [1, 2] => 3, (2, 5] => 3}.
*/
void merge(
Range<K> range,
@Nullable V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction);

/**
* Returns a view of this range map as an unmodifiable {@code Map<Range<K>, V>}. Modifications to
* this range map are guaranteed to read through to the returned {@code Map}.
Expand Down
96 changes: 96 additions & 0 deletions guava/src/com/google/common/collect/TreeRangeMap.java
Expand Up @@ -37,6 +37,7 @@
import java.util.NavigableMap;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.BiFunction;
import org.checkerframework.checker.nullness.qual.Nullable;

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

private void split(Cut<K> cut) {
/*
* The comments for this method will use | to indicate the cut point and ( ) to indicate the
* bounds of ranges in the range map.
*/
Entry<Cut<K>, RangeMapEntry<K, V>> mapEntryToSplit = entriesByLowerBound.lowerEntry(cut);
if (mapEntryToSplit == null) {
return;
}
// we know ( |
RangeMapEntry<K, V> rangeMapEntry = mapEntryToSplit.getValue();
if (rangeMapEntry.getUpperBound().compareTo(cut) <= 0) {
return;
}
// we know ( | )
putRangeMapEntry(rangeMapEntry.getLowerBound(), cut, rangeMapEntry.getValue());
putRangeMapEntry(cut, rangeMapEntry.getUpperBound(), rangeMapEntry.getValue());
}

@Override
public void merge(
Range<K> range,
@Nullable V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
checkNotNull(range);
checkNotNull(remappingFunction);

if (range.isEmpty()) {
return;
}
split(range.lowerBound);
split(range.upperBound);

// Due to the splitting of any entries spanning the range bounds, we know that any entry with a
// lower bound in the merge range is entirely contained by the merge range.
Set<Entry<Cut<K>, RangeMapEntry<K, V>>> entriesInMergeRange =
entriesByLowerBound.subMap(range.lowerBound, range.upperBound).entrySet();

// Create entries mapping any unmapped ranges in the merge range to the specified value.
ImmutableMap.Builder<Cut<K>, RangeMapEntry<K, V>> gaps = ImmutableMap.builder();
if (value != null) {
final Iterator<Entry<Cut<K>, RangeMapEntry<K, V>>> backingItr =
entriesInMergeRange.iterator();
Cut<K> lowerBound = range.lowerBound;
while (backingItr.hasNext()) {
RangeMapEntry<K, V> entry = backingItr.next().getValue();
Cut<K> upperBound = entry.getLowerBound();
if (!lowerBound.equals(upperBound)) {
gaps.put(lowerBound, new RangeMapEntry<K, V>(lowerBound, upperBound, value));
}
lowerBound = entry.getUpperBound();
}
if (!lowerBound.equals(range.upperBound)) {
gaps.put(lowerBound, new RangeMapEntry<K, V>(lowerBound, range.upperBound, value));
}
}

// Remap all existing entries in the merge range.
final Iterator<Entry<Cut<K>, RangeMapEntry<K, V>>> backingItr = entriesInMergeRange.iterator();
while (backingItr.hasNext()) {
Entry<Cut<K>, RangeMapEntry<K, V>> entry = backingItr.next();
V newValue = remappingFunction.apply(entry.getValue().getValue(), value);
if (newValue == null) {
backingItr.remove();
} else {
entry.setValue(
new RangeMapEntry<K, V>(
entry.getValue().getLowerBound(), entry.getValue().getUpperBound(), newValue));
}
}

entriesByLowerBound.putAll(gaps.build());
}

@Override
public Map<Range<K>, V> asMapOfRanges() {
return new AsMapOfRanges(entriesByLowerBound.values());
Expand Down Expand Up @@ -347,6 +422,14 @@ public void remove(Range range) {
checkNotNull(range);
}

@Override
@SuppressWarnings("rawtypes") // necessary for static EMPTY_SUB_RANGE_MAP instance
public void merge(Range range, @Nullable Object value, BiFunction remappingFunction) {
checkNotNull(range);
throw new IllegalArgumentException(
"Cannot merge range " + range + " into an empty subRangeMap");
}

@Override
public Map<Range, Object> asMapOfRanges() {
return Collections.emptyMap();
Expand Down Expand Up @@ -461,6 +544,19 @@ public void remove(Range<K> range) {
}
}

@Override
public void merge(
Range<K> range,
@Nullable V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
checkArgument(
subRange.encloses(range),
"Cannot merge range %s into a subRangeMap(%s)",
range,
subRange);
TreeRangeMap.this.merge(range, value, remappingFunction);
}

@Override
public RangeMap<K, V> subRangeMap(Range<K> range) {
if (!range.isConnected(subRange)) {
Expand Down

0 comments on commit 687252d

Please sign in to comment.