All Downloads are FREE. Search and download functionalities are using the official Maven repository.

convex.core.data.MapLeaf Maven / Gradle / Ivy

The newest version!
package convex.core.data;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;

import convex.core.exceptions.BadFormatException;
import convex.core.exceptions.InvalidDataException;
import convex.core.exceptions.Panic;
import convex.core.util.MergeFunction;
import convex.core.util.Utils;

/**
 * Limited size Persistent Merkle Map implemented as a small sorted list of
 * Key/Value pairs
 * 
 * Must be sorted by Key hash value to ensure uniqueness of representation
 *
 * @param  Type of keys
 * @param  Type of values
 */
public class MapLeaf extends AHashMap {
	/**
	 * Maximum number of entries in a MapLeaf
	 */
	public static final int MAX_ENTRIES = 8;

	static final MapEntry[] EMPTY_ENTRIES = new MapEntry[0];

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private static final MapLeaf EMPTY = new MapLeaf(EMPTY_ENTRIES);

	private final MapEntry[] entries;

	private MapLeaf(MapEntry[] items) {
		super(items.length);
		entries = items;
	}

	/**
	 * Creates a ListMap with the specified entries. Entries must have distinct keys
	 * but may otherwise be specified in any order.
	 * 
	 * Null entries are ignored/removed.
	 * 
	 * @param entries Entries for map
	 * @return New ListMap
	 */
	public static  MapLeaf create(MapEntry[] entries) {
		return create(entries, 0, entries.length);
	}

	/**
	 * Creates a MapLeaf with the specified entries. Null entries are
	 * ignored/removed.
	 * 
	 * @param entries
	 * @param offset  Offset into entries array
	 * @param length  Number of entries to take from entries array, starting at
	 *                offset
	 * @return A new ListMap
	 */
	protected static  MapLeaf create(MapEntry[] entries, int offset, int length) {
		if (length == 0) return emptyMap();
		if (length > MAX_ENTRIES) throw new IllegalArgumentException("Too many entries: " + entries.length);
		MapEntry[] sorted = Utils.copyOfRangeExcludeNulls(entries, offset, offset + length);
		if (sorted.length == 0) return emptyMap();
		Arrays.sort(sorted);
		return new MapLeaf(sorted);
	}

	@SuppressWarnings("unchecked")
	public static  MapLeaf create(MapEntry item) {
		return new MapLeaf((MapEntry[]) new MapEntry[] { item });
	}
	
	/**
	 * Creates a {@link MapLeaf} 
	 * @param  Type of keys
	 * @param  Type of values
	 * @param items Array of map entries
	 * @return Potentially invalid MapLeaf
	 */
	@SuppressWarnings("unchecked")
	public static  MapLeaf unsafeCreate(MapEntry... items) {
		return new MapLeaf(items);
	}

	@Override
	public MapEntry getEntry(ACell k) {
		int len = size();
		for (int i = 0; i < len; i++) {
			MapEntry e = entries[i];
			if (Cells.equals(k, e.getKey())) return e;
		}
		return null;
	}

	@Override
	public MapEntry getKeyRefEntry(Ref ref) {
		int len = size();
		for (int i = 0; i < len; i++) {
			MapEntry e = entries[i];
			if (ref.equals(e.getKeyRef())) return e;
		}
		return null;
	}

	@Override
	protected MapEntry getEntryByHash(Hash hash) {
		int len = size();
		for (int i = 0; i < len; i++) {
			MapEntry e = entries[i];
			if (hash.equals(e.getKeyHash())) return e;
		}
		return null;
	}

	@Override
	public boolean containsValue(ACell value) {
		int len = size();
		for (int i = 0; i < len; i++) {
			if (Cells.equals(value, entries[i].getValue())) return true;
		}
		return false;
	}

	@SuppressWarnings("unchecked")
	@Override
	public V get(ACell key) {
		MapEntry me = getEntry((K) key);
		return (me == null) ? null : me.getValue();
	}

	/**
	 * Gets the index of key k in the internal array, or -1 if not found
	 * 
	 * @param key
	 * @return
	 */
	private int seek(K key) {
		int len = size();
		for (int i = 0; i < len; i++) {
			if (Cells.equals(key, entries[i].getKey())) return i;
		}
		return -1;
	}

	private int seekKeyRef(Ref key) {
		int len = size();
		for (int i = 0; i < len; i++) {
			if (Utils.equals(key, entries[i].getKeyRef())) return i;
		}
		return -1;
	}

	@SuppressWarnings("unchecked")
	@Override
	public MapLeaf dissoc(ACell key) {
		int i = seek((K)key);
		if (i < 0) return this; // not found
		return dissocEntry(i);
	}

	@Override
	public MapLeaf dissocRef(Ref key) {
		int i = seekKeyRef(key);
		if (i < 0) return this; // not found
		return dissocEntry(i);
	}

	@SuppressWarnings("unchecked")
	private MapLeaf dissocEntry(int internalIndex) {
		int len = size();
		if (len == 1) return emptyMap();
		MapEntry[] newEntries = (MapEntry[]) new MapEntry[len - 1];
		System.arraycopy(entries, 0, newEntries, 0, internalIndex);
		System.arraycopy(entries, internalIndex + 1, newEntries, internalIndex, len - internalIndex - 1);
		return new MapLeaf(newEntries);
	}

	@Override
	public AHashMap assocEntry(MapEntry e) {
		return assocEntry(e, 0);
	}

	@Override
	public AHashMap assocEntry(MapEntry e, int shift) {
		int len = size();

		// first check for update with existing key
		for (int i = 0; i < len; i++) {
			MapEntry me = entries[i];
			if (e.equals(me)) return this;
			if (me.getKeyRef().equals(e.getKeyRef())) {
				// replace current entry
				MapEntry[] newEntries = entries.clone();
				newEntries[i] = e;
				return new MapLeaf(newEntries);
			}
		}

		// need to extend array, use new shift if promoting to TreeMap
		int newLen = len + 1;
		@SuppressWarnings("unchecked")
		MapEntry[] newEntries = (MapEntry[]) new MapEntry[newLen];
		System.arraycopy(entries, 0, newEntries, 0, len);
		newEntries[newLen - 1] = e;
		if (newLen <= MAX_ENTRIES) {
			// new size implies a ListMap
			Arrays.sort(newEntries);
			return new MapLeaf(newEntries);
		} else {
			// new size implies a TreeMap with the current given shift
			return MapTree.create(newEntries, shift);
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public AHashMap assoc(ACell key, ACell value) {
		return assoc((K)key, (V) value, 0);
	}

	protected AHashMap assoc(K key, V value, int shift) {
		int len = size();

		// first check for update with existing key
		for (int i = 0; i < len; i++) {
			MapEntry me = entries[i];
			if (Cells.equals(me.getKey(), key)) {
				if (Cells.equals(me.getValue(), value)) return this;
				MapEntry newEntry = me.withValue(value);
				if (me == newEntry) return this;

				// need to clone and update array
				MapEntry[] newEntries = entries.clone();
				newEntries[i] = newEntry;
				return new MapLeaf(newEntries);
			}
		}

		// Key not found, so need to extend array
		@SuppressWarnings("unchecked")
		MapEntry[] newEntries = (MapEntry[]) new MapEntry[len + 1];
		System.arraycopy(entries, 0, newEntries, 0, len);
		newEntries[len] = MapEntry.create(key, value);
		if (len + 1 <= MAX_ENTRIES) {
			// new size should be a ListMap
			Arrays.sort(newEntries);
			return new MapLeaf(newEntries);
		} else {
			// new Size should be a TreeMap with current shift
			return MapTree.create(newEntries, shift);
		}
	}

	@Override
	protected AHashMap assocRef(Ref keyRef, V value, int shift) {
		return assoc(keyRef.getValue(), value, shift);
	}

	@Override
	public AHashMap assocRef(Ref keyRef, V value) {
		return assocRef(keyRef, value, 0);
	}

	@Override
	public Set keySet() {
		int len = size();
		HashSet h = new HashSet(len);
		;
		for (int i = 0; i < len; i++) {
			MapEntry me = entries[i];
			h.add(me.getKey());
		}
		return h;
	}

	@Override
	protected void accumulateKeySet(Set h) {
		for (int i = 0; i < entries.length; i++) {
			MapEntry me = entries[i];
			h.add(me.getKey());
		}
	}

	@Override
	protected void accumulateValues(java.util.List al) {
		for (int i = 0; i < entries.length; i++) {
			MapEntry me = entries[i];
			al.add(me.getValue());
		}
	}

	@Override
	protected void accumulateEntrySet(Set> h) {
		for (int i = 0; i < entries.length; i++) {
			MapEntry me = entries[i];
			h.add(me);
		}
	}

	@Override
	public int encode(byte[] bs, int pos) {
		bs[pos++]=Tag.MAP;
		return encodeRaw(bs,pos);
	}
	
	@Override
	public int encodeRaw(byte[] bs, int pos) {
		pos = Format.writeVLCLong(bs,pos, count);

		for (int i = 0; i < count; i++) {
			// Note we encode the Map Entry refs only, skipping the general vector encoding
			pos = entries[i].encodeRefs(bs,pos);
		}
		return pos;
	}

	@Override
	public int estimatedEncodingSize() {
		// allow space for header, size byte, 2 refs per entry
		return 2 + 2* Format.MAX_EMBEDDED_LENGTH * size();
	}
	
	public static int MAX_ENCODING_LENGTH=  2 + 2 * MAX_ENTRIES * Format.MAX_EMBEDDED_LENGTH;

	/**
	 * Reads a MapLeaf from the provided Blob encoding.
	 * 
	 * @param b Blob to read from
	 * @param pos Start position in Blob (index of tag byte)
	 * @param count Count of map elements
	 * @return A Map as deserialised from the provided ByteBuffer
	 * @throws BadFormatException If encoding is invalid
	 */
	public static  MapLeaf read(Blob b, int pos, long count) throws BadFormatException {
		int epos=pos+2; // Note: Tag byte plus VLC length of count which is always 1
		
		@SuppressWarnings("unchecked")
		MapEntry[] items = (MapEntry[]) new MapEntry[(int) count];
		for (int i = 0; i < count; i++) {
			Ref kr=Format.readRef(b,epos);
			epos+=kr.getEncodingLength();
			Ref vr=Format.readRef(b,epos);
			epos+=vr.getEncodingLength();
			items[i] = MapEntry.createRef(kr, vr);
		}

		if (!isValidOrder(items)) {
			throw new BadFormatException("Bad ordering of keys!");
		}
		
		MapLeaf result=new MapLeaf<>(items);
		result.attachEncoding(b.slice(pos, epos));
		return result;
	}
	

	@SuppressWarnings("unchecked")
	public static  MapLeaf emptyMap() {
		return (MapLeaf) EMPTY;
	}

	@Override
	public void forEach(BiConsumer action) {
		for (MapEntry e : entries) {
			action.accept(e.getKey(), e.getValue());
		}
	}

	@Override
	public boolean isCanonical() {
		return true;
	}
	
	@Override public final boolean isCVMValue() {
		return true;
	}

	private static  boolean isValidOrder(MapEntry[] entries) {
		long count = entries.length;
		for (int i = 0; i < count - 1; i++) {
			Hash a = entries[i].getKeyHash();
			Hash b = entries[i + 1].getKeyHash();
			if (a.compareTo(b) >= 0) {
				return false;
			}
		}
		return true;
	}

	@Override
	public int getRefCount() {
		return 2 * entries.length;
	}

	@SuppressWarnings("unchecked")
	@Override
	public  Ref getRef(int i) {
		MapEntry e = entries[i >> 1]; // IndexOutOfBoundsException if out of range
		if ((i & 1) == 0) {
			return (Ref) e.getKeyRef();
		} else {
			return (Ref) e.getValueRef();
		}
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public MapLeaf updateRefs(IRefFunction func) {
		int n = entries.length;
		if (n == 0) return this;
		MapEntry[] newEntries = entries;
		for (int i = 0; i < n; i++) {
			MapEntry e = newEntries[i];
			MapEntry newEntry = e.updateRefs(func);
			if (e!=newEntry) {
				if (newEntries==entries) newEntries=entries.clone();
				newEntries[i]=newEntry;
			}
		}
		if (newEntries==entries) return this;
		// Note: we assume no key hashes have changed
		MapLeaf result= new MapLeaf(newEntries);
		result.attachEncoding(encoding); // this is an optimisation to avoid re-encoding
		return result;
	}

	/**
	 * Filters this ListMap to contain only key hashes with the hex digits specified
	 * in the given Mask
	 * 
	 * @param digitPos Position of the hex digit to filter
	 * @param mask     Mask of digits to include
	 * @return Filtered ListMap
	 */
	public MapLeaf filterHexDigits(int digitPos, int mask) {
		mask = mask & 0xFFFF;
		if (mask == 0) return emptyMap();
		if (mask == 0xFFFF) return this;
		int sel = 0;
		int n = size();
		for (int i = 0; i < n; i++) {
			Hash h = entries[i].getKeyHash();
			if ((mask & (1 << h.getHexDigit(digitPos))) != 0) {
				sel = sel | (1 << i); // include this index in selection
			}
		}
		if (sel == 0) return emptyMap(); // no entries selected
		return filterEntries(sel);
	}

	/**
	 * Filters entries using the given bit mask
	 * 
	 * @param selection
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private MapLeaf filterEntries(int selection) {
		if (selection == 0) return emptyMap(); // no items selected
		int n = size();
		if (selection == ((1 << n) - 1)) return this; // all items selected
		MapEntry[] newEntries = new MapEntry[Integer.bitCount(selection)];
		int ix = 0;
		for (int i = 0; i < n; i++) {
			if ((selection & (1 << i)) != 0) {
				newEntries[ix++] = entries[i];
			}
		}
		assert (ix == Integer.bitCount(selection));
		return new MapLeaf(newEntries);
	}

	@Override
	public MapEntry entryAt(long i) {
		return entries[(int)i];
	}

	@Override
	public AHashMap mergeWith(AHashMap b, MergeFunction func) {
		return mergeWith(b, func, 0);
	}

	@Override
	protected AHashMap mergeWith(AHashMap b, MergeFunction func, int shift) {
		if (b instanceof MapLeaf) return mergeWith((MapLeaf) b, func, shift);
		if (b instanceof MapTree) return ((MapTree) b).mergeWith(this, func.reverse());
		throw new Panic("Unhandled map type: " + b.getClass());
	}

	@SuppressWarnings("null")
	private AHashMap mergeWith(MapLeaf b, MergeFunction func, int shift) {
		int al = this.size();
		int bl = b.size();
		int ai = 0;
		int bi = 0;
		// Complexity to manage:
		// 1. Must step through two ListMaps in order, comparing for key hashes
		// 2. nulls can be produced to remove entries
		// 3. We use the creation of a results ArrayList to signal a change from
		// original value
		ArrayList> results = null;
		while ((ai < al) || (bi < bl)) {
			MapEntry ae = (ai < al) ? this.entries[ai] : null;
			MapEntry be = (bi < bl) ? b.entries[bi] : null;
			
			// comparison
			int c = (ae == null) ? 1 : ((be == null) ? -1 : ae.getKeyHash().compareTo(be.getKeyHash()));
			
			// new entry
			MapEntry newE = null;
			if (c < 0) {
				V r = func.merge(ae.getValue(), null);
				if (r != null) newE = ae.withValue(r);
			} else if (c > 0) {
				V r = func.merge(null, be.getValue());
				if (r != null) newE = be.withValue(r);
			} else {
				// we have matched keys
				V r = func.merge(ae.getValue(), be.getValue());
				if (r != null) newE = ae.withValue(r);
			}
			if ((results == null) && (newE != ((c <= 0) ? ae : null))) {
				// create new results array if difference detected
				results = new ArrayList<>(16);
				for (int i = 0; i < ai; i++) { // copy previous values in this map, up to ai
					results.add(entries[i]);
				}
			}
			if (c <= 0) ai++; // inc ai if we used ae
			if (c >= 0) bi++; // inc bi if we used be
			if ((results != null) && (newE != null)) results.add(newE);
		}
		if (results == null) return this; // no change detected
		return Maps.createWithShift(shift, results);
	}

	@Override
	public AHashMap mergeDifferences(AHashMap b, MergeFunction func) {
		return mergeDifferences(b,func,0);
	}
	
	@Override
	protected AHashMap mergeDifferences(AHashMap b, MergeFunction func, int shift) {
		if (b instanceof MapLeaf) return mergeDifferences((MapLeaf) b, func,shift);
		if (b instanceof MapTree) return b.mergeWith(this, func.reverse());
		throw new Panic("Unhandled map type: " + b.getClass());
	}

	@SuppressWarnings("null")
	public AHashMap mergeDifferences(MapLeaf b, MergeFunction func,int shift) {
		if (this.equals(b)) return this; // no change in identical case
		int al = this.size();
		int bl = b.size();
		int ai = 0;
		int bi = 0;
		ArrayList> results = null;
		while ((ai < al) || (bi < bl)) {
			MapEntry ae = (ai < al) ? this.entries[ai] : null;
			MapEntry be = (bi < bl) ? b.entries[bi] : null;
			int c = (ae == null) ? 1 : ((be == null) ? -1 : ae.getKeyHash().compareTo(be.getKeyHash()));
			MapEntry newE = null;
			if (c < 0) {
				// lowest key in this map only
				V r = func.merge(ae.getValue(), null);
				if (r != null) newE = ae.withValue(r);
			} else if (c > 0) {
				// lowest key in other map b only
				V r = func.merge(null, be.getValue());
				if (r != null) newE = be.withValue(r);
			} else {
				// keys are equal (i.e. value in both maps)
				V av = ae.getValue();
				V bv = be.getValue();
				V r = (Cells.equals(av, bv)) ? av : func.merge(ae.getValue(), be.getValue());
				if (r != null) newE = ae.withValue(r);
			}
			if ((results == null) && (newE != ((c <= 0) ? ae : null))) {
				// create new results array if difference detected
				results = new ArrayList<>(16);
				for (int i = 0; i < ai; i++) {
					results.add(entries[i]);
				}
			}
			if (c <= 0) ai++; // inc ai if we used ae
			if (c >= 0) bi++; // inc bi if we used be
			if ((results != null) && (newE != null)) results.add(newE);
		}
		if (results == null) return this;
		return Maps.createWithShift(shift,results);
	}

	@Override
	public  R reduceValues(BiFunction func, R initial) {
		int n = size();
		R result = initial;
		for (int i = 0; i < n; i++) {
			result = func.apply(result, entries[i].getValue());
		}
		return result;
	}

	@Override
	public  R reduceEntries(BiFunction, ? extends R> func, R initial) {
		int n = size();
		R result = initial;
		for (int i = 0; i < n; i++) {
			result = func.apply(result, entries[i]);
		}
		return result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public boolean equals(ACell a) {
		if (!(a instanceof MapLeaf)) return false;
		return equals((MapLeaf) a);
	}

	public boolean equals(MapLeaf a) {
		if (this == a) return true;
		int n = size();
		if (n != a.size()) return false;
		for (int i = 0; i < n; i++) {
			if (!entries[i].equals(a.entries[i])) return false;
		}
		return true;
	}

	@Override
	public MapLeaf mapEntries(Function, MapEntry> func) {
		MapEntry[] newEntries = entries;
		for (int i = 0; i < entries.length; i++) {
			MapEntry e = entries[i];
			MapEntry newE = func.apply(e);
			if (e != newE) {
				if ((newE != null) && (!(e.keyEquals(newE))))
					throw new IllegalArgumentException("Function changed Key: " + e.getKey());
				if (newEntries == entries) {
					newEntries = entries.clone();
				}
				newEntries[i] = newE;
			}
		}
		if (newEntries == entries) return this;
		return create(newEntries);
	}

	@Override
	protected void validateWithPrefix(String prefix) throws InvalidDataException {
		validate();
		for (int i = 0; i < entries.length; i++) {
			MapEntry e = entries[i];
			Hash h = e.getKeyRef().getHash();
			if (!h.toHexString().startsWith(prefix)) {
				throw new InvalidDataException("Prefix " + prefix + " invalid for map entry: " + e + " with hash: " + h,
						this);
			}
			e.validate();
		}
	}

	@Override
	public void validateCell() throws InvalidDataException {
		if ((count == 0) && (this != EMPTY)) {
			throw new InvalidDataException("Empty map not using canonical instance", this);
		}

		if (count > MAX_ENTRIES) {
			throw new InvalidDataException("Too many items in list map: " + entries.length, this);
		}

		// validates both key uniqueness and sort order
		if (!isValidOrder(entries)) {
			throw new InvalidDataException("Invalid key ordering", this);
		}
	}

	@Override
	public boolean containsAllKeys(AHashMap b) {
		if (this==b) return true;
		
		// if map is too big, can't possibly contain all keys
		if (b.count()>count) return false;
		
		// must be a mapleaf if this size or smaller
		return containsAllKeys((MapLeaf)b);
	}
	
	protected boolean containsAllKeys(MapLeaf b) {
		int ix=0;
		for (MapEntry meb:b.entries) {
			Hash bh=meb.getKeyHash();
			
			if (ix>=count) return false; // no remaining entries in this
			while (ix mea=entries[ix];
				Hash ah=mea.getKeyHash();
				int c=ah.compareTo(bh);
				if (c<0) {
					// ah is smaller than bh
					// need to advance ix and try next entry
					ix++;
					if (ix>=count) return false; // not found
					continue;
				} else if (c>0) {
					return false; // didn't contain the key entry
				} else {
					// found it, so advance to next entry in b and update ix
					ix++;
					break;
				}
			}
		}
		
		return true;
	}

	@Override
	public byte getTag() {
		return Tag.MAP;
	}

	@Override
	public ACell toCanonical() {
		return this;
	}

	@Override
	public MapLeaf slice(long start, long end) {
		if ((start<0)||(end>count)) return null;
		if (end[] nrefs=new MapEntry[n];
		System.arraycopy(entries, (int) start, nrefs, 0, n);
		return new MapLeaf(nrefs);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy