@rimbu/hashed
Version:
Immutable HashMap and HashSet implementations for TypeScript
505 lines (390 loc) • 13.5 kB
text/typescript
import { Arr, RimbuError } from '@rimbu/base';
import { EmptyBase, NonEmptyBase } from '@rimbu/collection-types/set-custom';
import {
type ArrayNonEmpty,
type RelatedTo,
type ToJSON,
TraverseState,
} from '@rimbu/common';
import type { List } from '@rimbu/list';
import { Stream, type StreamSource } from '@rimbu/stream';
import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom';
import type { HashSet } from '@rimbu/hashed/set';
import type { HashSetContext } from '@rimbu/hashed/set-custom';
export class HashSetEmpty<T = any> extends EmptyBase implements HashSet<T> {
_NonEmptyType!: HashSet.NonEmpty<T>;
readonly addAll: any;
constructor(readonly context: HashSetContext<T>) {
super();
this.addAll = context.from;
}
has(): false {
return false;
}
add(value: T): HashSet.NonEmpty<T> {
return this.context.emptyBlock().add(value);
}
remove(): this {
return this;
}
removeAll(): this {
return this;
}
union(other: StreamSource<T>): HashSet<T> | any {
if (
this.context.isHashSetBlock(other as any) ||
this.context.isHashSetCollision(other as any)
) {
if ((other as any).context === this.context) return other;
}
return this.context.from(other);
}
difference(): HashSet<T> {
return this.context.empty();
}
intersect(): HashSet<T> {
return this.context.empty();
}
symDifference(other: StreamSource<T>): HashSet<T> {
return this.union(other);
}
toBuilder(): HashSet.Builder<T> {
return this.context.builder();
}
toString(): string {
return `HashSet()`;
}
toJSON(): ToJSON<T[]> {
return {
dataType: this.context.typeTag,
value: [],
};
}
}
export abstract class HashSetNonEmptyBase<T>
extends NonEmptyBase<T>
implements HashSet.NonEmpty<T>
{
_NonEmptyType!: HashSet.NonEmpty<T>;
abstract get context(): HashSetContext<T>;
abstract get size(): number;
abstract stream(): Stream.NonEmpty<T>;
abstract forEach(
f: (value: T, index: number, halt: () => void) => void,
options?: { state?: TraverseState }
): void;
abstract has<U>(value: RelatedTo<T, U>): boolean;
abstract add(value: T): HashSetNonEmptyBase<T>;
abstract remove<U>(value: RelatedTo<T, U>): HashSet<T>;
abstract toArray(): ArrayNonEmpty<T>;
asNormal(): this {
return this;
}
addAll(values: StreamSource<T>): HashSet.NonEmpty<T> {
if (isEmptyStreamSourceInstance(values)) return this;
const builder = this.toBuilder();
builder.addAll(values);
return builder.build() as HashSet.NonEmpty<T>;
}
removeAll(values: StreamSource<T>): HashSet<T> {
if (isEmptyStreamSourceInstance(values)) return this;
const builder = this.toBuilder();
builder.removeAll(values);
return builder.build();
}
filter(
pred: (value: T, index: number, halt: () => void) => boolean,
options: { negate?: boolean | undefined } = {}
): any {
const builder = this.context.builder();
builder.addAll(this.stream().filter(pred, options));
if (builder.size === this.size) return this;
return builder.build();
}
union(other: StreamSource<T>): HashSet.NonEmpty<T> {
if (other === this) return this;
if (isEmptyStreamSourceInstance(other)) return this;
const builder = this.toBuilder();
builder.addAll(other);
return builder.build().assumeNonEmpty();
}
difference(other: StreamSource<T>): HashSet<T> {
if (other === this) return this.context.empty();
if (isEmptyStreamSourceInstance(other)) return this;
const builder = this.toBuilder();
builder.removeAll(other);
return builder.build();
}
intersect(other: StreamSource<T>): HashSet<T> {
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: T | typeof done;
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: StreamSource<T>): HashSet<T> {
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(): HashSet.Builder<T> {
return this.context.createBuilder(this);
}
toString(): string {
return this.stream().join({ start: 'HashSet(', sep: ', ', end: ')' });
}
toJSON(): ToJSON<T[]> {
return {
dataType: this.context.typeTag,
value: [],
};
}
}
export type SetEntrySet<T> = HashSetBlock<T> | HashSetCollision<T>;
export class HashSetBlock<T> extends HashSetNonEmptyBase<T> {
constructor(
readonly context: HashSetContext<T>,
readonly entries: readonly T[] | null,
readonly entrySets: readonly SetEntrySet<T>[] | null,
readonly size: number,
readonly level: number
) {
super();
}
copy(
entries = this.entries,
entrySets = this.entrySets,
size = this.size
): HashSetBlock<T> {
if (
entries === this.entries &&
entrySets === this.entrySets &&
size === this.size
) {
return this;
}
return new HashSetBlock(this.context, entries, entrySets, size, this.level);
}
stream(): Stream.NonEmpty<T> {
if (null !== this.entries) {
if (null === this.entrySets) {
return Stream.fromObjectValues(this.entries) as Stream.NonEmpty<T>;
}
return Stream.fromObjectValues(this.entries).concat(
Stream.fromObjectValues(this.entrySets).flatMap(
(entrySet): Stream.NonEmpty<T> => entrySet.stream()
)
) as Stream.NonEmpty<T>;
}
if (null === this.entrySets) {
RimbuError.throwInvalidStateError();
}
return Stream.fromObjectValues(this.entrySets).flatMap(
(entrySet): Stream.NonEmpty<T> => entrySet.stream()
) as Stream.NonEmpty<T>;
}
has<U>(value: RelatedTo<T, U>, inHash?: number): boolean {
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<U>(value, hash);
}
return false;
}
add(value: T, hash = this.context.hash(value)): HashSetBlock<T> {
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: T[] | null = 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<U>(value: RelatedTo<T, U>, hash?: number): HashSet<T> {
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<U>(
value,
hash
) as SetEntrySet<T>;
if (newEntrySet === currentEntrySet) return this;
if (newEntrySet.size === 1) {
let firstValue: T = undefined as any;
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: (entry: T, 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;
}
}
}
toArray(): ArrayNonEmpty<T> {
let result: T[] = [];
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 as ArrayNonEmpty<T>;
}
}
export class HashSetCollision<T> extends HashSetNonEmptyBase<T> {
constructor(
readonly context: HashSetContext<T>,
readonly entries: List.NonEmpty<T>
) {
super();
}
get size(): number {
return this.entries.length;
}
copy(entries = this.entries): HashSetCollision<T> {
if (entries === this.entries) return this;
return new HashSetCollision(this.context, entries);
}
stream(): Stream.NonEmpty<T> {
return this.entries.stream();
}
has<U>(value: RelatedTo<T, U>, inHash?: number): boolean {
if (!this.context.hasher.isValid(value)) return false;
return this.stream().contains(value, { eq: this.context.eq });
}
add(value: T): HashSetCollision<T> {
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<U>(value: RelatedTo<T, U>, hash?: number): HashSet<T> {
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: (entry: T, index: number, halt: () => void) => void,
options: { state?: TraverseState } = {}
): void {
const { state = TraverseState() } = options;
if (state.halted) return;
this.entries.forEach(f, { state });
}
toArray(): ArrayNonEmpty<T> {
return this.entries.toArray();
}
}