UNPKG

@rimbu/multimap

Version:

An immutable Map where each key can have multiple values

489 lines 15.7 kB
import { RimbuError } from '@rimbu/base'; import { EmptyBase, NonEmptyBase, } from '@rimbu/collection-types/map-custom'; import { OptLazy, TraverseState, } from '@rimbu/common'; import { Reducer, Stream } from '@rimbu/stream'; import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom'; export class MultiMapEmpty extends EmptyBase { constructor(context) { super(); this.context = context; } get keyMap() { return this.context.keyMapContext.empty(); } get keySize() { return 0; } streamKeys() { return Stream.empty(); } streamValues() { return Stream.empty(); } hasKey() { return false; } hasEntry() { return false; } add(key, value) { const values = this.context.keyMapValuesContext.of(value); const keyMap = this.context.keyMapContext.of([ key, values, ]); return this.context.createNonEmpty(keyMap, 1); } addEntries(entries) { return this.context.from(entries); } getValues() { return this.context.keyMapValuesContext.empty(); } setValues(key, values) { const valueSet = this.context.keyMapValuesContext.from(values); if (!valueSet.nonEmpty()) return this; const keyMap = this.context.keyMapContext.of([ key, valueSet, ]); return this.context.createNonEmpty(keyMap, valueSet.size); } modifyAt(atKey, options) { if (undefined === options.ifNew) return this; return this.setValues(atKey, OptLazy(options.ifNew)); } removeKey() { return this; } removeKeys() { return this; } removeKeyAndGet() { return undefined; } removeEntry() { return this; } removeEntries() { return this; } toBuilder() { return this.context.builder(); } toString() { return `${this.context.typeTag}()`; } toJSON() { return { dataType: this.context.typeTag, value: [], }; } } export class MultiMapNonEmpty extends NonEmptyBase { constructor(context, keyMap, size) { super(); this.context = context; this.keyMap = keyMap; this.size = size; } assumeNonEmpty() { return this; } asNormal() { return this; } copy(keyMap, size) { if (keyMap === this.keyMap) return this; return this.context.createNonEmpty(keyMap, size); } copyE(keyMap, size) { if (keyMap.nonEmpty()) { return this.copy(keyMap.assumeNonEmpty(), size); } return this.context.empty(); } stream() { return this.keyMap .stream() .flatMap(([key, values]) => values.stream().map((v) => [key, v])); } streamKeys() { return this.keyMap.streamKeys(); } streamValues() { return this.keyMap .streamValues() .flatMap((values) => values.stream()); } get keySize() { return this.keyMap.size; } hasKey(key) { return this.keyMap.hasKey(key); } hasEntry(key, value) { const values = this.keyMap.get(key); return values?.has(value) ?? false; } getValues(key) { return this.keyMap.get(key, this.context.keyMapValuesContext.empty()); } add(key, value) { let newSize = this.size; const newKeyMap = this.keyMap .modifyAt(key, { ifNew: () => { newSize++; return this.context.keyMapValuesContext.of(value); }, ifExists: (values) => { const newValues = values.add(value); if (newValues === values) return values; newSize -= values.size; newSize += newValues.size; return newValues; }, }) .assumeNonEmpty(); return this.copy(newKeyMap, newSize); } addEntries(entries) { if (isEmptyStreamSourceInstance(entries)) return this; const builder = this.toBuilder(); builder.addEntries(entries); return builder.build().assumeNonEmpty(); } setValues(key, values) { return this.modifyAt(key, { ifNew: values, ifExists: () => values }); } removeKey(key) { if (!this.context.keyMapContext.isValidKey(key)) return this; return this.modifyAt(key, { ifExists: () => [] }); } removeKeys(keys) { if (isEmptyStreamSourceInstance(keys)) return this; const builder = this.toBuilder(); builder.removeKeys(keys); return builder.build(); } removeKeyAndGet(key) { if (!this.context.keyMapContext.isValidKey(key)) return undefined; let removed = undefined; const result = this.modifyAt(key, { ifExists: (values) => { removed = values; return []; }, }); if (undefined === removed) return undefined; return [result, removed]; } removeEntry(key, value) { if (!this.context.keyMapContext.isValidKey(key)) return this; return this.modifyAt(key, { ifExists: (values) => values.remove(value), }); } removeEntries(entries) { if (isEmptyStreamSourceInstance(entries)) return this; const builder = this.toBuilder(); builder.removeEntries(entries); return builder.build(); } 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(); } forEach(f, options = {}) { const { state = TraverseState() } = options; if (state.halted) return; this.stream().forEach(f, { state }); } modifyAt(atKey, options) { let newSize = this.size; const { ifNew, ifExists } = options; const newKeyMap = this.keyMap.modifyAt(atKey, { ifNew: (none) => { if (undefined === ifNew) return none; const newValueStream = OptLazy(ifNew); const newValues = this.context.keyMapValuesContext.from(newValueStream); if (!newValues.nonEmpty()) return none; newSize += newValues.size; return newValues; }, ifExists: (currentValues, remove) => { if (undefined === ifExists) return currentValues; const newValueStream = ifExists instanceof Function ? ifExists(currentValues) : ifExists; const newValues = this.context.keyMapValuesContext.from(newValueStream); if (!newValues.nonEmpty()) { newSize -= currentValues.size; return remove; } newSize -= currentValues.size; newSize += newValues.size; return newValues; }, }); return this.copyE(newKeyMap, newSize); } toArray() { return this.stream().toArray(); } toString() { return this.keyMap.stream().join({ start: `${this.context.typeTag}(`, sep: ', ', end: ')', valueToString: ([key, values]) => `${key} -> ${values.stream().join({ start: '[', sep: ', ', end: ']' })}`, }); } toJSON() { return { dataType: this.context.typeTag, value: this.keyMap .stream() .map((entry) => [entry[0], entry[1].toArray()]) .toArray(), }; } toBuilder() { return this.context.createBuilder(this); } } export class MultiMapBuilder { constructor(context, source) { this.context = context; this.source = source; this._lock = 0; this._size = 0; // prettier-ignore this.getValues = (key) => { return (this.source?.getValues(key) ?? this.keyMap.get(key)?.build() ?? this.context.keyMapValuesContext.empty()); }; // prettier-ignore this.hasKey = (key) => { return this.source?.hasKey(key) ?? this.keyMap.hasKey(key); }; // prettier-ignore this.hasEntry = (key, value) => { return (this.source?.hasEntry(key, value) ?? this.keyMap.get(key)?.has(value) ?? false); }; this.add = (key, value) => { this.checkLock(); let changed = true; this.keyMap.modifyAt(key, { ifNew: () => { this._size++; const valueBuilder = this.context.keyMapValuesContext.builder(); valueBuilder.add(value); return valueBuilder; }, ifExists: (valueBuilder) => { this._size -= valueBuilder.size; changed = valueBuilder.add(value); this._size += valueBuilder.size; return valueBuilder; }, }); if (changed) this.source = undefined; return changed; }; this.addEntries = (source) => { this.checkLock(); return Stream.applyFilter(source, { pred: this.add }).count() > 0; }; this.setValues = (key, source) => { this.checkLock(); const values = this.context.keyMapValuesContext.from(source).toBuilder(); const size = values.size; if (size <= 0) return this.removeKey(key); return this.keyMap.modifyAt(key, { ifNew: () => { this._size += size; this.source = undefined; return values; }, ifExists: (oldValues) => { this._size -= oldValues.size; this._size += size; this.source = undefined; return values; }, }); }; this.removeEntry = (key, value) => { this.checkLock(); if (!this.context.keyMapContext.isValidKey(key)) return false; let changed = false; this.keyMap.modifyAt(key, { ifExists: (valueBuilder, remove) => { if (valueBuilder.remove(value)) { this._size--; changed = true; } if (valueBuilder.size <= 0) return remove; return valueBuilder; }, }); if (changed) this.source = undefined; return changed; }; this.removeEntries = (entries) => { this.checkLock(); return Stream.applyFilter(entries, { pred: this.removeEntry }).count() > 0; }; // prettier-ignore this.removeKey = (key) => { this.checkLock(); if (!this.context.keyMapContext.isValidKey(key)) return false; const changed = this.keyMap.modifyAt(key, { ifExists: (valueBuilder, remove) => { this._size -= valueBuilder.size; return remove; }, }); if (changed) this.source = undefined; return changed; }; // prettier-ignore this.removeKeys = (keys) => { this.checkLock(); return Stream.from(keys).filterPure({ pred: this.removeKey }).count() > 0; }; this.forEach = (f, options = {}) => { const { reversed = false, state = TraverseState() } = options; if (state.halted) return; this._lock++; this.keyMap.forEach(([key, values], _, outerHalt) => { values.forEach((value, index, halt) => f([key, value], index, halt), { reversed, state, }); if (state.halted) outerHalt(); }, { reversed }); this._lock--; }; this.build = () => { if (undefined !== this.source) return this.source; if (this.isEmpty) return this.context.empty(); return this.context.createNonEmpty(this.keyMap .buildMapValues((values) => values.build().assumeNonEmpty()) .assumeNonEmpty(), this.size); }; if (undefined !== source) this._size = source.size; } get keyMap() { if (undefined === this._keyMap) { if (undefined === this.source) { this._keyMap = this.context.keyMapContext.builder(); } else { this._keyMap = this.source.keyMap .mapValues((v) => v.toBuilder()) .toBuilder(); } } return this._keyMap; } checkLock() { if (this._lock) RimbuError.throwModifiedBuilderWhileLoopingOverItError(); } get size() { return this._size; } get isEmpty() { return this.size === 0; } } export class MultiMapContext { constructor(typeTag, keyMapContext, keyMapValuesContext) { this.typeTag = typeTag; this.keyMapContext = keyMapContext; this.keyMapValuesContext = keyMapValuesContext; this._empty = Object.freeze(new MultiMapEmpty(this)); this.empty = () => { return this._empty; }; this.from = (...sources) => { let builder = this.builder(); let i = -1; const length = sources.length; while (++i < length) { const source = sources[i]; if (isEmptyStreamSourceInstance(source)) continue; if (builder.isEmpty && this.isNonEmptyInstance(source) && source.context === this) { if (i === length - 1) return source; builder = source.toBuilder(); continue; } builder.addEntries(source); } return builder.build(); }; this.of = (...entries) => { return this.from(entries); }; this.builder = () => { return new MultiMapBuilder(this); }; this.reducer = (source) => { return Reducer.create(() => undefined === source ? this.builder() : this.from(source).toBuilder(), (builder, entry) => { builder.add(entry[0], entry[1]); return builder; }, (builder) => builder.build()); }; } isNonEmptyInstance(source) { return source instanceof MultiMapNonEmpty; } createNonEmpty(keyMap, size) { return new MultiMapNonEmpty(this, keyMap, size); } createBuilder(source) { return new MultiMapBuilder(this, source); } } //# sourceMappingURL=base.mjs.map