UNPKG

@naturalcycles/js-lib

Version:

Standard library for universal (browser + Node.js) javascript

185 lines (184 loc) 4.99 kB
import { _assert } from '../error/index.js'; /** * Maintains sorted array of keys. * Sorts **data access**, not on insertion. * * @experimental */ export class LazyKeySortedMap { map; maybeSortedKeys; keysAreSorted = false; constructor(entries = [], opt = {}) { this.#comparator = opt.comparator; this.map = new Map(entries); this.maybeSortedKeys = [...this.map.keys()]; } #comparator; /** * Convenience way to create KeySortedMap from object. */ static of(obj) { return new LazyKeySortedMap(Object.entries(obj)); } get size() { return this.map.size; } clear() { this.map.clear(); this.maybeSortedKeys.length = 0; this.keysAreSorted = true; } has(key) { return this.map.has(key); } get(key) { return this.map.get(key); } /** * Allows to set multiple key-value pairs at once. */ setMany(obj) { for (const [k, v] of Object.entries(obj)) { this.map.set(k, v); this.maybeSortedKeys.push(k); } this.keysAreSorted = false; return this; } /** * Insert or update. Keeps keys array sorted at all times. * Returns this (Map-like). */ set(key, value) { if (!this.map.has(key)) { this.maybeSortedKeys.push(key); this.keysAreSorted = false; } this.map.set(key, value); return this; } /** * Delete by key. Returns boolean like Map.delete. */ delete(key) { if (!this.map.has(key)) return false; this.map.delete(key); // Delete operation keeps the array **as-is**, it may have been sorted or not. const j = this.maybeSortedKeys.indexOf(key); if (j !== -1) this.maybeSortedKeys.splice(j, 1); return true; } /** * Iterables (Map-compatible), all in sorted order. */ *keys() { for (const key of this.getSortedKeys()) { yield key; } } *values() { for (const key of this.getSortedKeys()) { yield this.map.get(key); } } *entries() { for (const k of this.getSortedKeys()) { yield [k, this.map.get(k)]; } } [Symbol.iterator]() { return this.entries(); } [Symbol.toStringTag] = 'KeySortedMap'; /** * Zero-allocation callbacks over sorted data (faster than spreading to arrays). */ forEach(cb, thisArg) { const { map } = this; for (const k of this.getSortedKeys()) { cb.call(thisArg, map.get(k), k, this); } } firstKeyOrUndefined() { return this.getSortedKeys()[0]; } firstKey() { _assert(this.maybeSortedKeys.length, 'Map.firstKey called on empty map'); return this.getSortedKeys()[0]; } lastKeyOrUndefined() { if (!this.maybeSortedKeys.length) return; const keys = this.getSortedKeys(); return keys[keys.length - 1]; } lastKey() { const k = this.lastKeyOrUndefined(); _assert(k, 'Map.lastKey called on empty map'); return k; } firstValueOrUndefined() { if (!this.maybeSortedKeys.length) return; return this.map.get(this.getSortedKeys()[0]); } firstValue() { const v = this.firstValueOrUndefined(); _assert(v, 'Map.firstValue called on empty map'); return v; } lastValueOrUndefined() { if (!this.maybeSortedKeys.length) return; const keys = this.getSortedKeys(); return this.map.get(keys[keys.length - 1]); } lastValue() { const v = this.lastValueOrUndefined(); _assert(v, 'Map.lastValue called on empty map'); return v; } firstEntryOrUndefined() { if (!this.maybeSortedKeys.length) return; const k = this.getSortedKeys()[0]; return [k, this.map.get(k)]; } firstEntry() { const e = this.firstEntryOrUndefined(); _assert(e, 'Map.firstEntry called on empty map'); return e; } lastEntryOrUndefined() { if (!this.maybeSortedKeys.length) return; const keys = this.getSortedKeys(); const k = keys[keys.length - 1]; return [k, this.map.get(k)]; } lastEntry() { const e = this.firstEntryOrUndefined(); _assert(e, 'Map.lastEntry called on empty map'); return e; } toJSON() { return this.toObject(); } toObject() { return Object.fromEntries(this.entries()); } getSortedKeys() { if (!this.keysAreSorted) { return this.sortKeys(); } return this.maybeSortedKeys; } sortKeys() { this.maybeSortedKeys.sort(this.#comparator); this.keysAreSorted = true; return this.maybeSortedKeys; } }