UNPKG

@naturalcycles/js-lib

Version:

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

214 lines (213 loc) 6.18 kB
import { _assert } from '../error/index.js'; /** * Maintains sorted array of keys. * Sorts **on insertion**, not on retrieval. * * - set(): O(log n) search + O(n) splice only when inserting a NEW key * - get/has: O(1) * - delete: O(log n) search + O(n) splice if present * - iteration: O(n) over pre-sorted keys (no sorting at iteration time) * * @experimental */ export class KeySortedMap { map; #sortedKeys; constructor(entries = [], opt = {}) { this.#comparator = opt.comparator; this.map = new Map(entries); this.#sortedKeys = [...this.map.keys()]; this.sortKeys(); } #comparator; /** * Convenience way to create KeySortedMap from object. */ static of(obj) { return new KeySortedMap(Object.entries(obj)); } get size() { return this.map.size; } clear() { this.map.clear(); this.#sortedKeys.length = 0; } 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.#sortedKeys.push(k); } // Resort all at once this.sortKeys(); return this; } /** * Insert or update. Keeps keys array sorted at all times. * Returns this (Map-like). */ set(key, value) { if (this.map.has(key)) { // Update only; position unchanged. this.map.set(key, value); return this; } // Find insertion index (lower_bound). const i = this.lowerBound(key); // Only insert into keys when actually new. this.#sortedKeys.splice(i, 0, key); 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); // Remove from keys using binary search to avoid O(n) find. const i = this.lowerBound(key); // Because key existed, it must be at i. if (i < this.#sortedKeys.length && this.#sortedKeys[i] === key) { this.#sortedKeys.splice(i, 1); } else { // Extremely unlikely if external mutation happened; safe guard. // Fall back to linear search (shouldn't happen). const j = this.#sortedKeys.indexOf(key); if (j !== -1) this.#sortedKeys.splice(j, 1); } return true; } /** * Iterables (Map-compatible), all in sorted order. */ *keys() { for (const key of this.#sortedKeys) { yield key; } } *values() { for (const key of this.#sortedKeys) { yield this.map.get(key); } } *entries() { for (const k of this.#sortedKeys) { yield [k, this.map.get(k)]; } } [Symbol.iterator]() { return this.entries(); } toString() { console.log('toString called !!!!!!!!!!!!!!!!!!!!!'); return 'abc'; } [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.#sortedKeys) { cb.call(thisArg, map.get(k), k, this); } } firstKeyOrUndefined() { return this.#sortedKeys[0]; } firstKey() { _assert(this.#sortedKeys.length, 'Map.firstKey called on empty map'); return this.#sortedKeys[0]; } lastKeyOrUndefined() { return this.#sortedKeys.length ? this.#sortedKeys[this.#sortedKeys.length - 1] : undefined; } lastKey() { _assert(this.#sortedKeys.length, 'Map.lastKey called on empty map'); return this.#sortedKeys[this.#sortedKeys.length - 1]; } firstValueOrUndefined() { return this.map.get(this.#sortedKeys[0]); } firstValue() { _assert(this.#sortedKeys.length, 'Map.firstValue called on empty map'); return this.map.get(this.#sortedKeys[0]); } lastValueOrUndefined() { return this.#sortedKeys.length ? this.map.get(this.#sortedKeys[this.#sortedKeys.length - 1]) : undefined; } lastValue() { _assert(this.#sortedKeys.length, 'Map.lastValue called on empty map'); return this.map.get(this.#sortedKeys[this.#sortedKeys.length - 1]); } firstEntryOrUndefined() { if (!this.#sortedKeys.length) return; const k = this.#sortedKeys[0]; return [k, this.map.get(k)]; } firstEntry() { _assert(this.#sortedKeys.length, 'Map.firstEntry called on empty map'); const k = this.#sortedKeys[0]; return [k, this.map.get(k)]; } lastEntryOrUndefined() { if (!this.#sortedKeys.length) return; const k = this.#sortedKeys[this.#sortedKeys.length - 1]; return [k, this.map.get(k)]; } lastEntry() { _assert(this.#sortedKeys.length, 'Map.lastEntry called on empty map'); const k = this.#sortedKeys[this.#sortedKeys.length - 1]; return [k, this.map.get(k)]; } toJSON() { return this.toObject(); } toObject() { return Object.fromEntries(this.entries()); } /** * Clones the KeySortedMap into ordinary Map. */ toMap() { return new Map(this.entries()); } /** * lowerBound: first index i s.t. keys[i] >= target */ lowerBound(target) { let lo = 0; let hi = this.#sortedKeys.length; while (lo < hi) { // oxlint-disable-next-line no-bitwise const mid = (lo + hi) >>> 1; if (this.#sortedKeys[mid] < target) { lo = mid + 1; } else { hi = mid; } } return lo; } sortKeys() { this.#sortedKeys.sort(this.#comparator); } }