UNPKG

@rimbu/hashed

Version:

Immutable HashMap and HashSet implementations for TypeScript

366 lines (279 loc) 8.9 kB
import { Arr, RimbuError } from '@rimbu/base'; import { type RelatedTo, TraverseState } from '@rimbu/common'; import { List } from '@rimbu/list'; import { Stream, type StreamSource } from '@rimbu/stream'; import type { HashSet } from '@rimbu/hashed/set'; import type { HashSetBlock, HashSetCollision, HashSetContext, } from '@rimbu/hashed/set-custom'; import { BlockBuilderBase, CollisionBuilderBase } from '@rimbu/hashed/common'; export type SetBlockBuilderEntry<T> = | HashSetBlockBuilder<T> | HashSetCollisionBuilder<T>; export class HashSetBlockBuilder<T> extends BlockBuilderBase<T> implements HashSet.Builder<T> { constructor( readonly context: HashSetContext<T>, public source?: undefined | HashSetBlock<T>, public _entries?: undefined | T[], public _entrySets?: undefined | SetBlockBuilderEntry<T>[], public size = source?.size ?? 0, public level = source?.level ?? 0 ) { super(); } _lock = 0; checkLock(): void { if (this._lock) RimbuError.throwModifiedBuilderWhileLoopingOverItError(); } prepareMutate(): void { if (undefined === this._entries) { if (undefined !== this.source) { this._entries = null === this.source.entries ? [] : Arr.copySparse(this.source.entries); } else { this._entries = []; } } if (undefined === this._entrySets) { if (undefined !== this.source && null !== this.source.entrySets) { this._entrySets = Arr.mapSparse( this.source.entrySets, (entrySet): SetBlockBuilderEntry<T> => { if (this.context.isHashSetBlock(entrySet)) { return new HashSetBlockBuilder(this.context, entrySet); } return new HashSetCollisionBuilder(this.context, entrySet); } ); } else { this._entrySets = []; } } } get entries(): T[] { this.prepareMutate(); return this._entries!; } get entrySets(): SetBlockBuilderEntry<T>[] { this.prepareMutate(); return this._entrySets!; } // prettier-ignore has = <U,>(value: RelatedTo<T, U>): boolean => { if (undefined !== this.source) return this.source.has(value); if (!this.context.hasher.isValid(value)) return false; return this.hasInternal(value); }; hasInternal(value: T, hash = this.context.hash(value)): boolean { if (undefined !== this.source) return this.source.has(value, hash); const keyIndex = this.context.getKeyIndex(this.level, hash); if (keyIndex in this.entries) { return this.context.eq(value, this.entries[keyIndex]); } if (keyIndex in this.entrySets) { const currentEntrySet = this.entrySets[keyIndex]; return currentEntrySet.hasInternal(value, hash); } return false; } add = (value: T): boolean => { this.checkLock(); return this.addInternal(value); }; addAll = (source: StreamSource<T>): boolean => { this.checkLock(); return Stream.from(source).filterPure({ pred: this.add }).count() > 0; }; addInternal(value: T, hash = this.context.hash(value)): boolean { const keyIndex = this.context.getKeyIndex(this.level, hash); if (keyIndex in this.entries) { const currentEntry = this.entries[keyIndex]; if (this.context.eq(value, currentEntry)) return false; this.source = undefined; this.size++; delete this.entries[keyIndex]; if (this.level < this.context.maxDepth) { const newEntrySet = new HashSetBlockBuilder<T>( this.context, undefined, undefined, undefined, 0, this.level + 1 ); newEntrySet.addInternal(currentEntry); newEntrySet.addInternal(value, hash); this.entrySets[keyIndex] = newEntrySet; return true; } const newEntries = List.builder<T>(); newEntries.append(currentEntry); newEntries.append(value); const newEntrySet = new HashSetCollisionBuilder<T>( this.context, undefined, newEntries ); this.entrySets[keyIndex] = newEntrySet; return true; } if (keyIndex in this.entrySets) { const currentEntrySet = this.entrySets[keyIndex]; const preSize = currentEntrySet.size; const changed = currentEntrySet.addInternal(value, hash); if (changed) this.source = undefined; this.size += currentEntrySet.size - preSize; return changed; } this.source = undefined; this.size++; this.entries[keyIndex] = value; return true; } // prettier-ignore remove = <ST,>(value: ST): boolean => { this.checkLock(); if (!this.context.hasher.isValid(value)) return false; return this.removeInternal(value); }; // prettier-ignore removeAll = <ST,>(values: StreamSource<ST>): boolean => { this.checkLock(); return Stream.from(values).filterPure({ pred: this.remove }).count() > 0; }; removeInternal(value: T, hash = this.context.hash(value)): boolean { const index = this.context.getKeyIndex(this.level, hash); if (index in this.entries) { // potential match in entries const currentValue = this.entries[index]; if (!this.context.eq(value, currentValue)) return false; // exact match this.source = undefined; this.size--; delete this.entries[index]; return true; } if (index in this.entrySets) { // potential match in entrysets const entrySet = this.entrySets[index]; const preSize = entrySet.size; if (!entrySet.removeInternal(value, hash)) { return false; } this.source = undefined; this.size += entrySet.size - preSize; if (entrySet.size > 1) return true; // single entry needs to be pulled up let first: T = undefined as any; if (this.context.isHashSetBlockBuilder(entrySet)) { for (const i in entrySet.entries) { first = entrySet.entries[i]; break; } } else { first = entrySet.entries.get(0, RimbuError.throwInvalidStateError); } delete this.entrySets[index]; // if the sparse emptySets array is empty, set it to an empty array let hasEntrySets = false; for (const _ in this.entrySets) { hasEntrySets = true; break; } if (!hasEntrySets) { this.entrySets.length = 0; } this.entries[index] = first; return true; } return false; } forEach = ( f: (value: T, index: number, halt: () => void) => void, options: { state?: TraverseState } = {} ): void => { const { state = TraverseState() } = options; this._lock++; super.forEach(f, { state }); this._lock--; }; build = (): HashSet<T> => { if (this.size === 0) return this.context.empty(); return this.buildNE(); }; buildNE(): HashSetBlock<T> { if (undefined !== this.source) return this.source; const entries = this.entries.length === 0 ? null : Arr.copySparse(this.entries); const entrySets = this.entrySets.length === 0 ? null : Arr.mapSparse(this.entrySets, (entrySet) => entrySet.buildNE()); return this.context.block(entries, entrySets, this.size, this.level); } } export class HashSetCollisionBuilder<T> extends CollisionBuilderBase<T> { constructor( readonly context: HashSetContext<T>, public source?: undefined | HashSetCollision<T>, public _entries?: undefined | List.Builder<T> ) { super(); } hasInternal(value: T, hash?: number): boolean { if (undefined !== this.source) return this.source.has(value, hash); let result = false; this.entries.forEach((v, _, halt): void => { if (this.context.eq(v, value)) { result = true; halt(); } }); return result; } addInternal(value: T): boolean { let index = -1; this.entries.forEach((v, i, halt): void => { if (this.context.eq(v, value)) { index = i; halt(); } }); if (index < 0) { this.source = undefined; this.entries.append(value); return true; } const token = Symbol(); const oldValue = this.entries.set(index, value, token); const changed = token === oldValue || !this.context.eq(oldValue, value); if (changed) this.source = undefined; return changed; } removeInternal(value: T): boolean { let index = -1; this.entries.forEach((v, i, halt): void => { if (this.context.eq(v, value)) { index = i; halt(); } }); if (index < 0) return false; this.source = undefined; this.entries.remove(index); return true; } buildNE(): HashSetCollision<T> { return ( this.source ?? this.context.collision(this.entries.build().assumeNonEmpty()) ); } }