UNPKG

deque-typed

Version:
456 lines (404 loc) 14 kB
/** * TreeMap (ordered map) — a restricted, native-like API backed by RedBlackTree. * * Design goals: * - No node exposure (no node inputs/outputs) * - Native Map-like surface + Java NavigableMap-like helpers * - Strict default comparator (number/string/Date), otherwise require comparator */ import type { Comparator } from '../../types'; import type { TreeMapEntryCallback, TreeMapOptions, TreeMapRangeOptions, TreeMapReduceCallback } from '../../types'; import { RedBlackTree } from './red-black-tree'; /** * An ordered Map backed by a red-black tree. * * - Iteration order is ascending by key. * - No node exposure: all APIs use keys/values only. */ export class TreeMap<K = any, V = any, R = [K, V]> implements Iterable<[K, V | undefined]> { readonly #core: RedBlackTree<K, V>; readonly #isDefaultComparator: boolean; readonly #userComparator?: Comparator<K>; /** * Create a TreeMap from an iterable of `[key, value]` entries or raw elements. * * @param entries - Iterable of `[key, value]` tuples, or raw elements if `toEntryFn` is provided. * @param options - Configuration options including optional `toEntryFn` to transform raw elements. * @throws {TypeError} If any entry is not a 2-tuple-like value (when no toEntryFn), or when using * the default comparator and encountering unsupported/invalid keys (e.g. `NaN`, invalid `Date`). * @example * // Standard usage with entries * const map = new TreeMap([['a', 1], ['b', 2]]); * * // Using toEntryFn to transform raw objects * const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]; * const map = new TreeMap<number, User, User>(users, { toEntryFn: u => [u.id, u] }); */ constructor( entries: Iterable<R> | Iterable<[K, V | undefined]> = [], options: TreeMapOptions<K, V, R> = {} ) { this.#userComparator = options.comparator; const toEntryFn = options.toEntryFn; const comparator = options.comparator ?? TreeMap.createDefaultComparator<K>(); this.#isDefaultComparator = options.comparator === undefined; this.#core = new RedBlackTree<K, V>([], { comparator, isMapMode: options.isMapMode }); for (const item of entries) { let k: K; let v: V | undefined; if (toEntryFn) { // Use toEntryFn to transform raw element [k, v] = toEntryFn(item as R); } else { // Validate entries like native Map: each item must be a 2-tuple-like value. if (!Array.isArray(item) || item.length < 2) { throw new TypeError('TreeMap: each entry must be a [key, value] tuple'); } k = item[0] as K; v = item[1] as V | undefined; } this.set(k, v); } } /** * Create the strict default comparator. * * Supports: * - `number` (rejects `NaN`; treats `-0` and `0` as equal) * - `string` * - `Date` (orders by `getTime()`, rejects invalid dates) * * For other key types, a custom comparator must be provided. */ static createDefaultComparator<K>(): Comparator<K> { return (a: K, b: K): number => { if (typeof a === 'number' && typeof b === 'number') { if (Number.isNaN(a) || Number.isNaN(b)) throw new TypeError('TreeMap: NaN is not a valid key'); const aa = Object.is(a, -0) ? 0 : a; const bb = Object.is(b, -0) ? 0 : b; return aa > bb ? 1 : aa < bb ? -1 : 0; } if (typeof a === 'string' && typeof b === 'string') { return a > b ? 1 : a < b ? -1 : 0; } if (a instanceof Date && b instanceof Date) { const ta = a.getTime(); const tb = b.getTime(); if (Number.isNaN(ta) || Number.isNaN(tb)) throw new TypeError('TreeMap: invalid Date key'); return ta > tb ? 1 : ta < tb ? -1 : 0; } throw new TypeError('TreeMap: comparator is required for non-number/non-string/non-Date keys'); }; } private _validateKey(key: K): void { if (!this.#isDefaultComparator) return; if (typeof key === 'number') { if (Number.isNaN(key)) throw new TypeError('TreeMap: NaN is not a valid key'); return; } if (typeof key === 'string') return; if (key instanceof Date) { if (Number.isNaN(key.getTime())) throw new TypeError('TreeMap: invalid Date key'); return; } throw new TypeError('TreeMap: comparator is required for non-number/non-string/non-Date keys'); } /** * Number of entries in the map. */ get size(): number { return this.#core.size; } /** * Whether the map is empty. */ isEmpty(): boolean { return this.size === 0; } /** * Set or overwrite a value for a key. * @remarks Expected time O(log n) */ set(key: K, value: V | undefined): this { this._validateKey(key); this.#core.set(key, value as V); return this; } /** * Get the value under a key. * @remarks Expected time O(log n) */ get(key: K): V | undefined { this._validateKey(key); return this.#core.get(key); } /** * Test whether a key exists. * @remarks Expected time O(log n) */ has(key: K): boolean { this._validateKey(key); return this.#core.has(key); } /** * Delete a key. * @returns `true` if the key existed; otherwise `false`. * @remarks Expected time O(log n) */ delete(key: K): boolean { this._validateKey(key); const res = this.#core.delete(key); return Array.isArray(res) && res.length > 0 && !!res[0]?.deleted; } /** * Remove all entries. */ clear(): void { this.#core.clear(); } /** * Iterate over keys in ascending order. */ keys(): IterableIterator<K> { return this.#core.keys(); } private _entryFromKey(k: K): [K, V | undefined] { // Keys come from `keys()` which only yields existing keys. // We allow `undefined` as a stored value (native Map behavior), so entries are typed as `[K, V | undefined]`. return [k, this.#core.get(k)]; } /** * Iterate over values in ascending key order. * * Note: values may be `undefined` (TreeMap allows storing `undefined`, like native `Map`). */ *values(): IterableIterator<V | undefined> { for (const k of this.keys()) yield this._entryFromKey(k)[1]; } /** * Iterate over `[key, value]` entries in ascending key order. * * Note: values may be `undefined`. */ *entries(): IterableIterator<[K, V | undefined]> { for (const k of this.keys()) yield this._entryFromKey(k); } [Symbol.iterator](): IterableIterator<[K, V | undefined]> { return this.entries(); } /** * Visit each entry in ascending key order. * * Note: callback value may be `undefined`. */ forEach(cb: (value: V | undefined, key: K, map: TreeMap<K, V>) => void, thisArg?: any): void { for (const [k, v] of this) cb.call(thisArg, v, k, this); } /** * Create a new TreeMap by mapping each entry to a new `[key, value]` entry. * * This mirrors `RedBlackTree.map`: mapping produces a new ordered container. * @remarks Time O(n log n) expected, Space O(n) */ map<MK, MV>( callbackfn: TreeMapEntryCallback<K, V, [MK, MV], TreeMap<K, V>>, options: Omit<TreeMapOptions<MK, MV>, 'toEntryFn'> & { comparator?: (a: MK, b: MK) => number } = {}, thisArg?: unknown ): TreeMap<MK, MV> { const out = new TreeMap<MK, MV>([], options as TreeMapOptions<MK, MV>); let index = 0; for (const [k, v] of this) { const [mk, mv] = thisArg === undefined ? callbackfn(v, k, index++, this) : (callbackfn as (this: unknown, v: V | undefined, k: K, i: number, self: TreeMap<K, V>) => [MK, MV]).call(thisArg, v, k, index++, this); out.set(mk, mv); } return out; } /** * Create a new TreeMap containing only entries that satisfy the predicate. * @remarks Time O(n log n) expected, Space O(n) */ filter(callbackfn: TreeMapEntryCallback<K, V, boolean, TreeMap<K, V>>, thisArg?: unknown): TreeMap<K, V> { const out = new TreeMap<K, V>([], { comparator: this.#userComparator }); let index = 0; for (const [k, v] of this) { const ok = thisArg === undefined ? callbackfn(v, k, index++, this) : (callbackfn as (this: unknown, v: V | undefined, k: K, i: number, self: TreeMap<K, V>) => boolean).call(thisArg, v, k, index++, this); if (ok) out.set(k, v); } return out; } /** * Reduce entries into a single accumulator. * @remarks Time O(n), Space O(1) */ reduce<A>(callbackfn: TreeMapReduceCallback<K, V, A, TreeMap<K, V>>, initialValue: A): A { let acc = initialValue; let index = 0; for (const [k, v] of this) acc = callbackfn(acc, v, k, index++, this); return acc; } /** * Test whether all entries satisfy a predicate. * @remarks Time O(n), Space O(1) */ every(callbackfn: TreeMapEntryCallback<K, V, boolean, TreeMap<K, V>>, thisArg?: unknown): boolean { let index = 0; for (const [k, v] of this) { const ok = thisArg === undefined ? callbackfn(v, k, index++, this) : (callbackfn as (this: unknown, v: V | undefined, k: K, i: number, self: TreeMap<K, V>) => boolean).call(thisArg, v, k, index++, this); if (!ok) return false; } return true; } /** * Test whether any entry satisfies a predicate. * @remarks Time O(n), Space O(1) */ some(callbackfn: TreeMapEntryCallback<K, V, boolean, TreeMap<K, V>>, thisArg?: unknown): boolean { let index = 0; for (const [k, v] of this) { const ok = thisArg === undefined ? callbackfn(v, k, index++, this) : (callbackfn as (this: unknown, v: V | undefined, k: K, i: number, self: TreeMap<K, V>) => boolean).call(thisArg, v, k, index++, this); if (ok) return true; } return false; } /** * Find the first entry that satisfies a predicate. * @returns The first matching `[key, value]` tuple, or `undefined`. * @remarks Time O(n), Space O(1) */ find(callbackfn: TreeMapEntryCallback<K, V, boolean, TreeMap<K, V>>, thisArg?: unknown): [K, V | undefined] | undefined { let index = 0; for (const [k, v] of this) { const ok = thisArg === undefined ? callbackfn(v, k, index++, this) : (callbackfn as (this: unknown, v: V | undefined, k: K, i: number, self: TreeMap<K, V>) => boolean).call(thisArg, v, k, index++, this); if (ok) return [k, v]; } return undefined; } /** * Materialize the map into an array of `[key, value]` tuples. * @remarks Time O(n), Space O(n) */ toArray(): Array<[K, V | undefined]> { return [...this]; } /** * Print a human-friendly representation. * @remarks Time O(n), Space O(n) */ print(): void { // Delegate to the underlying tree's visualization. this.#core.print(); } // Navigable operations (return entry tuples) // Note: returned tuple values may be `undefined`. /** * Smallest entry by key. */ first(): [K, V | undefined] | undefined { const k = this.#core.getLeftMost(); return k === undefined ? undefined : this._entryFromKey(k); } /** * Largest entry by key. */ last(): [K, V | undefined] | undefined { const k = this.#core.getRightMost(); return k === undefined ? undefined : this._entryFromKey(k); } /** * Remove and return the smallest entry. */ pollFirst(): [K, V | undefined] | undefined { const entry = this.first(); if (!entry) return undefined; this.delete(entry[0]); return entry; } /** * Remove and return the largest entry. */ pollLast(): [K, V | undefined] | undefined { const entry = this.last(); if (!entry) return undefined; this.delete(entry[0]); return entry; } /** * Smallest entry whose key is >= the given key. */ ceiling(key: K): [K, V | undefined] | undefined { this._validateKey(key); const k = this.#core.ceiling(key); return k === undefined ? undefined : this._entryFromKey(k); } /** * Largest entry whose key is <= the given key. */ floor(key: K): [K, V | undefined] | undefined { this._validateKey(key); const k = this.#core.floor(key); return k === undefined ? undefined : this._entryFromKey(k); } /** * Smallest entry whose key is > the given key. */ higher(key: K): [K, V | undefined] | undefined { this._validateKey(key); const k = this.#core.higher(key); return k === undefined ? undefined : this._entryFromKey(k); } /** * Largest entry whose key is < the given key. */ lower(key: K): [K, V | undefined] | undefined { this._validateKey(key); const k = this.#core.lower(key); return k === undefined ? undefined : this._entryFromKey(k); } /** * Return all entries in a given key range. * * @param range `[low, high]` * @param options Inclusive/exclusive bounds (defaults to inclusive). */ rangeSearch(range: [K, K], options: TreeMapRangeOptions = {}): Array<[K, V | undefined]> { const { lowInclusive = true, highInclusive = true } = options; const [low, high] = range; this._validateKey(low); this._validateKey(high); const keys = this.#core.rangeSearch([low, high]) as (K | undefined)[]; const out: Array<[K, V | undefined]> = []; const cmp = this.#core.comparator; for (const k of keys) { if (k === undefined) continue; if (!lowInclusive && cmp(k, low) === 0) continue; if (!highInclusive && cmp(k, high) === 0) continue; out.push(this._entryFromKey(k)); } return out; } /** * Creates a shallow clone of this map. * @remarks Time O(n log n), Space O(n) * @example * const original = new TreeMap([['a', 1], ['b', 2]]); * const copy = original.clone(); * copy.set('c', 3); * original.has('c'); // false (original unchanged) */ clone(): TreeMap<K, V> { return new TreeMap<K, V>(this, { comparator: this.#isDefaultComparator ? undefined : this.#userComparator, isMapMode: this.#core.isMapMode }); } }