UNPKG

@rimbu/hashed

Version:

Immutable HashMap and HashSet implementations for TypeScript

408 lines 15.7 kB
import { Arr, RimbuError, Token } from '@rimbu/base'; import { OptLazy, OptLazyOr, TraverseState, Update, } from '@rimbu/common'; import { List } from '@rimbu/list'; import { Stream } from '@rimbu/stream'; import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom'; import { BlockBuilderBase, CollisionBuilderBase } from '@rimbu/hashed/common'; export class HashMapBlockBuilder 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; this.get = (key, otherwise, hash) => { if (undefined !== this.source) return this.source.get(key, otherwise); if (!this.context.hasher.isValid(key)) return OptLazy(otherwise); const keyHash = hash ?? this.context.hash(key); const keyIndex = this.context.getKeyIndex(this.level, keyHash); if (keyIndex in this.entries) { const currentEntry = this.entries[keyIndex]; if (this.context.eq(key, currentEntry[0])) return currentEntry[1]; return OptLazy(otherwise); } if (keyIndex in this.entrySets) { const currentEntrySet = this.entrySets[keyIndex]; return currentEntrySet.get(key, otherwise, keyHash); } return OptLazy(otherwise); }; // prettier-ignore this.hasKey = (key) => { const token = Symbol(); return token !== this.get(key, token); }; this.addEntry = (entry) => { this.checkLock(); return this.addEntryInternal(entry); }; this.addEntries = (source) => { this.checkLock(); if (isEmptyStreamSourceInstance(source)) return false; return Stream.from(source).filterPure({ pred: this.addEntry }).count() > 0; }; this.set = (key, value) => { this.checkLock(); return this.addEntryInternal([key, value]); }; this.modifyAt = (key, options, keyHash = this.context.hash(key)) => { this.checkLock(); const keyIndex = this.context.getKeyIndex(this.level, keyHash); if (keyIndex in this.entries) { // potential match in entries const currentEntry = this.entries[keyIndex]; const [currentKey, currentValue] = currentEntry; if (this.context.eq(key, currentKey)) { // exact match if (undefined === options.ifExists) return false; const newValue = options.ifExists instanceof Function ? options.ifExists(currentValue, Token) : options.ifExists; if (Object.is(newValue, currentValue)) { return false; } this.source = undefined; if (Token === newValue) { this.size--; delete this.entries[keyIndex]; return true; } // replace current value const newEntry = [key, newValue]; this.entries[keyIndex] = newEntry; return true; } if (undefined === options.ifNew) return false; // no match, replace entry with entryset containing both entries const newValue = OptLazyOr(options.ifNew, Token); if (Token === newValue) return false; this.source = undefined; this.size++; delete this.entries[keyIndex]; const newEntrySet = this.level < this.context.maxDepth ? new HashMapBlockBuilder(this.context, undefined, undefined, undefined, 0, this.level + 1) : new HashMapCollisionBuilder(this.context); newEntrySet.addEntryInternal(currentEntry); newEntrySet.addEntryInternal([key, newValue], keyHash); this.entrySets[keyIndex] = newEntrySet; return true; } if (keyIndex in this.entrySets) { // potential match in entrysets const entrySet = this.entrySets[keyIndex]; const preSize = entrySet.size; const result = entrySet.modifyAt(key, options, keyHash); if (result) this.source = undefined; this.size += entrySet.size - preSize; if (entrySet.size > 1) return result; // single entry needs to be pulled up let first = undefined; if (this.context.isHashMapBlockBuilder(entrySet)) { for (const index in entrySet.entries) { first = entrySet.entries[index]; break; } } else { first = entrySet.entries.get(0, RimbuError.throwInvalidStateError); } delete this.entrySets[keyIndex]; this.entries[keyIndex] = first; return true; } if (undefined === options.ifNew) return false; // no matching entry or entrySet const newValue = OptLazyOr(options.ifNew, Token); if (Token === newValue) return false; this.source = undefined; this.size++; this.entries[keyIndex] = [key, newValue]; return true; }; // prettier-ignore this.updateAt = (key, update, otherwise) => { let result; let found = false; this.modifyAt(key, { ifExists: (value) => { result = value; found = true; return Update(value, update); }, }); if (!found) return OptLazy(otherwise); return result; }; this.removeKey = (key, otherwise) => { this.checkLock(); if (!this.context.hasher.isValid(key)) return OptLazy(otherwise); let removedValue; let found = false; this.modifyAt(key, { ifExists: (currentValue, remove) => { removedValue = currentValue; found = true; return remove; }, }); if (!found) return OptLazy(otherwise); return removedValue; }; // prettier-ignore this.removeKeys = (keys) => { this.checkLock(); if (isEmptyStreamSourceInstance(keys)) return false; const notFound = Symbol(); return (Stream.from(keys) .mapPure(this.removeKey, notFound) .countElement(notFound, { negate: true }) > 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(); }; // prettier-ignore this.buildMapValues = (f) => { if (this.size === 0) { return this.context.empty(); } if (undefined !== this.source) return this.source.mapValues(f); const entries = this.entries.length === 0 ? null : Arr.mapSparse(this.entries, (e) => [ e[0], f(e[1], e[0]), ]); const entrySets = this.entrySets.length === 0 ? null : Arr.mapSparse(this.entrySets, (entrySet) => entrySet.buildMapValues(f)); return this.context.block(entries, entrySets, this.size, this.level); }; } 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) { this._entrySets = null === this.source.entrySets ? [] : (Arr.mapSparse(this.source.entrySets, (entrySet) => { if (this.context.isHashMapBlock(entrySet)) { return new HashMapBlockBuilder(this.context, entrySet); } return new HashMapCollisionBuilder(this.context, entrySet); }) ?? []); } else { this._entrySets = []; } } } get entries() { this.prepareMutate(); return this._entries; } get entrySets() { this.prepareMutate(); return this._entrySets; } addEntryInternal(entry, hash = this.context.hash(entry[0])) { const keyIndex = this.context.getKeyIndex(this.level, hash); if (keyIndex in this.entries) { const currentEntry = this.entries[keyIndex]; if (this.context.eq(entry[0], currentEntry[0])) { if (Object.is(entry[1], currentEntry[1])) return false; this.source = undefined; this.entries[keyIndex] = entry; return true; } this.source = undefined; this.size++; delete this.entries[keyIndex]; if (this.level < this.context.maxDepth) { const newEntrySet = new HashMapBlockBuilder(this.context, undefined, undefined, undefined, 0, this.level + 1); newEntrySet.addEntryInternal(currentEntry); newEntrySet.addEntryInternal(entry, hash); this.entrySets[keyIndex] = newEntrySet; return true; } const newEntries = List.builder(); newEntries.append(currentEntry); newEntries.append(entry); const newEntrySet = new HashMapCollisionBuilder(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.addEntryInternal(entry, hash); if (changed) this.source = undefined; this.size += currentEntrySet.size - preSize; return changed; } this.source = undefined; this.size++; this.entries[keyIndex] = entry; return true; } 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 HashMapCollisionBuilder extends CollisionBuilderBase { constructor(context, source, _entries) { super(); this.context = context; this.source = source; this._entries = _entries; } get(key, otherwise, hash) { if (!this.context.hasher.isValid(key)) return OptLazy(otherwise); if (undefined !== this.source) return this.source.get(key, otherwise); const token = Symbol(); let result = token; this.entries.forEach((e, _, halt) => { if (this.context.eq(key, e[0])) { result = e[1]; halt(); } }); if (token === result) return OptLazy(otherwise); return result; } addEntryInternal(entry) { let index = -1; this.entries.forEach((e, i, halt) => { if (this.context.eq(e[0], entry[0])) { index = i; halt(); } }); if (index < 0) { this.source = undefined; this.entries.append(entry); return true; } const oldEntry = this.entries.updateAt(index, (currentEntry) => { if (Object.is(currentEntry[1], entry[1])) return currentEntry; return entry; }); const changed = undefined === oldEntry || !Object.is(oldEntry[1], entry[1]) || !Object.is(oldEntry[0], entry[0]); if (changed) { this.source = undefined; } return changed; } set(key, value) { return this.addEntryInternal([key, value]); } modifyAt(atKey, options) { let index = -1; let foundEntry = undefined; this.entries.forEach((e, i, halt) => { if (this.context.eq(e[0], atKey)) { index = i; foundEntry = e; halt(); } }); if (undefined === foundEntry) { if (undefined === options.ifNew) return false; const newValue = OptLazyOr(options.ifNew, Token); if (Token === newValue) return false; this.source = undefined; this.entries.append([atKey, newValue]); return true; } if (undefined === options.ifExists) return false; const newValue = options.ifExists instanceof Function ? options.ifExists(foundEntry[1], Token) : options.ifExists; if (Object.is(newValue, foundEntry[1])) return false; if (Token === newValue) { this.source = undefined; this.entries.remove(index); return true; } const result = this.entries.set(index, [atKey, newValue]); const changed = undefined !== result; if (changed) this.source = undefined; return changed; } buildNE() { if (undefined !== this.source) return this.source; return this.context.collision(this.entries.build().assumeNonEmpty()); } buildMapValues(f) { if (undefined !== this.source) return this.source.mapValues(f); return this.context.collision(this.entries .buildMap((entry) => [ entry[0], f(entry[1], entry[0]), ]) .assumeNonEmpty()); } } //# sourceMappingURL=builder.mjs.map