diff --git a/runtime/JavaScript/spec/HashMapSpec.js b/runtime/JavaScript/spec/HashMapSpec.js new file mode 100644 index 0000000000..0f328ad9d9 --- /dev/null +++ b/runtime/JavaScript/spec/HashMapSpec.js @@ -0,0 +1,60 @@ +import HashMap from "../src/antlr4/misc/HashMap.js"; +import HashCode from "../src/antlr4/misc/HashCode.js"; + +class Thing { + + value1 = Math.random(); + value2 = Math.random(); + + hashCode() { + return HashCode.hashStuff(this.value1); + } + + equals(other) { + return other instanceof Thing + && other.value1 === this.value1 + && other.value2 === this.value2; + } +} + +describe('test HashMap', () => { + + it("sets a thing", () => { + const t1 = new Thing(); + const map = new HashMap(); + map.set("abc", t1); + expect(map.containsKey("abc")).toBeTrue(); + expect(map.containsKey("def")).toBeFalse(); + expect(map.length).toEqual(1); + }) + + it("gets a thing", () => { + const t1 = new Thing(); + const map = new HashMap(); + map.set("abc", t1); + const t2 = map.get("abc"); + expect(t2).toEqual(t1); + }) + + it("replaces a thing", () => { + const t1 = new Thing(); + const t2 = new Thing(); + const map = new HashMap(); + map.set("abc", t1); + map.set("abc", t2); + const t3 = map.get("abc"); + expect(t3).toEqual(t2); + }) + + it("returns correct length", () => { + const t1 = new Thing(); + const t2 = new Thing(); + const map = new HashMap(); + expect(map.length).toEqual(0); + map.set("abc", t1); + expect(map.length).toEqual(1); + map.set("def", t2); + expect(map.length).toEqual(2); + }) + +}); diff --git a/runtime/JavaScript/src/antlr4/misc/HashMap.js b/runtime/JavaScript/src/antlr4/misc/HashMap.js index 13c46cada2..8740816a2e 100644 --- a/runtime/JavaScript/src/antlr4/misc/HashMap.js +++ b/runtime/JavaScript/src/antlr4/misc/HashMap.js @@ -5,80 +5,110 @@ import standardEqualsFunction from "../utils/standardEqualsFunction.js"; import standardHashCodeFunction from "../utils/standardHashCodeFunction.js"; -const HASH_KEY_PREFIX = "h-"; +const DEFAULT_LOAD_FACTOR = 0.75; +const INITIAL_CAPACITY = 16 export default class HashMap { constructor(hashFunction, equalsFunction) { - this.data = {}; + this.buckets = new Array(INITIAL_CAPACITY); + this.threshold = Math.floor(INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); + this.itemCount = 0; this.hashFunction = hashFunction || standardHashCodeFunction; this.equalsFunction = equalsFunction || standardEqualsFunction; } set(key, value) { - const hashKey = HASH_KEY_PREFIX + this.hashFunction(key); - if (hashKey in this.data) { - const entries = this.data[hashKey]; - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (this.equalsFunction(key, entry.key)) { - const oldValue = entry.value; - entry.value = value; - return oldValue; - } - } - entries.push({key:key, value:value}); + this._expand(); + const slot = this._getSlot(key); + let bucket = this.buckets[slot]; + if (!bucket) { + bucket = [[key, value]]; + this.buckets[slot] = bucket; + this.itemCount++; return value; + } + const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this); + if(existing) { + const result = existing[1]; + existing[1] = value; + return result; } else { - this.data[hashKey] = [{key:key, value:value}]; + bucket.push([key, value]); + this.itemCount++; return value; } } containsKey(key) { - const hashKey = HASH_KEY_PREFIX + this.hashFunction(key); - if(hashKey in this.data) { - const entries = this.data[hashKey]; - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (this.equalsFunction(key, entry.key)) - return true; - } + const bucket = this._getBucket(key); + if(!bucket) { + return false; } - return false; + const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this); + return !!existing; } get(key) { - const hashKey = HASH_KEY_PREFIX + this.hashFunction(key); - if(hashKey in this.data) { - const entries = this.data[hashKey]; - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (this.equalsFunction(key, entry.key)) - return entry.value; - } + const bucket = this._getBucket(key); + if(!bucket) { + return null; } - return null; + const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this); + return existing ? existing[1] : null; } entries() { - return Object.keys(this.data).filter(key => key.startsWith(HASH_KEY_PREFIX)).flatMap(key => this.data[key], this); + return this.buckets.filter(b => b != null).flat(1); } getKeys() { - return this.entries().map(e => e.key); + return this.entries().map(pair => pair[0]); } getValues() { - return this.entries().map(e => e.value); + return this.entries().map(pair => pair[1]); } toString() { - const ss = this.entries().map(e => '{' + e.key + ':' + e.value + '}'); + const ss = this.entries().map(e => '{' + e[0] + ':' + e[1] + '}'); return '[' + ss.join(", ") + ']'; } get length() { - return Object.keys(this.data).filter(key => key.startsWith(HASH_KEY_PREFIX)).map(key => this.data[key].length, this).reduce((accum, item) => accum + item, 0); + return this.itemCount; + } + + _getSlot(key) { + const hash = this.hashFunction(key); + return hash & this.buckets.length - 1; + } + _getBucket(key) { + return this.buckets[this._getSlot(key)]; } + + _expand() { + if (this.itemCount <= this.threshold) { + return; + } + const old_buckets = this.buckets; + const newCapacity = this.buckets.length * 2; + this.buckets = new Array(newCapacity); + this.threshold = Math.floor(newCapacity * DEFAULT_LOAD_FACTOR); + for (const bucket of old_buckets) { + if (!bucket) { + continue; + } + for (const pair of bucket) { + const slot = this._getSlot(pair[0]); + let newBucket = this.buckets[slot]; + if (!newBucket) { + newBucket = []; + this.buckets[slot] = newBucket; + } + newBucket.push(pair); + } + } + } + } diff --git a/runtime/JavaScript/src/antlr4/utils/standardEqualsFunction.js b/runtime/JavaScript/src/antlr4/utils/standardEqualsFunction.js index 3269df835e..a6efbd5a24 100644 --- a/runtime/JavaScript/src/antlr4/utils/standardEqualsFunction.js +++ b/runtime/JavaScript/src/antlr4/utils/standardEqualsFunction.js @@ -3,5 +3,5 @@ * can be found in the LICENSE.txt file in the project root. */ export default function standardEqualsFunction(a, b) { - return a ? a.equals(b) : a===b; + return a && a.equals ? a.equals(b) : a===b; }