From 687252d355dc804d07bd1f3cc6bd6bf6abe08634 Mon Sep 17 00:00:00 2001 From: davidmandle Date: Wed, 17 Jul 2019 14:06:42 -0700 Subject: [PATCH] Add RangeMap#merge, analogous to Map#merge. Rollback of 8000dc992d0124372972729b20df40a42b27a849 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=258637808 --- .../common/collect/TreeRangeMapTest.java | 163 ++++++++++++++++++ .../common/collect/ImmutableRangeMap.java | 16 ++ .../com/google/common/collect/RangeMap.java | 20 +++ .../google/common/collect/TreeRangeMap.java | 96 +++++++++++ 4 files changed, 295 insertions(+) diff --git a/guava-tests/test/com/google/common/collect/TreeRangeMapTest.java b/guava-tests/test/com/google/common/collect/TreeRangeMapTest.java index 439411afb08f..fff188d9d6b5 100644 --- a/guava-tests/test/com/google/common/collect/TreeRangeMapTest.java +++ b/guava-tests/test/com/google/common/collect/TreeRangeMapTest.java @@ -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; @@ -525,6 +526,156 @@ public void testPutCoalescingComplex() { rangeMap.asMapOfRanges()); } + public void testMergeOntoRangeOverlappingLowerBound() { + // {[0..2): 1} + RangeMap 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 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 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 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 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 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 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 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 range1 : RANGES) { + for (Range range2 : RANGES) { + for (Range range3 : RANGES) { + Map model = Maps.newHashMap(); + mergeModel(model, range1, 1, Integer::sum); + mergeModel(model, range2, 2, Integer::sum); + mergeModel(model, range3, 3, Integer::sum); + RangeMap 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 range1 : RANGES) { for (Range range2 : RANGES) { @@ -724,4 +875,16 @@ private static void removeModel(Map model, Range rang } } } + + private static void mergeModel( + Map model, + Range range, + int value, + BiFunction remappingFunction) { + for (int i = MIN_BOUND - 1; i <= MAX_BOUND + 1; i++) { + if (range.contains(i)) { + model.merge(i, value, remappingFunction); + } + } + } } diff --git a/guava/src/com/google/common/collect/ImmutableRangeMap.java b/guava/src/com/google/common/collect/ImmutableRangeMap.java index 56f36f3f1b06..983da60c9a0c 100644 --- a/guava/src/com/google/common/collect/ImmutableRangeMap.java +++ b/guava/src/com/google/common/collect/ImmutableRangeMap.java @@ -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; @@ -271,6 +272,21 @@ public void remove(Range 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 range, + @Nullable V value, + BiFunction remappingFunction) { + throw new UnsupportedOperationException(); + } + @Override public ImmutableMap, V> asMapOfRanges() { if (ranges.isEmpty()) { diff --git a/guava/src/com/google/common/collect/RangeMap.java b/guava/src/com/google/common/collect/RangeMap.java index 13ec7f0d3ea5..bd0cdcb48712 100644 --- a/guava/src/com/google/common/collect/RangeMap.java +++ b/guava/src/com/google/common/collect/RangeMap.java @@ -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; /** @@ -107,6 +108,25 @@ public interface RangeMap { */ void remove(Range range); + /** + * Merges a value into the map over a range by applying a remapping function. + * + *

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}. + * + *

Any existing map entry spanning either range boundary may be split at the boundary, even if + * the merge does not affect its value. + * + *

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 range, + @Nullable V value, + BiFunction remappingFunction); + /** * Returns a view of this range map as an unmodifiable {@code Map, V>}. Modifications to * this range map are guaranteed to read through to the returned {@code Map}. diff --git a/guava/src/com/google/common/collect/TreeRangeMap.java b/guava/src/com/google/common/collect/TreeRangeMap.java index 72f6d369b828..eb52028f2b97 100644 --- a/guava/src/com/google/common/collect/TreeRangeMap.java +++ b/guava/src/com/google/common/collect/TreeRangeMap.java @@ -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; /** @@ -239,6 +240,80 @@ public void remove(Range rangeToRemove) { entriesByLowerBound.subMap(rangeToRemove.lowerBound, rangeToRemove.upperBound).clear(); } + private void split(Cut 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, RangeMapEntry> mapEntryToSplit = entriesByLowerBound.lowerEntry(cut); + if (mapEntryToSplit == null) { + return; + } + // we know ( | + RangeMapEntry 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 range, + @Nullable V value, + BiFunction 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, RangeMapEntry>> entriesInMergeRange = + entriesByLowerBound.subMap(range.lowerBound, range.upperBound).entrySet(); + + // Create entries mapping any unmapped ranges in the merge range to the specified value. + ImmutableMap.Builder, RangeMapEntry> gaps = ImmutableMap.builder(); + if (value != null) { + final Iterator, RangeMapEntry>> backingItr = + entriesInMergeRange.iterator(); + Cut lowerBound = range.lowerBound; + while (backingItr.hasNext()) { + RangeMapEntry entry = backingItr.next().getValue(); + Cut upperBound = entry.getLowerBound(); + if (!lowerBound.equals(upperBound)) { + gaps.put(lowerBound, new RangeMapEntry(lowerBound, upperBound, value)); + } + lowerBound = entry.getUpperBound(); + } + if (!lowerBound.equals(range.upperBound)) { + gaps.put(lowerBound, new RangeMapEntry(lowerBound, range.upperBound, value)); + } + } + + // Remap all existing entries in the merge range. + final Iterator, RangeMapEntry>> backingItr = entriesInMergeRange.iterator(); + while (backingItr.hasNext()) { + Entry, RangeMapEntry> entry = backingItr.next(); + V newValue = remappingFunction.apply(entry.getValue().getValue(), value); + if (newValue == null) { + backingItr.remove(); + } else { + entry.setValue( + new RangeMapEntry( + entry.getValue().getLowerBound(), entry.getValue().getUpperBound(), newValue)); + } + } + + entriesByLowerBound.putAll(gaps.build()); + } + @Override public Map, V> asMapOfRanges() { return new AsMapOfRanges(entriesByLowerBound.values()); @@ -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 asMapOfRanges() { return Collections.emptyMap(); @@ -461,6 +544,19 @@ public void remove(Range range) { } } + @Override + public void merge( + Range range, + @Nullable V value, + BiFunction remappingFunction) { + checkArgument( + subRange.encloses(range), + "Cannot merge range %s into a subRangeMap(%s)", + range, + subRange); + TreeRangeMap.this.merge(range, value, remappingFunction); + } + @Override public RangeMap subRangeMap(Range range) { if (!range.isConnected(subRange)) {