Added table feature
This commit is contained in:
30
experiments/feature-stub.mjs
Normal file
30
experiments/feature-stub.mjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
import { Stub } from '@efforting.tech/feature/stub';
|
||||||
|
/*
|
||||||
|
|
||||||
|
export function Stub(meta, name, description, module_name, function_name) {
|
||||||
|
return function stub() {
|
||||||
|
throw new Error(`The feature "${name}" of "${meta.url}" is not enabled. Enable it by calling "${function_name}(${this.name})" imported from "${module_name}"`); //TODO - specific error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Thing {
|
||||||
|
|
||||||
|
static from_stuff = Stub(import.meta, 'stuff-loader', 'Creates Thing from stuff', '@efforting.tech/stuff/loader', 'enable_stuff_loader');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Thing.from_stuff()
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Error: The feature "stuff-loader" of "file:///srv/Projekt/efforting.tech/nodejs.esm-library/experiments/generic-parser-2.mjs" is not enabled. Enable it by calling "enable_stuff_loader(Thing)" imported from "@efforting.tech/stuff/loader"
|
||||||
|
at Thing.stub [as from_stuff] (file:///srv/Projekt/efforting.tech/nodejs.esm-library/build/packages/feature/stub.mjs:4:9)
|
||||||
|
at file:///srv/Projekt/efforting.tech/nodejs.esm-library/experiments/generic-parser-2.mjs:10:7
|
||||||
|
at ModuleJob.run (node:internal/modules/esm/module_job:430:25)
|
||||||
|
at async node:internal/modules/esm/loader:639:26
|
||||||
|
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
|
||||||
|
|
||||||
|
*/
|
||||||
0
experiments/generic-parser-2.mjs
Normal file
0
experiments/generic-parser-2.mjs
Normal file
23
experiments/table-1.mjs
Normal file
23
experiments/table-1.mjs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Row_Based_Table } from '@efforting.tech/table'
|
||||||
|
|
||||||
|
|
||||||
|
const t = new Row_Based_Table({ column_names: 'SKU, Quantity, Price' });
|
||||||
|
|
||||||
|
//console.log(t.column_names_lut); /* { SKU: 0, Quantity: 1, Price: 2 } */
|
||||||
|
|
||||||
|
t.push_rows(
|
||||||
|
['VOLVO-1', 3, 120_000],
|
||||||
|
['VOLVO-2', 4, 140_000],
|
||||||
|
)
|
||||||
|
|
||||||
|
const [A] = t.read_rows(0);
|
||||||
|
const [B] = t.snapshot_rows(1);
|
||||||
|
|
||||||
|
console.log(A.value);
|
||||||
|
console.log(B.value);
|
||||||
|
A.update({Quantity: 5});
|
||||||
|
console.log(A.value);
|
||||||
|
|
||||||
|
for (const r of t) {
|
||||||
|
console.log(r.object);
|
||||||
|
}
|
||||||
34
experiments/table-2.mjs
Normal file
34
experiments/table-2.mjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Row_Based_Table } from '@efforting.tech/table';
|
||||||
|
import { load_raster_table } from '@efforting.tech/table/raster-table';
|
||||||
|
|
||||||
|
|
||||||
|
const t = load_raster_table(`
|
||||||
|
|
||||||
|
SKU Quantity Price
|
||||||
|
--- -------- -----
|
||||||
|
V-1 2 120_000
|
||||||
|
V-2 3 140_000
|
||||||
|
|
||||||
|
`, Row_Based_Table);
|
||||||
|
|
||||||
|
|
||||||
|
// { row_name, column_name, row_index, column_index, row, cell }
|
||||||
|
t.replace_all_cells(
|
||||||
|
({column_name, cell}) => {
|
||||||
|
const translation = {
|
||||||
|
SKU: (s) => s.trim(),
|
||||||
|
Quantity: parseInt,
|
||||||
|
Price: (p) => parseFloat(p.replace(/_/g, '')),
|
||||||
|
}[column_name];
|
||||||
|
return translation ? translation(cell) : cell;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const r of t) {
|
||||||
|
console.log(r.object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
{ SKU: 'V-1', Quantity: 2, Price: 120000 }
|
||||||
|
{ SKU: 'V-2', Quantity: 3, Price: 140000 }
|
||||||
|
*/
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# TODO: Add a way to help making sure we keep internal-dependencies up to date
|
||||||
scope: '@efforting.tech'
|
scope: '@efforting.tech'
|
||||||
registry: 'https://npm.efforting.tech/'
|
registry: 'https://npm.efforting.tech/'
|
||||||
version: 0.2.9
|
version: 0.2.9
|
||||||
@@ -28,6 +29,17 @@ packages:
|
|||||||
internal-dependencies:
|
internal-dependencies:
|
||||||
- schema
|
- schema
|
||||||
|
|
||||||
|
table:
|
||||||
|
path: source/table
|
||||||
|
description: Table management
|
||||||
|
internal-dependencies:
|
||||||
|
- schema
|
||||||
|
- text
|
||||||
|
|
||||||
|
feature:
|
||||||
|
path: source/feature
|
||||||
|
description: Feature management
|
||||||
|
|
||||||
schema:
|
schema:
|
||||||
path: source/schema
|
path: source/schema
|
||||||
#documentation: documentation/schema
|
#documentation: documentation/schema
|
||||||
|
|||||||
7
source/feature/stub.mjs
Normal file
7
source/feature/stub.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
export function Stub(meta, name, description, module_name, function_name) {
|
||||||
|
return function stub() {
|
||||||
|
throw new Error(`The feature "${name}" of "${meta.url}" is not enabled. Enable it by calling "${function_name}(${this.name})" imported from "${module_name}"`); //TODO - specific error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
46
source/table/raster-table.mjs
Normal file
46
source/table/raster-table.mjs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { tabs_to_spaces } from '@efforting.tech/text/text-utilities';
|
||||||
|
|
||||||
|
const raster_table_pattern = /^((?:[^\S\n]|[\w-])+)\n((?:[^\S\n]|-)+)\n(.+)/ms;
|
||||||
|
const column_pattern = /\s*(.+?)(?=\s{2,}|$)/gd;
|
||||||
|
const row_pattern = /^(.+)$/mg
|
||||||
|
|
||||||
|
// If loader is null we return a raw representation
|
||||||
|
export function load_raster_table(raster, loader=null, tab_width=4, null_padding=true) {
|
||||||
|
if (raster.match(/\t/g)) {
|
||||||
|
raster = tabs_to_spaces(raster, tab_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null_padding) { //TODO - this is just experimental proof of concept
|
||||||
|
raster = raster.replace(/〃/g, '\0〃');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const m = raster.match(raster_table_pattern);
|
||||||
|
const column_matches = [...m[1].matchAll(column_pattern)];
|
||||||
|
const column_positions = [...column_matches.map(cm => cm.indices[1][0])]
|
||||||
|
const column_names = [...column_matches.map(cm => cm[1])]
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
for (const row of m[3].matchAll(row_pattern)) {
|
||||||
|
|
||||||
|
const pending_row = [];
|
||||||
|
for (let ci=0; ci<column_positions.length; ci++) {
|
||||||
|
const cell = row[1].slice(column_positions[ci], column_positions[ci+1]);
|
||||||
|
if (null_padding) {
|
||||||
|
pending_row.push(cell.replace(/\0/, ''));
|
||||||
|
} else {
|
||||||
|
pending_row.push(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.push(pending_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loader) {
|
||||||
|
return new loader({ rows, column_names });
|
||||||
|
} else {
|
||||||
|
return {column_names, column_positions, rows};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: load_ditto_raster_table
|
||||||
157
source/table/table.mjs
Normal file
157
source/table/table.mjs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
30
source/text/text-utilities.mjs
Normal file
30
source/text/text-utilities.mjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export const char_width_lut = {
|
||||||
|
'〃': 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//NOTE - this one only support posix newlines, this should be fixed later
|
||||||
|
export function tabs_to_spaces(text, tab_width=4) {
|
||||||
|
let pending_column_index=0;
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
for (const char of text) {
|
||||||
|
if (char == '\t') {
|
||||||
|
const new_column_index = (Math.floor(pending_column_index / tab_width) + 1) * tab_width;
|
||||||
|
result += ' '.repeat(new_column_index - pending_column_index);
|
||||||
|
pending_column_index = new_column_index;
|
||||||
|
} else {
|
||||||
|
if (char.charCodeAt(0) === 10) {
|
||||||
|
result += '\n';
|
||||||
|
pending_column_index = 0;
|
||||||
|
/* } else if (char.charCodeAt(0) < 32) {
|
||||||
|
result += '<27>';
|
||||||
|
*/ } else {
|
||||||
|
pending_column_index += (char_width_lut[char] ?? 1)
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -88,6 +88,9 @@ for (const [package_name, package_data] of Object.entries(manifest.packages)) {
|
|||||||
const linked_docs = pkg.documentation ? link_tree(pkg.documentation, pkg_dir).map(p => path.relative(pkg_dir, p)) : [];
|
const linked_docs = pkg.documentation ? link_tree(pkg.documentation, pkg_dir).map(p => path.relative(pkg_dir, p)) : [];
|
||||||
//console.log('DOCS', { linked_docs });
|
//console.log('DOCS', { linked_docs });
|
||||||
|
|
||||||
|
|
||||||
|
// A file named after its package (e.g. table.mjs in the table package) becomes the root export '.'
|
||||||
|
// All other files become named subpath exports e.g. './raster-table'
|
||||||
const exports_map = {};
|
const exports_map = {};
|
||||||
for (const file of linked_sources) {
|
for (const file of linked_sources) {
|
||||||
const name = path.basename(file, '.mjs');
|
const name = path.basename(file, '.mjs');
|
||||||
|
|||||||
Reference in New Issue
Block a user