@rimbu/hashed
Version:
Immutable HashMap and HashSet implementations for TypeScript
729 lines (583 loc) • 19.9 kB
text/typescript
import { Arr, Entry, RimbuError, Token } from '@rimbu/base';
import { EmptyBase, NonEmptyBase } from '@rimbu/collection-types/map-custom';
import {
type ArrayNonEmpty,
OptLazy,
OptLazyOr,
type RelatedTo,
type ToJSON,
TraverseState,
Update,
} from '@rimbu/common';
import type { List } from '@rimbu/list';
import { Stream, type StreamSource } from '@rimbu/stream';
import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom';
import type { HashMap } from '@rimbu/hashed/map';
import type { HashMapContext } from '@rimbu/hashed/map-custom';
export class HashMapEmpty<K = any, V = any>
extends EmptyBase
implements HashMap<K, V>
{
_NonEmptyType!: HashMap.NonEmpty<K, V>;
constructor(readonly context: HashMapContext<K>) {
super();
}
streamKeys(): Stream<K> {
return Stream.empty();
}
streamValues(): Stream<V> {
return Stream.empty();
}
get<_, O>(key: K, otherwise?: OptLazy<O>): O {
return OptLazy(otherwise) as O;
}
hasKey(): false {
return false;
}
set(key: K, value: V): HashMap.NonEmpty<K, V> {
return this.context.emptyBlock<V>().set(key, value);
}
addEntry(entry: readonly [K, V]): HashMap.NonEmpty<K, V> {
return this.context.emptyBlock().addEntry(entry);
}
addEntries(entries: StreamSource<readonly [K, V]>): HashMap.NonEmpty<K, V> {
return this.context.from(entries) as HashMap.NonEmpty<K, V>;
}
removeKeyAndGet(): undefined {
return undefined;
}
removeKey(): HashMap<K, V> {
return this;
}
removeKeys(): HashMap<K, V> {
return this;
}
modifyAt(
atKey: K,
options: {
ifNew?: OptLazyOr<V, Token>;
}
): HashMap<K, V> {
if (undefined !== options.ifNew) {
const value = OptLazyOr<V, Token>(options.ifNew, Token);
if (Token === value) return this;
return this.set(atKey, value);
}
return this;
}
mapValues<V2>(): HashMap<K, V2> {
return this as any;
}
updateAt(): HashMap<K, V> {
return this;
}
toBuilder(): HashMap.Builder<K, V> {
return this.context.builder();
}
toString(): string {
return `HashMap()`;
}
toJSON(): ToJSON<(readonly [K, V])[]> {
return {
dataType: this.context.typeTag,
value: [],
};
}
}
export abstract class HashMapNonEmptyBase<K, V>
extends NonEmptyBase<readonly [K, V]>
implements HashMap.NonEmpty<K, V>
{
_NonEmptyType!: HashMap.NonEmpty<K, V>;
abstract get context(): HashMapContext<K>;
abstract get size(): number;
abstract get<UK, O>(key: RelatedTo<K, UK>, otherwise?: OptLazy<O>): V | O;
abstract addEntry(
entry: readonly [K, V],
hash?: number
): HashMap.NonEmpty<K, V>;
abstract forEach(
f: (entry: readonly [K, V], index: number, halt: () => void) => void,
options?: { state?: TraverseState }
): void;
abstract modifyAt(
atKey: K,
options: {
ifNew?: OptLazyOr<V, Token>;
ifExists?: ((currentEntry: V, remove: Token) => V | Token) | V;
}
): HashMap<K, V> | any;
abstract mapValues<V2>(
mapFun: (value: V, key: K) => V2
): HashMap.NonEmpty<K, V2>;
abstract toArray(): ArrayNonEmpty<readonly [K, V]>;
asNormal(): this {
return this;
}
streamKeys(): Stream.NonEmpty<K> {
return this.stream().map(Entry.first);
}
streamValues(): Stream.NonEmpty<V> {
return this.stream().map(Entry.second);
}
hasKey<U>(key: RelatedTo<K, U>): boolean {
const token = Symbol();
return token !== this.get(key, token);
}
set(key: K, value: V): HashMap.NonEmpty<K, V> {
return this.addEntry([key, value]);
}
addEntries(entries: StreamSource<readonly [K, V]>): HashMap.NonEmpty<K, V> {
if (isEmptyStreamSourceInstance(entries)) return this;
const builder = this.toBuilder();
builder.addEntries(entries);
return builder.build().assumeNonEmpty();
}
removeKeys<UK>(keys: StreamSource<RelatedTo<K, UK>>): HashMap<K, V> {
if (isEmptyStreamSourceInstance(keys)) return this;
const builder = this.toBuilder();
builder.removeKeys(keys);
return builder.build();
}
updateAt<UK>(
key: RelatedTo<K, UK>,
update: Update<V>
): HashMap.NonEmpty<K, V> {
if (!this.context.isValidKey(key)) return this;
return this.modifyAt(key, {
ifExists: (value): V => Update(value, update),
});
}
removeKey<UK>(key: RelatedTo<K, UK>): HashMap<K, V> {
if (!this.context.hasher.isValid(key)) return this;
return this.modifyAt(key, {
ifExists: (_, remove): typeof remove => remove,
});
}
removeKeyAndGet<UK>(key: RelatedTo<K, UK>): [HashMap<K, V>, V] | undefined {
if (!this.context.hasher.isValid(key)) return undefined;
const token = Symbol();
let currentValue: V | typeof token = token;
const newMap = this.modifyAt(key, {
ifExists: (value, remove): V | typeof remove => {
currentValue = value;
return remove;
},
});
if (token === currentValue) return undefined;
return [newMap, currentValue];
}
filter(
pred: (entry: readonly [K, V], index: number, halt: () => void) => boolean,
options: { negate?: boolean } = {}
): HashMap<K, V> {
const builder = this.context.builder<K, V>();
builder.addEntries(this.stream().filter(pred, options));
if (builder.size === this.size) return this;
return builder.build();
}
toBuilder(): HashMap.Builder<K, V> {
return this.context.createBuilder<K, V>(this);
}
toString(): string {
return this.stream().join({
start: 'HashMap(',
sep: ', ',
end: ')',
valueToString: (entry) => `${entry[0]} -> ${entry[1]}`,
});
}
toJSON(): ToJSON<(readonly [K, V])[]> {
return {
dataType: this.context.typeTag,
value: this.toArray(),
};
}
}
export type MapEntrySet<K, V> = HashMapBlock<K, V> | HashMapCollision<K, V>;
export class HashMapBlock<K, V> extends HashMapNonEmptyBase<K, V> {
constructor(
readonly context: HashMapContext<K>,
readonly entries: readonly (readonly [K, V])[] | null,
readonly entrySets: readonly MapEntrySet<K, V>[] | null,
readonly size: number,
readonly level: number
) {
super();
}
copy(
entries = this.entries,
entrySets = this.entrySets,
size = this.size
): HashMapBlock<K, V> {
if (
entries === this.entries &&
entrySets === this.entrySets &&
size === this.size
) {
return this;
}
return new HashMapBlock(this.context, entries, entrySets, size, this.level);
}
stream(): Stream.NonEmpty<readonly [K, V]> {
if (null !== this.entries) {
if (null === this.entrySets)
return Stream.fromObjectValues(this.entries) as Stream.NonEmpty<[K, V]>;
return Stream.fromObjectValues(this.entries).concat(
Stream.fromObjectValues(this.entrySets).flatMap(
(entrySet: MapEntrySet<K, V>): Stream.NonEmpty<readonly [K, V]> =>
entrySet.stream()
)
) as Stream.NonEmpty<readonly [K, V]>;
}
if (null === this.entrySets) RimbuError.throwInvalidStateError();
return Stream.fromObjectValues(this.entrySets).flatMap(
(entrySet: MapEntrySet<K, V>): Stream.NonEmpty<readonly [K, V]> =>
entrySet.stream()
) as Stream.NonEmpty<readonly [K, V]>;
}
get<UK, O>(
key: RelatedTo<K, UK>,
otherwise?: OptLazy<O>,
hash?: number
): V | O {
if (!this.context.hasher.isValid(key)) return OptLazy(otherwise) as O;
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) as O;
}
if (null !== this.entrySets && atKeyIndex in this.entrySets) {
const entrySet = this.entrySets[atKeyIndex];
return entrySet.get(key, otherwise, keyHash);
}
return OptLazy(otherwise) as O;
}
addEntry(
entry: readonly [K, V],
hash = this.context.hash(entry[0])
): HashMap<K, V> | any {
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: (readonly [K, V])[] | null = 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 as MapEntrySet<K, V>;
return this.copy(newEntries, newEntrySets, this.size + 1);
}
const newEntrySet = this.context.collision<V>(
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) as MapEntrySet<
K,
V
>;
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: K,
options: {
ifNew?: OptLazyOr<V, Token>;
ifExists?: ((currentEntry: V, remove: Token) => V | Token) | V;
},
atKeyHash = this.context.hash(atKey)
): HashMap<K, V> {
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<V, Token>(options.ifNew, Token);
if (Token === newValue) return this;
let newEntries: (readonly [K, V])[] | null = 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) as MapEntrySet<K, V>;
const newEntrySets =
null === this.entrySets ? [] : Arr.copySparse(this.entrySets);
newEntrySets[atKeyIndex] = newEntrySet;
return this.copy(newEntries, newEntrySets, this.size + 1);
}
// create collision
const newEntry: [K, V] = [atKey, newValue];
const newEntrySet = this.context.collision<V>(
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: readonly [K, V] = undefined as any;
if (this.context.isHashMapBlock<K, V>(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<V, Token>(options.ifNew, Token);
if (Token === newValue) return this;
const newEntry: [K, V] = [atKey, newValue];
const newEntries =
null === this.entries ? [] : Arr.copySparse(this.entries);
newEntries[atKeyIndex] = newEntry;
return this.copy(newEntries, undefined, this.size + 1);
}
forEach(
f: (entry: readonly [K, V], index: number, halt: () => void) => void,
options: { state?: TraverseState } = {}
): void {
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<V2>(mapFun: (value: V, key: K) => V2): HashMap.NonEmpty<K, V2> {
const newEntries =
null === this.entries
? null
: Arr.mapSparse(this.entries, (e): [K, V2] => [
e[0],
mapFun(e[1], e[0]),
]);
const newEntrySets =
null === this.entrySets
? null
: Arr.mapSparse(
this.entrySets,
(es: MapEntrySet<K, V>): MapEntrySet<K, V2> =>
es.mapValues(mapFun) as MapEntrySet<K, V2>
);
return new HashMapBlock<K, V2>(
this.context as any,
newEntries,
newEntrySets,
this.size,
this.level
);
}
toArray(): ArrayNonEmpty<[K, V]> {
let result: (readonly [K, V])[] = [];
if (null !== this.entries) {
result = Stream.fromObjectValues(this.entries).toArray();
}
if (null !== this.entrySets) {
Stream.fromObjectValues(this.entrySets).forEach(
(entrySet: MapEntrySet<K, V>): void => {
result = result.concat(entrySet.toArray());
}
);
}
return result as ArrayNonEmpty<[K, V]>;
}
}
export class HashMapCollision<K, V> extends HashMapNonEmptyBase<K, V> {
constructor(
readonly context: HashMapContext<K>,
readonly entries: List.NonEmpty<readonly [K, V]>
) {
super();
}
get size(): number {
return this.entries.length;
}
copy(entries = this.entries): HashMapCollision<K, V> {
if (entries === this.entries) return this;
return new HashMapCollision(this.context, entries);
}
stream(): Stream.NonEmpty<readonly [K, V]> {
return this.entries.stream();
}
get<U, O>(
key: RelatedTo<K, U>,
otherwise?: OptLazy<O>,
keyHash?: number
): V | O {
if (!this.context.hasher.isValid(key)) return OptLazy(otherwise) as O;
const token = Symbol();
const stream = this.stream();
const foundEntry = stream.find(
(entry): boolean => this.context.eq(entry[0], key),
{
otherwise: token,
} as const
);
if (token === foundEntry) return OptLazy(otherwise) as O;
return foundEntry[1];
}
addEntry(entry: readonly [K, V], hash?: number): HashMapCollision<K, V> {
const currentIndex = this.stream().indexWhere((currentEntry): boolean =>
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): readonly [K, V] => {
if (Object.is(currentEntry[1], entry[1])) return currentEntry;
return entry;
})
);
}
modifyAt(
atKey: K,
options: {
ifNew?: OptLazyOr<V, Token>;
ifExists?: ((currentValue: V, remove: Token) => V | Token) | V;
},
atKeyHash?: number
): HashMap<K, V> | any {
const currentIndex = this.stream().indexWhere((entry): boolean =>
this.context.eq(entry[0], atKey)
);
if (undefined === currentIndex) {
if (undefined === options.ifNew) return this;
const newValue = OptLazyOr<V, Token>(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: [K, V] = [atKey, newValue];
const newEntries = this.entries.updateAt(currentIndex, newEntry);
return this.copy(newEntries);
}
forEach(
f: (entry: readonly [K, V], index: number, halt: () => void) => void,
options: { state?: TraverseState } = {}
): void {
const { state = TraverseState() } = options;
if (state.halted) return;
this.entries.forEach(f, { state });
}
mapValues<V2>(mapFun: (value: V, key: K) => V2): HashMap.NonEmpty<K, V2> {
const newEntries = this.entries.map((e): readonly [K, V2] => [
e[0],
mapFun(e[1], e[0]),
]);
return new HashMapCollision(this.context as any, newEntries);
}
toArray(): ArrayNonEmpty<readonly [K, V]> {
return this.entries.toArray();
}
}