import * as F from '@efforting.tech/schema/field-configuration-factories'; import { parse_csv } from '@efforting.tech/data/string-utilities'; export const Row_Based_Table_Settings = new F.Schema({ rows: F.factory(() => [], 'Rows to initialize table with'), row_names: F.typed_factory((v) => typeof v === 'string' ? parse_csv(v) : v, () => [], 'Names of rows'), column_names: F.typed_factory((v) => typeof v === 'string' ? parse_csv(v) : v, () => [], 'Names of columns'), }, 'Row based table settings'); export class Table_Row_Reference { constructor(table, index, snapshot=null) { Object.assign(this, { table, index, snapshot }); } get value() { const { table, index, snapshot } = this; return snapshot ?? table.read_row(index); } get object() { const { table, index, snapshot } = this; const { column_names } = table; const value = snapshot ?? table.read_row(index); //TODO - check shape of column_names vs the value return Object.fromEntries(value.map((cell, column_index) => [column_names[column_index], cell])); } update(updates) { const { table, index, snapshot } = this; if (snapshot) { throw new Error('Can not update snapshot references, clear the snapshot to reuse this index'); //TODO - proper error } table.write_row(index, updates); } } export class Row_Based_Table { constructor(settings) { const { rows, column_names, row_names } = Row_Based_Table_Settings.load(settings); Object.assign(this, { rows }); this.set_column_names(...column_names); this.set_row_names(...row_names); } // General operations get width() { const { rows, column_names } = this; return rows[0]?.length || column_names.length || undefined; } get length() { const { rows } = this; return rows.length; } get size() { const { width, length } = this; return [width, length]; } replace_all_cells(replacement_fn) { for (const [row_index, row] of this.rows.entries()) { const pending_row = []; for (const [column_index, cell] of row.entries()) { const row_name = this.row_names[row_index]; const column_name = this.column_names[column_index]; pending_row.push(replacement_fn({ row_name, column_name, row_index, column_index, row, cell })); } this.rows[row_index] = pending_row; } } // TODO: Make sure column and row operations covers the same uses // TODO: Sub table operations // Column operations set_column_names(...names) { this.column_names = names; this.column_names_lut = Object.fromEntries(names.map((name, index) => [name, index])); } // Row operations set_row_names(...names) { this.row_names = names; this.row_names_lut = Object.fromEntries(names.map((name, index) => [name, index])); } read_rows(...indices) { const result = []; for (const index of indices) { result.push(new Table_Row_Reference(this, index)); } return result; } snapshot_rows(...indices) { //NOTE: Snapshot doesn't include current layout or other settings, just the contents of the row at the time, and it currently is a shallow copy const result = []; for (const index of indices) { result.push(new Table_Row_Reference(this, index, [...this.rows[index]])); } return result; } push_rows(...rows) { //TODO - verify shape this.rows.push(...rows); } read_row(index) { return this.rows[index]; } write_row(index, row_data) { const { rows, column_names_lut } = this; if (Array.isArray(row_data)) { //TODO - shape check rows[index] = row_data; } else { //TODO - possibly allow array of array for using numerical indices const work_row = rows[index]; for (const [key, value] of Object.entries(row_data)) { const col_index = column_names_lut[key]; if (col_index === undefined) { throw new Error(`Unknown column: ${key}`); //TODO - proper error } work_row[col_index] = value; } } } *[Symbol.iterator]() { for (const index of this.rows.keys()) { yield new Table_Row_Reference(this, index); } } }