diff --git a/frontend_tests/node_tests/int_dict.js b/frontend_tests/node_tests/int_dict.js new file mode 100644 index 0000000000..e3e6106173 --- /dev/null +++ b/frontend_tests/node_tests/int_dict.js @@ -0,0 +1,157 @@ +set_global('blueslip', global.make_zblueslip()); +const IntDict = zrequire('int_dict').IntDict; + +run_test('basic', () => { + const d = new IntDict(); + + assert(d.is_empty()); + + assert.deepEqual(d.keys(), []); + + d.set(101, 'bar'); + assert.equal(d.get(101), 'bar'); + assert(!d.is_empty()); + + d.set(101, 'baz'); + assert.equal(d.get(101), 'baz'); + assert.equal(d.num_items(), 1); + + d.set(102, 'qux'); + assert.equal(d.get(101), 'baz'); + assert.equal(d.get(102), 'qux'); + assert.equal(d.num_items(), 2); + + assert.equal(d.has(102), true); + assert.equal(d.has(999), false); + + assert.deepEqual(d.keys(), [101, 102]); + assert.deepEqual(d.values(), ['baz', 'qux']); + + d.del(102); + assert.equal(d.has(102), false); + assert.strictEqual(d.get(102), undefined); + + assert.deepEqual(d.keys(), [101]); + + const val = ['fred']; + const res = d.set(103, val); + assert.equal(val, res); +}); + + +run_test('each', () => { + const d = new IntDict(); + d.set(4, 40); + d.set(5, 50); + d.set(6, 60); + + let unseen_keys = d.keys(); + + let cnt = 0; + d.each(function (v, k) { + assert.equal(v, d.get(k)); + unseen_keys = _.without(unseen_keys, k); + cnt += 1; + }); + + assert.equal(cnt, d.keys().length); + assert.equal(unseen_keys.length, 0); +}); + +/* +run_test('benchmark', () => { + const d = new IntDict(); + const n = 5000; + const t1 = new Date().getTime(); + + _.each(_.range(n), (i) => { + d.set(i, i); + }); + + _.each(_.range(n), (i) => { + d.get(i, i); + }); + + const t2 = new Date().getTime(); + const elapsed = t2 - t1; + console.log('elapsed (milli)', elapsed); + console.log('per (micro)', 1000 * elapsed / n); +}); +*/ + +run_test('undefined_keys', () => { + blueslip.clear_test_data(); + blueslip.set_test_data('error', 'Tried to call a IntDict method with an undefined key.'); + + const d = new IntDict(); + + assert.equal(d.has(undefined), false); + assert.strictEqual(d.get(undefined), undefined); + assert.equal(blueslip.get_test_logs('error').length, 2); +}); + +run_test('non integers', () => { + blueslip.clear_test_data(); + blueslip.set_test_data('error', 'Tried to call a IntDict method with a non-integer.'); + + const d = new IntDict(); + + assert.equal(d.has('some-string'), false); + assert.equal(blueslip.get_test_logs('error').length, 1); + + // verify stringified ints still work + blueslip.clear_test_data(); + blueslip.set_test_data('error', 'Tried to call a IntDict method with a non-integer.'); + + d.set('5', 'five'); + assert.equal(d.has(5), true); + assert.equal(d.has('5'), true); + + assert.equal(d.get(5), 'five'); + assert.equal(d.get('5'), 'five'); + assert.equal(blueslip.get_test_logs('error').length, 3); +}); + +run_test('num_items', () => { + const d = new IntDict(); + assert.equal(d.num_items(), 0); + assert(d.is_empty()); + + d.set(101, 1); + assert.equal(d.num_items(), 1); + assert(!d.is_empty()); + + d.set(101, 2); + assert.equal(d.num_items(), 1); + assert(!d.is_empty()); + + d.set(102, 1); + assert.equal(d.num_items(), 2); + d.del(101); + assert.equal(d.num_items(), 1); +}); + +run_test('clear', () => { + const d = new IntDict(); + + function populate() { + d.set(101, 1); + assert.equal(d.get(101), 1); + d.set(102, 2); + assert.equal(d.get(102), 2); + } + + populate(); + assert.equal(d.num_items(), 2); + assert(!d.is_empty()); + + d.clear(); + assert.equal(d.get(101), undefined); + assert.equal(d.get(102), undefined); + assert.equal(d.num_items(), 0); + assert(d.is_empty()); + + // make sure it still works after clearing + populate(); + assert.equal(d.num_items(), 2); +}); diff --git a/static/js/bundles/app.js b/static/js/bundles/app.js index b19f82e48f..5f4f023e32 100644 --- a/static/js/bundles/app.js +++ b/static/js/bundles/app.js @@ -33,6 +33,7 @@ import "../lightbox_canvas.js"; import "../rtl.js"; import "../lazy_set.js"; import "../dict.ts"; +import "../int_dict.ts"; import "../fold_dict.ts"; import "../scroll_util.js"; import "../components.js"; diff --git a/static/js/int_dict.ts b/static/js/int_dict.ts new file mode 100644 index 0000000000..e1aa044d6f --- /dev/null +++ b/static/js/int_dict.ts @@ -0,0 +1,84 @@ +import * as _ from 'underscore'; + +/* + If we know our keys are ints, the + map-based implementation is about + 20% faster than if we have to normalize + keys as strings. Of course, this + requires us to be a bit careful in the + calling code. We validate ints, which + is cheap, but we don't handle them; we + just report errors. + + This has a subset of methods from our old + Dict class, so it's not quite a drop-in + replacement. For things like setdefault, + it's easier to just use a two-liner in the + calling code. If your Dict uses from_array, + convert it to a Set, not an IntDict. +*/ + +export class IntDict { + private _map = new Map(); + + get(key: number): V | undefined { + key = this._convert(key); + return this._map.get(key); + } + + set(key: number, value: V): V { + key = this._convert(key); + this._map.set(key, value); + return value; + } + + has(key: number): boolean { + key = this._convert(key); + return this._map.has(key); + } + + del(key: number): void { + key = this._convert(key); + this._map.delete(key); + } + + keys(): number[] { + return Array.from(this._map.keys()); + } + + values(): V[] { + return Array.from(this._map.values()); + } + + num_items(): number { + return this._map.size; + } + + is_empty(): boolean { + return this._map.size === 0; + } + + each(f: (v: V, k?: number) => void): void { + this._map.forEach(f); + } + + clear(): void { + this._map.clear(); + } + + private _convert(key: number): number { + // These checks are cheap! (at least on node.js) + if (key === undefined) { + blueslip.error("Tried to call a IntDict method with an undefined key."); + return key; + } + + if (typeof key !== 'number') { + blueslip.error("Tried to call a IntDict method with a non-integer."); + // @ts-ignore + return parseInt(key, 10); + } + + return key; + } +} diff --git a/tools/test-js-with-node b/tools/test-js-with-node index 34cf9847c4..61c47b96b6 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -51,6 +51,7 @@ enforce_fully_covered = { 'static/js/hash_util.js', 'static/js/keydown_util.js', 'static/js/input_pill.js', + 'static/js/int_dict.ts', 'static/js/list_cursor.js', 'static/js/markdown.js', 'static/js/message_store.js',