@rimbu/multimap
Version:
An immutable Map where each key can have multiple values
489 lines • 15.7 kB
JavaScript
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