UNPKG

@rimbu/hashed

Version:

Immutable HashMap and HashSet implementations for TypeScript

262 lines 9.4 kB
import { Arr, RimbuError } from '@rimbu/base'; import { TraverseState } from '@rimbu/common'; import { List } from '@rimbu/list'; import { Stream } from '@rimbu/stream'; import { BlockBuilderBase, CollisionBuilderBase } from '@rimbu/hashed/common'; export class HashSetBlockBuilder extends BlockBuilderBase { constructor(context, source, _entries, _entrySets, size = source?.size ?? 0, level = source?.level ?? 0) { super(); this.context = context; this.source = source; this._entries = _entries; this._entrySets = _entrySets; this.size = size; this.level = level; this._lock = 0; // prettier-ignore this.has = (value) => { if (undefined !== this.source) return this.source.has(value); if (!this.context.hasher.isValid(value)) return false; return this.hasInternal(value); }; this.add = (value) => { this.checkLock(); return this.addInternal(value); }; this.addAll = (source) => { this.checkLock(); return Stream.from(source).filterPure({ pred: this.add }).count() > 0; }; // prettier-ignore this.remove = (value) => { this.checkLock(); if (!this.context.hasher.isValid(value)) return false; return this.removeInternal(value); }; // prettier-ignore this.removeAll = (values) => { this.checkLock(); return Stream.from(values).filterPure({ pred: this.remove }).count() > 0; }; this.forEach = (f, options = {}) => { const { state = TraverseState() } = options; this._lock++; super.forEach(f, { state }); this._lock--; }; this.build = () => { if (this.size === 0) return this.context.empty(); return this.buildNE(); }; } checkLock() { if (this._lock) RimbuError.throwModifiedBuilderWhileLoopingOverItError(); } prepareMutate() { 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) => { if (this.context.isHashSetBlock(entrySet)) { return new HashSetBlockBuilder(this.context, entrySet); } return new HashSetCollisionBuilder(this.context, entrySet); }); } else { this._entrySets = []; } } } get entries() { this.prepareMutate(); return this._entries; } get entrySets() { this.prepareMutate(); return this._entrySets; } hasInternal(value, hash = this.context.hash(value)) { 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; } addInternal(value, hash = this.context.hash(value)) { 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(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(); newEntries.append(currentEntry); newEntries.append(value); const newEntrySet = new HashSetCollisionBuilder(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; } removeInternal(value, hash = this.context.hash(value)) { 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 = undefined; 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; } buildNE() { 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 extends CollisionBuilderBase { constructor(context, source, _entries) { super(); this.context = context; this.source = source; this._entries = _entries; } hasInternal(value, hash) { if (undefined !== this.source) return this.source.has(value, hash); let result = false; this.entries.forEach((v, _, halt) => { if (this.context.eq(v, value)) { result = true; halt(); } }); return result; } addInternal(value) { let index = -1; this.entries.forEach((v, i, halt) => { 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) { let index = -1; this.entries.forEach((v, i, halt) => { if (this.context.eq(v, value)) { index = i; halt(); } }); if (index < 0) return false; this.source = undefined; this.entries.remove(index); return true; } buildNE() { return (this.source ?? this.context.collision(this.entries.build().assumeNonEmpty())); } } //# sourceMappingURL=builder.mjs.map