UNPKG

@rimbu/multimap

Version:

An immutable Map where each key can have multiple values

753 lines (599 loc) 18.2 kB
import { RimbuError } from '@rimbu/base'; import type { RMap, RSet } from '@rimbu/collection-types'; import { EmptyBase, NonEmptyBase, type KeyValue, type WithKeyValue, } from '@rimbu/collection-types/map-custom'; import { OptLazy, TraverseState, type ArrayNonEmpty, type RelatedTo, type ToJSON, } from '@rimbu/common'; import { Reducer, Stream, type StreamSource } from '@rimbu/stream'; import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom'; import type { MultiMap } from '@rimbu/multimap'; import type { MultiMapBase } from '@rimbu/multimap/custom'; export interface ContextImplTypes extends MultiMapBase.Types { readonly context: MultiMapContext<this['_K'], this['_V'], string>; } export class MultiMapEmpty<K, V, Tp extends ContextImplTypes> extends EmptyBase implements MultiMapBase<K, V, Tp> { _NonEmptyType!: WithKeyValue<Tp, K, V>['nonEmpty']; constructor(readonly context: WithKeyValue<Tp, K, V>['context']) { super(); } get keyMap(): WithKeyValue<Tp, K, V>['keyMap'] { return this.context.keyMapContext.empty(); } get keySize(): 0 { return 0; } streamKeys(): Stream<K> { return Stream.empty(); } streamValues(): Stream<V> { return Stream.empty(); } hasKey(): false { return false; } hasEntry(): false { return false; } add(key: K, value: V): WithKeyValue<Tp, K, V>['nonEmpty'] { const values = this.context.keyMapValuesContext.of(value); const keyMap = this.context.keyMapContext.of<K, RSet.NonEmpty<V>>([ key, values, ]) as WithKeyValue<Tp, K, V>['keyMapNonEmpty']; return this.context.createNonEmpty(keyMap, 1); } addEntries(entries: StreamSource<readonly [K, V]>): any { return this.context.from(entries); } getValues(): WithKeyValue<Tp, K, V>['keyMapValues'] { return this.context.keyMapValuesContext.empty(); } setValues( key: K, values: StreamSource<V> ): WithKeyValue<Tp, K, V>['nonEmpty'] { const valueSet: RSet<V> = this.context.keyMapValuesContext.from(values); if (!valueSet.nonEmpty()) return this as any; const keyMap = this.context.keyMapContext.of<K, RSet.NonEmpty<V>>([ key, valueSet, ]) as WithKeyValue<Tp, K, V>['keyMapNonEmpty']; return this.context.createNonEmpty(keyMap, valueSet.size); } modifyAt( atKey: K, options: { ifNew?: OptLazy<StreamSource<V>> } ): WithKeyValue<Tp, K, V>['normal'] { if (undefined === options.ifNew) return this as any; return this.setValues(atKey, OptLazy(options.ifNew)); } removeKey(): WithKeyValue<Tp, K, V>['normal'] { return this as any; } removeKeys(): WithKeyValue<Tp, K, V>['normal'] { return this as any; } removeKeyAndGet(): undefined { return undefined; } removeEntry(): WithKeyValue<Tp, K, V>['normal'] { return this as any; } removeEntries(): WithKeyValue<Tp, K, V>['normal'] { return this as any; } toBuilder(): WithKeyValue<Tp, K, V>['builder'] { return this.context.builder(); } toString(): string { return `${this.context.typeTag}()`; } toJSON(): ToJSON<[K, V[]][]> { return { dataType: this.context.typeTag, value: [], }; } } export class MultiMapNonEmpty< K, V, Tp extends ContextImplTypes, TpG extends WithKeyValue<Tp, K, V> = WithKeyValue<Tp, K, V>, > extends NonEmptyBase<[K, V]> implements MultiMapBase.NonEmpty<K, V, Tp> { _NonEmptyType!: TpG['nonEmpty']; constructor( readonly context: TpG['context'], readonly keyMap: TpG['keyMapNonEmpty'], readonly size: number ) { super(); } assumeNonEmpty(): any { return this; } asNormal(): any { return this; } copy(keyMap: TpG['keyMapNonEmpty'], size: number): TpG['nonEmpty'] { if (keyMap === this.keyMap) return this as any; return this.context.createNonEmpty<K, V>(keyMap as any, size); } copyE(keyMap: TpG['keyMap'], size: number): TpG['normal'] { if (keyMap.nonEmpty()) { return this.copy(keyMap.assumeNonEmpty(), size) as TpG['normal']; } return this.context.empty(); } stream(): Stream.NonEmpty<[K, V]> { return this.keyMap .stream() .flatMap( ([key, values]): Stream.NonEmpty<[K, V]> => values.stream().map((v): [K, V] => [key, v]) ); } streamKeys(): Stream.NonEmpty<K> { return this.keyMap.streamKeys(); } streamValues(): Stream.NonEmpty<V> { return this.keyMap .streamValues() .flatMap((values): Stream.NonEmpty<V> => values.stream()); } get keySize(): number { return this.keyMap.size; } hasKey<U>(key: RelatedTo<K, U>): boolean { return this.keyMap.hasKey<U>(key); } hasEntry<U>(key: RelatedTo<K, U>, value: V): boolean { const values = this.keyMap.get(key); return values?.has(value) ?? false; } getValues<U>(key: RelatedTo<K, U>): TpG['keyMapValues'] { return this.keyMap.get(key, this.context.keyMapValuesContext.empty()); } add(key: K, value: V): TpG['nonEmpty'] { 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: StreamSource<readonly [K, V]>): TpG['nonEmpty'] { if (isEmptyStreamSourceInstance(entries)) return this as any; const builder = this.toBuilder(); builder.addEntries(entries); return builder.build().assumeNonEmpty(); } setValues(key: K, values: any): any { return this.modifyAt(key, { ifNew: values, ifExists: () => values }); } removeKey<UK>(key: RelatedTo<K, UK>): TpG['normal'] { if (!this.context.keyMapContext.isValidKey(key)) return this as any; return this.modifyAt(key, { ifExists: () => [] }); } removeKeys<UK>(keys: StreamSource<RelatedTo<K, UK>>): TpG['normal'] { if (isEmptyStreamSourceInstance(keys)) return this as any; const builder = this.toBuilder(); builder.removeKeys(keys); return builder.build(); } removeKeyAndGet<UK>( key: RelatedTo<K, UK> ): [TpG['normal'], TpG['keyMapValuesNonEmpty']] | undefined { if (!this.context.keyMapContext.isValidKey(key)) return undefined; let removed: TpG['keyMapValuesNonEmpty'] | undefined = undefined; const result = this.modifyAt(key, { ifExists: (values) => { removed = values; return []; }, }); if (undefined === removed) return undefined; return [result, removed]; } removeEntry<UK, UV>( key: RelatedTo<K, UK>, value: RelatedTo<V, UV> ): TpG['normal'] { if (!this.context.keyMapContext.isValidKey(key)) return this as any; return this.modifyAt(key, { ifExists: (values: TpG['keyMapValuesNonEmpty']) => values.remove(value), }); } removeEntries<UK, UV>( entries: StreamSource<[RelatedTo<K, UK>, RelatedTo<V, UV>]> ): TpG['normal'] { if (isEmptyStreamSourceInstance(entries)) return this as any; const builder = this.toBuilder(); builder.removeEntries(entries); return builder.build(); } filter( pred: (entry: [K, V], index: number, halt: () => void) => boolean, options: { negate?: boolean } = {} ): TpG['normal'] { const builder = this.context.builder(); builder.addEntries(this.stream().filter(pred, options)); if (builder.size === this.size) return this as any; return builder.build(); } forEach( f: (entry: [K, V], index: number, halt: () => void) => void, options: { state?: TraverseState } = {} ): void { const { state = TraverseState() } = options; if (state.halted) return; this.stream().forEach(f, { state }); } modifyAt( atKey: K, options: { ifNew?: OptLazy<StreamSource<V>>; ifExists?: | ((currentValues: TpG['keyMapValuesNonEmpty']) => StreamSource<V>) | StreamSource<V>; } ): TpG['normal'] { 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(): ArrayNonEmpty<[K, V]> { return this.stream().toArray(); } toString(): string { return this.keyMap.stream().join({ start: `${this.context.typeTag}(`, sep: ', ', end: ')', valueToString: ([key, values]) => `${key} -> ${values.stream().join({ start: '[', sep: ', ', end: ']' })}`, }); } toJSON(): ToJSON<[K, V[]][]> { return { dataType: this.context.typeTag, value: this.keyMap .stream() .map((entry) => [entry[0], entry[1].toArray()] as [K, V[]]) .toArray(), }; } toBuilder(): TpG['builder'] { return this.context.createBuilder(this as any); } } export class MultiMapBuilder< K, V, Tp extends ContextImplTypes, TpG extends WithKeyValue<Tp, K, V> = WithKeyValue<Tp, K, V>, > implements MultiMapBase.Builder<K, V, Tp> { _lock = 0; _size = 0; constructor( readonly context: TpG['context'], public source?: MultiMap.NonEmpty<K, V> ) { if (undefined !== source) this._size = source.size; } _keyMap?: RMap.Builder<K, RSet.Builder<V>>; get keyMap(): RMap.Builder<K, RSet.Builder<V>> { 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(): void { if (this._lock) RimbuError.throwModifiedBuilderWhileLoopingOverItError(); } get size(): number { return this._size; } get isEmpty(): boolean { return this.size === 0; } // prettier-ignore getValues = <UK,>(key: RelatedTo<K, UK>): any => { return ( this.source?.getValues(key) ?? this.keyMap.get(key)?.build() ?? this.context.keyMapValuesContext.empty() ); }; // prettier-ignore hasKey = <UK,>(key: RelatedTo<K, UK>): boolean => { return this.source?.hasKey(key) ?? this.keyMap.hasKey(key); }; // prettier-ignore hasEntry = <UK,>(key: RelatedTo<K, UK>, value: V): boolean => { return ( this.source?.hasEntry(key, value) ?? this.keyMap.get(key)?.has(value) ?? false ); }; add = (key: K, value: V): boolean => { 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; }; addEntries = (source: StreamSource<readonly [K, V]>): boolean => { this.checkLock(); return Stream.applyFilter(source, { pred: this.add }).count() > 0; }; setValues = (key: K, source: StreamSource<V>): boolean => { 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; }, }); }; removeEntry = <UK, UV>( key: RelatedTo<K, UK>, value: RelatedTo<V, UV> ): boolean => { 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; }; removeEntries = <UK, UV>( entries: StreamSource<[RelatedTo<K, UK>, RelatedTo<V, UV>]> ): boolean => { this.checkLock(); return Stream.applyFilter(entries, { pred: this.removeEntry }).count() > 0; }; // prettier-ignore removeKey = <UK,>(key: RelatedTo<K, UK>): boolean => { 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 removeKeys = <UK,>(keys: StreamSource<RelatedTo<K, UK>>): boolean => { this.checkLock(); return Stream.from(keys).filterPure({ pred: this.removeKey }).count() > 0; }; forEach = ( f: (entry: [K, V], index: number, halt: () => void) => void, options: { reversed?: boolean; state?: TraverseState } = {} ): void => { const { reversed = false, state = TraverseState() } = options; if (state.halted) return; this._lock++; this.keyMap.forEach( ([key, values], _, outerHalt): void => { values.forEach( (value, index, halt): void => f([key, value], index, halt), { reversed, state, } as any ); if (state.halted) outerHalt(); }, { reversed } as any ); this._lock--; }; build = (): TpG['normal'] => { 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 ) as TpG['normal']; }; } export class MultiMapContext< UK, UV, N extends string, Tp extends ContextImplTypes = ContextImplTypes, > implements MultiMapBase.Context<UK, UV, Tp> { constructor( readonly typeTag: N, readonly keyMapContext: (Tp & KeyValue<UK, UV>)['keyMapContext'], readonly keyMapValuesContext: (Tp & KeyValue<UK, UV>)['keyMapValuesContext'] ) {} readonly _empty = Object.freeze( new MultiMapEmpty<UK, UV, Tp>(this as any) as WithKeyValue< Tp, UK, UV >['normal'] ); isNonEmptyInstance<K, V>( source: any ): source is WithKeyValue<Tp, K, V>['nonEmpty'] { return source instanceof MultiMapNonEmpty; } createNonEmpty<K extends UK, V extends UV>( keyMap: WithKeyValue<Tp, K, V>['keyMapNonEmpty'], size: number ): WithKeyValue<Tp, K, V>['nonEmpty'] { return new MultiMapNonEmpty<K, V, Tp>( this as any, keyMap, size ) as WithKeyValue<Tp, K, V>['nonEmpty']; } readonly empty = <K extends UK, V extends UV>(): WithKeyValue< Tp, K, V >['normal'] => { return this._empty; }; readonly from: any = <K extends UK, V extends UV>( ...sources: ArrayNonEmpty<StreamSource<readonly [K, V]>> ): WithKeyValue<Tp, K, V>['normal'] => { let builder = this.builder<K, V>(); let i = -1; const length = sources.length; while (++i < length) { const source = sources[i]; if (isEmptyStreamSourceInstance(source)) continue; if ( builder.isEmpty && this.isNonEmptyInstance<K, V>(source) && source.context === this ) { if (i === length - 1) return source; builder = source.toBuilder(); continue; } builder.addEntries(source); } return builder.build(); }; readonly of = <K extends UK, V extends UV>( ...entries: ArrayNonEmpty<readonly [K, V]> ): [K, V] extends [UK, UV] ? WithKeyValue<Tp, K, V>['nonEmpty'] : never => { return this.from(entries); }; readonly builder = <K extends UK, V extends UV>(): WithKeyValue< Tp, K, V >['builder'] => { return new MultiMapBuilder<K, V, Tp>(this as any) as WithKeyValue< Tp, K, V >['builder']; }; readonly reducer = <K extends UK, V extends UV>( source?: StreamSource<readonly [K, V]> ): Reducer<readonly [K, V], WithKeyValue<Tp, K, V>['normal']> => { return Reducer.create( () => undefined === source ? this.builder<K, V>() : (this.from(source) as WithKeyValue<Tp, K, V>['normal']).toBuilder(), (builder, entry) => { builder.add(entry[0], entry[1]); return builder; }, (builder) => builder.build() ); }; createBuilder<K, V>( source?: MultiMap.NonEmpty<K, V> ): WithKeyValue<Tp, K, V>['builder'] { return new MultiMapBuilder<K, V, Tp>(this as any, source) as WithKeyValue< Tp, K, V >['builder']; } }