@rimbu/hashed
Version:
Immutable HashMap and HashSet implementations for TypeScript
262 lines • 9.4 kB
JavaScript
import { Arr, RimbuError } from '@rimbu/base';
import { TraverseState } from '@rimbu/common';
import { List } from '@rimbu/list';
import { Stream } from '@rimbu/stream';
import { BlockBuilderBase, CollisionBuilderBase } from '@rimbu/hashed/common';
export class HashSetBlockBuilder 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;
// prettier-ignore
this.has = (value) => {
if (undefined !== this.source)
return this.source.has(value);
if (!this.context.hasher.isValid(value))
return false;
return this.hasInternal(value);
};
this.add = (value) => {
this.checkLock();
return this.addInternal(value);
};
this.addAll = (source) => {
this.checkLock();
return Stream.from(source).filterPure({ pred: this.add }).count() > 0;
};
// prettier-ignore
this.remove = (value) => {
this.checkLock();
if (!this.context.hasher.isValid(value))
return false;
return this.removeInternal(value);
};
// prettier-ignore
this.removeAll = (values) => {
this.checkLock();
return Stream.from(values).filterPure({ pred: this.remove }).count() > 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();
};
}
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 && null !== this.source.entrySets) {
this._entrySets = Arr.mapSparse(this.source.entrySets, (entrySet) => {
if (this.context.isHashSetBlock(entrySet)) {
return new HashSetBlockBuilder(this.context, entrySet);
}
return new HashSetCollisionBuilder(this.context, entrySet);
});
}
else {
this._entrySets = [];
}
}
}
get entries() {
this.prepareMutate();
return this._entries;
}
get entrySets() {
this.prepareMutate();
return this._entrySets;
}
hasInternal(value, hash = this.context.hash(value)) {
if (undefined !== this.source)
return this.source.has(value, hash);
const keyIndex = this.context.getKeyIndex(this.level, hash);
if (keyIndex in this.entries) {
return this.context.eq(value, this.entries[keyIndex]);
}
if (keyIndex in this.entrySets) {
const currentEntrySet = this.entrySets[keyIndex];
return currentEntrySet.hasInternal(value, hash);
}
return false;
}
addInternal(value, hash = this.context.hash(value)) {
const keyIndex = this.context.getKeyIndex(this.level, hash);
if (keyIndex in this.entries) {
const currentEntry = this.entries[keyIndex];
if (this.context.eq(value, currentEntry))
return false;
this.source = undefined;
this.size++;
delete this.entries[keyIndex];
if (this.level < this.context.maxDepth) {
const newEntrySet = new HashSetBlockBuilder(this.context, undefined, undefined, undefined, 0, this.level + 1);
newEntrySet.addInternal(currentEntry);
newEntrySet.addInternal(value, hash);
this.entrySets[keyIndex] = newEntrySet;
return true;
}
const newEntries = List.builder();
newEntries.append(currentEntry);
newEntries.append(value);
const newEntrySet = new HashSetCollisionBuilder(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.addInternal(value, hash);
if (changed)
this.source = undefined;
this.size += currentEntrySet.size - preSize;
return changed;
}
this.source = undefined;
this.size++;
this.entries[keyIndex] = value;
return true;
}
removeInternal(value, hash = this.context.hash(value)) {
const index = this.context.getKeyIndex(this.level, hash);
if (index in this.entries) {
// potential match in entries
const currentValue = this.entries[index];
if (!this.context.eq(value, currentValue))
return false;
// exact match
this.source = undefined;
this.size--;
delete this.entries[index];
return true;
}
if (index in this.entrySets) {
// potential match in entrysets
const entrySet = this.entrySets[index];
const preSize = entrySet.size;
if (!entrySet.removeInternal(value, hash)) {
return false;
}
this.source = undefined;
this.size += entrySet.size - preSize;
if (entrySet.size > 1)
return true;
// single entry needs to be pulled up
let first = undefined;
if (this.context.isHashSetBlockBuilder(entrySet)) {
for (const i in entrySet.entries) {
first = entrySet.entries[i];
break;
}
}
else {
first = entrySet.entries.get(0, RimbuError.throwInvalidStateError);
}
delete this.entrySets[index];
// if the sparse emptySets array is empty, set it to an empty array
let hasEntrySets = false;
for (const _ in this.entrySets) {
hasEntrySets = true;
break;
}
if (!hasEntrySets) {
this.entrySets.length = 0;
}
this.entries[index] = first;
return true;
}
return false;
}
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 HashSetCollisionBuilder extends CollisionBuilderBase {
constructor(context, source, _entries) {
super();
this.context = context;
this.source = source;
this._entries = _entries;
}
hasInternal(value, hash) {
if (undefined !== this.source)
return this.source.has(value, hash);
let result = false;
this.entries.forEach((v, _, halt) => {
if (this.context.eq(v, value)) {
result = true;
halt();
}
});
return result;
}
addInternal(value) {
let index = -1;
this.entries.forEach((v, i, halt) => {
if (this.context.eq(v, value)) {
index = i;
halt();
}
});
if (index < 0) {
this.source = undefined;
this.entries.append(value);
return true;
}
const token = Symbol();
const oldValue = this.entries.set(index, value, token);
const changed = token === oldValue || !this.context.eq(oldValue, value);
if (changed)
this.source = undefined;
return changed;
}
removeInternal(value) {
let index = -1;
this.entries.forEach((v, i, halt) => {
if (this.context.eq(v, value)) {
index = i;
halt();
}
});
if (index < 0)
return false;
this.source = undefined;
this.entries.remove(index);
return true;
}
buildNE() {
return (this.source ??
this.context.collision(this.entries.build().assumeNonEmpty()));
}
}
//# sourceMappingURL=builder.mjs.map