UNPKG

@rimbu/hashed

Version:

Immutable HashMap and HashSet implementations for TypeScript

505 lines (390 loc) 13.5 kB
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(); } }