@rimbu/hashed
Version:
Immutable HashMap and HashSet implementations for TypeScript
355 lines • 12.7 kB
JavaScript
import { Arr, RimbuError } from '@rimbu/base';
import { EmptyBase, NonEmptyBase } from '@rimbu/collection-types/set-custom';
import { TraverseState, } from '@rimbu/common';
import { Stream } from '@rimbu/stream';
import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom';
export class HashSetEmpty extends EmptyBase {
constructor(context) {
super();
this.context = context;
this.addAll = context.from;
}
has() {
return false;
}
add(value) {
return this.context.emptyBlock().add(value);
}
remove() {
return this;
}
removeAll() {
return this;
}
union(other) {
if (this.context.isHashSetBlock(other) ||
this.context.isHashSetCollision(other)) {
if (other.context === this.context)
return other;
}
return this.context.from(other);
}
difference() {
return this.context.empty();
}
intersect() {
return this.context.empty();
}
symDifference(other) {
return this.union(other);
}
toBuilder() {
return this.context.builder();
}
toString() {
return `HashSet()`;
}
toJSON() {
return {
dataType: this.context.typeTag,
value: [],
};
}
}
export class HashSetNonEmptyBase extends NonEmptyBase {
asNormal() {
return this;
}
addAll(values) {
if (isEmptyStreamSourceInstance(values))
return this;
const builder = this.toBuilder();
builder.addAll(values);
return builder.build();
}
removeAll(values) {
if (isEmptyStreamSourceInstance(values))
return this;
const builder = this.toBuilder();
builder.removeAll(values);
return builder.build();
}
filter(pred, options = {}) {
const builder = this.context.builder();
builder.addAll(this.stream().filter(pred, options));
if (builder.size === this.size)
return this;
return builder.build();
}
union(other) {
if (other === this)
return this;
if (isEmptyStreamSourceInstance(other))
return this;
const builder = this.toBuilder();
builder.addAll(other);
return builder.build().assumeNonEmpty();
}
difference(other) {
if (other === this)
return this.context.empty();
if (isEmptyStreamSourceInstance(other))
return this;
const builder = this.toBuilder();
builder.removeAll(other);
return builder.build();
}
intersect(other) {
if (other === this)
return this;
if (isEmptyStreamSourceInstance(other))
return this.context.empty();
const builder = this.context.builder();
const it = Stream.from(other)[Symbol.iterator]();
const done = Symbol('Done');
let value;
while (done !== (value = it.fastNext(done))) {
if (this.has(value))
builder.add(value);
}
if (builder.size === this.size)
return this;
return builder.build();
}
symDifference(other) {
if (other === this)
return this.context.empty();
if (isEmptyStreamSourceInstance(other))
return this;
const builder = this.toBuilder();
Stream.from(other)
.filterPure({ pred: builder.remove, negate: true })
.forEach(builder.add);
return builder.build();
}
toBuilder() {
return this.context.createBuilder(this);
}
toString() {
return this.stream().join({ start: 'HashSet(', sep: ', ', end: ')' });
}
toJSON() {
return {
dataType: this.context.typeTag,
value: [],
};
}
}
export class HashSetBlock extends HashSetNonEmptyBase {
constructor(context, entries, entrySets, size, level) {
super();
this.context = context;
this.entries = entries;
this.entrySets = entrySets;
this.size = size;
this.level = level;
}
copy(entries = this.entries, entrySets = this.entrySets, size = this.size) {
if (entries === this.entries &&
entrySets === this.entrySets &&
size === this.size) {
return this;
}
return new HashSetBlock(this.context, entries, entrySets, size, this.level);
}
stream() {
if (null !== this.entries) {
if (null === this.entrySets) {
return Stream.fromObjectValues(this.entries);
}
return Stream.fromObjectValues(this.entries).concat(Stream.fromObjectValues(this.entrySets).flatMap((entrySet) => entrySet.stream()));
}
if (null === this.entrySets) {
RimbuError.throwInvalidStateError();
}
return Stream.fromObjectValues(this.entrySets).flatMap((entrySet) => entrySet.stream());
}
has(value, inHash) {
if (!this.context.hasher.isValid(value))
return false;
const hash = inHash ?? this.context.hash(value);
const atKeyIndex = this.context.getKeyIndex(this.level, hash);
if (null !== this.entries && atKeyIndex in this.entries) {
const entry = this.entries[atKeyIndex];
return this.context.eq(entry, value);
}
if (null !== this.entrySets && atKeyIndex in this.entrySets) {
const entrySet = this.entrySets[atKeyIndex];
return entrySet.has(value, hash);
}
return false;
}
add(value, hash = this.context.hash(value)) {
const atKeyIndex = this.context.getKeyIndex(this.level, hash);
if (null !== this.entries && atKeyIndex in this.entries) {
const currentValue = this.entries[atKeyIndex];
if (this.context.eq(value, currentValue))
return this;
let newEntries = Arr.copySparse(this.entries);
delete newEntries[atKeyIndex];
let isEmpty = true;
/* eslint-disable @typescript-eslint/no-unused-vars */
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)
.add(currentValue)
.add(value, hash);
const newEntrySets = null === this.entrySets ? [] : Arr.copySparse(this.entrySets);
newEntrySets[atKeyIndex] = newEntrySet;
return this.copy(newEntries, newEntrySets, this.size + 1);
}
const newEntrySet = this.context.collision(this.context.listContext.of(currentValue, value));
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.add(value, hash);
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] = value;
return this.copy(newEntries, undefined, this.size + 1);
}
remove(value, hash) {
if (!this.context.hasher.isValid(value))
return this;
const valueHash = hash ?? this.context.hash(value);
const atKeyIndex = this.context.getKeyIndex(this.level, valueHash);
if (null !== this.entries && atKeyIndex in this.entries) {
const currentValue = this.entries[atKeyIndex];
if (!this.context.eq(currentValue, value))
return this;
if (this.size === 1)
return this.context.empty();
const newEntries = Arr.copySparse(this.entries);
delete newEntries[atKeyIndex];
for (const _ in newEntries) {
return this.copy(newEntries, undefined, this.size - 1);
}
return this.copy(null, undefined, this.size - 1);
}
if (null !== this.entrySets && atKeyIndex in this.entrySets) {
// key is in entrySet
const currentEntrySet = this.entrySets[atKeyIndex];
const newEntrySet = currentEntrySet.remove(value, hash);
if (newEntrySet === currentEntrySet)
return this;
if (newEntrySet.size === 1) {
let firstValue = undefined;
if (this.context.isHashSetBlock(newEntrySet)) {
for (const key in newEntrySet.entries) {
firstValue = newEntrySet.entries[key];
break;
}
}
else {
firstValue = newEntrySet.entries.first();
}
const newEntries = null === this.entries ? [] : Arr.copySparse(this.entries);
newEntries[atKeyIndex] = firstValue;
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 - currentEntrySet.size + newEntrySet.size);
}
return this;
}
forEach(f, options = {}) {
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;
}
}
}
toArray() {
let result = [];
if (null !== this.entries) {
for (const key in this.entries) {
result.push(this.entries[key]);
}
}
if (null !== this.entrySets) {
for (const key in this.entrySets) {
result = result.concat(this.entrySets[key].toArray());
}
}
return result;
}
}
export class HashSetCollision extends HashSetNonEmptyBase {
constructor(context, entries) {
super();
this.context = context;
this.entries = entries;
}
get size() {
return this.entries.length;
}
copy(entries = this.entries) {
if (entries === this.entries)
return this;
return new HashSetCollision(this.context, entries);
}
stream() {
return this.entries.stream();
}
has(value, inHash) {
if (!this.context.hasher.isValid(value))
return false;
return this.stream().contains(value, { eq: this.context.eq });
}
add(value) {
const currentIndex = this.stream().indexOf(value, { eq: this.context.eq });
if (undefined === currentIndex) {
return this.copy(this.entries.append(value));
}
return this.copy(this.entries.updateAt(currentIndex, value));
}
remove(value, hash) {
if (!this.context.hasher.isValid(value))
return this;
const currentIndex = this.stream().indexOf(value, { eq: this.context.eq });
if (undefined === currentIndex)
return this;
const newEntries = this.entries.remove(currentIndex).assumeNonEmpty();
return this.copy(newEntries);
}
forEach(f, options = {}) {
const { state = TraverseState() } = options;
if (state.halted)
return;
this.entries.forEach(f, { state });
}
toArray() {
return this.entries.toArray();
}
}
//# sourceMappingURL=immutable.mjs.map