deque-typed
Version:
424 lines (375 loc) • 12.1 kB
text/typescript
/**
* TreeSet (ordered set) — a restricted, native-like API backed by RedBlackTree.
*
* Design goals:
* - No node exposure (no node inputs/outputs)
* - Native Set-like surface + Java NavigableSet-like helpers
* - Strict default comparator (number/string/Date), otherwise require comparator
*/
import type { Comparator } from '../../types';
import type { TreeSetElementCallback, TreeSetOptions, TreeSetRangeOptions, TreeSetReduceCallback } from '../../types';
import { RedBlackTree } from './red-black-tree';
/**
* An ordered Set backed by a red-black tree.
*
* - Iteration order is ascending by key.
* - No node exposure: all APIs use keys only.
*/
export class TreeSet<K = any, R = K> implements Iterable<K> {
readonly #core: RedBlackTree<K, undefined>;
readonly #isDefaultComparator: boolean;
readonly #userComparator?: Comparator<K>;
/**
* Create a TreeSet from an iterable of keys or raw elements.
*
* @param elements - Iterable of keys, or raw elements if `toElementFn` is provided.
* @param options - Configuration options including optional `toElementFn` to transform raw elements.
* @throws {TypeError} When using the default comparator and encountering unsupported key types,
* or invalid keys (e.g. `NaN`, invalid `Date`).
* @example
* // Standard usage with keys
* const set = new TreeSet([3, 1, 2]);
*
* // Using toElementFn to transform raw objects
* const users = [{ id: 3, name: 'Alice' }, { id: 1, name: 'Bob' }];
* const set = new TreeSet<number, User>(users, { toElementFn: u => u.id });
*/
constructor(elements: Iterable<R> | Iterable<K> = [], options: TreeSetOptions<K, R> = {}) {
this.#userComparator = options.comparator;
const toElementFn = options.toElementFn;
const comparator = options.comparator ?? TreeSet.createDefaultComparator<K>();
this.#isDefaultComparator = options.comparator === undefined;
// RedBlackTree expects an iterable of keys/entries/nodes/raws; for TreeSet we only accept keys.
this.#core = new RedBlackTree<K, undefined>([], { comparator, isMapMode: options.isMapMode });
for (const item of elements) {
const k = toElementFn ? toElementFn(item as R) : item as K;
this.add(k);
}
}
/**
* 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 => {
// numbers
if (typeof a === 'number' && typeof b === 'number') {
if (Number.isNaN(a) || Number.isNaN(b)) throw new TypeError('TreeSet: NaN is not a valid key');
// treat -0 and 0 as equal
const aa = Object.is(a, -0) ? 0 : a;
const bb = Object.is(b, -0) ? 0 : b;
return aa > bb ? 1 : aa < bb ? -1 : 0;
}
// strings
if (typeof a === 'string' && typeof b === 'string') {
return a > b ? 1 : a < b ? -1 : 0;
}
// Date
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('TreeSet: invalid Date key');
return ta > tb ? 1 : ta < tb ? -1 : 0;
}
throw new TypeError('TreeSet: comparator is required for non-number/non-string/non-Date keys');
};
}
/**
* Number of elements in the set.
*/
get size(): number {
return this.#core.size;
}
/**
* Whether the set is empty.
*/
isEmpty(): boolean {
return this.size === 0;
}
private _validateKey(key: K): void {
if (!this.#isDefaultComparator) return;
if (typeof key === 'number') {
if (Number.isNaN(key)) throw new TypeError('TreeSet: NaN is not a valid key');
return;
}
if (typeof key === 'string') return;
if (key instanceof Date) {
if (Number.isNaN(key.getTime())) throw new TypeError('TreeSet: invalid Date key');
return;
}
// Other key types should have provided a comparator, so reaching here means misuse.
throw new TypeError('TreeSet: comparator is required for non-number/non-string/non-Date keys');
}
/**
* Add a key to the set (no-op if already present).
* @remarks Expected time O(log n)
*/
add(key: K): this {
this._validateKey(key);
// RBT.set returns boolean; Set.add returns this.
this.#core.set(key, undefined);
return this;
}
/**
* 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 keys.
*/
clear(): void {
this.#core.clear();
}
/**
* Iterate over keys in ascending order.
*/
keys(): IterableIterator<K> {
return this.#core.keys();
}
/**
* Iterate over values in ascending order.
*
* Note: for Set-like containers, `values()` is the same as `keys()`.
*/
values(): IterableIterator<K> {
return this.keys();
}
/**
* Iterate over `[value, value]` pairs (native Set convention).
*
* Note: TreeSet stores only keys internally; `[k, k]` is created on-the-fly during iteration.
*/
*entries(): IterableIterator<[K, K]> {
for (const k of this.keys()) yield [k, k];
}
[Symbol.iterator](): IterableIterator<K> {
return this.keys();
}
/**
* Visit each value in ascending order.
*
* Callback follows native Set convention: `(value, value2, set)`.
*/
forEach(cb: (value: K, value2: K, set: TreeSet<K>) => void, thisArg?: any): void {
for (const k of this) cb.call(thisArg, k, k, this);
}
/**
* Create a new TreeSet by mapping each value to a new key.
*
* This mirrors `RedBlackTree.map`: mapping produces a new ordered container.
* @remarks Time O(n log n) expected, Space O(n)
*/
map<MK>(
callbackfn: TreeSetElementCallback<K, MK, TreeSet<K>>,
options: Omit<TreeSetOptions<MK>, 'toElementFn'> & { comparator?: (a: MK, b: MK) => number } = {},
thisArg?: unknown
): TreeSet<MK> {
const out = new TreeSet<MK>([], options as TreeSetOptions<MK>);
let index = 0;
for (const v of this) {
const mk = thisArg === undefined
? callbackfn(v, index++, this)
: (callbackfn as (this: unknown, v: K, i: number, self: TreeSet<K>) => MK).call(thisArg, v, index++, this);
out.add(mk);
}
return out;
}
/**
* Create a new TreeSet containing only values that satisfy the predicate.
* @remarks Time O(n log n) expected, Space O(n)
*/
filter(callbackfn: TreeSetElementCallback<K, boolean, TreeSet<K>>, thisArg?: unknown): TreeSet<K> {
const out = new TreeSet<K>([], { comparator: this.#userComparator });
let index = 0;
for (const v of this) {
const ok = thisArg === undefined
? callbackfn(v, index++, this)
: (callbackfn as (this: unknown, v: K, i: number, self: TreeSet<K>) => boolean).call(thisArg, v, index++, this);
if (ok) out.add(v);
}
return out;
}
/**
* Reduce values into a single accumulator.
* @remarks Time O(n), Space O(1)
*/
reduce<A>(callbackfn: TreeSetReduceCallback<K, A, TreeSet<K>>, initialValue: A): A {
let acc = initialValue;
let index = 0;
for (const v of this) acc = callbackfn(acc, v, index++, this);
return acc;
}
/**
* Test whether all values satisfy a predicate.
* @remarks Time O(n), Space O(1)
*/
every(callbackfn: TreeSetElementCallback<K, boolean, TreeSet<K>>, thisArg?: unknown): boolean {
let index = 0;
for (const v of this) {
const ok = thisArg === undefined
? callbackfn(v, index++, this)
: (callbackfn as (this: unknown, v: K, i: number, self: TreeSet<K>) => boolean).call(thisArg, v, index++, this);
if (!ok) return false;
}
return true;
}
/**
* Test whether any value satisfies a predicate.
* @remarks Time O(n), Space O(1)
*/
some(callbackfn: TreeSetElementCallback<K, boolean, TreeSet<K>>, thisArg?: unknown): boolean {
let index = 0;
for (const v of this) {
const ok = thisArg === undefined
? callbackfn(v, index++, this)
: (callbackfn as (this: unknown, v: K, i: number, self: TreeSet<K>) => boolean).call(thisArg, v, index++, this);
if (ok) return true;
}
return false;
}
/**
* Find the first value that satisfies a predicate.
* @remarks Time O(n), Space O(1)
*/
find(callbackfn: TreeSetElementCallback<K, boolean, TreeSet<K>>, thisArg?: unknown): K | undefined {
let index = 0;
for (const v of this) {
const ok = thisArg === undefined
? callbackfn(v, index++, this)
: (callbackfn as (this: unknown, v: K, i: number, self: TreeSet<K>) => boolean).call(thisArg, v, index++, this);
if (ok) return v;
}
return undefined;
}
/**
* Materialize the set into an array of keys.
* @remarks Time O(n), Space O(n)
*/
toArray(): K[] {
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
/**
* Smallest key in the set.
*/
first(): K | undefined {
return this.#core.getLeftMost();
}
/**
* Largest key in the set.
*/
last(): K | undefined {
return this.#core.getRightMost();
}
/**
* Remove and return the smallest key.
*/
pollFirst(): K | undefined {
const k = this.first();
if (k === undefined) return undefined;
this.delete(k);
return k;
}
/**
* Remove and return the largest key.
*/
pollLast(): K | undefined {
const k = this.last();
if (k === undefined) return undefined;
this.delete(k);
return k;
}
/**
* Smallest key that is >= the given key.
*/
ceiling(key: K): K | undefined {
this._validateKey(key);
return this.#core.ceiling(key);
}
/**
* Largest key that is <= the given key.
*/
floor(key: K): K | undefined {
this._validateKey(key);
return this.#core.floor(key);
}
/**
* Smallest key that is > the given key.
*/
higher(key: K): K | undefined {
this._validateKey(key);
return this.#core.higher(key);
}
/**
* Largest key that is < the given key.
*/
lower(key: K): K | undefined {
this._validateKey(key);
return this.#core.lower(key);
}
/**
* Return all keys in a given range.
*
* @param range `[low, high]`
* @param options Inclusive/exclusive bounds (defaults to inclusive).
*/
rangeSearch(range: [K, K], options: TreeSetRangeOptions = {}): K[] {
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: K[] = [];
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(k);
}
return out;
}
/**
* Creates a shallow clone of this set.
* @remarks Time O(n log n), Space O(n)
* @example
* const original = new TreeSet([1, 2, 3]);
* const copy = original.clone();
* copy.add(4);
* original.has(4); // false (original unchanged)
*/
clone(): TreeSet<K> {
return new TreeSet<K>(this, {
comparator: this.#isDefaultComparator ? undefined : this.#userComparator,
isMapMode: this.#core.isMapMode
});
}
}