diff --git a/src/as/com/threerings/util/HashMap.as b/src/as/com/threerings/util/HashMap.as index 1e49baec9..8360b7217 100644 --- a/src/as/com/threerings/util/HashMap.as +++ b/src/as/com/threerings/util/HashMap.as @@ -1,305 +1,21 @@ package com.threerings.util { -import flash.utils.Dictionary; - import mx.utils.ObjectUtil; /** - * An implementation of a HashMap in actionscript. + * A Hashtable implementation that utilizes ObjectUtil.compare and + * ObjectUtil.toString for hashing keys that are non-simple and do + * not implement Hashable. */ -public class HashMap +public class HashMap extends Hashtable { public function HashMap (loadFactor :Number = 1.75) { - _loadFactor = loadFactor; - } - - public function clear () :void - { - _simpleData = null; - _entries = null; - _size = 0; - } - - public function containsKey (key :Object) :Boolean - { - return (undefined !== get(key)); - } - - /** - * Get the value stored in the map for the specified key, or - * undefined if there is no value for that key. - */ - public function get (key :Object) :* - { - if (ObjectUtil.isSimple(key)) { - return (_simpleData == null) ? undefined : _simpleData[key]; - } - - if (_entries == null) { - return undefined; - } - - var hkey :Hashable = keyFor(key); - var hash :int = hkey.hashCode(); - var index :int = indexFor(hash); - var e :Entry = (_entries[index] as Entry); - while (true) { - if (e == null) { - return null; - } - if (e.hash == hash && e.key.equals(hkey)) { - return e.value; - } - e = e.next; - } - } - - public function isEmpty () :Boolean - { - return (_size == 0); - } - - public function put (key :Object, value :Object) :* - { - if (ObjectUtil.isSimple(key)) { - if (_simpleData == null) { - _simpleData = new Dictionary(); - } - - var oldValue :* = _simpleData[key]; - _simpleData[key] = value; - if (oldValue === undefined) { - _size++; - } - return oldValue; - } - - // lazy-create the array holding other hashables - if (_entries == null) { - _entries = new Array(); - _entries.length = DEFAULT_BUCKETS; - } - - var hkey :Hashable = keyFor(key); - var hash :int = hkey.hashCode(); - var index :int = indexFor(hash); - var firstEntry :Entry = (_entries[index] as Entry); - for (var e :Entry = firstEntry; e != null; e = e.next) { - if (e.hash == hash && e.key.equals(hkey)) { - var oldValue :* = e.value; - e.value = value; - return oldValue; // size did not change - } - } - - _entries[index] = new Entry(hash, hkey, value, firstEntry); - _size++; - // check to see if we should grow the map - if (_size > _entries.length * _loadFactor) { - resize(2 * _entries.length); - } - // indicate that there was no value previously stored for the key - return undefined; - } - - public function remove (key :Object) :* - { - if (ObjectUtil.isSimple(key)) { - if (_simpleData == null) { - return undefined; - } - - var oldValue :* = _simpleData[key]; - if (oldValue !== undefined) { - _size--; - } - delete _simpleData[key]; - return oldValue; - } - - if (_entries == null) { - return undefined; - } - - var hkey :Hashable = keyFor(key); - var hash :int = hkey.hashCode(); - var index :int = indexFor(hash); - var prev :Entry = (_entries[index] as Entry); - var e :Entry = prev; - - while (e != null) { - var next :Entry = e.next; - if (e.hash == hash && e.key.equals(hkey)) { - if (prev == e) { - _entries[index] = next; - } else { - prev.next = next; - } - _size--; - // check to see if we should shrink the map - if ((_entries.length > DEFAULT_BUCKETS) && - (_size < _entries.length * _loadFactor * .125)) { - resize(Math.max(DEFAULT_BUCKETS, _entries.length / 2)); - } - return e.value; - } - prev = e; - e = next; - } - - return undefined; // never found - } - - public function size () :int - { - return _size; - } - - public function keys () :Array - { - var keys :Array = new Array(); - - // get the simple keys first - if (_simpleData != null) { - for (var key :* in _simpleData) { - keys.push(key); - } - } - - // get the more complex keys - if (_entries != null) { - for (var ii :int = _entries.length - 1; ii >= 0; ii--) { - for (var e :Entry = (_entries[ii] as Entry); e != null; - e = e.next) { - if (e.key is KeyWrapper) { - keys.push((e.key as KeyWrapper).key); - - } else { - keys.push(e.key) - } - } - } - } - - return keys; - } - - public function values () :Array - { - var vals :Array = new Array(); - - // get the simple properties first - if (_simpleData != null) { - for each (var value :* in _simpleData) { - vals.push(value); - } - } - - // get the more complex properties - if (_entries != null) { - for (var ii :int = _entries.length - 1; ii >= 0; ii--) { - for (var e :Entry = (_entries[ii] as Entry); e != null; - e = e.next) { - vals.push(e.value); - } - } - } - - return vals; - } - - protected function keyFor (key :Object) :Hashable - { - return (key is Hashable) ? (key as Hashable) : new KeyWrapper(key); - } - - protected function indexFor (hash :int) :int - { - return Math.abs(hash % _entries.length); - } - - protected function resize (newSize :int) :void - { - var oldEntries :Array = _entries; - _entries = new Array(); - _entries.length = newSize; - - // place all the old entries in the new map - for (var ii :int = 0; ii < oldEntries.length; ii++) { - var e :Entry = (oldEntries[ii] as Entry); - if (e != null) { - do { - var next :Entry = e.next; - var index :int = indexFor(e.hash); - e.next = _entries[index]; - _entries[index] = e; - e = next; - } while (e != null); - } - } - } - - protected var _size :int = 0; - - protected var _loadFactor :Number; - - protected var _simpleData :Dictionary - - protected var _entries :Array; - - /** The default size for the bucketed hashmap. */ - protected static const DEFAULT_BUCKETS :int = 16; -} - -} // end: package - -import com.threerings.util.Hashable; - -class Entry -{ - public var key :Hashable; - public var value :Object; - public var hash :int; - public var next :Entry; - - public function Entry (hash :int, key :Hashable, value :Object, next :Entry) - { - this.hash = hash; - this.key = key; - this.value = value; - this.next = next; + super(loadFactor, + function (o1 :Object, o2 :Object) :Boolean { + return (0 == ObjectUtil.compare(o1, o2)); + }, + ObjectUtil.toString); } } - -import mx.utils.ObjectUtil; - -class KeyWrapper - implements Hashable -{ - public var key :Object; - - public function KeyWrapper (key :Object) - { - this.key = key; - _hash = 0; - var uid :String = ObjectUtil.toString(key); - for (var ii :int = 0; ii < uid.length; ii++) { - _hash = (_hash << 1) ^ int(uid.charCodeAt(ii)); - } - } - - public function equals (other :Object) :Boolean - { - return (other is KeyWrapper) && - (0 == ObjectUtil.compare(key, (other as KeyWrapper).key)); - } - - public function hashCode () :int - { - return _hash; - } - - protected var _key :Object; - protected var _hash :int; } diff --git a/src/as/com/threerings/util/Hashtable.as b/src/as/com/threerings/util/Hashtable.as new file mode 100644 index 000000000..ef7921fae --- /dev/null +++ b/src/as/com/threerings/util/Hashtable.as @@ -0,0 +1,371 @@ +package com.threerings.util { + +import flash.errors.IllegalOperationError; + +import flash.utils.Dictionary; + +/** + * An implementation of a Hashtable in actionscript. Any object (and null) may + * be used as a key. Simple keys (Number, int, uint, Boolean, String) utilize + * a Dictionary internally for storage; keys that implement Hashable are + * stored efficiently, and any other key can also be used if the equalsFn + * and hashFn are specified to the constructor. + */ +public class Hashtable + implements Map +{ + /** + * Construct a Hashtable + * + * @param loadFactor - A measure of how full the hashtable is allowed to + * get before it is automatically resized. The default + * value of 1.75 should be fine. + * @param equalsFn - (Optional) A function to use to compare object + * equality for keys that are neither simple nor + * implement Hashable. The signature should be + * "function (o1, o2) :Boolean". + * @param hashFn - (Optional) A function to use to generate a hash + * code for keys that are neither simple nor + * implement Hashable. The signature should be + * "function (obj) :*", where the return type is + * numeric or String. Two objects that are equals + * according to the specified equalsFn *must* + * generate equal values when passed to the hashFn. + */ + public function Hashtable ( + loadFactor :Number = 1.75, + equalsFn :Function = null, + hashFn :Function = null) + { + if ((equalsFn != null) != (hashFn != null)) { + throw new ArgumentError("Both the equals and hash functions " + + "must be specified, or neither."); + } + _loadFactor = loadFactor; + _equalsFn = equalsFn; + _hashFn = hashFn; + } + + public function clear () :void + { + _simpleData = null; + _entries = null; + _size = 0; + } + + public function containsKey (key :Object) :Boolean + { + return (undefined !== get(key)); + } + + /** + * Get the value stored in the map for the specified key, or + * undefined if there is no value for that key. + */ + public function get (key :Object) :* + { + if (isSimple(key)) { + return (_simpleData == null) ? undefined : _simpleData[key]; + } + + if (_entries == null) { + return undefined; + } + + var hkey :Hashable = keyFor(key); + var hash :int = hkey.hashCode(); + var index :int = indexFor(hash); + var e :Entry = (_entries[index] as Entry); + while (true) { + if (e == null) { + return null; + } + if (e.hash == hash && e.key.equals(hkey)) { + return e.value; + } + e = e.next; + } + } + + public function isEmpty () :Boolean + { + return (_size == 0); + } + + public function put (key :Object, value :Object) :* + { + if (isSimple(key)) { + if (_simpleData == null) { + _simpleData = new Dictionary(); + } + + var oldValue :* = _simpleData[key]; + _simpleData[key] = value; + if (oldValue === undefined) { + _size++; + } + return oldValue; + } + + // lazy-create the array holding other hashables + if (_entries == null) { + _entries = new Array(); + _entries.length = DEFAULT_BUCKETS; + } + + var hkey :Hashable = keyFor(key); + var hash :int = hkey.hashCode(); + var index :int = indexFor(hash); + var firstEntry :Entry = (_entries[index] as Entry); + for (var e :Entry = firstEntry; e != null; e = e.next) { + if (e.hash == hash && e.key.equals(hkey)) { + var oldValue :* = e.value; + e.value = value; + return oldValue; // size did not change + } + } + + _entries[index] = new Entry(hash, hkey, value, firstEntry); + _size++; + // check to see if we should grow the map + if (_size > _entries.length * _loadFactor) { + resize(2 * _entries.length); + } + // indicate that there was no value previously stored for the key + return undefined; + } + + public function remove (key :Object) :* + { + if (isSimple(key)) { + if (_simpleData == null) { + return undefined; + } + + var oldValue :* = _simpleData[key]; + if (oldValue !== undefined) { + _size--; + } + delete _simpleData[key]; + return oldValue; + } + + if (_entries == null) { + return undefined; + } + + var hkey :Hashable = keyFor(key); + var hash :int = hkey.hashCode(); + var index :int = indexFor(hash); + var prev :Entry = (_entries[index] as Entry); + var e :Entry = prev; + + while (e != null) { + var next :Entry = e.next; + if (e.hash == hash && e.key.equals(hkey)) { + if (prev == e) { + _entries[index] = next; + } else { + prev.next = next; + } + _size--; + // check to see if we should shrink the map + if ((_entries.length > DEFAULT_BUCKETS) && + (_size < _entries.length * _loadFactor * .125)) { + resize(Math.max(DEFAULT_BUCKETS, _entries.length / 2)); + } + return e.value; + } + prev = e; + e = next; + } + + return undefined; // never found + } + + public function size () :int + { + return _size; + } + + public function keys () :Array + { + var keys :Array = new Array(); + + // get the simple keys first + if (_simpleData != null) { + for (var key :* in _simpleData) { + keys.push(key); + } + } + + // get the more complex keys + if (_entries != null) { + for (var ii :int = _entries.length - 1; ii >= 0; ii--) { + for (var e :Entry = (_entries[ii] as Entry); e != null; + e = e.next) { + if (e.key is KeyWrapper) { + keys.push((e.key as KeyWrapper).key); + + } else { + keys.push(e.key) + } + } + } + } + + return keys; + } + + public function values () :Array + { + var vals :Array = new Array(); + + // get the simple properties first + if (_simpleData != null) { + for each (var value :* in _simpleData) { + vals.push(value); + } + } + + // get the more complex properties + if (_entries != null) { + for (var ii :int = _entries.length - 1; ii >= 0; ii--) { + for (var e :Entry = (_entries[ii] as Entry); e != null; + e = e.next) { + vals.push(e.value); + } + } + } + + return vals; + } + + protected function keyFor (key :Object) :Hashable + { + if (key is Hashable) { + return (key as Hashable); + + } else if (_hashFn == null) { + throw new IllegalOperationError("Illegal key specified " + + "for Hashtable created without hashing functions."); + + } else { + return new KeyWrapper(key, _equalsFn, _hashFn); + } + } + + protected function indexFor (hash :int) :int + { + return Math.abs(hash % _entries.length); + } + + protected function isSimple (key :Object) :Boolean + { + return (key is String) || (key is Number) || (key is Boolean); + } + + protected function resize (newSize :int) :void + { + var oldEntries :Array = _entries; + _entries = new Array(); + _entries.length = newSize; + + // place all the old entries in the new map + for (var ii :int = 0; ii < oldEntries.length; ii++) { + var e :Entry = (oldEntries[ii] as Entry); + if (e != null) { + do { + var next :Entry = e.next; + var index :int = indexFor(e.hash); + e.next = _entries[index]; + _entries[index] = e; + e = next; + } while (e != null); + } + } + } + + /** The current number of key/value pairs stored in the map. */ + protected var _size :int = 0; + + /** The load factor. */ + protected var _loadFactor :Number; + + /** If non-null, contains simple key/value pairs. */ + protected var _simpleData :Dictionary + + /** If non-null, contains Hashable keys and their values. */ + protected var _entries :Array; + + /** The hashing function to use for non-Hashable complex keys. */ + protected var _hashFn :Function; + + /** The equality function to use for non-Hashable complex keys. */ + protected var _equalsFn :Function; + + /** The default size for the bucketed hashmap. */ + protected static const DEFAULT_BUCKETS :int = 16; +} + +} // end: package com.threerings.util + +import com.threerings.util.Hashable; + +class Entry +{ + public var key :Hashable; + public var value :Object; + public var hash :int; + public var next :Entry; + + public function Entry (hash :int, key :Hashable, value :Object, next :Entry) + { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } +} + +class KeyWrapper + implements Hashable +{ + public var key :Object; + + public function KeyWrapper ( + key :Object, equalsFn :Function, hashFn :Function) + { + this.key = key; + _equalsFn = equalsFn; + var hashValue :* = hashFn(key); + if (hashValue is String) { + var uid :String = (hashValue as String); + // examine at most 32 characters of the string + var inc :int = int(Math.max(1, Math.ceil(uid.length / 32))); + for (var ii :int = 0; ii < uid.length; ii += inc) { + _hash = (_hash << 1) ^ int(uid.charCodeAt(ii)); + } + + } else { + _hash = int(hashValue); + } + } + + // documentation inherited from interface Hashable + public function equals (other :Object) :Boolean + { + return (other is KeyWrapper) && + Boolean(_equalsFn(key, (other as KeyWrapper).key)); + } + + // documentation inherited from interface Hashable + public function hashCode () :int + { + return _hash; + } + + protected var _key :Object; + protected var _hash :int; + protected var _equalsFn :Function; +} diff --git a/src/as/com/threerings/util/Map.as b/src/as/com/threerings/util/Map.as new file mode 100644 index 000000000..d4c80c719 --- /dev/null +++ b/src/as/com/threerings/util/Map.as @@ -0,0 +1,20 @@ +package com.threerings.util { + +public interface Map +{ + function clear () :void; + + function containsKey (key :Object) :Boolean; + + function get (key :Object) :*; + + function isEmpty () :Boolean; + + function put (key :Object, value :Object) :*; + + function remove (key :Object) :*; + + function size () :int; +} + +}