io.permazen.util.ImmutableNavigableMap Maven / Gradle / Ivy
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen.util;
import com.google.common.base.Preconditions;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.NoSuchElementException;
import java.util.Objects;
/**
* An immutable {@link NavigableMap} implementation optimized for read efficiency.
*
* @param key type
* @param value type
*/
@SuppressWarnings("serial")
public class ImmutableNavigableMap extends AbstractNavigableMap {
private final K[] keys;
private final V[] vals;
private final int minIndex;
private final int maxIndex;
private final Comparator super K> comparator;
private final Comparator super K> actualComparator;
/**
* Constructor.
*
* @param source data source
* @throws IllegalArgumentException if {@code source} is null
*/
@SuppressWarnings("unchecked")
public ImmutableNavigableMap(NavigableMap source) {
this((K[])source.keySet().toArray(), (V[])source.values().toArray(), source.comparator());
}
/**
* Convenience constructor.
*
*
* Equivalent to: {@code ImmutableNavigableMap(keys, vals, 0, Math.min(keys.length, vals.length), comparator)}.
*
* @param keys sorted key array; this array is not copied and must be already sorted
* @param vals value array corresponding to {@code keys}; this array is not copied
* @param comparator key comparator, or null for natural ordering
* @throws IllegalArgumentException if {@code keys} or {@code vals} is null
* @throws IllegalArgumentException if {@code keys} and {@code vals} have different lengths
*/
public ImmutableNavigableMap(K[] keys, V[] vals, Comparator super K> comparator) {
this(new Bounds<>(), keys, vals, 0, Math.min(keys.length, vals.length), comparator);
}
/**
* Primary constructor.
*
* @param keys sorted key array; this array is not copied and must be already sorted
* @param vals value array corresponding to {@code keys}; this array is not copied
* @param minIndex minimum index into arrays (inclusive)
* @param maxIndex maximum index into arrays (exclusive)
* @param comparator key comparator, or null for natural ordering
* @throws IllegalArgumentException if {@code keys} or {@code vals} is null
* @throws IllegalArgumentException if {@code keys} and {@code vals} has length less than {@code maxIndex}
* @throws IllegalArgumentException if {@code minIndex > maxIndex}
*/
public ImmutableNavigableMap(K[] keys, V[] vals, int minIndex, int maxIndex, Comparator super K> comparator) {
this(new Bounds<>(), keys, vals, minIndex, maxIndex, comparator);
}
@SuppressWarnings("unchecked")
private ImmutableNavigableMap(Bounds bounds, K[] keys, V[] vals,
int minIndex, int maxIndex, Comparator super K> comparator) {
super(bounds);
Preconditions.checkArgument(keys != null);
Preconditions.checkArgument(vals != null);
Preconditions.checkArgument(minIndex >= 0 && maxIndex >= minIndex);
Preconditions.checkArgument(keys.length >= maxIndex);
Preconditions.checkArgument(vals.length >= maxIndex);
this.keys = keys;
this.vals = vals;
this.minIndex = minIndex;
this.maxIndex = maxIndex;
this.comparator = comparator;
this.actualComparator = this.comparator != null ? this.comparator : (Comparator)Comparator.naturalOrder();
for (int i = minIndex + 1; i < maxIndex; i++)
assert this.actualComparator.compare(this.keys[i - 1], this.keys[i]) < 0;
}
/**
* Search for the given element in the underlying array.
*
*
* This method works like {@link Arrays#binarySearch(Object[], Object) Arrays.binarySearch()}, returning
* either the index of {@code key} in the underlying array given to the constructor if found, or else
* the one's complement of {@code key}'s insertion point.
*
*
* The array searched is the array given to the constructor, or if {@link #ImmutableNavigableMap(NavigableMap)}
* was used, an array containing all of the keys in this map.
*
* @param key key to search for
* @return index of {@code key}, or {@code -(insertion point) - 1} if not found
*/
public int binarySearch(K key) {
return this.find(key);
}
// NavigableSet
@Override
public Comparator super K> comparator() {
return this.comparator;
}
@Override
public boolean isEmpty() {
return this.minIndex == this.maxIndex;
}
@Override
public int size() {
return this.maxIndex - this.minIndex;
}
@Override
public boolean containsKey(Object obj) {
return this.find(obj) >= 0;
}
@Override
@SuppressWarnings("unchecked")
public V get(Object obj) {
final int index = this.find(obj);
return index >= 0 ? this.vals[index] : null;
}
@Override
public List values() {
List list = Arrays.asList(this.vals);
if (this.minIndex > 0 || this.maxIndex < this.vals.length)
list = list.subList(this.minIndex, this.maxIndex);
return Collections.unmodifiableList(list);
}
@Override
public boolean containsValue(Object obj) {
for (int i = this.minIndex; i < this.maxIndex; i++) {
if (Objects.equals(obj, this.vals[i]))
return true;
}
return false;
}
@Override
public K firstKey() {
return this.keys[this.checkIndex(this.minIndex)];
}
@Override
public Map.Entry firstEntry() {
return !this.isEmpty() ? this.createEntry(this.minIndex) : null;
}
@Override
public K lastKey() {
return this.keys[this.checkIndex(this.maxIndex - 1)];
}
@Override
public Map.Entry lastEntry() {
return !this.isEmpty() ? this.createEntry(this.maxIndex - 1) : null;
}
@Override
public K lowerKey(K maxKey) {
return this.findKey(maxKey, -1, -1);
}
@Override
public Map.Entry lowerEntry(K maxKey) {
return this.findEntry(maxKey, -1, -1);
}
@Override
public K floorKey(K maxKey) {
return this.findKey(maxKey, -1, 0);
}
@Override
public Map.Entry floorEntry(K maxKey) {
return this.findEntry(maxKey, -1, 0);
}
@Override
public K higherKey(K minKey) {
return this.findKey(minKey, 0, 1);
}
@Override
public Map.Entry higherEntry(K minKey) {
return this.findEntry(minKey, 0, 1);
}
@Override
public K ceilingKey(K minKey) {
return this.findKey(minKey, 0, 0);
}
@Override
public Map.Entry ceilingEntry(K minKey) {
return this.findEntry(minKey, 0, 0);
}
@Override
public Map.Entry pollFirstEntry() {
throw new UnsupportedOperationException();
}
@Override
public Map.Entry pollLastEntry() {
throw new UnsupportedOperationException();
}
@Override
public NavigableSet navigableKeySet() {
return new ImmutableNavigableSet<>(this.bounds, this.keys, this.minIndex, this.maxIndex, this.comparator);
}
@Override
@SuppressWarnings("unchecked")
public ImmutableNavigableSet> entrySet() {
final int size = this.size();
final AbstractMap.SimpleImmutableEntry[] entries
= (AbstractMap.SimpleImmutableEntry[])new AbstractMap.SimpleImmutableEntry, ?>[size];
for (int i = this.minIndex; i < this.maxIndex; i++)
entries[i - this.minIndex] = this.createEntry(i);
return new ImmutableNavigableSet<>(entries, Comparator.comparing(Map.Entry::getKey, this.actualComparator));
}
@Override
protected ImmutableNavigableMap createSubMap(boolean reverse, Bounds newBounds) {
// Get upper and lower bounds; note: "newBounds" are already reversed
final K minBound = reverse ? newBounds.getUpperBound() : newBounds.getLowerBound();
final K maxBound = reverse ? newBounds.getLowerBound() : newBounds.getUpperBound();
final BoundType minBoundType = reverse ? newBounds.getUpperBoundType() : newBounds.getLowerBoundType();
final BoundType maxBoundType = reverse ? newBounds.getLowerBoundType() : newBounds.getUpperBoundType();
// Calculate the index range in our current array corresponding to the new bounds
final int newMinIndex;
switch (minBoundType) {
case INCLUSIVE:
newMinIndex = this.findNearby(minBound, 0, 0);
break;
case EXCLUSIVE:
newMinIndex = this.findNearby(minBound, 0, 1);
break;
case NONE:
newMinIndex = this.minIndex;
break;
default:
throw new RuntimeException("internal error");
}
final int newMaxIndex;
switch (maxBoundType) {
case INCLUSIVE:
newMaxIndex = this.findNearby(maxBound, 0, 1);
break;
case EXCLUSIVE:
newMaxIndex = this.findNearby(maxBound, 0, 0);
break;
case NONE:
newMaxIndex = this.maxIndex;
break;
default:
throw new RuntimeException("internal error");
}
// Create new instance
if (reverse) {
final int newSize = newMaxIndex - newMinIndex;
return new ImmutableNavigableMap(newBounds,
ImmutableNavigableSet.reverseArray(Arrays.copyOfRange(this.keys, newMinIndex, newMaxIndex)),
ImmutableNavigableSet.reverseArray(Arrays.copyOfRange(this.vals, newMinIndex, newMaxIndex)),
0, newSize, ImmutableNavigableSet.reversedComparator(this.comparator));
} else
return new ImmutableNavigableMap(newBounds, this.keys, this.vals, newMinIndex, newMaxIndex, this.comparator);
}
private K findKey(Object key, int notFoundOffset, int foundOffset) {
final int index = this.findNearby(key, notFoundOffset, foundOffset);
if (index < this.minIndex || index >= this.maxIndex)
return null;
return this.keys[index];
}
private Map.Entry findEntry(Object key, int notFoundOffset, int foundOffset) {
final int index = this.findNearby(key, notFoundOffset, foundOffset);
if (index < this.minIndex || index >= this.maxIndex)
return null;
return this.createEntry(index);
}
private int findNearby(Object key, int notFoundOffset, int foundOffset) {
final int index = this.find(key);
return index < 0 ? ~index + notFoundOffset : index + foundOffset;
}
@SuppressWarnings("unchecked")
private int find(Object key) {
return Arrays.binarySearch(this.keys, this.minIndex, this.maxIndex, (K)key, this.actualComparator);
}
private int checkIndex(final int index) {
if (index < this.minIndex || index >= this.maxIndex)
throw new NoSuchElementException();
return index;
}
private AbstractMap.SimpleImmutableEntry createEntry(int index) {
assert index >= this.minIndex && index < this.maxIndex;
return new AbstractMap.SimpleImmutableEntry<>(this.keys[index], this.vals[index]);
}
}