UNPKG

@rimbu/hashed

Version:

Immutable HashMap and HashSet implementations for TypeScript

355 lines 12.7 kB
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