package arkref.ext.fig.basic;
import static arkref.ext.fig.basic.LogInfo.errors;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* Provides a map from objects to doubles.
* Motivation: provides a specialized data structure for
* mapping objects to doubles which is both fast and space efficient.
* Feature 1:
* You can switch between two representations of the map:
* - Sorted list (lookups involve binary search)
* - Hash table with linear probing (lookups involve hashing)
* Feature 2:
* Sometimes, we want several maps with the same set of keys.
* If we lock the map, we can share the same keys between several
* maps, which saves space.
*
* Note: in the sorted list, we first sort the keys by
* hash code, and then for equal hash code, we sort by the objects
* values. We hope that hash code collisions will be rare enough
* that we won't have to resort to comparing objects.
*
* Typical usage:
* - Construct a map using a hash table.
* - To save space, switch to a sorted list representation.
*
* Will get runtime exception if try to used sorted list and keys are not
* comparable.
*
* TODO: support remove operation.
*/
public class TFloatMap<T> extends AbstractTMap<T>
implements Iterable<TFloatMap<T>.Entry>, Serializable
{
protected static final long serialVersionUID = 42;
public TFloatMap() {
this(AbstractTMap.defaultFunctionality, defaultExpectedSize);
}
public TFloatMap(Functionality<T> keyFunc) {
this(keyFunc, defaultExpectedSize);
}
public TFloatMap(int expectedSize) {
this(AbstractTMap.defaultFunctionality, expectedSize);
}
// If keys are locked, we can share the same keys.
public TFloatMap(AbstractTMap<T> map) {
this(map.keyFunc);
this.mapType = map.mapType;
this.locked = map.locked;
this.num = map.num;
this.keys = map.locked ? map.keys : (T[])map.keys.clone(); // Share keys! CHECKED
if(map instanceof TFloatMap)
this.values = ((TFloatMap<T>)map).values.clone();
else
this.values = new float[keys.length];
}
/**
* expectedSize: expected number of entries we're going to have in the map.
*/
public TFloatMap(Functionality<T> keyFunc, int expectedSize) {
this.keyFunc = keyFunc;
this.mapType = MapType.HASH_TABLE;
this.locked = false;
this.num = 0;
allocate(getCapacity(num, false));
this.numCollisions = 0;
}
// Main operations
public boolean containsKey(T key) {
return find(key, false) != -1;
}
public float get(T key, float defaultValue)
{
int i = find(key, false);
return i == -1 ? defaultValue : values[i];
}
public float getWithErrorMsg(T key, float defaultValue)
{
int i = find(key, false);
if(i == -1) errors("%s not in map, using %f", key, defaultValue);
return i == -1 ? defaultValue : values[i];
}
public float getSure(T key)
{
// Throw exception if key doesn't exist.
int i = find(key, false);
if(i == -1) throw new RuntimeException("Missing key: " + key);
return values[i];
}
public void put(T key, float value)
{
assert !Double.isNaN(value);
int i = find(key, true);
keys[i] = key;
values[i] = value;
}
public void put(T key, float value, boolean keepHigher)
{
assert !Double.isNaN(value);
int i = find(key, true);
keys[i] = key;
if (keepHigher && values[i] > value) return;
values[i] = value;
}
public void incr(T key, float dValue)
{
int i = find(key, true);
keys[i] = key;
if(Double.isNaN(values[i])) values[i] = dValue; // New value
else values[i] += dValue;
}
public void scale(T key, float dValue)
{
int i = find(key, true);
if(i == -1) return;
values[i] *= dValue;
}
public int size() { return num; }
public int capacity() { return keys.length; }
/*public void clear() { // Keep the same capacity
num = 0;
for(int i = 0; i < keys.length; i++)
keys[i] = null;
}*/
public void gut() { values = null; } // Save memory
// Simple operations on values
// Implement them here for maximum efficiency.
public float sum()
{
float sum = 0;
for(int i = 0; i < keys.length; i++)
if(keys[i] != null)
sum += values[i];
return sum;
}
public void putAll(float value)
{
for(int i = 0; i < keys.length; i++)
if(keys[i] != null)
values[i] = value;
}
public void incrAll(float dValue)
{
for(int i = 0; i < keys.length; i++)
if(keys[i] != null)
values[i] += dValue;
}
public void multAll(float dValue)
{
for(int i = 0; i < keys.length; i++)
if(keys[i] != null)
values[i] *= dValue;
}
// Return the key with the maximum value
public T argmax() {
int besti = -1;
for(int i = 0; i < keys.length; i++)
if(keys[i] != null && (besti == -1 || values[i] > values[besti]))
besti = i;
return besti == -1 ? null : keys[besti];
}
// Return the maximum value
public float max()
{
int besti = -1;
for(int i = 0; i < keys.length; i++)
if(keys[i] != null && (besti == -1 || values[i] > values[besti]))
besti = i;
return besti == -1 ? Float.NEGATIVE_INFINITY : values[besti];
}
// For each (key, value) in map, increment this's key by factor*value
public void incrMap(TFloatMap<T> map, float factor)
{
for(int i = 0; i < map.keys.length; i++)
if(map.keys[i] != null)
incr(map.keys[i], factor*map.values[i]);
}
// If keys are locked, we can share the same keys.
public TFloatMap<T> copy() {
TFloatMap<T> newMap = new TFloatMap<T>(keyFunc);
newMap.mapType = mapType;
newMap.locked = locked;
newMap.num = num;
newMap.keys = locked ? keys : (T[])keys.clone(); // Share keys! CHECKED
newMap.values = values.clone();
return newMap;
}
// Return a map with only keys in the set
public TFloatMap<T> restrict(Set<T> set) {
TFloatMap<T> newMap = new TFloatMap<T>(keyFunc);
newMap.mapType = mapType;
if(mapType == MapType.SORTED_LIST) {
allocate(getCapacity(num, false));
for(int i = 0; i < keys.length; i++) {
if(set.contains(keys[i])) {
newMap.keys[newMap.num] = keys[i];
newMap.values[newMap.num] = values[i];
newMap.num++;
}
}
}
else if(mapType == MapType.HASH_TABLE) {
for(int i = 0; i < keys.length; i++)
if(keys[i] != null && set.contains(keys[i]))
newMap.put(keys[i], values[i]);
}
newMap.locked = locked;
return newMap;
}
// For sorting the entries.
// Warning: this class has the overhead of the parent class
private class FullEntry implements Comparable<FullEntry> {
private FullEntry(T key, float value)
{
this.key = key;
this.value = value;
}
public int compareTo(FullEntry e) {
int h1 = hash(key);
int h2 = hash(e.key);
if(h1 != h2) return h1-h2;
return ((Comparable)key).compareTo(e.key);
}
private final T key;
private final float value;
}
// Compare by value.
public class EntryValueComparator implements Comparator<Entry> {
public int compare(Entry e1, Entry e2) {
return Double.compare(values[e1.i], values[e2.i]);
}
}
public EntryValueComparator entryValueComparator() { return new EntryValueComparator(); }
// For iterating.
public class Entry {
private Entry(int i) { this.i = i; }
public T getKey() { return keys[i]; }
public float getValue()
{
return values[i];
}
public void setValue(float newValue)
{
values[i] = newValue;
}
private final int i;
}
public void lock() {
locked = true;
}
public void switchToSortedList() {
switchMapType(MapType.SORTED_LIST);
}
public void switchToHashTable() {
switchMapType(MapType.HASH_TABLE);
}
////////////////////////////////////////////////////////////
public class EntrySet extends AbstractSet<Entry> {
@Override
public Iterator<Entry> iterator() { return new EntryIterator(); }
@Override
public int size() { return num; }
@Override
public boolean contains(Object o) { throw new UnsupportedOperationException(); }
@Override
public boolean remove(Object o) { throw new UnsupportedOperationException(); }
@Override
public void clear() { throw new UnsupportedOperationException(); }
}
public class KeySet extends AbstractSet<T> {
@Override
public Iterator<T> iterator() { return new KeyIterator(); }
@Override
public int size() { return num; }
@Override
public boolean contains(Object o) { return containsKey((T)o); } // CHECKED
@Override
public boolean remove(Object o) { throw new UnsupportedOperationException(); }
@Override
public void clear() { throw new UnsupportedOperationException(); }
}
public class ValueCollection extends AbstractCollection<Float>
{
@Override
public Iterator<Float> iterator()
{
return new ValueIterator();
}
@Override
public int size() { return num; }
@Override
public boolean contains(Object o) { throw new UnsupportedOperationException(); }
@Override
public void clear() { throw new UnsupportedOperationException(); }
}
public EntryIterator iterator() { return new EntryIterator(); }
public EntrySet entrySet() { return new EntrySet(); }
public KeySet keySet() { return new KeySet(); }
public ValueCollection values() { return new ValueCollection(); }
// WARNING: no checks that this iterator is only used when
// the map is not being structurally changed
private class EntryIterator extends MapIterator<Entry> {
public Entry next() { return new Entry(nextIndex()); }
}
private class KeyIterator extends MapIterator<T> {
public T next() { return keys[nextIndex()]; }
}
private class ValueIterator extends MapIterator<Float>
{
public Float next()
{
return values[nextIndex()];
}
}
private abstract class MapIterator<E> implements Iterator<E> {
public MapIterator() {
if(mapType == MapType.SORTED_LIST) end = size();
else end = capacity();
next = -1;
nextIndex();
}
public boolean hasNext() { return next < end; }
int nextIndex() {
int curr = next;
do { next++; } while(next < end && keys[next] == null);
return curr;
}
public void remove() { throw new UnsupportedOperationException(); }
private int next, end;
}
////////////////////////////////////////////////////////////
/** How much capacity do we need for this type of map,
* given that we want n elements.
* compact: whether we want to save space and don't plan on growing.
*/
private int getCapacity(int n, boolean compact) {
int capacity;
if(mapType == MapType.SORTED_LIST)
capacity = compact ? n : n*growFactor;
else if(mapType == MapType.HASH_TABLE) {
capacity = n*growFactor+2; // Make sure there's enough room for n+2 more entries
}
else throw new RuntimeException("Internal bug");
return Math.max(capacity, 1);
}
/**
* Convert the map to the given type.
*/
private void switchMapType(MapType newMapType) {
assert !locked;
//System.out.println("switchMapType(" + newMapType + ", " + compact + ")");
// Save old keys and values, allocate space
T[] oldKeys = keys;
float[] oldValues = values;
mapType = newMapType;
allocate(getCapacity(num, true));
numCollisions = 0;
if(newMapType == MapType.SORTED_LIST) {
// Sort the keys
List<FullEntry> entries = new ArrayList<FullEntry>(num);
for(int i = 0; i < oldKeys.length; i++)
if(oldKeys[i] != null)
entries.add(new FullEntry(oldKeys[i], oldValues[i]));
Collections.sort(entries);
// Populate the sorted list
for(int i = 0; i < num; i++) {
keys[i] = entries.get(i).key;
values[i] = entries.get(i).value;
}
}
else if(mapType == MapType.HASH_TABLE) {
// Populate the hash table
num = 0;
for(int i = 0; i < oldKeys.length; i++) {
if(oldKeys[i] != null)
put(oldKeys[i], oldValues[i]);
}
}
}
/**
* Return the first index i for which the target key is less than or equal to
* key i (00001111). Should insert target key at position i.
* If target is larger than all of the elements, return size().
*/
private int binarySearch(T targetKey) {
int targetHash = hash(targetKey);
int l = 0, u = num;
while(l < u) {
//System.out.println(l);
int m = (l+u) >> 1;
int keyHash = hash(keys[m]);
if(targetHash < keyHash || (targetHash == keyHash && ((Comparable)targetKey).compareTo(keys[m]) <= 0))
u = m;
else
l = m+1;
}
return l;
}
// Modified hash (taken from HashMap.java).
private int hash(T x) {
int h = x.hashCode();
h += ~(h << 9);
h ^= (h >>> 14);
h += (h << 4);
h ^= (h >>> 10);
if(h < 0) h = -h; // New
return h;
}
/**
* Modify is whether to make room for the new key if it doesn't exist.
* If a new entry is created, the value at that position will be Double.NaN.
* Here's where all the magic happens.
*/
private int find(T key, boolean modify) {
//System.out.println("find " + key + " " + modify + " " + mapType + " " + capacity());
if(mapType == MapType.SORTED_LIST) {
// Binary search
int i = binarySearch(key);
if(i < num && keys[i] != null && key.equals(keys[i])) return i;
if(modify) {
if(locked)
throw new RuntimeException("Cannot make new entry for " + key + ", because map is locked");
if(num == capacity())
changeSortedListCapacity(getCapacity(num+1, false));
// Shift everything forward
for(int j = num; j > i; j--) {
keys[j] = keys[j-1];
values[j] = values[j-1];
}
num++;
values[i] = Float.NaN;
return i;
}
else
return -1;
}
else if(mapType == MapType.HASH_TABLE) {
int capacity = capacity();
int keyHash = hash(key);
int i = keyHash % capacity;
if(i < 0) i = -i; // Arbitrary transformation
// Make sure big enough
if(!locked && modify && (num > loadFactor*capacity || capacity <= num+1)) {
/*if(locked)
throw new RuntimeException("Cannot make new entry for " + key + ", because map is locked");*/
switchMapType(MapType.HASH_TABLE);
return find(key, modify);
}
//System.out.println("!!! " + keyHash + " " + capacity);
if(num == capacity)
throw new RuntimeException("Hash table is full: " + capacity);
while(keys[i] != null && !keys[i].equals(key)) { // Collision
// Warning: infinite loop if the hash table is full
// (but this shouldn't happen based on the check above)
i++;
numCollisions++;
if(i == capacity) i = 0;
}
if(keys[i] != null) { // Found
assert key.equals(keys[i]);
return i;
}
if(modify) { // Not found
num++;
values[i] = Float.NaN;
return i;
}
else
return -1;
}
else
throw new RuntimeException("Internal bug: " + mapType);
}
private void allocate(int n) {
keys = keyFunc.createArray(n);
values = new float[n];
}
// Resize the sorted list to the new capacity.
private void changeSortedListCapacity(int newCapacity) {
assert mapType == MapType.SORTED_LIST;
assert newCapacity >= num;
T[] oldKeys = keys;
float[] oldValues = values;
allocate(newCapacity);
System.arraycopy(oldKeys, 0, keys, 0, num);
System.arraycopy(oldValues, 0, values, 0, num);
}
// Check consistency of data structure.
private void repCheck() {
assert capacity() > 0;
if(mapType == MapType.SORTED_LIST) {
assert num <= capacity();
for(int i = 1; i < num; i++) { // Make sure keys are sorted.
int h1 = hash(keys[i-1]);
int h2 = hash(keys[i]);
assert h1 <= h2;
if(h1 == h2)
assert ((Comparable)keys[i-1]).compareTo(keys[i]) < 0;
}
}
}
public void debugDump() {
LogInfo.logsForce("--------------------");
LogInfo.logsForce("mapType = " + mapType);
LogInfo.logsForce("locked = " + locked);
LogInfo.logsForce("size/capacity = " + size() + "/" + capacity());
LogInfo.logsForce("numCollisions = " + numCollisions);
/*for(int i = 0; i < keys.length; i++) {
System.out.printf("[%d] %s (%d) => %f\n", i, keys[i], (keys[i] == null ? 0 : keys[i].hashCode()), values[i]);
}*/
}
/**
* Format: mapType, num, (key, value) pairs
*/
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(mapType);
out.writeInt(num);
for(Entry e : this) {
out.writeObject(e.getKey());
out.writeDouble(e.getValue());
}
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
this.mapType = (MapType)in.readObject();
this.num = 0;
this.locked = false;
int n = in.readInt();
allocate(getCapacity(n, true));
for(int i = 0; i < n; i++) {
T key = keyFunc.intern((T)in.readObject()); // CHECKED
float value = in.readFloat();
if(mapType == MapType.SORTED_LIST) {
// Assume keys and values serialized in sorted order
keys[num] = key;
values[num] = value;
num++;
}
else if(mapType == MapType.HASH_TABLE) {
put(key, value);
}
}
}
// Construct a map from a list of key, value, key value arguments.
public static <T> TFloatMap newMap(Object... args) {
if(args.length % 2 != 0) throw Exceptions.bad;
TFloatMap map = new TFloatMap();
for(int i = 0; i < args.length; i += 2) {
T key = (T)args[i];
Object value = args[i+1];
if (value instanceof Integer) value = (float) ((Integer) value);
map.put((T) args[i], (Float) value);
}
return map;
}
@Override
public String toString(){
StringBuilder sb = new StringBuilder();
sb.append("[");
for (TFloatMap<T>.Entry entry : entrySet()){
sb.append(entry.getKey() + ":" + entry.getValue()+", ");
}
sb.append("]");
return sb.toString();
}
private float[] values;
}