tova 0.1.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/tova.js +811 -154
- package/package.json +8 -2
- package/src/analyzer/analyzer.js +297 -58
- package/src/analyzer/scope.js +38 -1
- package/src/analyzer/type-registry.js +72 -0
- package/src/analyzer/types.js +478 -0
- package/src/codegen/base-codegen.js +371 -0
- package/src/codegen/client-codegen.js +62 -10
- package/src/codegen/codegen.js +111 -2
- package/src/codegen/server-codegen.js +175 -3
- package/src/config/edit-toml.js +100 -0
- package/src/config/package-json.js +52 -0
- package/src/config/resolve.js +100 -0
- package/src/config/toml.js +209 -0
- package/src/lexer/lexer.js +2 -2
- package/src/lsp/server.js +284 -30
- package/src/parser/ast.js +105 -0
- package/src/parser/parser.js +202 -2
- package/src/runtime/ai.js +305 -0
- package/src/runtime/devtools.js +228 -0
- package/src/runtime/embedded.js +3 -1
- package/src/runtime/io.js +240 -0
- package/src/runtime/reactivity.js +264 -19
- package/src/runtime/ssr.js +196 -24
- package/src/runtime/table.js +522 -0
- package/src/stdlib/collections.js +245 -0
- package/src/stdlib/core.js +87 -0
- package/src/stdlib/datetime.js +88 -0
- package/src/stdlib/encoding.js +35 -0
- package/src/stdlib/functional.js +82 -0
- package/src/stdlib/inline.js +334 -67
- package/src/stdlib/math.js +93 -0
- package/src/stdlib/string.js +95 -0
- package/src/stdlib/url.js +33 -0
- package/src/stdlib/validation.js +29 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
// Table<T> — First-class tabular data runtime for Tova
|
|
2
|
+
// Thin wrapper around arrays of objects (row-based storage).
|
|
3
|
+
// All operations return new Tables (immutable).
|
|
4
|
+
|
|
5
|
+
export class Table {
|
|
6
|
+
constructor(rows = [], columns = null) {
|
|
7
|
+
this._rows = Array.isArray(rows) ? rows : [];
|
|
8
|
+
this._columns = columns || (this._rows.length > 0 ? Object.keys(this._rows[0]) : []);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ── Properties ──────────────────────────────────────
|
|
12
|
+
get rows() { return this._rows.length; }
|
|
13
|
+
get columns() { return [...this._columns]; }
|
|
14
|
+
get shape() { return [this._rows.length, this._columns.length]; }
|
|
15
|
+
get length() { return this._rows.length; }
|
|
16
|
+
|
|
17
|
+
// ── Iteration ───────────────────────────────────────
|
|
18
|
+
[Symbol.iterator]() {
|
|
19
|
+
return this._rows[Symbol.iterator]();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Access ──────────────────────────────────────────
|
|
23
|
+
// table[0] → row struct, table[.name] → column array (via getColumn)
|
|
24
|
+
at(index) {
|
|
25
|
+
if (index < 0) index = this._rows.length + index;
|
|
26
|
+
return this._rows[index] ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Slice: table[10:20] → Table
|
|
30
|
+
slice(start, end) {
|
|
31
|
+
return new Table(this._rows.slice(start, end), this._columns);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get column as array
|
|
35
|
+
getColumn(name) {
|
|
36
|
+
return this._rows.map(r => r[name]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Core Operations ─────────────────────────────────
|
|
40
|
+
|
|
41
|
+
toArray() {
|
|
42
|
+
return [...this._rows];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
toJSON() {
|
|
46
|
+
return this._rows;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toString() {
|
|
50
|
+
if (this._rows.length === 0) return 'Table(0 rows, 0 columns)';
|
|
51
|
+
return `Table(${this._rows.length} rows, ${this._columns.length} columns)`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// For printing/debugging — format as aligned table
|
|
55
|
+
_format(maxRows = 10, title = null) {
|
|
56
|
+
const lines = [];
|
|
57
|
+
if (title) lines.push(`── ${title} ──`);
|
|
58
|
+
|
|
59
|
+
const cols = this._columns;
|
|
60
|
+
const displayRows = this._rows.slice(0, maxRows);
|
|
61
|
+
|
|
62
|
+
if (cols.length === 0 || displayRows.length === 0) {
|
|
63
|
+
lines.push('(empty table)');
|
|
64
|
+
lines.push(`${this._rows.length} rows × ${cols.length} columns`);
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Calculate column widths
|
|
69
|
+
const widths = {};
|
|
70
|
+
for (const col of cols) {
|
|
71
|
+
widths[col] = col.length;
|
|
72
|
+
for (const row of displayRows) {
|
|
73
|
+
const val = row[col];
|
|
74
|
+
const str = val === null || val === undefined ? 'nil' : String(val);
|
|
75
|
+
widths[col] = Math.max(widths[col], str.length);
|
|
76
|
+
}
|
|
77
|
+
widths[col] = Math.min(widths[col], 30); // cap width
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Header
|
|
81
|
+
const header = cols.map(c => c.padEnd(widths[c])).join(' │ ');
|
|
82
|
+
const separator = cols.map(c => '─'.repeat(widths[c])).join('─┼─');
|
|
83
|
+
lines.push(header);
|
|
84
|
+
lines.push(separator);
|
|
85
|
+
|
|
86
|
+
// Rows
|
|
87
|
+
for (const row of displayRows) {
|
|
88
|
+
const cells = cols.map(c => {
|
|
89
|
+
const val = row[c];
|
|
90
|
+
const str = val === null || val === undefined ? 'nil' : String(val);
|
|
91
|
+
return str.slice(0, 30).padEnd(widths[c]);
|
|
92
|
+
});
|
|
93
|
+
lines.push(cells.join(' │ '));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this._rows.length > maxRows) {
|
|
97
|
+
lines.push(`... ${this._rows.length - maxRows} more rows`);
|
|
98
|
+
}
|
|
99
|
+
lines.push(`${this._rows.length} rows × ${cols.length} columns`);
|
|
100
|
+
|
|
101
|
+
return lines.join('\n');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Table Operation Functions ──────────────────────────
|
|
106
|
+
// All functions are standalone and pipe-friendly: table |> where(predicate)
|
|
107
|
+
|
|
108
|
+
export function table_where(table, predicate) {
|
|
109
|
+
const rows = table._rows.filter(predicate);
|
|
110
|
+
return new Table(rows, table._columns);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function table_select(table, ...args) {
|
|
114
|
+
// args can be column name strings or exclude descriptors { __exclude: "name" }
|
|
115
|
+
let cols;
|
|
116
|
+
if (args.length === 1 && args[0] && args[0].__exclude) {
|
|
117
|
+
const excludeSet = new Set(Array.isArray(args[0].__exclude) ? args[0].__exclude : [args[0].__exclude]);
|
|
118
|
+
cols = table._columns.filter(c => !excludeSet.has(c));
|
|
119
|
+
} else if (args.every(a => typeof a === 'string')) {
|
|
120
|
+
cols = args;
|
|
121
|
+
} else {
|
|
122
|
+
// Mix of includes and excludes
|
|
123
|
+
const excludes = new Set();
|
|
124
|
+
const includes = [];
|
|
125
|
+
for (const a of args) {
|
|
126
|
+
if (a && a.__exclude) {
|
|
127
|
+
const e = Array.isArray(a.__exclude) ? a.__exclude : [a.__exclude];
|
|
128
|
+
e.forEach(x => excludes.add(x));
|
|
129
|
+
} else if (typeof a === 'string') {
|
|
130
|
+
includes.push(a);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
cols = includes.length > 0
|
|
134
|
+
? includes.filter(c => !excludes.has(c))
|
|
135
|
+
: table._columns.filter(c => !excludes.has(c));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const rows = table._rows.map(r => {
|
|
139
|
+
const row = {};
|
|
140
|
+
for (const c of cols) row[c] = r[c];
|
|
141
|
+
return row;
|
|
142
|
+
});
|
|
143
|
+
return new Table(rows, cols);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function table_derive(table, derivations) {
|
|
147
|
+
// derivations is an object: { colName: (row) => value, ... }
|
|
148
|
+
const newCols = [...table._columns];
|
|
149
|
+
for (const key of Object.keys(derivations)) {
|
|
150
|
+
if (!newCols.includes(key)) newCols.push(key);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rows = table._rows.map(r => {
|
|
154
|
+
const row = { ...r };
|
|
155
|
+
for (const [key, fn] of Object.entries(derivations)) {
|
|
156
|
+
row[key] = typeof fn === 'function' ? fn(r) : fn;
|
|
157
|
+
}
|
|
158
|
+
return row;
|
|
159
|
+
});
|
|
160
|
+
return new Table(rows, newCols);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function table_group_by(table, keyFn) {
|
|
164
|
+
const groups = new Map();
|
|
165
|
+
for (const row of table._rows) {
|
|
166
|
+
const key = typeof keyFn === 'function' ? keyFn(row) : row[keyFn];
|
|
167
|
+
const keyStr = String(key);
|
|
168
|
+
if (!groups.has(keyStr)) groups.set(keyStr, { key, rows: [] });
|
|
169
|
+
groups.get(keyStr).rows.push(row);
|
|
170
|
+
}
|
|
171
|
+
// Return a GroupedTable-like structure that agg can consume
|
|
172
|
+
return { __grouped: true, groups, columns: table._columns };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function table_agg(grouped, aggregations) {
|
|
176
|
+
if (!grouped || !grouped.__grouped) {
|
|
177
|
+
throw new Error('agg() must be called after group_by()');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const rows = [];
|
|
181
|
+
const groupKeyCol = grouped.columns[0]; // first column used in group_by typically
|
|
182
|
+
|
|
183
|
+
for (const [, { key, rows: groupRows }] of grouped.groups) {
|
|
184
|
+
const row = {};
|
|
185
|
+
// Determine group key column name — we use the key value
|
|
186
|
+
// If key is an object, spread it; if primitive, use generic 'group' key
|
|
187
|
+
if (typeof key === 'object' && key !== null) {
|
|
188
|
+
Object.assign(row, key);
|
|
189
|
+
} else {
|
|
190
|
+
row._group = key;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const [name, aggFn] of Object.entries(aggregations)) {
|
|
194
|
+
row[name] = aggFn(groupRows);
|
|
195
|
+
}
|
|
196
|
+
rows.push(row);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const cols = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
200
|
+
return new Table(rows, cols);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function table_sort_by(table, keyFn, opts = {}) {
|
|
204
|
+
const desc = opts.desc || false;
|
|
205
|
+
const rows = [...table._rows].sort((a, b) => {
|
|
206
|
+
const ka = typeof keyFn === 'function' ? keyFn(a) : a[keyFn];
|
|
207
|
+
const kb = typeof keyFn === 'function' ? keyFn(b) : b[keyFn];
|
|
208
|
+
let cmp = 0;
|
|
209
|
+
if (ka < kb) cmp = -1;
|
|
210
|
+
else if (ka > kb) cmp = 1;
|
|
211
|
+
return desc ? -cmp : cmp;
|
|
212
|
+
});
|
|
213
|
+
return new Table(rows, table._columns);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function table_limit(table, n) {
|
|
217
|
+
return new Table(table._rows.slice(0, n), table._columns);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function table_join(table, other, opts = {}) {
|
|
221
|
+
const { left, right, how = 'inner' } = opts;
|
|
222
|
+
if (!left || !right) throw new Error('join() requires left and right key functions');
|
|
223
|
+
|
|
224
|
+
const rows = [];
|
|
225
|
+
const rightIndex = new Map();
|
|
226
|
+
for (const r of other._rows) {
|
|
227
|
+
const key = typeof right === 'function' ? right(r) : r[right];
|
|
228
|
+
const keyStr = String(key);
|
|
229
|
+
if (!rightIndex.has(keyStr)) rightIndex.set(keyStr, []);
|
|
230
|
+
rightIndex.get(keyStr).push(r);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const combinedCols = [...new Set([...table._columns, ...other._columns])];
|
|
234
|
+
|
|
235
|
+
for (const lr of table._rows) {
|
|
236
|
+
const key = typeof left === 'function' ? left(lr) : lr[left];
|
|
237
|
+
const keyStr = String(key);
|
|
238
|
+
const matches = rightIndex.get(keyStr) || [];
|
|
239
|
+
|
|
240
|
+
if (matches.length > 0) {
|
|
241
|
+
for (const rr of matches) {
|
|
242
|
+
rows.push({ ...lr, ...rr });
|
|
243
|
+
}
|
|
244
|
+
} else if (how === 'left' || how === 'outer') {
|
|
245
|
+
const row = { ...lr };
|
|
246
|
+
for (const c of other._columns) {
|
|
247
|
+
if (!(c in row)) row[c] = null;
|
|
248
|
+
}
|
|
249
|
+
rows.push(row);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (how === 'right' || how === 'outer') {
|
|
254
|
+
const leftIndex = new Set();
|
|
255
|
+
for (const lr of table._rows) {
|
|
256
|
+
const key = typeof left === 'function' ? left(lr) : lr[left];
|
|
257
|
+
leftIndex.add(String(key));
|
|
258
|
+
}
|
|
259
|
+
for (const rr of other._rows) {
|
|
260
|
+
const key = typeof right === 'function' ? right(rr) : rr[right];
|
|
261
|
+
if (!leftIndex.has(String(key))) {
|
|
262
|
+
const row = { ...rr };
|
|
263
|
+
for (const c of table._columns) {
|
|
264
|
+
if (!(c in row)) row[c] = null;
|
|
265
|
+
}
|
|
266
|
+
rows.push(row);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return new Table(rows, combinedCols);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function table_pivot(table, opts = {}) {
|
|
275
|
+
const { index, columns: colFn, values: valFn } = opts;
|
|
276
|
+
if (!index || !colFn || !valFn) throw new Error('pivot() requires index, columns, and values');
|
|
277
|
+
|
|
278
|
+
const pivotMap = new Map();
|
|
279
|
+
const allPivotCols = new Set();
|
|
280
|
+
|
|
281
|
+
for (const row of table._rows) {
|
|
282
|
+
const idxKey = typeof index === 'function' ? index(row) : row[index];
|
|
283
|
+
const col = typeof colFn === 'function' ? colFn(row) : row[colFn];
|
|
284
|
+
const val = typeof valFn === 'function' ? valFn(row) : row[valFn];
|
|
285
|
+
|
|
286
|
+
const keyStr = String(idxKey);
|
|
287
|
+
if (!pivotMap.has(keyStr)) pivotMap.set(keyStr, { _index: idxKey });
|
|
288
|
+
pivotMap.get(keyStr)[String(col)] = val;
|
|
289
|
+
allPivotCols.add(String(col));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const rows = [...pivotMap.values()];
|
|
293
|
+
const cols = ['_index', ...allPivotCols];
|
|
294
|
+
return new Table(rows, cols);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function table_unpivot(table, opts = {}) {
|
|
298
|
+
const { id, columns: unpivotCols } = opts;
|
|
299
|
+
if (!id || !unpivotCols) throw new Error('unpivot() requires id and columns');
|
|
300
|
+
|
|
301
|
+
const colNames = unpivotCols.map(c => typeof c === 'function' ? null : c).filter(Boolean);
|
|
302
|
+
const rows = [];
|
|
303
|
+
|
|
304
|
+
for (const row of table._rows) {
|
|
305
|
+
const idVal = typeof id === 'function' ? id(row) : row[id];
|
|
306
|
+
for (const col of colNames) {
|
|
307
|
+
rows.push({ id: idVal, variable: col, value: row[col] });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return new Table(rows, ['id', 'variable', 'value']);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function table_explode(table, colFn) {
|
|
315
|
+
const rows = [];
|
|
316
|
+
for (const row of table._rows) {
|
|
317
|
+
const arr = typeof colFn === 'function' ? colFn(row) : row[colFn];
|
|
318
|
+
if (Array.isArray(arr)) {
|
|
319
|
+
for (const val of arr) {
|
|
320
|
+
rows.push({ ...row });
|
|
321
|
+
// Replace the exploded column with individual value
|
|
322
|
+
const colName = typeof colFn === 'string' ? colFn : Object.keys(row).find(k => row[k] === arr);
|
|
323
|
+
if (colName) rows[rows.length - 1][colName] = val;
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
rows.push({ ...row });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return new Table(rows, table._columns);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function table_union(table, other) {
|
|
333
|
+
const cols = [...new Set([...table._columns, ...other._columns])];
|
|
334
|
+
const rows = [...table._rows, ...other._rows];
|
|
335
|
+
return new Table(rows, cols);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function table_drop_duplicates(table, opts = {}) {
|
|
339
|
+
const { by } = opts;
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
const rows = [];
|
|
342
|
+
|
|
343
|
+
for (const row of table._rows) {
|
|
344
|
+
const key = by ? (typeof by === 'function' ? String(by(row)) : String(row[by])) : JSON.stringify(row);
|
|
345
|
+
if (!seen.has(key)) {
|
|
346
|
+
seen.add(key);
|
|
347
|
+
rows.push(row);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return new Table(rows, table._columns);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function table_rename(table, oldName, newName) {
|
|
355
|
+
const cols = table._columns.map(c => c === oldName ? newName : c);
|
|
356
|
+
const rows = table._rows.map(r => {
|
|
357
|
+
const row = {};
|
|
358
|
+
for (const c of table._columns) {
|
|
359
|
+
row[c === oldName ? newName : c] = r[c];
|
|
360
|
+
}
|
|
361
|
+
return row;
|
|
362
|
+
});
|
|
363
|
+
return new Table(rows, cols);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Aggregation helpers ───────────────────────────────
|
|
367
|
+
|
|
368
|
+
export function agg_sum(fn) {
|
|
369
|
+
return (rows) => rows.reduce((acc, r) => acc + (typeof fn === 'function' ? fn(r) : r[fn]), 0);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function agg_count(fn) {
|
|
373
|
+
if (!fn) return (rows) => rows.length;
|
|
374
|
+
return (rows) => rows.filter(fn).length;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function agg_mean(fn) {
|
|
378
|
+
return (rows) => {
|
|
379
|
+
if (rows.length === 0) return 0;
|
|
380
|
+
const total = rows.reduce((acc, r) => acc + (typeof fn === 'function' ? fn(r) : r[fn]), 0);
|
|
381
|
+
return total / rows.length;
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function agg_median(fn) {
|
|
386
|
+
return (rows) => {
|
|
387
|
+
if (rows.length === 0) return 0;
|
|
388
|
+
const vals = rows.map(r => typeof fn === 'function' ? fn(r) : r[fn]).sort((a, b) => a - b);
|
|
389
|
+
const mid = Math.floor(vals.length / 2);
|
|
390
|
+
return vals.length % 2 !== 0 ? vals[mid] : (vals[mid - 1] + vals[mid]) / 2;
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function agg_min(fn) {
|
|
395
|
+
return (rows) => {
|
|
396
|
+
if (rows.length === 0) return null;
|
|
397
|
+
return Math.min(...rows.map(r => typeof fn === 'function' ? fn(r) : r[fn]));
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function agg_max(fn) {
|
|
402
|
+
return (rows) => {
|
|
403
|
+
if (rows.length === 0) return null;
|
|
404
|
+
return Math.max(...rows.map(r => typeof fn === 'function' ? fn(r) : r[fn]));
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Data Exploration ──────────────────────────────────
|
|
409
|
+
|
|
410
|
+
export function peek(table, opts = {}) {
|
|
411
|
+
const { title, n = 10 } = typeof opts === 'object' ? opts : {};
|
|
412
|
+
console.log(table._format ? table._format(n, title) : String(table));
|
|
413
|
+
return table; // pass-through for pipeline transparency
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function describe(table) {
|
|
417
|
+
const stats = [];
|
|
418
|
+
for (const col of table._columns) {
|
|
419
|
+
const values = table._rows.map(r => r[col]).filter(v => v !== null && v !== undefined);
|
|
420
|
+
const nonNull = values.length;
|
|
421
|
+
const stat = { Column: col, Type: 'Unknown', 'Non-Null': nonNull };
|
|
422
|
+
|
|
423
|
+
if (values.length > 0) {
|
|
424
|
+
const sample = values[0];
|
|
425
|
+
if (typeof sample === 'number') {
|
|
426
|
+
stat.Type = Number.isInteger(sample) ? 'Int' : 'Float';
|
|
427
|
+
stat.Mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
428
|
+
stat.Min = Math.min(...values);
|
|
429
|
+
stat.Max = Math.max(...values);
|
|
430
|
+
} else if (typeof sample === 'string') {
|
|
431
|
+
stat.Type = 'String';
|
|
432
|
+
stat.Unique = new Set(values).size;
|
|
433
|
+
} else if (typeof sample === 'boolean') {
|
|
434
|
+
stat.Type = 'Bool';
|
|
435
|
+
stat.True = values.filter(v => v).length;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
stats.push(stat);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Print as table
|
|
442
|
+
const descTable = new Table(stats);
|
|
443
|
+
console.log(descTable._format(100, 'describe()'));
|
|
444
|
+
return descTable;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function schema_of(table) {
|
|
448
|
+
const schema = {};
|
|
449
|
+
if (table._rows.length === 0) {
|
|
450
|
+
for (const col of table._columns) schema[col] = 'Unknown';
|
|
451
|
+
} else {
|
|
452
|
+
const sample = table._rows[0];
|
|
453
|
+
for (const col of table._columns) {
|
|
454
|
+
const val = sample[col];
|
|
455
|
+
if (val === null || val === undefined) schema[col] = 'Nil';
|
|
456
|
+
else if (typeof val === 'number') schema[col] = Number.isInteger(val) ? 'Int' : 'Float';
|
|
457
|
+
else if (typeof val === 'string') schema[col] = 'String';
|
|
458
|
+
else if (typeof val === 'boolean') schema[col] = 'Bool';
|
|
459
|
+
else if (Array.isArray(val)) schema[col] = 'Array';
|
|
460
|
+
else schema[col] = 'Object';
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
console.log('Schema:');
|
|
464
|
+
for (const [col, type] of Object.entries(schema)) {
|
|
465
|
+
console.log(` ${col}: ${type}`);
|
|
466
|
+
}
|
|
467
|
+
return schema;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ── Data Cleaning ─────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
export function cast(table, colFn, targetType) {
|
|
473
|
+
const colName = typeof colFn === 'string' ? colFn : null;
|
|
474
|
+
const rows = table._rows.map(r => {
|
|
475
|
+
const row = { ...r };
|
|
476
|
+
const key = colName || Object.keys(r).find(k => colFn(r) === r[k]);
|
|
477
|
+
if (key && key in row) {
|
|
478
|
+
const val = row[key];
|
|
479
|
+
switch (targetType) {
|
|
480
|
+
case 'Int': row[key] = parseInt(val, 10) || 0; break;
|
|
481
|
+
case 'Float': row[key] = parseFloat(val) || 0; break;
|
|
482
|
+
case String: case 'String': row[key] = String(val); break;
|
|
483
|
+
case Boolean: case 'Bool': row[key] = Boolean(val); break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return row;
|
|
487
|
+
});
|
|
488
|
+
return new Table(rows, table._columns);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function drop_nil(table, colFn) {
|
|
492
|
+
const colName = typeof colFn === 'string' ? colFn : null;
|
|
493
|
+
const rows = table._rows.filter(r => {
|
|
494
|
+
const val = colName ? r[colName] : colFn(r);
|
|
495
|
+
return val !== null && val !== undefined;
|
|
496
|
+
});
|
|
497
|
+
return new Table(rows, table._columns);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function fill_nil(table, colFn, defaultValue) {
|
|
501
|
+
const colName = typeof colFn === 'string' ? colFn : null;
|
|
502
|
+
const rows = table._rows.map(r => {
|
|
503
|
+
const row = { ...r };
|
|
504
|
+
if (colName) {
|
|
505
|
+
if (row[colName] === null || row[colName] === undefined) {
|
|
506
|
+
row[colName] = defaultValue;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return row;
|
|
510
|
+
});
|
|
511
|
+
return new Table(rows, table._columns);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function filter_ok(table) {
|
|
515
|
+
const rows = table._rows.filter(r => r && r.__tag === 'Ok').map(r => r.value);
|
|
516
|
+
return new Table(rows);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function filter_err(table) {
|
|
520
|
+
const rows = table._rows.filter(r => r && r.__tag === 'Err').map(r => r.error);
|
|
521
|
+
return new Table(rows);
|
|
522
|
+
}
|