@rimbu/hashed
Version:
Immutable HashMap and HashSet implementations for TypeScript
471 lines • 17.6 kB
JavaScript
import { Arr, Entry, RimbuError, Token } from '@rimbu/base';
import { EmptyBase, NonEmptyBase } from '@rimbu/collection-types/map-custom';
import { OptLazy, OptLazyOr, TraverseState, Update, } from '@rimbu/common';
import { Stream } from '@rimbu/stream';
import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom';
export class HashMapEmpty extends EmptyBase {
constructor(context) {
super();
this.context = context;
}
streamKeys() {
return Stream.empty();
}
streamValues() {
return Stream.empty();
}
get(key, otherwise) {
return OptLazy(otherwise);
}
hasKey() {
return false;
}
set(key, value) {
return this.context.emptyBlock().set(key, value);
}
addEntry(entry) {
return this.context.emptyBlock().addEntry(entry);
}
addEntries(entries) {
return this.context.from(entries);
}
removeKeyAndGet() {
return undefined;
}
removeKey() {
return this;
}
removeKeys() {
return this;
}
modifyAt(atKey, options) {
if (undefined !== options.ifNew) {
const value = OptLazyOr(options.ifNew, Token);
if (Token === value)
return this;
return this.set(atKey, value);
}
return this;
}
mapValues() {
return this;
}
updateAt() {
return this;
}
toBuilder() {
return this.context.builder();
}
toString() {
return `HashMap()`;
}
toJSON() {
return {
dataType: this.context.typeTag,
value: [],
};
}
}
export class HashMapNonEmptyBase extends NonEmptyBase {
asNormal() {
return this;
}
streamKeys() {
return this.stream().map(Entry.first);
}
streamValues() {
return this.stream().map(Entry.second);
}
hasKey(key) {
const token = Symbol();
return token !== this.get(key, token);
}
set(key, value) {
return this.addEntry([key, value]);
}
addEntries(entries) {
if (isEmptyStreamSourceInstance(entries))
return this;
const builder = this.toBuilder();
builder.addEntries(entries);
return builder.build().assumeNonEmpty();
}
removeKeys(keys) {
if (isEmptyStreamSourceInstance(keys))
return this;
const builder = this.toBuilder();
builder.removeKeys(keys);
return builder.build();
}
updateAt(key, update) {
if (!this.context.isValidKey(key))
return this;
return this.modifyAt(key, {
ifExists: (value) => Update(value, update),
});
}
removeKey(key) {
if (!this.context.hasher.isValid(key))
return this;
return this.modifyAt(key, {
ifExists: (_, remove) => remove,
});
}
removeKeyAndGet(key) {
if (!this.context.hasher.isValid(key))
return undefined;
const token = Symbol();
let currentValue = token;
const newMap = this.modifyAt(key, {
ifExists: (value, remove) => {
currentValue = value;
return remove;
},
});
if (token === currentValue)
return undefined;
return [newMap, currentValue];
}
filter(pred, options = {}) {
const builder = this.context.builder();
builder.addEntries(this.stream().filter(pred, options));
if (builder.size === this.size)
return this;
return builder.build();
}
toBuilder() {
return this.context.createBuilder(this);
}
toString() {
return this.stream().join({
start: 'HashMap(',
sep: ', ',
end: ')',
valueToString: (entry) => `${entry[0]} -> ${entry[1]}`,
});
}
toJSON() {
return {
dataType: this.context.typeTag,
value: this.toArray(),
};
}
}
export class HashMapBlock extends HashMapNonEmptyBase {
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 HashMapBlock(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());
}
get(key, otherwise, hash) {
if (!this.context.hasher.isValid(key))
return OptLazy(otherwise);
const keyHash = hash ?? this.context.hash(key);
const atKeyIndex = this.context.getKeyIndex(this.level, keyHash);
if (null !== this.entries && atKeyIndex in this.entries) {
const entry = this.entries[atKeyIndex];
if (this.context.eq(entry[0], key))
return entry[1];
return OptLazy(otherwise);
}
if (null !== this.entrySets && atKeyIndex in this.entrySets) {
const entrySet = this.entrySets[atKeyIndex];
return entrySet.get(key, otherwise, keyHash);
}
return OptLazy(otherwise);
}
addEntry(entry, hash = this.context.hash(entry[0])) {
const atKeyIndex = this.context.getKeyIndex(this.level, hash);
if (null !== this.entries && atKeyIndex in this.entries) {
const currentEntry = this.entries[atKeyIndex];
if (this.context.eq(entry[0], currentEntry[0])) {
if (Object.is(entry[1], currentEntry[1]))
return this;
const newEntries = Arr.copySparse(this.entries);
newEntries[atKeyIndex] = entry;
return this.copy(newEntries);
}
let newEntries = Arr.copySparse(this.entries);
delete newEntries[atKeyIndex];
let isEmpty = true;
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)
.addEntry(currentEntry)
.addEntry(entry, 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(currentEntry, entry));
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.addEntry(entry, 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] = entry;
return this.copy(newEntries, undefined, this.size + 1);
}
modifyAt(atKey, options, atKeyHash = this.context.hash(atKey)) {
const atKeyIndex = this.context.getKeyIndex(this.level, atKeyHash);
if (null !== this.entries && atKeyIndex in this.entries) {
const currentEntry = this.entries[atKeyIndex];
if (this.context.eq(atKey, currentEntry[0])) {
// exact key match
if (undefined === options.ifExists)
return this;
const currentValue = currentEntry[1];
const newValue = options.ifExists instanceof Function
? options.ifExists(currentValue, Token)
: options.ifExists;
if (Object.is(newValue, currentValue))
return this;
const newEntries = Arr.copySparse(this.entries);
if (Token === newValue) {
delete newEntries[atKeyIndex];
for (const _ in newEntries) {
return this.copy(newEntries, undefined, this.size - 1);
}
if (this.size === 1)
return this.context.empty();
return this.copy(null, undefined, this.size - 1);
}
newEntries[atKeyIndex] = [atKey, newValue];
return this.copy(newEntries);
}
// no exact match, but key collision
if (undefined === options.ifNew)
return this;
const newValue = OptLazyOr(options.ifNew, Token);
if (Token === newValue)
return this;
let newEntries = Arr.copySparse(this.entries);
delete newEntries[atKeyIndex];
let isEmpty = true;
for (const _ in newEntries) {
isEmpty = false;
break;
}
if (isEmpty)
newEntries = null;
if (this.level < this.context.maxDepth) {
// create next level block
const newEntrySet = this.context
.block(null, null, 0, this.level + 1)
.addEntry(currentEntry)
.set(atKey, newValue);
const newEntrySets = null === this.entrySets ? [] : Arr.copySparse(this.entrySets);
newEntrySets[atKeyIndex] = newEntrySet;
return this.copy(newEntries, newEntrySets, this.size + 1);
}
// create collision
const newEntry = [atKey, newValue];
const newEntrySet = this.context.collision(this.context.listContext.of(currentEntry, newEntry));
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) {
// key is in entrySet
const currentEntrySet = this.entrySets[atKeyIndex];
const newEntrySet = currentEntrySet.modifyAt(atKey, options, atKeyHash);
if (newEntrySet === currentEntrySet)
return this;
if (newEntrySet.size === 1) {
let firstEntry = undefined;
if (this.context.isHashMapBlock(newEntrySet)) {
for (const key in newEntrySet.entries) {
firstEntry = newEntrySet.entries[key];
break;
}
}
else {
firstEntry = newEntrySet.entries.first();
}
const newEntries = null === this.entries ? [] : Arr.copySparse(this.entries);
newEntries[atKeyIndex] = firstEntry;
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 + newEntrySet.size - currentEntrySet.size);
}
if (undefined === options.ifNew)
return this;
const newValue = OptLazyOr(options.ifNew, Token);
if (Token === newValue)
return this;
const newEntry = [atKey, newValue];
const newEntries = null === this.entries ? [] : Arr.copySparse(this.entries);
newEntries[atKeyIndex] = newEntry;
return this.copy(newEntries, undefined, this.size + 1);
}
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;
}
}
}
mapValues(mapFun) {
const newEntries = null === this.entries
? null
: Arr.mapSparse(this.entries, (e) => [
e[0],
mapFun(e[1], e[0]),
]);
const newEntrySets = null === this.entrySets
? null
: Arr.mapSparse(this.entrySets, (es) => es.mapValues(mapFun));
return new HashMapBlock(this.context, newEntries, newEntrySets, this.size, this.level);
}
toArray() {
let result = [];
if (null !== this.entries) {
result = Stream.fromObjectValues(this.entries).toArray();
}
if (null !== this.entrySets) {
Stream.fromObjectValues(this.entrySets).forEach((entrySet) => {
result = result.concat(entrySet.toArray());
});
}
return result;
}
}
export class HashMapCollision extends HashMapNonEmptyBase {
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 HashMapCollision(this.context, entries);
}
stream() {
return this.entries.stream();
}
get(key, otherwise, keyHash) {
if (!this.context.hasher.isValid(key))
return OptLazy(otherwise);
const token = Symbol();
const stream = this.stream();
const foundEntry = stream.find((entry) => this.context.eq(entry[0], key), {
otherwise: token,
});
if (token === foundEntry)
return OptLazy(otherwise);
return foundEntry[1];
}
addEntry(entry, hash) {
const currentIndex = this.stream().indexWhere((currentEntry) => this.context.eq(currentEntry[0], entry[0]));
if (undefined === currentIndex) {
return this.copy(this.entries.append(entry));
}
return this.copy(this.entries.updateAt(currentIndex, (currentEntry) => {
if (Object.is(currentEntry[1], entry[1]))
return currentEntry;
return entry;
}));
}
modifyAt(atKey, options, atKeyHash) {
const currentIndex = this.stream().indexWhere((entry) => this.context.eq(entry[0], atKey));
if (undefined === currentIndex) {
if (undefined === options.ifNew)
return this;
const newValue = OptLazyOr(options.ifNew, Token);
if (Token === newValue)
return this;
const newEntries = this.entries.append([atKey, newValue]);
return this.copy(newEntries);
}
if (undefined === options.ifExists)
return this;
const currentEntry = this.entries.get(currentIndex, RimbuError.throwInvalidStateError);
const currentValue = currentEntry[1];
const newValue = options.ifExists instanceof Function
? options.ifExists(currentValue, Token)
: options.ifExists;
if (Token === newValue) {
const newEntries = this.entries.remove(currentIndex).assumeNonEmpty();
return this.copy(newEntries);
}
if (Object.is(newValue, currentValue))
return this;
const newEntry = [atKey, newValue];
const newEntries = this.entries.updateAt(currentIndex, newEntry);
return this.copy(newEntries);
}
forEach(f, options = {}) {
const { state = TraverseState() } = options;
if (state.halted)
return;
this.entries.forEach(f, { state });
}
mapValues(mapFun) {
const newEntries = this.entries.map((e) => [
e[0],
mapFun(e[1], e[0]),
]);
return new HashMapCollision(this.context, newEntries);
}
toArray() {
return this.entries.toArray();
}
}
//# sourceMappingURL=immutable.mjs.map