@rimbu/table
Version:
Immutable spreadsheet-like data structures containing row keys, column keys, and cell values
651 lines • 21.3 kB
JavaScript
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