UNPKG

@rimbu/hashed

Version:

Immutable HashMap and HashSet implementations for TypeScript

471 lines 17.6 kB
import { Arr, Entry, RimbuError, Token } from '@rimbu/base'; import { EmptyBase, NonEmptyBase } from '@rimbu/collection-types/map-custom'; import { OptLazy, OptLazyOr, TraverseState, Update, } from '@rimbu/common'; import { Stream } from '@rimbu/stream'; import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom'; export class HashMapEmpty extends EmptyBase { constructor(context) { super(); this.context = context; } streamKeys() { return Stream.empty(); } streamValues() { return Stream.empty(); } get(key, otherwise) { return OptLazy(otherwise); } hasKey() { return false; } set(key, value) { return this.context.emptyBlock().set(key, value); } addEntry(entry) { return this.context.emptyBlock().addEntry(entry); } addEntries(entries) { return this.context.from(entries); } removeKeyAndGet() { return undefined; } removeKey() { return this; } removeKeys() { return this; } modifyAt(atKey, options) { if (undefined !== options.ifNew) { const value = OptLazyOr(options.ifNew, Token); if (Token === value) return this; return this.set(atKey, value); } return this; } mapValues() { return this; } updateAt() { return this; } toBuilder() { return this.context.builder(); } toString() { return `HashMap()`; } toJSON() { return { dataType: this.context.typeTag, value: [], }; } } export class HashMapNonEmptyBase extends NonEmptyBase { asNormal() { return this; } streamKeys() { return this.stream().map(Entry.first); } streamValues() { return this.stream().map(Entry.second); } hasKey(key) { const token = Symbol(); return token !== this.get(key, token); } set(key, value) { return this.addEntry([key, value]); } addEntries(entries) { if (isEmptyStreamSourceInstance(entries)) return this; const builder = this.toBuilder(); builder.addEntries(entries); return builder.build().assumeNonEmpty(); } removeKeys(keys) { if (isEmptyStreamSourceInstance(keys)) return this; const builder = this.toBuilder(); builder.removeKeys(keys); return builder.build(); } updateAt(key, update) { if (!this.context.isValidKey(key)) return this; return this.modifyAt(key, { ifExists: (value) => Update(value, update), }); } removeKey(key) { if (!this.context.hasher.isValid(key)) return this; return this.modifyAt(key, { ifExists: (_, remove) => remove, }); } removeKeyAndGet(key) { if (!this.context.hasher.isValid(key)) return undefined; const token = Symbol(); let currentValue = token; const newMap = this.modifyAt(key, { ifExists: (value, remove) => { currentValue = value; return remove; }, }); if (token === currentValue) return undefined; return [newMap, currentValue]; } filter(pred, options = {}) { const builder = this.context.builder(); builder.addEntries(this.stream().filter(pred, options)); if (builder.size === this.size) return this; return builder.build(); } toBuilder() { return this.context.createBuilder(this); } toString() { return this.stream().join({ start: 'HashMap(', sep: ', ', end: ')', valueToString: (entry) => `${entry[0]} -> ${entry[1]}`, }); } toJSON() { return { dataType: this.context.typeTag, value: this.toArray(), }; } } export class HashMapBlock extends HashMapNonEmptyBase { constructor(context, entries, entrySets, size, level) { super(); this.context = context; this.entries = entries; this.entrySets = entrySets; this.size = size; this.level = level; } copy(entries = this.entries, entrySets = this.entrySets, size = this.size) { if (entries === this.entries && entrySets === this.entrySets && size === this.size) { return this; } return new HashMapBlock(this.context, entries, entrySets, size, this.level); } stream() { if (null !== this.entries) { if (null === this.entrySets) return Stream.fromObjectValues(this.entries); return Stream.fromObjectValues(this.entries).concat(Stream.fromObjectValues(this.entrySets).flatMap((entrySet) => entrySet.stream())); } if (null === this.entrySets) RimbuError.throwInvalidStateError(); return Stream.fromObjectValues(this.entrySets).flatMap((entrySet) => entrySet.stream()); } get(key, otherwise, hash) { if (!this.context.hasher.isValid(key)) return OptLazy(otherwise); const keyHash = hash ?? this.context.hash(key); const atKeyIndex = this.context.getKeyIndex(this.level, keyHash); if (null !== this.entries && atKeyIndex in this.entries) { const entry = this.entries[atKeyIndex]; if (this.context.eq(entry[0], key)) return entry[1]; return OptLazy(otherwise); } if (null !== this.entrySets && atKeyIndex in this.entrySets) { const entrySet = this.entrySets[atKeyIndex]; return entrySet.get(key, otherwise, keyHash); } return OptLazy(otherwise); } addEntry(entry, hash = this.context.hash(entry[0])) { const atKeyIndex = this.context.getKeyIndex(this.level, hash); if (null !== this.entries && atKeyIndex in this.entries) { const currentEntry = this.entries[atKeyIndex]; if (this.context.eq(entry[0], currentEntry[0])) { if (Object.is(entry[1], currentEntry[1])) return this; const newEntries = Arr.copySparse(this.entries); newEntries[atKeyIndex] = entry; return this.copy(newEntries); } let newEntries = Arr.copySparse(this.entries); delete newEntries[atKeyIndex]; let isEmpty = true; for (const _ in newEntries) { isEmpty = false; break; } if (isEmpty) newEntries = null; if (this.level < this.context.maxDepth) { const newEntrySet = this.context .block(null, null, 0, this.level + 1) .addEntry(currentEntry) .addEntry(entry, hash); const newEntrySets = null === this.entrySets ? [] : Arr.copySparse(this.entrySets); newEntrySets[atKeyIndex] = newEntrySet; return this.copy(newEntries, newEntrySets, this.size + 1); } const newEntrySet = this.context.collision(this.context.listContext.of(currentEntry, entry)); const newEntrySets = null === this.entrySets ? [] : Arr.copySparse(this.entrySets); newEntrySets[atKeyIndex] = newEntrySet; return this.copy(newEntries, newEntrySets, this.size + 1); } if (null !== this.entrySets && atKeyIndex in this.entrySets) { const currentEntrySet = this.entrySets[atKeyIndex]; const newEntrySet = currentEntrySet.addEntry(entry, hash); if (newEntrySet === currentEntrySet) return this; const newEntrySets = Arr.copySparse(this.entrySets); newEntrySets[atKeyIndex] = newEntrySet; return this.copy(undefined, newEntrySets, this.size + newEntrySet.size - currentEntrySet.size); } const newEntries = null === this.entries ? [] : Arr.copySparse(this.entries); newEntries[atKeyIndex] = entry; return this.copy(newEntries, undefined, this.size + 1); } modifyAt(atKey, options, atKeyHash = this.context.hash(atKey)) { const atKeyIndex = this.context.getKeyIndex(this.level, atKeyHash); if (null !== this.entries && atKeyIndex in this.entries) { const currentEntry = this.entries[atKeyIndex]; if (this.context.eq(atKey, currentEntry[0])) { // exact key match if (undefined === options.ifExists) return this; const currentValue = currentEntry[1]; const newValue = options.ifExists instanceof Function ? options.ifExists(currentValue, Token) : options.ifExists; if (Object.is(newValue, currentValue)) return this; const newEntries = Arr.copySparse(this.entries); if (Token === newValue) { delete newEntries[atKeyIndex]; for (const _ in newEntries) { return this.copy(newEntries, undefined, this.size - 1); } if (this.size === 1) return this.context.empty(); return this.copy(null, undefined, this.size - 1); } newEntries[atKeyIndex] = [atKey, newValue]; return this.copy(newEntries); } // no exact match, but key collision if (undefined === options.ifNew) return this; const newValue = OptLazyOr(options.ifNew, Token); if (Token === newValue) return this; let newEntries = Arr.copySparse(this.entries); delete newEntries[atKeyIndex]; let isEmpty = true; for (const _ in newEntries) { isEmpty = false; break; } if (isEmpty) newEntries = null; if (this.level < this.context.maxDepth) { // create next level block const newEntrySet = this.context .block(null, null, 0, this.level + 1) .addEntry(currentEntry) .set(atKey, newValue); const newEntrySets = null === this.entrySets ? [] : Arr.copySparse(this.entrySets); newEntrySets[atKeyIndex] = newEntrySet; return this.copy(newEntries, newEntrySets, this.size + 1); } // create collision const newEntry = [atKey, newValue]; const newEntrySet = this.context.collision(this.context.listContext.of(currentEntry, newEntry)); const newEntrySets = null === this.entrySets ? [] : Arr.copySparse(this.entrySets); newEntrySets[atKeyIndex] = newEntrySet; return this.copy(newEntries, newEntrySets, this.size + 1); } if (null !== this.entrySets && atKeyIndex in this.entrySets) { // key is in entrySet const currentEntrySet = this.entrySets[atKeyIndex]; const newEntrySet = currentEntrySet.modifyAt(atKey, options, atKeyHash); if (newEntrySet === currentEntrySet) return this; if (newEntrySet.size === 1) { let firstEntry = undefined; if (this.context.isHashMapBlock(newEntrySet)) { for (const key in newEntrySet.entries) { firstEntry = newEntrySet.entries[key]; break; } } else { firstEntry = newEntrySet.entries.first(); } const newEntries = null === this.entries ? [] : Arr.copySparse(this.entries); newEntries[atKeyIndex] = firstEntry; const newEntrySets = Arr.copySparse(this.entrySets); delete newEntrySets[atKeyIndex]; return this.copy(newEntries, newEntrySets, this.size - 1); } const newEntrySets = Arr.copySparse(this.entrySets); newEntrySets[atKeyIndex] = newEntrySet; return this.copy(undefined, newEntrySets, this.size + newEntrySet.size - currentEntrySet.size); } if (undefined === options.ifNew) return this; const newValue = OptLazyOr(options.ifNew, Token); if (Token === newValue) return this; const newEntry = [atKey, newValue]; const newEntries = null === this.entries ? [] : Arr.copySparse(this.entries); newEntries[atKeyIndex] = newEntry; return this.copy(newEntries, undefined, this.size + 1); } forEach(f, options = {}) { const { state = TraverseState() } = options; if (state.halted) return; const { halt } = state; if (null !== this.entries) { for (const key in this.entries) { f(this.entries[key], state.nextIndex(), halt); if (state.halted) return; } } if (null !== this.entrySets) { for (const key in this.entrySets) { this.entrySets[key].forEach(f, { state }); if (state.halted) return; } } } mapValues(mapFun) { const newEntries = null === this.entries ? null : Arr.mapSparse(this.entries, (e) => [ e[0], mapFun(e[1], e[0]), ]); const newEntrySets = null === this.entrySets ? null : Arr.mapSparse(this.entrySets, (es) => es.mapValues(mapFun)); return new HashMapBlock(this.context, newEntries, newEntrySets, this.size, this.level); } toArray() { let result = []; if (null !== this.entries) { result = Stream.fromObjectValues(this.entries).toArray(); } if (null !== this.entrySets) { Stream.fromObjectValues(this.entrySets).forEach((entrySet) => { result = result.concat(entrySet.toArray()); }); } return result; } } export class HashMapCollision extends HashMapNonEmptyBase { constructor(context, entries) { super(); this.context = context; this.entries = entries; } get size() { return this.entries.length; } copy(entries = this.entries) { if (entries === this.entries) return this; return new HashMapCollision(this.context, entries); } stream() { return this.entries.stream(); } get(key, otherwise, keyHash) { if (!this.context.hasher.isValid(key)) return OptLazy(otherwise); const token = Symbol(); const stream = this.stream(); const foundEntry = stream.find((entry) => this.context.eq(entry[0], key), { otherwise: token, }); if (token === foundEntry) return OptLazy(otherwise); return foundEntry[1]; } addEntry(entry, hash) { const currentIndex = this.stream().indexWhere((currentEntry) => this.context.eq(currentEntry[0], entry[0])); if (undefined === currentIndex) { return this.copy(this.entries.append(entry)); } return this.copy(this.entries.updateAt(currentIndex, (currentEntry) => { if (Object.is(currentEntry[1], entry[1])) return currentEntry; return entry; })); } modifyAt(atKey, options, atKeyHash) { const currentIndex = this.stream().indexWhere((entry) => this.context.eq(entry[0], atKey)); if (undefined === currentIndex) { if (undefined === options.ifNew) return this; const newValue = OptLazyOr(options.ifNew, Token); if (Token === newValue) return this; const newEntries = this.entries.append([atKey, newValue]); return this.copy(newEntries); } if (undefined === options.ifExists) return this; const currentEntry = this.entries.get(currentIndex, RimbuError.throwInvalidStateError); const currentValue = currentEntry[1]; const newValue = options.ifExists instanceof Function ? options.ifExists(currentValue, Token) : options.ifExists; if (Token === newValue) { const newEntries = this.entries.remove(currentIndex).assumeNonEmpty(); return this.copy(newEntries); } if (Object.is(newValue, currentValue)) return this; const newEntry = [atKey, newValue]; const newEntries = this.entries.updateAt(currentIndex, newEntry); return this.copy(newEntries); } forEach(f, options = {}) { const { state = TraverseState() } = options; if (state.halted) return; this.entries.forEach(f, { state }); } mapValues(mapFun) { const newEntries = this.entries.map((e) => [ e[0], mapFun(e[1], e[0]), ]); return new HashMapCollision(this.context, newEntries); } toArray() { return this.entries.toArray(); } } //# sourceMappingURL=immutable.mjs.map