@rimbu/hashed
Version:
Immutable HashMap and HashSet implementations for TypeScript
408 lines • 15.7 kB
JavaScript
import { Arr, RimbuError, Token } from '@rimbu/base';
import { OptLazy, OptLazyOr, TraverseState, Update, } from '@rimbu/common';
import { List } from '@rimbu/list';
import { Stream } from '@rimbu/stream';
import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom';
import { BlockBuilderBase, CollisionBuilderBase } from '@rimbu/hashed/common';
export class HashMapBlockBuilder extends BlockBuilderBase {
constructor(context, source, _entries, _entrySets, size = source?.size ?? 0, level = source?.level ?? 0) {
super();
this.context = context;
this.source = source;
this._entries = _entries;
this._entrySets = _entrySets;
this.size = size;
this.level = level;
this._lock = 0;
this.get = (key, otherwise, hash) => {
if (undefined !== this.source)
return this.source.get(key, otherwise);
if (!this.context.hasher.isValid(key))
return OptLazy(otherwise);
const keyHash = hash ?? this.context.hash(key);
const keyIndex = this.context.getKeyIndex(this.level, keyHash);
if (keyIndex in this.entries) {
const currentEntry = this.entries[keyIndex];
if (this.context.eq(key, currentEntry[0]))
return currentEntry[1];
return OptLazy(otherwise);
}
if (keyIndex in this.entrySets) {
const currentEntrySet = this.entrySets[keyIndex];
return currentEntrySet.get(key, otherwise, keyHash);
}
return OptLazy(otherwise);
};
// prettier-ignore
this.hasKey = (key) => {
const token = Symbol();
return token !== this.get(key, token);
};
this.addEntry = (entry) => {
this.checkLock();
return this.addEntryInternal(entry);
};
this.addEntries = (source) => {
this.checkLock();
if (isEmptyStreamSourceInstance(source))
return false;
return Stream.from(source).filterPure({ pred: this.addEntry }).count() > 0;
};
this.set = (key, value) => {
this.checkLock();
return this.addEntryInternal([key, value]);
};
this.modifyAt = (key, options, keyHash = this.context.hash(key)) => {
this.checkLock();
const keyIndex = this.context.getKeyIndex(this.level, keyHash);
if (keyIndex in this.entries) {
// potential match in entries
const currentEntry = this.entries[keyIndex];
const [currentKey, currentValue] = currentEntry;
if (this.context.eq(key, currentKey)) {
// exact match
if (undefined === options.ifExists)
return false;
const newValue = options.ifExists instanceof Function
? options.ifExists(currentValue, Token)
: options.ifExists;
if (Object.is(newValue, currentValue)) {
return false;
}
this.source = undefined;
if (Token === newValue) {
this.size--;
delete this.entries[keyIndex];
return true;
}
// replace current value
const newEntry = [key, newValue];
this.entries[keyIndex] = newEntry;
return true;
}
if (undefined === options.ifNew)
return false;
// no match, replace entry with entryset containing both entries
const newValue = OptLazyOr(options.ifNew, Token);
if (Token === newValue)
return false;
this.source = undefined;
this.size++;
delete this.entries[keyIndex];
const newEntrySet = this.level < this.context.maxDepth
? new HashMapBlockBuilder(this.context, undefined, undefined, undefined, 0, this.level + 1)
: new HashMapCollisionBuilder(this.context);
newEntrySet.addEntryInternal(currentEntry);
newEntrySet.addEntryInternal([key, newValue], keyHash);
this.entrySets[keyIndex] = newEntrySet;
return true;
}
if (keyIndex in this.entrySets) {
// potential match in entrysets
const entrySet = this.entrySets[keyIndex];
const preSize = entrySet.size;
const result = entrySet.modifyAt(key, options, keyHash);
if (result)
this.source = undefined;
this.size += entrySet.size - preSize;
if (entrySet.size > 1)
return result;
// single entry needs to be pulled up
let first = undefined;
if (this.context.isHashMapBlockBuilder(entrySet)) {
for (const index in entrySet.entries) {
first = entrySet.entries[index];
break;
}
}
else {
first = entrySet.entries.get(0, RimbuError.throwInvalidStateError);
}
delete this.entrySets[keyIndex];
this.entries[keyIndex] = first;
return true;
}
if (undefined === options.ifNew)
return false;
// no matching entry or entrySet
const newValue = OptLazyOr(options.ifNew, Token);
if (Token === newValue)
return false;
this.source = undefined;
this.size++;
this.entries[keyIndex] = [key, newValue];
return true;
};
// prettier-ignore
this.updateAt = (key, update, otherwise) => {
let result;
let found = false;
this.modifyAt(key, {
ifExists: (value) => {
result = value;
found = true;
return Update(value, update);
},
});
if (!found)
return OptLazy(otherwise);
return result;
};
this.removeKey = (key, otherwise) => {
this.checkLock();
if (!this.context.hasher.isValid(key))
return OptLazy(otherwise);
let removedValue;
let found = false;
this.modifyAt(key, {
ifExists: (currentValue, remove) => {
removedValue = currentValue;
found = true;
return remove;
},
});
if (!found)
return OptLazy(otherwise);
return removedValue;
};
// prettier-ignore
this.removeKeys = (keys) => {
this.checkLock();
if (isEmptyStreamSourceInstance(keys))
return false;
const notFound = Symbol();
return (Stream.from(keys)
.mapPure(this.removeKey, notFound)
.countElement(notFound, { negate: true }) > 0);
};
this.forEach = (f, options = {}) => {
const { state = TraverseState() } = options;
this._lock++;
super.forEach(f, { state });
this._lock--;
};
this.build = () => {
if (this.size === 0)
return this.context.empty();
return this.buildNE();
};
// prettier-ignore
this.buildMapValues = (f) => {
if (this.size === 0) {
return this.context.empty();
}
if (undefined !== this.source)
return this.source.mapValues(f);
const entries = this.entries.length === 0
? null
: Arr.mapSparse(this.entries, (e) => [
e[0],
f(e[1], e[0]),
]);
const entrySets = this.entrySets.length === 0
? null
: Arr.mapSparse(this.entrySets, (entrySet) => entrySet.buildMapValues(f));
return this.context.block(entries, entrySets, this.size, this.level);
};
}
checkLock() {
if (this._lock)
RimbuError.throwModifiedBuilderWhileLoopingOverItError();
}
prepareMutate() {
if (undefined === this._entries) {
if (undefined !== this.source) {
this._entries =
null === this.source.entries
? []
: Arr.copySparse(this.source.entries);
}
else {
this._entries = [];
}
}
if (undefined === this._entrySets) {
if (undefined !== this.source) {
this._entrySets =
null === this.source.entrySets
? []
: (Arr.mapSparse(this.source.entrySets, (entrySet) => {
if (this.context.isHashMapBlock(entrySet)) {
return new HashMapBlockBuilder(this.context, entrySet);
}
return new HashMapCollisionBuilder(this.context, entrySet);
}) ?? []);
}
else {
this._entrySets = [];
}
}
}
get entries() {
this.prepareMutate();
return this._entries;
}
get entrySets() {
this.prepareMutate();
return this._entrySets;
}
addEntryInternal(entry, hash = this.context.hash(entry[0])) {
const keyIndex = this.context.getKeyIndex(this.level, hash);
if (keyIndex in this.entries) {
const currentEntry = this.entries[keyIndex];
if (this.context.eq(entry[0], currentEntry[0])) {
if (Object.is(entry[1], currentEntry[1]))
return false;
this.source = undefined;
this.entries[keyIndex] = entry;
return true;
}
this.source = undefined;
this.size++;
delete this.entries[keyIndex];
if (this.level < this.context.maxDepth) {
const newEntrySet = new HashMapBlockBuilder(this.context, undefined, undefined, undefined, 0, this.level + 1);
newEntrySet.addEntryInternal(currentEntry);
newEntrySet.addEntryInternal(entry, hash);
this.entrySets[keyIndex] = newEntrySet;
return true;
}
const newEntries = List.builder();
newEntries.append(currentEntry);
newEntries.append(entry);
const newEntrySet = new HashMapCollisionBuilder(this.context, undefined, newEntries);
this.entrySets[keyIndex] = newEntrySet;
return true;
}
if (keyIndex in this.entrySets) {
const currentEntrySet = this.entrySets[keyIndex];
const preSize = currentEntrySet.size;
const changed = currentEntrySet.addEntryInternal(entry, hash);
if (changed)
this.source = undefined;
this.size += currentEntrySet.size - preSize;
return changed;
}
this.source = undefined;
this.size++;
this.entries[keyIndex] = entry;
return true;
}
buildNE() {
if (undefined !== this.source)
return this.source;
const entries = this.entries.length === 0 ? null : Arr.copySparse(this.entries);
const entrySets = this.entrySets.length === 0
? null
: Arr.mapSparse(this.entrySets, (entrySet) => entrySet.buildNE());
return this.context.block(entries, entrySets, this.size, this.level);
}
}
export class HashMapCollisionBuilder extends CollisionBuilderBase {
constructor(context, source, _entries) {
super();
this.context = context;
this.source = source;
this._entries = _entries;
}
get(key, otherwise, hash) {
if (!this.context.hasher.isValid(key))
return OptLazy(otherwise);
if (undefined !== this.source)
return this.source.get(key, otherwise);
const token = Symbol();
let result = token;
this.entries.forEach((e, _, halt) => {
if (this.context.eq(key, e[0])) {
result = e[1];
halt();
}
});
if (token === result)
return OptLazy(otherwise);
return result;
}
addEntryInternal(entry) {
let index = -1;
this.entries.forEach((e, i, halt) => {
if (this.context.eq(e[0], entry[0])) {
index = i;
halt();
}
});
if (index < 0) {
this.source = undefined;
this.entries.append(entry);
return true;
}
const oldEntry = this.entries.updateAt(index, (currentEntry) => {
if (Object.is(currentEntry[1], entry[1]))
return currentEntry;
return entry;
});
const changed = undefined === oldEntry ||
!Object.is(oldEntry[1], entry[1]) ||
!Object.is(oldEntry[0], entry[0]);
if (changed) {
this.source = undefined;
}
return changed;
}
set(key, value) {
return this.addEntryInternal([key, value]);
}
modifyAt(atKey, options) {
let index = -1;
let foundEntry = undefined;
this.entries.forEach((e, i, halt) => {
if (this.context.eq(e[0], atKey)) {
index = i;
foundEntry = e;
halt();
}
});
if (undefined === foundEntry) {
if (undefined === options.ifNew)
return false;
const newValue = OptLazyOr(options.ifNew, Token);
if (Token === newValue)
return false;
this.source = undefined;
this.entries.append([atKey, newValue]);
return true;
}
if (undefined === options.ifExists)
return false;
const newValue = options.ifExists instanceof Function
? options.ifExists(foundEntry[1], Token)
: options.ifExists;
if (Object.is(newValue, foundEntry[1]))
return false;
if (Token === newValue) {
this.source = undefined;
this.entries.remove(index);
return true;
}
const result = this.entries.set(index, [atKey, newValue]);
const changed = undefined !== result;
if (changed)
this.source = undefined;
return changed;
}
buildNE() {
if (undefined !== this.source)
return this.source;
return this.context.collision(this.entries.build().assumeNonEmpty());
}
buildMapValues(f) {
if (undefined !== this.source)
return this.source.mapValues(f);
return this.context.collision(this.entries
.buildMap((entry) => [
entry[0],
f(entry[1], entry[0]),
])
.assumeNonEmpty());
}
}
//# sourceMappingURL=builder.mjs.map