Skip to content

Commit

Permalink
implement faster HashMap and test spec
Browse files Browse the repository at this point in the history
Signed-off-by: Robert Einhorn <[email protected]>
  • Loading branch information
ericvergnaud authored and RobEin committed Mar 18, 2024
1 parent 1f5e64b commit fcd47de
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 39 deletions.
60 changes: 60 additions & 0 deletions runtime/JavaScript/spec/HashMapSpec.js
Original file line number Diff line number Diff line change
@@ -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);
})

});
106 changes: 68 additions & 38 deletions runtime/JavaScript/src/antlr4/misc/HashMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit fcd47de

Please sign in to comment.