@rimbu/sorted
Version:
Immutable SortedMap and SortedSet implementations for TypeScript
366 lines (276 loc) • 9.49 kB
text/typescript
import { Token } from '@rimbu/base';
import { OptLazy, OptLazyOr, type RelatedTo, Update } from '@rimbu/common';
import { Stream, type StreamSource } from '@rimbu/stream';
import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom';
import type { SortedMap } from '@rimbu/sorted/map';
import type { SortedMapContext, SortedMapNode } from '@rimbu/sorted/map-custom';
import { SortedBuilder, SortedIndex } from '@rimbu/sorted/common';
export class SortedMapBuilder<K, V>
extends SortedBuilder<readonly [K, V]>
implements SortedMap.Builder<K, V>
{
constructor(
readonly context: SortedMapContext<K>,
public source?: undefined | SortedMap<K, V>,
public _entries?: undefined | (readonly [K, V])[],
public _children?: undefined | SortedMapBuilder<K, V>[],
public size = source?.size ?? 0
) {
super();
}
createNew(
source?: undefined | SortedMap<K, V>,
_entries?: undefined | (readonly [K, V])[],
_children?: undefined | SortedMapBuilder<K, V>[],
size?: undefined | number
): SortedMapBuilder<K, V> {
return new SortedMapBuilder(
this.context,
source,
_entries,
_children,
size
);
}
prepareMutate(): void {
if (undefined === this._entries) {
if (undefined !== this.source) {
if (this.context.isSortedMapEmpty(this.source)) {
this._entries = [];
this._children = [];
} else if (this.context.isSortedMapLeaf<K, V>(this.source)) {
this._entries = this.source.entries.slice();
} else if (this.context.isSortedMapInner<K, V>(this.source)) {
this._entries = this.source.entries.slice();
this._children = this.source.children.map(
(child): SortedMapBuilder<K, V> => this.createNew(child)
);
}
}
if (undefined === this._entries) {
this._entries = [];
}
}
}
get children(): SortedMapBuilder<K, V>[] {
this.prepareMutate();
return this._children!;
}
set children(value: SortedMapBuilder<K, V>[]) {
this.prepareMutate();
this.source = undefined;
this._children = value;
}
get = <UK, O>(key: RelatedTo<K, UK>, otherwise?: OptLazy<O>): V | O => {
if (!this.context.comp.isComparable(key)) return OptLazy(otherwise) as O;
if (undefined !== this.source) return this.source.get(key, otherwise!);
const entryIndex = this.context.findIndex(key, this.entries);
if (entryIndex >= 0) {
return this.entries[entryIndex][1];
}
if (this.hasChildren) {
const childIndex = SortedIndex.next(entryIndex);
const child = this.children[childIndex];
return child.get(key, otherwise);
}
return OptLazy(otherwise) as O;
};
// prettier-ignore
hasKey = <UK,>(key: RelatedTo<K, UK>): boolean => {
return Token !== this.get(key, Token);
};
addEntry = (entry: readonly [K, V]): boolean => {
this.checkLock();
const result = this.addEntryInternal(entry);
this.normalize();
return result;
};
addEntries = (source: StreamSource<readonly [K, V]>): boolean => {
this.checkLock();
return Stream.from(source).filterPure({ pred: this.addEntry }).count() > 0;
};
set = (key: K, value: V): boolean => {
return this.addEntry([key, value]);
};
removeKey = <UK, O>(key: RelatedTo<K, UK>, otherwise?: OptLazy<O>): V | O => {
this.checkLock();
if (!this.context.comp.isComparable(key)) return OptLazy(otherwise) as O;
const result = this.removeInternal(key, otherwise);
this.normalize();
return result;
};
// prettier-ignore
removeKeys = <UK,>(keys: StreamSource<RelatedTo<K, UK>>): boolean => {
this.checkLock();
if (isEmptyStreamSourceInstance(keys)) return false;
const notFound = Symbol();
return (
Stream.from(keys)
.mapPure(this.removeKey, notFound)
.countElement(notFound, { negate: true }) > 0
);
};
modifyAt = (
key: K,
options: {
ifNew?: OptLazyOr<V, Token>;
ifExists?: ((currentValue: V, remove: Token) => V | Token) | V;
}
): boolean => {
this.checkLock();
const result = this.modifyAtInternal(key, options);
this.normalize();
return result;
};
// prettier-ignore
updateAt = <O,>(key: K, update: Update<V>, otherwise?: OptLazy<O>): V | O => {
let result: V;
let found = false;
this.modifyAt(key, {
ifExists: (value): V => {
result = value;
found = true;
return Update(value, update);
},
});
if (!found) return OptLazy(otherwise) as O;
return result!;
};
build = (): SortedMap<K, V> => {
if (undefined !== this.source) return this.source;
if (this.size === 0) return this.context.empty();
if (!this.hasChildren) {
return this.context.leaf(this.entries.slice());
}
return this.context.inner(
this.entries.slice(),
this.children.map(
(child): SortedMapNode<K, V> => child.build() as SortedMapNode<K, V>
),
this.size
);
};
// prettier-ignore
buildMapValues = <V2,>(f: (value: V, key: K) => V2): SortedMap<K, V2> => {
if (undefined !== this.source) return this.source.mapValues(f);
if (this.size === 0) return this.context.empty();
const newEntries = this.entries.map((entry): [K, V2] => [
entry[0],
f(entry[1], entry[0]),
]);
if (!this.hasChildren) {
return this.context.leaf(newEntries);
}
return this.context.inner(
newEntries,
this.children.map(
(c): SortedMapNode<K, V2> => c.buildMapValues(f) as SortedMapNode<K, V2>
),
this.size
);
};
addEntryInternal(entry: readonly [K, V]): boolean {
const entryIndex = this.context.findIndex(entry[0], this.entries);
if (entryIndex >= 0) {
const currentEntry = this.entries[entryIndex];
if (Object.is(currentEntry[1], entry[1])) return false;
this.source = undefined;
this.entries[entryIndex] = entry;
return true;
}
const childIndex = SortedIndex.next(entryIndex);
if (!this.hasChildren) {
this.source = undefined;
this.size++;
this.entries.splice(childIndex, 0, entry);
return true;
}
const child = this.children[childIndex];
const preSize = child.size;
const changed = child.addEntryInternal(entry);
if (!changed) return false;
this.source = undefined;
this.size += child.size - preSize;
this.normalizeChildDecrease(childIndex);
return true;
}
removeInternal<O>(key: K, otherwise?: OptLazy<O>): V | O {
if (this.size === 0) return OptLazy(otherwise) as O;
const entryIndex = this.context.findIndex(key, this.entries);
if (entryIndex >= 0) {
this.source = undefined;
this.size--;
if (!this.hasChildren) {
const removed = this.entries.splice(entryIndex, 1);
return removed[0][1];
}
const leftChild = this.children[entryIndex];
const rightChild = this.children[entryIndex + 1];
const removed = this.entries[entryIndex];
if (leftChild.size >= rightChild.size) {
this.entries[entryIndex] = leftChild.deleteMax();
this.normalizeChildIncrease(entryIndex);
} else {
this.entries[entryIndex] = rightChild.deleteMin();
this.normalizeChildIncrease(entryIndex + 1);
}
return removed[1];
}
if (!this.hasChildren) return OptLazy(otherwise) as O;
const childIndex = SortedIndex.next(entryIndex);
const child = this.children[childIndex];
const preSize = child.size;
const token = Symbol();
const oldValue = child.removeInternal(key, token);
if (token === oldValue) return OptLazy(otherwise) as O;
this.source = undefined;
this.size += child.size - preSize;
this.normalizeChildIncrease(childIndex);
return oldValue;
}
modifyAtInternal(
key: K,
options: {
ifNew?: OptLazyOr<V, Token>;
ifExists?: ((currentValue: V, remove: Token) => V | Token) | V;
}
): boolean {
const entryIndex = this.context.findIndex(key, this.entries);
if (entryIndex >= 0) {
if (undefined === options.ifExists) return false;
const currentEntry = this.entries[entryIndex];
const currentValue = currentEntry[1];
const newValue =
options.ifExists instanceof Function
? options.ifExists(currentValue, Token)
: options.ifExists;
if (newValue === currentValue) return false;
if (Token === newValue) {
return Token !== this.removeInternal(key, Token);
}
this.source = undefined;
const newEntry: [K, V] = [key, newValue];
this.entries[entryIndex] = newEntry;
return true;
}
const childIndex = SortedIndex.next(entryIndex);
if (!this.hasChildren) {
if (undefined === options.ifNew) return false;
const newValue = OptLazyOr<V, Token>(options.ifNew, Token);
if (Token === newValue) return false;
this.source = undefined;
this.size++;
this.entries.splice(childIndex, 0, [key, newValue]);
return true;
}
const child = this.children[childIndex];
const preSize = child.size;
const changed = child.modifyAtInternal(key, options);
if (!changed) return false;
this.source = undefined;
this.size += child.size - preSize;
this.normalizeChildDecrease(childIndex);
this.normalizeChildIncrease(childIndex);
return changed;
}
}