UNPKG

@rimbu/table

Version:

Immutable spreadsheet-like data structures containing row keys, column keys, and cell values

651 lines 21.3 kB
import { RimbuError, Token } from '@rimbu/base'; import { EmptyBase, NonEmptyBase, } from '@rimbu/collection-types/map-custom'; import { OptLazy, OptLazyOr, TraverseState, Update, } from '@rimbu/common'; import { Reducer, Stream } from '@rimbu/stream'; import { isEmptyStreamSourceInstance } from '@rimbu/stream/custom'; export class TableEmpty extends EmptyBase { constructor(context) { super(); this.context = context; } set(row, column, value) { const columnMap = this.context.columnContext.of([column, value]); const rowMap = this.context.rowContext.of([row, columnMap]); return this.context.createNonEmpty(rowMap, 1); } get rowMap() { return this.context.rowContext.empty(); } get amountRows() { return 0; } streamRows() { return Stream.empty(); } streamValues() { return Stream.empty(); } addEntry(entry) { return this.set(entry[0], entry[1], entry[2]); } addEntries(entries) { return this.context.from(entries); } remove() { return this; } removeRow() { return this; } removeRows() { return this; } removeAndGet() { return undefined; } removeRowAndGet() { return undefined; } removeEntries() { return this; } hasRowKey() { return false; } hasValueAt() { return false; } get(row, column, otherwise) { return OptLazy(otherwise); } getRow() { return this.context.columnContext.empty(); } modifyAt(row, column, options) { if (undefined !== options.ifNew) { const value = OptLazyOr(options.ifNew, Token); if (Token === value) return this; return this.set(row, column, value); } return this; } updateAt() { return this; } filterRows() { return this; } mapValues() { return this; } toBuilder() { return this.context.builder(); } toString() { return `${this.context.typeTag}()`; } toJSON() { return { dataType: this.context.typeTag, value: [], }; } } export class TableNonEmpty extends NonEmptyBase { constructor(context, rowMap, size) { super(); this.context = context; this.rowMap = rowMap; this.size = size; } assumeNonEmpty() { return this; } asNormal() { return this; } copy(rowMap, size) { if (rowMap === this.rowMap) return this; return this.context.createNonEmpty(rowMap, size); } copyE(rowMap, size) { if (rowMap.nonEmpty()) { return this.copy(rowMap.assumeNonEmpty(), size); } return this.context.empty(); } stream() { return this.rowMap .stream() .flatMap(([row, columns]) => columns .stream() .map(([column, value]) => [row, column, value])); } streamRows() { return this.rowMap.streamKeys(); } streamValues() { return this.rowMap .streamValues() .flatMap((columns) => columns.streamValues()); } get amountRows() { return this.rowMap.size; } hasRowKey(row) { return this.rowMap.hasKey(row); } hasValueAt(row, column) { const token = Symbol(); return token !== this.get(row, column, token); } get(row, column, otherwise) { const token = Symbol(); const result = this.rowMap.get(row, token); if (token === result) return OptLazy(otherwise); return result.get(column, otherwise); } getRow(row) { return this.rowMap.get(row, this.context.columnContext.empty()); } set(row, column, value) { return this.modifyAt(row, column, { ifNew: value, ifExists: () => value, }).assumeNonEmpty(); } addEntry(entry) { return this.set(entry[0], entry[1], entry[2]); } addEntries(entries) { if (isEmptyStreamSourceInstance(entries)) return this; const builder = this.toBuilder(); builder.addEntries(entries); return builder.build().assumeNonEmpty(); } modifyAt(row, column, options) { let newSize = this.size; const newRowMap = this.rowMap.modifyAt(row, { ifNew: (none) => { const { ifNew } = options; if (undefined === ifNew) { return none; } const value = OptLazyOr(ifNew, none); if (none === value) { return none; } newSize++; return this.context.columnContext.of([column, value]); }, ifExists: (row, remove) => { const newRow = row.modifyAt(column, options); if (newRow === row) { return row; } if (!newRow.nonEmpty()) { return remove; } newSize += newRow.size - row.size; return newRow; }, }); return this.copyE(newRowMap, newSize); } updateAt(row, column, update) { if (!this.context.rowContext.isValidKey(row)) return this; if (!this.context.columnContext.isValidKey(column)) return this; return this.modifyAt(row, column, { ifExists: (value) => Update(value, update), }).assumeNonEmpty(); } remove(row, column) { const resultOpt = this.removeAndGet(row, column); return resultOpt?.[0] ?? this; } removeRow(row) { const resultOpt = this.removeRowAndGet(row); return resultOpt?.[0] ?? this; } removeRows(rows) { if (isEmptyStreamSourceInstance(rows)) return this; const builder = this.toBuilder(); builder.removeRows(rows); return builder.build(); } removeAndGet(row, column) { if (!this.context.rowContext.isValidKey(row)) return undefined; if (!this.context.columnContext.isValidKey(column)) return undefined; let newSize = this.size; const token = Symbol(); let removedValue = token; const newRows = this.rowMap.modifyAt(row, { ifExists: (columns, remove) => { const newColumns = columns.modifyAt(column, { ifExists: (currentValue, remove) => { removedValue = currentValue; newSize--; return remove; }, }); if (newColumns.nonEmpty()) return newColumns; return remove; }, }); if (token === removedValue) return undefined; const newSelf = this.copyE(newRows, newSize); return [newSelf, removedValue]; } removeRowAndGet(row) { if (!this.context.rowContext.isValidKey(row)) return undefined; let newSize = this.size; let removedRow; const newRows = this.rowMap.modifyAt(row, { ifExists: (columns, remove) => { removedRow = columns; newSize -= columns.size; return remove; }, }); if (undefined === removedRow) return undefined; const newSelf = this.copyE(newRows, newSize); return [newSelf, removedRow]; } removeEntries(entries) { if (isEmptyStreamSourceInstance(entries)) return this; const builder = this.toBuilder(); builder.removeEntries(entries); return builder.build(); } forEach(f, options = {}) { const { state = TraverseState() } = options; if (state.halted) return; const rowIt = this.rowMap[Symbol.iterator](); let rowEntry; const { halt } = state; while (!state.halted && undefined !== (rowEntry = rowIt.fastNext())) { const columnIt = rowEntry[1][Symbol.iterator](); let columnEntry; while (!state.halted && undefined !== (columnEntry = columnIt.fastNext())) { f([rowEntry[0], columnEntry[0], columnEntry[1]], state.nextIndex(), halt); } } } 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(); } filterRows(pred, options = {}) { const { negate = false } = options; let newSize = 0; const newRowMap = this.rowMap.filter((e, i, halt) => { const result = pred(e, i, halt); if (result !== negate) newSize += e[1].size; return result; }); return this.copyE(newRowMap, newSize); } mapValues(mapFun) { return this.copy(this.rowMap.mapValues((row, r) => row.mapValues((v, c) => mapFun(v, r, c))), this.size); } toArray() { const result = []; const rowIt = this.rowMap.stream()[Symbol.iterator](); let rowEntry; while (undefined !== (rowEntry = rowIt.fastNext())) { const columnIt = rowEntry[1].stream()[Symbol.iterator](); let columnEntry; while (undefined !== (columnEntry = columnIt.fastNext())) { result.push([rowEntry[0], columnEntry[0], columnEntry[1]]); } } return result; } toString() { return this.stream().join({ start: `${this.context.typeTag}(`, sep: `, `, end: `)`, valueToString: (entry) => `[${entry[0]}, ${entry[1]}] -> ${entry[2]}`, }); } toJSON() { return { dataType: this.context.typeTag, value: this.rowMap .stream() .map((entry) => [entry[0], entry[1].toJSON().value]) .toArray(), }; } toBuilder() { return this.context.createBuilder(this); } } export class TableBuilder { constructor(context, source) { this.context = context; this.source = source; //implements TableBase.Builder<R, C, V> this._lock = 0; this._size = 0; this.get = (row, column, otherwise) => { if (undefined !== this.source) { return this.source.get(row, column, otherwise); } const token = Symbol(); const result = this.rowMap.get(row, token); if (token === result) return OptLazy(otherwise); return result.get(column, otherwise); }; // prettier-ignore this.getRow = (row) => { if (undefined !== this.source) return this.source.getRow(row); const token = Symbol(); const result = this.rowMap.get(row, token); if (token === result) return this.context.columnContext.empty(); return result.build(); }; this.hasValueAt = (row, column) => { if (undefined !== this.source) return this.source.hasValueAt(row, column); const token = Symbol(); return token !== this.get(row, column, token); }; // prettier-ignore this.hasRowKey = (row) => { return this.source?.hasRowKey(row) ?? this.rowMap.hasKey(row); }; this.set = (row, column, value) => { this.checkLock(); let columnBuilder = undefined; this.rowMap.modifyAt(row, { ifNew: () => { columnBuilder = this.context.columnContext.builder(); return columnBuilder; }, ifExists: (b) => { columnBuilder = b; return b; }, }); let changed = true; columnBuilder.modifyAt(column, { ifNew: () => { this._size++; return value; }, ifExists: (currentValue) => { if (Object.is(currentValue, value)) changed = false; return value; }, }); if (changed) this.source = undefined; return changed; }; this.addEntry = (entry) => { return this.set(entry[0], entry[1], entry[2]); }; this.addEntries = (source) => { this.checkLock(); return Stream.applyFilter(source, { pred: this.set }).count() > 0; }; this.remove = (row, column, otherwise) => { this.checkLock(); const columnMap = this.rowMap.get(row); if (undefined === columnMap) return OptLazy(otherwise); if (!this.context.columnContext.isValidKey(column)) { return OptLazy(otherwise); } let removedValue = Token; columnMap.modifyAt(column, { ifExists: (currentValue, remove) => { removedValue = currentValue; this._size--; return remove; }, }); if (columnMap.isEmpty) this.rowMap.removeKey(row); if (Token === removedValue) return OptLazy(otherwise); this.source = undefined; return removedValue; }; // prettier-ignore this.removeRow = (row) => { this.checkLock(); if (!this.context.rowContext.isValidKey(row)) return false; return this.rowMap.modifyAt(row, { ifExists: (row, remove) => { this.source = undefined; this._size -= row.size; return remove; }, }); }; // prettier-ignore this.removeRows = (rows) => { this.checkLock(); return Stream.from(rows).filterPure({ pred: this.removeRow }).count() > 0; }; this.removeEntries = (entries) => { this.checkLock(); const notFound = Symbol(); return (Stream.applyMap(entries, this.remove, notFound).countElement(notFound, { negate: true, }) > 0); }; this.modifyAt = (row, column, options) => { this.checkLock(); let changed = false; this.rowMap.modifyAt(row, { ifNew: (none) => { const { ifNew } = options; if (undefined === ifNew) { return none; } const newValue = OptLazyOr(ifNew, none); if (newValue === none) { return none; } const rowMap = this.context.columnContext.builder(); rowMap.set(column, newValue); changed = true; this._size++; return rowMap; }, ifExists: (curMap, remove) => { const preSize = curMap.size; changed = curMap.modifyAt(column, options); if (changed) { const postSize = curMap.size; this._size += postSize - preSize; if (postSize <= 0) { return remove; } } return curMap; }, }); if (changed) { this.source = undefined; } return changed; }; // prettier-ignore this.updateAt = (row, column, update, otherwise) => { this.checkLock(); let oldValue; let found = false; this.modifyAt(row, column, { ifExists: (value) => { oldValue = value; found = true; return Update(value, update); }, }); if (!found) return OptLazy(otherwise); this.source = undefined; return oldValue; }; this.forEach = (f, options = {}) => { const { state = TraverseState() } = options; if (state.halted) return; this._lock++; if (undefined !== this.source) { this.source.forEach(f, { state }); } else { const { halt } = state; this.rowMap.forEach(([rowKey, column], _, rowHalt) => { column.forEach(([columnKey, value], _, columnHalt) => { f([rowKey, columnKey, value], state.nextIndex(), halt); if (state.halted) { rowHalt(); columnHalt(); } }); }); } this._lock--; }; this.build = () => { if (undefined !== this.source) return this.source; if (this.isEmpty) return this.context.empty(); return this.context.createNonEmpty(this.rowMap .buildMapValues((row) => row.build().assumeNonEmpty()) .assumeNonEmpty(), this.size); }; // prettier-ignore this.buildMapValues = (mapFun) => { if (undefined !== this.source) return this.source.mapValues(mapFun); if (this.isEmpty) return this.context.empty(); const newRowMap = this.rowMap .buildMapValues((row, rowKey) => row .buildMapValues((value, columnKey) => mapFun(value, rowKey, columnKey)) .assumeNonEmpty()) .assumeNonEmpty(); return this.context.createNonEmpty(newRowMap, this.size); }; if (undefined !== source) this._size = source.size; } get rowMap() { if (undefined === this._rowMap) { if (undefined === this.source) { this._rowMap = this.context.rowContext.builder(); } else { this._rowMap = this.source.rowMap .mapValues((v) => v.toBuilder()) .toBuilder(); } } return this._rowMap; } checkLock() { if (this._lock) RimbuError.throwModifiedBuilderWhileLoopingOverItError(); } get size() { return this._size; } get isEmpty() { return this.size === 0; } get amountRows() { return this.source?.amountRows ?? this.rowMap.size; } } export class TableContext { constructor(typeTag, rowContext, columnContext) { this.typeTag = typeTag; this.rowContext = rowContext; this.columnContext = columnContext; this._empty = Object.freeze(new TableEmpty(this)); this.empty = () => { return this._empty; }; this.from = (...sources) => { let builder = this.builder(); let i = -1; const length = sources.length; while (++i < length) { const source = sources[i]; if (isEmptyStreamSourceInstance(source)) continue; if (builder.isEmpty && this.isNonEmptyInstance(source) && source.context === this) { if (i === length - 1) return source; builder = source.toBuilder(); continue; } builder.addEntries(source); } return builder.build(); }; this.of = (...entries) => { return this.from(entries); }; this.builder = () => { return new TableBuilder(this); }; this.reducer = (source) => { return Reducer.create(() => undefined === source ? this.builder() : this.from(source).toBuilder(), (builder, entry) => { builder.addEntry(entry); return builder; }, (builder) => builder.build()); }; } get _types() { return undefined; } isNonEmptyInstance(source) { return source instanceof TableNonEmpty; } createNonEmpty(rowMap, size) { return new TableNonEmpty(this, rowMap, size); } createBuilder(source) { return new TableBuilder(this, source); } } //# sourceMappingURL=base.mjs.map