com.google.common.collect.TreeRangeMap Maven / Gradle / Ivy
/*
* Copyright (C) 2012 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.common.collect;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.compose;
import static com.google.common.base.Predicates.in;
import static com.google.common.base.Predicates.not;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicate;
import com.google.common.collect.Maps.IteratorBasedAbstractMap;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.BiFunction;
import javax.annotation.CheckForNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* An implementation of {@code RangeMap} based on a {@code TreeMap}, supporting all optional
* operations.
*
* Like all {@code RangeMap} implementations, this supports neither null keys nor null values.
*
* @author Louis Wasserman
* @since 14.0
*/
@SuppressWarnings("rawtypes") // https://github.com/google/guava/issues/989
@GwtIncompatible // NavigableMap
@ElementTypesAreNonnullByDefault
public final class TreeRangeMap implements RangeMap {
private final NavigableMap, RangeMapEntry> entriesByLowerBound;
public static TreeRangeMap create() {
return new TreeRangeMap<>();
}
private TreeRangeMap() {
this.entriesByLowerBound = Maps.newTreeMap();
}
private static final class RangeMapEntry
extends AbstractMapEntry, V> {
private final Range range;
private final V value;
RangeMapEntry(Cut lowerBound, Cut upperBound, V value) {
this(Range.create(lowerBound, upperBound), value);
}
RangeMapEntry(Range range, V value) {
this.range = range;
this.value = value;
}
@Override
public Range getKey() {
return range;
}
@Override
public V getValue() {
return value;
}
public boolean contains(K value) {
return range.contains(value);
}
Cut getLowerBound() {
return range.lowerBound;
}
Cut getUpperBound() {
return range.upperBound;
}
}
@Override
@CheckForNull
public V get(K key) {
Entry, V> entry = getEntry(key);
return (entry == null) ? null : entry.getValue();
}
@Override
@CheckForNull
public Entry, V> getEntry(K key) {
Entry, RangeMapEntry> mapEntry =
entriesByLowerBound.floorEntry(Cut.belowValue(key));
if (mapEntry != null && mapEntry.getValue().contains(key)) {
return mapEntry.getValue();
} else {
return null;
}
}
@Override
public void put(Range range, V value) {
if (!range.isEmpty()) {
checkNotNull(value);
remove(range);
entriesByLowerBound.put(range.lowerBound, new RangeMapEntry(range, value));
}
}
@Override
public void putCoalescing(Range range, V value) {
// don't short-circuit if the range is empty - it may be between two ranges we can coalesce.
if (entriesByLowerBound.isEmpty()) {
put(range, value);
return;
}
Range coalescedRange = coalescedRange(range, checkNotNull(value));
put(coalescedRange, value);
}
/** Computes the coalesced range for the given range+value - does not mutate the map. */
private Range coalescedRange(Range range, V value) {
Range coalescedRange = range;
Entry, RangeMapEntry> lowerEntry =
entriesByLowerBound.lowerEntry(range.lowerBound);
coalescedRange = coalesce(coalescedRange, value, lowerEntry);
Entry, RangeMapEntry> higherEntry =
entriesByLowerBound.floorEntry(range.upperBound);
coalescedRange = coalesce(coalescedRange, value, higherEntry);
return coalescedRange;
}
/** Returns the range that spans the given range and entry, if the entry can be coalesced. */
private static Range coalesce(
Range range, V value, @CheckForNull Entry, RangeMapEntry> entry) {
if (entry != null
&& entry.getValue().getKey().isConnected(range)
&& entry.getValue().getValue().equals(value)) {
return range.span(entry.getValue().getKey());
}
return range;
}
@Override
public void putAll(RangeMap rangeMap) {
for (Entry, ? extends V> entry : rangeMap.asMapOfRanges().entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public void clear() {
entriesByLowerBound.clear();
}
@Override
public Range span() {
Entry, RangeMapEntry> firstEntry = entriesByLowerBound.firstEntry();
Entry, RangeMapEntry> lastEntry = entriesByLowerBound.lastEntry();
// Either both are null or neither is, but we check both to satisfy the nullness checker.
if (firstEntry == null || lastEntry == null) {
throw new NoSuchElementException();
}
return Range.create(
firstEntry.getValue().getKey().lowerBound, lastEntry.getValue().getKey().upperBound);
}
private void putRangeMapEntry(Cut lowerBound, Cut upperBound, V value) {
entriesByLowerBound.put(lowerBound, new RangeMapEntry(lowerBound, upperBound, value));
}
@Override
public void remove(Range rangeToRemove) {
if (rangeToRemove.isEmpty()) {
return;
}
/*
* The comments for this method will use [ ] to indicate the bounds of rangeToRemove and ( ) to
* indicate the bounds of ranges in the range map.
*/
Entry, RangeMapEntry> mapEntryBelowToTruncate =
entriesByLowerBound.lowerEntry(rangeToRemove.lowerBound);
if (mapEntryBelowToTruncate != null) {
// we know ( [
RangeMapEntry rangeMapEntry = mapEntryBelowToTruncate.getValue();
if (rangeMapEntry.getUpperBound().compareTo(rangeToRemove.lowerBound) > 0) {
// we know ( [ )
if (rangeMapEntry.getUpperBound().compareTo(rangeToRemove.upperBound) > 0) {
// we know ( [ ] ), so insert the range ] ) back into the map --
// it's being split apart
putRangeMapEntry(
rangeToRemove.upperBound,
rangeMapEntry.getUpperBound(),
mapEntryBelowToTruncate.getValue().getValue());
}
// overwrite mapEntryToTruncateBelow with a truncated range
putRangeMapEntry(
rangeMapEntry.getLowerBound(),
rangeToRemove.lowerBound,
mapEntryBelowToTruncate.getValue().getValue());
}
}
Entry, RangeMapEntry> mapEntryAboveToTruncate =
entriesByLowerBound.lowerEntry(rangeToRemove.upperBound);
if (mapEntryAboveToTruncate != null) {
// we know ( ]
RangeMapEntry rangeMapEntry = mapEntryAboveToTruncate.getValue();
if (rangeMapEntry.getUpperBound().compareTo(rangeToRemove.upperBound) > 0) {
// we know ( ] ), and since we dealt with truncating below already,
// we know [ ( ] )
putRangeMapEntry(
rangeToRemove.upperBound,
rangeMapEntry.getUpperBound(),
mapEntryAboveToTruncate.getValue().getValue());
}
}
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());
}
/**
* @since 28.1
*/
@Override
public void merge(
Range range,
@CheckForNull V value,
BiFunction super V, ? super @Nullable V, ? extends @Nullable 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, 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());
}
@Override
public Map, V> asDescendingMapOfRanges() {
return new AsMapOfRanges(entriesByLowerBound.descendingMap().values());
}
private final class AsMapOfRanges extends IteratorBasedAbstractMap, V> {
final Iterable, V>> entryIterable;
@SuppressWarnings("unchecked") // it's safe to upcast iterables
AsMapOfRanges(Iterable> entryIterable) {
this.entryIterable = (Iterable) entryIterable;
}
@Override
public boolean containsKey(@CheckForNull Object key) {
return get(key) != null;
}
@Override
@CheckForNull
public V get(@CheckForNull Object key) {
if (key instanceof Range) {
Range> range = (Range>) key;
RangeMapEntry rangeMapEntry = entriesByLowerBound.get(range.lowerBound);
if (rangeMapEntry != null && rangeMapEntry.getKey().equals(range)) {
return rangeMapEntry.getValue();
}
}
return null;
}
@Override
public int size() {
return entriesByLowerBound.size();
}
@Override
Iterator, V>> entryIterator() {
return entryIterable.iterator();
}
}
@Override
public RangeMap subRangeMap(Range subRange) {
if (subRange.equals(Range.all())) {
return this;
} else {
return new SubRangeMap(subRange);
}
}
@SuppressWarnings("unchecked")
private RangeMap emptySubRangeMap() {
return (RangeMap) (RangeMap, ?>) EMPTY_SUB_RANGE_MAP;
}
@SuppressWarnings("ConstantCaseForConstants") // This RangeMap is immutable.
private static final RangeMap, Object> EMPTY_SUB_RANGE_MAP =
new RangeMap, Object>() {
@Override
@CheckForNull
public Object get(Comparable> key) {
return null;
}
@Override
@CheckForNull
public Entry>, Object> getEntry(Comparable> key) {
return null;
}
@Override
public Range> span() {
throw new NoSuchElementException();
}
@Override
public void put(Range> range, Object value) {
checkNotNull(range);
throw new IllegalArgumentException(
"Cannot insert range " + range + " into an empty subRangeMap");
}
@Override
public void putCoalescing(Range> range, Object value) {
checkNotNull(range);
throw new IllegalArgumentException(
"Cannot insert range " + range + " into an empty subRangeMap");
}
@Override
public void putAll(RangeMap, ? extends Object> rangeMap) {
if (!rangeMap.asMapOfRanges().isEmpty()) {
throw new IllegalArgumentException(
"Cannot putAll(nonEmptyRangeMap) into an empty subRangeMap");
}
}
@Override
public void clear() {}
@Override
public void remove(Range> range) {
checkNotNull(range);
}
@Override
// https://github.com/jspecify/jspecify-reference-checker/issues/162
@SuppressWarnings("nullness")
public void merge(
Range> range,
@CheckForNull Object value,
BiFunction super Object, ? super @Nullable Object, ? extends @Nullable Object>
remappingFunction) {
checkNotNull(range);
throw new IllegalArgumentException(
"Cannot merge range " + range + " into an empty subRangeMap");
}
@Override
public Map>, Object> asMapOfRanges() {
return Collections.emptyMap();
}
@Override
public Map>, Object> asDescendingMapOfRanges() {
return Collections.emptyMap();
}
@Override
public RangeMap, Object> subRangeMap(Range> range) {
checkNotNull(range);
return this;
}
};
private class SubRangeMap implements RangeMap {
private final Range subRange;
SubRangeMap(Range subRange) {
this.subRange = subRange;
}
@Override
@CheckForNull
public V get(K key) {
return subRange.contains(key) ? TreeRangeMap.this.get(key) : null;
}
@Override
@CheckForNull
public Entry, V> getEntry(K key) {
if (subRange.contains(key)) {
Entry, V> entry = TreeRangeMap.this.getEntry(key);
if (entry != null) {
return Maps.immutableEntry(entry.getKey().intersection(subRange), entry.getValue());
}
}
return null;
}
@Override
public Range span() {
Cut lowerBound;
Entry, RangeMapEntry> lowerEntry =
entriesByLowerBound.floorEntry(subRange.lowerBound);
if (lowerEntry != null
&& lowerEntry.getValue().getUpperBound().compareTo(subRange.lowerBound) > 0) {
lowerBound = subRange.lowerBound;
} else {
lowerBound = entriesByLowerBound.ceilingKey(subRange.lowerBound);
if (lowerBound == null || lowerBound.compareTo(subRange.upperBound) >= 0) {
throw new NoSuchElementException();
}
}
Cut upperBound;
Entry, RangeMapEntry> upperEntry =
entriesByLowerBound.lowerEntry(subRange.upperBound);
if (upperEntry == null) {
throw new NoSuchElementException();
} else if (upperEntry.getValue().getUpperBound().compareTo(subRange.upperBound) >= 0) {
upperBound = subRange.upperBound;
} else {
upperBound = upperEntry.getValue().getUpperBound();
}
return Range.create(lowerBound, upperBound);
}
@Override
public void put(Range range, V value) {
checkArgument(
subRange.encloses(range), "Cannot put range %s into a subRangeMap(%s)", range, subRange);
TreeRangeMap.this.put(range, value);
}
@Override
public void putCoalescing(Range range, V value) {
if (entriesByLowerBound.isEmpty() || !subRange.encloses(range)) {
put(range, value);
return;
}
Range coalescedRange = coalescedRange(range, checkNotNull(value));
// only coalesce ranges within the subRange
put(coalescedRange.intersection(subRange), value);
}
@Override
public void putAll(RangeMap rangeMap) {
if (rangeMap.asMapOfRanges().isEmpty()) {
return;
}
Range span = rangeMap.span();
checkArgument(
subRange.encloses(span),
"Cannot putAll rangeMap with span %s into a subRangeMap(%s)",
span,
subRange);
TreeRangeMap.this.putAll(rangeMap);
}
@Override
public void clear() {
TreeRangeMap.this.remove(subRange);
}
@Override
public void remove(Range range) {
if (range.isConnected(subRange)) {
TreeRangeMap.this.remove(range.intersection(subRange));
}
}
@Override
public void merge(
Range range,
@CheckForNull V value,
BiFunction super V, ? super @Nullable V, ? extends @Nullable 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 subRangeMap(Range range) {
if (!range.isConnected(subRange)) {
return emptySubRangeMap();
} else {
return TreeRangeMap.this.subRangeMap(range.intersection(subRange));
}
}
@Override
public Map, V> asMapOfRanges() {
return new SubRangeMapAsMap();
}
@Override
public Map, V> asDescendingMapOfRanges() {
return new SubRangeMapAsMap() {
@Override
Iterator, V>> entryIterator() {
if (subRange.isEmpty()) {
return Iterators.emptyIterator();
}
final Iterator> backingItr =
entriesByLowerBound
.headMap(subRange.upperBound, false)
.descendingMap()
.values()
.iterator();
return new AbstractIterator, V>>() {
@Override
@CheckForNull
protected Entry, V> computeNext() {
if (backingItr.hasNext()) {
RangeMapEntry entry = backingItr.next();
if (entry.getUpperBound().compareTo(subRange.lowerBound) <= 0) {
return endOfData();
}
return Maps.immutableEntry(entry.getKey().intersection(subRange), entry.getValue());
}
return endOfData();
}
};
}
};
}
@Override
public boolean equals(@CheckForNull Object o) {
if (o instanceof RangeMap) {
RangeMap, ?> rangeMap = (RangeMap, ?>) o;
return asMapOfRanges().equals(rangeMap.asMapOfRanges());
}
return false;
}
@Override
public int hashCode() {
return asMapOfRanges().hashCode();
}
@Override
public String toString() {
return asMapOfRanges().toString();
}
class SubRangeMapAsMap extends AbstractMap, V> {
@Override
public boolean containsKey(@CheckForNull Object key) {
return get(key) != null;
}
@Override
@CheckForNull
public V get(@CheckForNull Object key) {
try {
if (key instanceof Range) {
@SuppressWarnings("unchecked") // we catch ClassCastExceptions
Range r = (Range) key;
if (!subRange.encloses(r) || r.isEmpty()) {
return null;
}
RangeMapEntry candidate = null;
if (r.lowerBound.compareTo(subRange.lowerBound) == 0) {
// r could be truncated on the left
Entry, RangeMapEntry> entry =
entriesByLowerBound.floorEntry(r.lowerBound);
if (entry != null) {
candidate = entry.getValue();
}
} else {
candidate = entriesByLowerBound.get(r.lowerBound);
}
if (candidate != null
&& candidate.getKey().isConnected(subRange)
&& candidate.getKey().intersection(subRange).equals(r)) {
return candidate.getValue();
}
}
} catch (ClassCastException e) {
return null;
}
return null;
}
@Override
@CheckForNull
public V remove(@CheckForNull Object key) {
V value = get(key);
if (value != null) {
// it's definitely in the map, so the cast and requireNonNull are safe
@SuppressWarnings("unchecked")
Range range = (Range) requireNonNull(key);
TreeRangeMap.this.remove(range);
return value;
}
return null;
}
@Override
public void clear() {
SubRangeMap.this.clear();
}
private boolean removeEntryIf(Predicate super Entry, V>> predicate) {
List> toRemove = Lists.newArrayList();
for (Entry, V> entry : entrySet()) {
if (predicate.apply(entry)) {
toRemove.add(entry.getKey());
}
}
for (Range range : toRemove) {
TreeRangeMap.this.remove(range);
}
return !toRemove.isEmpty();
}
@Override
public Set> keySet() {
return new Maps.KeySet, V>(SubRangeMapAsMap.this) {
@Override
public boolean remove(@CheckForNull Object o) {
return SubRangeMapAsMap.this.remove(o) != null;
}
@Override
public boolean retainAll(Collection> c) {
return removeEntryIf(compose(not(in(c)), Maps.>keyFunction()));
}
};
}
@Override
public Set, V>> entrySet() {
return new Maps.EntrySet, V>() {
@Override
Map, V> map() {
return SubRangeMapAsMap.this;
}
@Override
public Iterator, V>> iterator() {
return entryIterator();
}
@Override
public boolean retainAll(Collection> c) {
return removeEntryIf(not(in(c)));
}
@Override
public int size() {
return Iterators.size(iterator());
}
@Override
public boolean isEmpty() {
return !iterator().hasNext();
}
};
}
Iterator, V>> entryIterator() {
if (subRange.isEmpty()) {
return Iterators.emptyIterator();
}
Cut cutToStart =
MoreObjects.firstNonNull(
entriesByLowerBound.floorKey(subRange.lowerBound), subRange.lowerBound);
final Iterator> backingItr =
entriesByLowerBound.tailMap(cutToStart, true).values().iterator();
return new AbstractIterator, V>>() {
@Override
@CheckForNull
protected Entry, V> computeNext() {
while (backingItr.hasNext()) {
RangeMapEntry entry = backingItr.next();
if (entry.getLowerBound().compareTo(subRange.upperBound) >= 0) {
return endOfData();
} else if (entry.getUpperBound().compareTo(subRange.lowerBound) > 0) {
// this might not be true e.g. at the start of the iteration
return Maps.immutableEntry(entry.getKey().intersection(subRange), entry.getValue());
}
}
return endOfData();
}
};
}
@Override
public Collection values() {
return new Maps.Values, V>(this) {
@Override
public boolean removeAll(Collection> c) {
return removeEntryIf(compose(in(c), Maps.valueFunction()));
}
@Override
public boolean retainAll(Collection> c) {
return removeEntryIf(compose(not(in(c)), Maps.valueFunction()));
}
};
}
}
}
@Override
public boolean equals(@CheckForNull Object o) {
if (o instanceof RangeMap) {
RangeMap, ?> rangeMap = (RangeMap, ?>) o;
return asMapOfRanges().equals(rangeMap.asMapOfRanges());
}
return false;
}
@Override
public int hashCode() {
return asMapOfRanges().hashCode();
}
@Override
public String toString() {
return entriesByLowerBound.values().toString();
}
}