package com.ajjpj.abase.collection.immutable;
import com.ajjpj.abase.collection.ACompositeIterator;
import com.ajjpj.abase.collection.AEquality;
import com.ajjpj.abase.collection.APair;
import com.ajjpj.abase.function.AFunction1;
import java.util.*;
/**
* This is an immutable hash map based on 32-way hash tries. Its implementation is optimized to minimize copying when
* the map is modified.
*
* The code in this class is essentially a port of the HashMap class from the Scala standard library. Thank you for
* the excellent code, Scala team!
*
* @author arno
*/
public class AHashMap implements AMap {
private static final int LEVEL_INCREMENT = 5;
private static final AEquality DEFAULT_EQUALITY = AEquality.EQUALS;
private static final AHashMap emptyEquals = new AHashMap<>(AEquality.EQUALS);
private static final AHashMap emptyIdentity = new AHashMap<>(AEquality.IDENTITY);
final AEquality equality;
private Integer cachedHashcode = null; // intentionally not volatile: This class is immutable, so recalculating per thread works
/**
* Returns an empty AHashMap instance with default (i.e. equals-based) equalityForEquals. Calling this factory method instead
* of the constructor allows internal reuse of empty map instances since they are immutable.
*/
public static AHashMap empty() {
return empty(DEFAULT_EQUALITY);
}
/**
* Returns an empty AHashMap instance with the given equalityForEquals strategy. Calling this factory method instead of
* the constructor allows internal reuse of empty map instances since they are immutable.
*/
@SuppressWarnings("unchecked")
public static AHashMap empty(AEquality equality) {
// for typical equalityForEquals implementations, return pre-instantiated objects
if(equality == AEquality.EQUALS) return (AHashMap) emptyEquals;
if(equality == AEquality.IDENTITY) return (AHashMap) emptyIdentity;
return new AHashMap<>(equality);
}
/**
* Returns an AHashMap instance with default (i.e. equals-based) equalityForEquals, initializing it from the contents of
* a given java.util.Map
.
*/
@SuppressWarnings("unused")
public static AHashMap fromJavaUtilMap(Map map) {
return fromJavaUtilMap(DEFAULT_EQUALITY, map);
}
/**
* Returns an AHashMap instance for a given equalityForEquals, initializing it from the contents of a given
* java.util.Map
.
*/
public static AHashMap fromJavaUtilMap(AEquality equality, Map map) {
AHashMap result = new AHashMap<>(equality);
for(Map.Entry entry: map.entrySet()) {
result = result.updated(entry.getKey(), entry.getValue());
}
return result;
}
/**
* Returns an AHashMap instance with default (i.e. equals-based) equalityForEquals, initializing it from separate 'keys'
* and 'values' collections. Both collections are iterated exactly once, and are expected to have the same size.
*/
@SuppressWarnings("unused")
public static AHashMap fromKeysAndValues(Iterable keys, Iterable values) {
return fromKeysAndValues(DEFAULT_EQUALITY, keys, values);
}
/**
* Returns an AHashMap instance with a given equalityForEquals, initializing it from separate 'keys'
* and 'values' collections. Both collections are iterated exactly once, and are expected to have the same size.
*/
public static AHashMap fromKeysAndValues(AEquality equality, Iterable keys, Iterable values) {
final Iterator ki = keys.iterator();
final Iterator vi = values.iterator();
AHashMap result = AHashMap.empty(equality);
while(ki.hasNext()) {
final K key = ki.next();
final V value = vi.next();
result = result.updated(key, value);
}
return result;
}
/**
* Returns an AHashMap instance with default (i.e. equals-based) equalityForEquals, initializing it from a collection of
* keys and a function. For each element of the keys
collection, the function is called once to
* determine the corresponding value, and the pair is then stored in the map.
*/
@SuppressWarnings("unused")
public static AHashMap fromKeysAndFunction(Iterable keys, AFunction1 super K, ? extends V, E> f) throws E {
return fromKeysAndFunction(DEFAULT_EQUALITY, keys, f);
}
/**
* Returns an AHashMap instance with a given equalityForEquals, initializing it from a collection of
* keys and a function. For each element of the keys
collection, the function is called once to
* determine the corresponding value, and the pair is then stored in the map.
*/
public static AHashMap fromKeysAndFunction(AEquality equality, Iterable keys, AFunction1 super K, ? extends V, E> f) throws E {
final Iterator ki = keys.iterator();
AHashMap result = AHashMap.empty(equality);
while(ki.hasNext()) {
final K key = ki.next();
final V value = f.apply(key);
result = result.updated(key, value);
}
return result;
}
private AHashMap(AEquality equality) {
this.equality = equality;
}
@Override
public int size() {
return 0;
}
@Override
public boolean isEmpty() {
return size() == 0;
}
@Override
public boolean nonEmpty() {
return size() > 0;
}
@Override
public boolean containsKey(K key) {
return get(key).isDefined();
}
@Override
public boolean containsValue(V value) {
for(V cur: values()) {
if(equality.equals(value, cur)) {
return true;
}
}
return false;
}
@Override
public AOption get(K key) {
return doGet(key, computeHash(key, equality), 0);
}
@Override
public V getRequired(K key) {
return get(key).get();
}
@Override
public AHashMap updated(K key, V value) {
return doUpdated(key, computeHash(key, equality), 0, value);
}
@Override
public AHashMap removed(K key) {
return doRemoved(key, computeHash(key, equality), 0);
}
@Override
public AMap withDefaultValue(V defaultValue) {
return new AMapWithDefaultValue<>(this, defaultValue);
}
@Override
public AMap withDefault(AFunction1 super K, ? extends V, ? extends RuntimeException> function) {
return new AMapWithDefault<>(this, function);
}
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object o) {
if(o == this) {
return true;
}
if(! (o instanceof AHashMap)) {
return false;
}
final AHashMap other = (AHashMap) o;
if(size() != other.size()) {
return false;
}
for(APair el: this) {
final AOption otherValue = other.get(el._1);
if(otherValue.isEmpty()) {
return false;
}
if(! equality.equals(el._2, otherValue.get())) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
if(cachedHashcode == null) {
int result = 0;
for(APair el: this) {
result = result ^ (31*equality.hashCode(el._1) + equality.hashCode(el._2));
}
cachedHashcode = result;
}
return cachedHashcode;
}
@Override
public Iterator> iterator() {
return new Iterator>() {
@Override
public boolean hasNext() {
return false;
}
@Override
public APair next() {
throw new NoSuchElementException();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override public Set keys() {
return Collections.emptySet();
}
@Override public Collection values() {
return Collections.emptyList();
}
@Override
public Map asJavaUtilMap() {
return new JavaUtilMapWrapper<>(this);
}
/**
* @param level number of least significant bits of the hash to discard for local hash lookup. This mechanism
* is used to create a 32-way hash trie - level increases by 5 at each level
*/
AOption doGet(K key, int hash, int level) {
return AOption.none();
}
AHashMap doUpdated(K key, int hash, int level, V value) {
return new HashMap1<> (key, hash, value, equality);
}
AHashMap doRemoved(K key, int hash, int level) {
return this;
}
private static int computeHash(Object key, AEquality equality) { //TODO ask why this algorithm is used
int h = equality.hashCode(key);
h = h + ~(h << 9);
h = h ^ (h >>> 14);
h = h + (h << 4);
return h ^ (h >>> 10);
}
@SuppressWarnings("unchecked")
private static AHashMap[] createArray(int size) {
return new AHashMap[size];
}
@Override
public String toString() {
final StringBuilder result = new StringBuilder("{");
boolean first = true;
for(APair e: this) {
if(first) {
first = false;
}
else {
result.append(", ");
}
result.append(e._1).append("->").append(e._2);
}
result.append("}");
return result.toString();
}
/**
* very internal method. It assumes hash0 != hash1.
*/
private static HashTrieMap mergeLeafMaps(int hash0, AHashMap elem0, int hash1, AHashMap elem1, int level, int size, AEquality equality) {
final int index0 = (hash0 >>> level) & 0x1f;
final int index1 = (hash1 >>> level) & 0x1f;
if(index0 != index1) {
final int bitmap = (1 << index0) | (1 << index1);
final AHashMap[] elems = createArray(2);
if(index0 < index1) {
elems[0] = elem0;
elems[1] = elem1;
}
else {
elems[0] = elem1;
elems[1] = elem0;
}
return new HashTrieMap<>(bitmap, elems, size, equality);
}
else {
final AHashMap[] elems = createArray(1);
final int bitmap = (1 << index0);
// try again, based on the
elems[0] = mergeLeafMaps(hash0, elem0, hash1, elem1, level + LEVEL_INCREMENT, size, equality);
return new HashTrieMap<>(bitmap, elems, size, equality);
}
}
static class HashMap1 extends AHashMap {
private final K key;
private final int hash;
private final V value;
HashMap1(K key, int hash, V value, AEquality equality) {
super(equality);
this.key = key;
this.hash = hash;
this.value = value;
}
@Override
public int size() {
return 1;
}
@Override
AOption doGet(K key, int hash, int level) {
if(equality.equals(this.key, key)) {
return AOption.some(value);
}
return AOption.none();
}
@Override
AHashMap doUpdated(K key, int hash, int level, V value) {
if (hash == this.hash && equality.equals(key, this.key)) {
if(this.value == value) {
return this;
}
else {
return new HashMap1<>(key, hash, value, equality);
}
}
else {
if (hash != this.hash) {
// they have different hashes, but may collide at this level - find a level at which they don't
final AHashMap that = new HashMap1<>(key, hash, value, equality);
return mergeLeafMaps(this.hash, this, hash, that, level, 2, equality);
}
else {
// hash collision --> store all elements in the same bin
return new HashMapCollision1<> (hash, AListMap.empty(equality).updated(this.key,this.value).updated(key,value));
}
}
}
@Override
AHashMap doRemoved(K key, int hash, int level) {
if (hash == this.hash && equality.equals(key, this.key)) {
return empty(equality);
}
else {
return this;
}
}
@Override
public Iterator> iterator() {
return new Iterator>() {
boolean initial = true;
@Override
public boolean hasNext() {
return initial;
}
@Override
public APair next() {
if(initial) {
initial = false;
return new APair<> (key, value);
}
throw new NoSuchElementException();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public Set keys() {
return Collections.singleton(key);
}
@Override
public Collection values() {
return Collections.singletonList(value);
}
}
static class HashMapCollision1 extends AHashMap {
private final int hash;
private final AListMap kvs;
HashMapCollision1(int hash, AListMap kvs) {
super(kvs.equality);
this.hash = hash;
this.kvs = kvs;
}
@Override
public int size() {
return kvs.size();
}
@Override
AOption doGet(K key, int hash, int level) {
if (hash == this.hash) {
return kvs.get(key);
}
else {
return AOption.none();
}
}
@Override
AHashMap doUpdated(K key, int hash, int level, V value) {
if (hash == this.hash) {
return new HashMapCollision1<>(hash, kvs.updated(key, value));
}
else {
final HashMap1 that = new HashMap1<>(key, hash, value, equality);
return mergeLeafMaps(this.hash, this, hash, that, level, size() + 1, equality);
}
}
@Override
AHashMap doRemoved(K key, int hash, int level) {
if (hash == this.hash) {
final AListMap kvs1 = kvs.removed(key);
if (kvs1.isEmpty()) {
return AHashMap.empty(equality);
}
else if(kvs1.tail().isEmpty()) {
return new HashMap1<>(kvs1.key(), computeHash(kvs1.key(), equality), kvs1.value(), equality);
}
else {
return new HashMapCollision1<>(hash, kvs1);
}
}
else {
return this;
}
}
@Override
public Iterator> iterator() {
return kvs.iterator();
}
@Override
public Set keys() {
return kvs.keys();
}
@Override
public Collection values() {
return kvs.values();
}
}
static class HashTrieMap extends AHashMap {
final int bitmap;
final AHashMap[] elems;
final int size;
HashTrieMap(int bitmap, AHashMap[] elems, int size, AEquality equality) {
super(equality);
this.bitmap = bitmap;
this.elems = elems;
this.size = size;
}
@Override
public int size() {
return size;
}
@Override
public Iterator> iterator() {
final List>> innerIter = new ArrayList<>(elems.length);
for(AHashMap m: elems) {
innerIter.add(m.iterator());
}
return new ACompositeIterator<>(innerIter);
}
@Override
public Set keys() {
return new KeySet();
}
@Override
public Collection values() {
return new ValueCollection();
}
@SuppressWarnings({"NullableProblems", "unchecked", "SuspiciousToArrayCall"})
class KeySet implements Set {
@Override public int size() { return size; }
@Override public boolean isEmpty() { return size == 0; }
@Override public boolean contains(Object o) { return containsKey((K) o); }
@Override public Iterator iterator() {
final List> innerIter = new ArrayList<>(elems.length);
for(AHashMap m: elems) {
innerIter.add(m.keys().iterator());
}
return new ACompositeIterator<>(innerIter);
}
@Override public Object[] toArray() { return new ArrayList<>(this).toArray(); }
@Override public T[] toArray(T[] a) { return new ArrayList<>(this).toArray(a); }
@Override public boolean add(K k) { throw new UnsupportedOperationException(); }
@Override public boolean remove(Object o) { throw new UnsupportedOperationException(); }
@Override public boolean containsAll(Collection> c) {
for(Object o: c) {
if(!contains(o)) {
return false;
}
}
return true;
}
@Override public boolean addAll(Collection extends K> c) { throw new UnsupportedOperationException(); }
@Override public boolean retainAll(Collection> c) { throw new UnsupportedOperationException(); }
@Override public boolean removeAll(Collection> c) { throw new UnsupportedOperationException(); }
@Override public void clear() { throw new UnsupportedOperationException(); }
}
@SuppressWarnings({"NullableProblems", "unchecked", "SuspiciousToArrayCall"})
class ValueCollection implements Collection {
@Override public int size() { return size; }
@Override public boolean isEmpty() { return size == 0; }
@Override public boolean contains(Object o) { return containsValue((V) o); }
@Override public Iterator iterator() {
final List> innerIter = new ArrayList<>(elems.length);
for(AHashMap m: elems) {
innerIter.add(m.values().iterator());
}
return new ACompositeIterator<>(innerIter);
}
@Override public Object[] toArray() { return new ArrayList<>(this).toArray(); }
@Override public T[] toArray(T[] a) { return new ArrayList<>(this).toArray(a); }
@Override public boolean add(V v) { throw new UnsupportedOperationException(); }
@Override public boolean remove(Object o) { throw new UnsupportedOperationException(); }
@Override public boolean containsAll(Collection> c) {
for(Object o: c) {
if(!contains(o)) {
return false;
}
}
return true;
}
@Override public boolean addAll(Collection extends V> c) { throw new UnsupportedOperationException(); }
@Override public boolean retainAll(Collection> c) { throw new UnsupportedOperationException(); }
@Override public boolean removeAll(Collection> c) { throw new UnsupportedOperationException(); }
@Override public void clear() { throw new UnsupportedOperationException(); }
}
@Override
AOption doGet(K key, int hash, int level) {
final int index = (hash >>> level) & 0x1f;
final int mask = 1 << index;
if (bitmap == - 1) {
return elems[index & 0x1f].doGet(key, hash, level + LEVEL_INCREMENT);
}
else if ((bitmap & mask) != 0) {
final int offset = Integer.bitCount(bitmap & (mask - 1));
return elems[offset].doGet(key, hash, level + LEVEL_INCREMENT);
}
else {
return AOption.none();
}
}
@Override
AHashMap doUpdated(K key, int hash, int level, V value) {
final int index = (hash >>> level) & 0x1f;
final int mask = (1 << index);
final int offset = Integer.bitCount(bitmap & (mask - 1));
if ((bitmap & mask) != 0) {
final AHashMap sub = elems[offset];
final AHashMap subNew = sub.doUpdated(key, hash, level + LEVEL_INCREMENT, value);
if(subNew == sub) {
return this;
}
else {
final AHashMap[] elemsNew = createArray(elems.length);
System.arraycopy(elems, 0, elemsNew, 0, elems.length);
elemsNew[offset] = subNew;
return new HashTrieMap<> (bitmap, elemsNew, size + (subNew.size() - sub.size()), equality);
}
}
else {
final AHashMap[] elemsNew = createArray(elems.length + 1);
System.arraycopy(elems, 0, elemsNew, 0, offset);
elemsNew[offset] = new HashMap1<>(key, hash, value, equality);
System.arraycopy(elems, offset, elemsNew, offset + 1, elems.length - offset);
return new HashTrieMap<>(bitmap | mask, elemsNew, size + 1, equality);
}
}
@Override
AHashMap doRemoved(K key, int hash, int level) {
final int index = (hash >>> level) & 0x1f;
final int mask = (1 << index);
final int offset = Integer.bitCount(bitmap & (mask - 1));
if ((bitmap & mask) != 0) {
final AHashMap sub = elems[offset];
final AHashMap subNew = sub.doRemoved(key, hash, level + LEVEL_INCREMENT);
if (subNew == sub) {
return this;
}
else if (subNew.isEmpty()) {
final int bitmapNew = bitmap ^ mask;
if (bitmapNew != 0) {
final AHashMap[] elemsNew = createArray(elems.length - 1);
System.arraycopy(elems, 0, elemsNew, 0, offset);
System.arraycopy(elems, offset + 1, elemsNew, offset, elems.length - offset - 1);
final int sizeNew = size - sub.size();
if (elemsNew.length == 1 && ! (elemsNew[0] instanceof HashTrieMap)) {
return elemsNew[0];
}
else {
return new HashTrieMap<>(bitmapNew, elemsNew, sizeNew, equality);
}
}
else {
return AHashMap.empty(equality);
}
}
else if(elems.length == 1 && ! (subNew instanceof HashTrieMap)) {
return subNew;
}
else {
final AHashMap[] elemsNew = createArray(elems.length);
System.arraycopy(elems, 0, elemsNew, 0, elems.length);
elemsNew[offset] = subNew;
final int sizeNew = size + (subNew.size() - sub.size());
return new HashTrieMap<>(bitmap, elemsNew, sizeNew, equality);
}
} else {
return this;
}
}
}
}