@trebco/treb 32.9.3 → 32.11.0
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/bun.lock +656 -0
- package/dist/languages/treb-i18n-da.mjs +1 -1
- package/dist/languages/treb-i18n-de.mjs +1 -1
- package/dist/languages/treb-i18n-es.mjs +1 -1
- package/dist/languages/treb-i18n-fr.mjs +1 -1
- package/dist/languages/treb-i18n-it.mjs +1 -1
- package/dist/languages/treb-i18n-nl.mjs +1 -1
- package/dist/languages/treb-i18n-no.mjs +1 -1
- package/dist/languages/treb-i18n-pl.mjs +1 -1
- package/dist/languages/treb-i18n-pt.mjs +1 -1
- package/dist/languages/treb-i18n-sv.mjs +1 -1
- package/dist/treb-export-worker.mjs +2 -2
- package/dist/treb-spreadsheet.mjs +18 -15
- package/dist/treb.d.ts +1 -1
- package/package.json +53 -53
- package/treb-calculator/src/expression-calculator.ts +3 -9
- package/treb-calculator/src/functions/base-functions.ts +200 -7
- package/treb-parser/src/parser.ts +6 -1
package/dist/treb.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@trebco/treb",
|
|
3
|
-
"version": "32.
|
|
4
|
-
"license": "LGPL-3.0-or-later",
|
|
5
|
-
"homepage": "https://treb.app",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "https://github.com/trebco/treb.git"
|
|
9
|
-
},
|
|
10
|
-
"main": "dist/treb-spreadsheet.mjs",
|
|
11
|
-
"types": "dist/treb.d.ts",
|
|
12
|
-
"type": "module",
|
|
13
|
-
"devDependencies": {
|
|
14
|
-
"@types/html-minifier": "^4.0.
|
|
15
|
-
"@types/node": "^
|
|
16
|
-
"@types/uzip": "^0.20201231.
|
|
17
|
-
"base64-js": "^1.5.1",
|
|
18
|
-
"cssnano": "^7.
|
|
19
|
-
"esbuild": "^0.25.
|
|
20
|
-
"eslint": "^9.
|
|
21
|
-
"fast-xml-parser": "^
|
|
22
|
-
"html-minifier-terser": "^7.2.0",
|
|
23
|
-
"sass": "^1.
|
|
24
|
-
"treb-base-types": "file:treb-base-types",
|
|
25
|
-
"treb-calculator": "file:treb-calculator",
|
|
26
|
-
"treb-charts": "file:treb-charts",
|
|
27
|
-
"treb-data-model": "file:treb-data-model",
|
|
28
|
-
"treb-export": "file:treb-export",
|
|
29
|
-
"treb-format": "file:treb-format",
|
|
30
|
-
"treb-grid": "file:treb-grid",
|
|
31
|
-
"treb-parser": "file:treb-parser",
|
|
32
|
-
"treb-utils": "file:treb-utils",
|
|
33
|
-
"ts-node": "^10.9.2",
|
|
34
|
-
"tslib": "^2.
|
|
35
|
-
"tsx": "^4.
|
|
36
|
-
"typescript": "^5.
|
|
37
|
-
"typescript-eslint": "^8.
|
|
38
|
-
"uzip": "^0.20201231.0"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"build": "node esbuild-composite.mjs",
|
|
42
|
-
"dev": "node esbuild-composite.mjs --dev",
|
|
43
|
-
"clean": "rm -fr build dist declaration",
|
|
44
|
-
"watch": "node --watch esbuild-composite.mjs --watch --dev",
|
|
45
|
-
"watch-production": "node --watch esbuild-composite.mjs --watch",
|
|
46
|
-
"tsc": "tsc -b treb-embed/tsconfig.json",
|
|
47
|
-
"rebuild-tsc": "tsc -b --force treb-embed/tsconfig.json",
|
|
48
|
-
"watch-tsc": "tsc -b treb-embed/tsconfig.json -w",
|
|
49
|
-
"watch-api": "tsx --watch api-generator/api-generator.ts --config api-config.json",
|
|
50
|
-
"generate-api": "tsx api-generator/api-generator.ts --config api-config.json",
|
|
51
|
-
"release": "npm run rebuild-tsc && npm run build && npm run generate-api"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@trebco/treb",
|
|
3
|
+
"version": "32.11.0",
|
|
4
|
+
"license": "LGPL-3.0-or-later",
|
|
5
|
+
"homepage": "https://treb.app",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/trebco/treb.git"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/treb-spreadsheet.mjs",
|
|
11
|
+
"types": "dist/treb.d.ts",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/html-minifier": "^4.0.5",
|
|
15
|
+
"@types/node": "^24.3.1",
|
|
16
|
+
"@types/uzip": "^0.20201231.2",
|
|
17
|
+
"base64-js": "^1.5.1",
|
|
18
|
+
"cssnano": "^7.1.1",
|
|
19
|
+
"esbuild": "^0.25.9",
|
|
20
|
+
"eslint": "^9.35.0",
|
|
21
|
+
"fast-xml-parser": "^5.2.5",
|
|
22
|
+
"html-minifier-terser": "^7.2.0",
|
|
23
|
+
"sass": "^1.92.1",
|
|
24
|
+
"treb-base-types": "file:treb-base-types",
|
|
25
|
+
"treb-calculator": "file:treb-calculator",
|
|
26
|
+
"treb-charts": "file:treb-charts",
|
|
27
|
+
"treb-data-model": "file:treb-data-model",
|
|
28
|
+
"treb-export": "file:treb-export",
|
|
29
|
+
"treb-format": "file:treb-format",
|
|
30
|
+
"treb-grid": "file:treb-grid",
|
|
31
|
+
"treb-parser": "file:treb-parser",
|
|
32
|
+
"treb-utils": "file:treb-utils",
|
|
33
|
+
"ts-node": "^10.9.2",
|
|
34
|
+
"tslib": "^2.8.1",
|
|
35
|
+
"tsx": "^4.20.5",
|
|
36
|
+
"typescript": "^5.9.2",
|
|
37
|
+
"typescript-eslint": "^8.43.0",
|
|
38
|
+
"uzip": "^0.20201231.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "node esbuild-composite.mjs",
|
|
42
|
+
"dev": "node esbuild-composite.mjs --dev",
|
|
43
|
+
"clean": "rm -fr build dist declaration",
|
|
44
|
+
"watch": "node --watch esbuild-composite.mjs --watch --dev",
|
|
45
|
+
"watch-production": "node --watch esbuild-composite.mjs --watch",
|
|
46
|
+
"tsc": "tsc -b treb-embed/tsconfig.json",
|
|
47
|
+
"rebuild-tsc": "tsc -b --force treb-embed/tsconfig.json",
|
|
48
|
+
"watch-tsc": "tsc -b treb-embed/tsconfig.json -w",
|
|
49
|
+
"watch-api": "tsx --watch api-generator/api-generator.ts --config api-config.json",
|
|
50
|
+
"generate-api": "tsx api-generator/api-generator.ts --config api-config.json",
|
|
51
|
+
"release": "npm run rebuild-tsc && npm run build && npm run generate-api"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -87,6 +87,7 @@ export interface ReferenceMetadata {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export type BindingFrame = Record<string, ExpressionUnit>; // FIXME (type?)
|
|
90
|
+
|
|
90
91
|
export type PositionalFrame = ExpressionUnit[];
|
|
91
92
|
|
|
92
93
|
export interface CalculationContext {
|
|
@@ -464,8 +465,6 @@ export class ExpressionCalculator {
|
|
|
464
465
|
return ExpressionError();
|
|
465
466
|
}
|
|
466
467
|
|
|
467
|
-
// let frame = value.bindings(expr.args);
|
|
468
|
-
|
|
469
468
|
const frame: BindingFrame = {};
|
|
470
469
|
|
|
471
470
|
for (let i = 0; i < value.bindings.length; i++) {
|
|
@@ -479,8 +478,6 @@ export class ExpressionCalculator {
|
|
|
479
478
|
}
|
|
480
479
|
}
|
|
481
480
|
|
|
482
|
-
// frame = this.NormalizeBindings(frame); // inline
|
|
483
|
-
|
|
484
481
|
const munged = JSON.parse(JSON.stringify(value.func));
|
|
485
482
|
|
|
486
483
|
this.parser.Walk2(munged, (unit: ExpressionUnit) => {
|
|
@@ -493,11 +490,6 @@ export class ExpressionCalculator {
|
|
|
493
490
|
}
|
|
494
491
|
return true;
|
|
495
492
|
});
|
|
496
|
-
|
|
497
|
-
// console.info({frame, func, munged});
|
|
498
|
-
|
|
499
|
-
// const exec_result = this.CalculateExpression(munged as ExtendedExpressionUnit);
|
|
500
|
-
// return exec_result || ExpressionError();
|
|
501
493
|
|
|
502
494
|
return this.CalculateExpression(munged as ExtendedExpressionUnit);
|
|
503
495
|
|
|
@@ -582,6 +574,7 @@ export class ExpressionCalculator {
|
|
|
582
574
|
* the parsing stage, it's a simple translation between the two
|
|
583
575
|
*/
|
|
584
576
|
protected ImplicitCall(): (expr: UnitImplicitCall|UnitCall) => UnionValue {
|
|
577
|
+
|
|
585
578
|
return (expr: UnitImplicitCall|UnitCall) => {
|
|
586
579
|
|
|
587
580
|
if (expr.type === 'call') {
|
|
@@ -634,6 +627,7 @@ export class ExpressionCalculator {
|
|
|
634
627
|
const upper_case = outer.name.toUpperCase();
|
|
635
628
|
|
|
636
629
|
const binding = this.LookupBinding(upper_case);
|
|
630
|
+
|
|
637
631
|
if (binding) {
|
|
638
632
|
return this.ImplicitCall();
|
|
639
633
|
}
|
|
@@ -234,6 +234,79 @@ const TrigFunction = (real: (value: number) => number, complex: (value: Complex)
|
|
|
234
234
|
};
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* helper for sorting. kind of weird rules
|
|
239
|
+
*/
|
|
240
|
+
const SortHelper = (order: number, av: CellValue, bv: CellValue) => {
|
|
241
|
+
|
|
242
|
+
// OK from what I can tell the rules are
|
|
243
|
+
//
|
|
244
|
+
// (1) no type coercion (but see below for blanks)
|
|
245
|
+
// (2) strings are sorted case-insensitive
|
|
246
|
+
// (3) blank values are coerced -> 0 but these are not sorted as numbers
|
|
247
|
+
// (4) order is numbers, strings, booleans, then blanks
|
|
248
|
+
//
|
|
249
|
+
// some of which makes sense, I guess...
|
|
250
|
+
//
|
|
251
|
+
// blanks are always sorted last, irrespective of sort order. which
|
|
252
|
+
// means we can't sort and then reverse, because reverse sort is not
|
|
253
|
+
// the inverse of forward sort
|
|
254
|
+
|
|
255
|
+
// special case
|
|
256
|
+
|
|
257
|
+
if (av === bv) {
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// special case
|
|
262
|
+
|
|
263
|
+
if (av === undefined) {
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
if (bv === undefined) {
|
|
267
|
+
return -1;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// actual comparisons
|
|
271
|
+
|
|
272
|
+
if (typeof av === 'number' && typeof bv === 'number') {
|
|
273
|
+
return (av - bv) * order;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (typeof av === 'string' && typeof bv === 'string') {
|
|
277
|
+
return av.toLocaleLowerCase().localeCompare(bv.toLowerCase()) * order;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (typeof av === 'boolean' && typeof bv === 'boolean') {
|
|
281
|
+
return av ? order : -order;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (IsComplex(av) && IsComplex(bv)) {
|
|
285
|
+
return 0; // no sort order
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// type rules
|
|
289
|
+
|
|
290
|
+
const types = [av, bv].map(x => {
|
|
291
|
+
switch (typeof x) {
|
|
292
|
+
case 'number':
|
|
293
|
+
return 0;
|
|
294
|
+
case 'string':
|
|
295
|
+
return 2;
|
|
296
|
+
case 'boolean':
|
|
297
|
+
return 3;
|
|
298
|
+
default:
|
|
299
|
+
if (IsComplex(x)) {
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
return 4;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return (types[0] - types[1]) * order;
|
|
307
|
+
|
|
308
|
+
};
|
|
309
|
+
|
|
237
310
|
/**
|
|
238
311
|
* alternate functions. these are used (atm) only for changing complex
|
|
239
312
|
* behavior.
|
|
@@ -1176,29 +1249,149 @@ export const BaseFunctionLibrary: FunctionMap = {
|
|
|
1176
1249
|
|
|
1177
1250
|
},
|
|
1178
1251
|
|
|
1252
|
+
/**
|
|
1253
|
+
* sortby allows multiple sort indexes, but no column sorting
|
|
1254
|
+
*/
|
|
1255
|
+
SortBy: {
|
|
1256
|
+
arguments: [
|
|
1257
|
+
{ name: 'array', },
|
|
1258
|
+
{ name: 'index', },
|
|
1259
|
+
{ name: 'order', description: 'Set to -1 to sort in descending order', default: 1 }
|
|
1260
|
+
],
|
|
1261
|
+
fn: (ref: CellValue|CellValue[][], ...args: (CellValue|CellValue[][])[]): UnionValue => {
|
|
1262
|
+
|
|
1263
|
+
if (!Array.isArray(ref)) {
|
|
1264
|
+
ref = [[ref]];
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// must have at least one sort order?
|
|
1268
|
+
|
|
1269
|
+
if (args.length < 1) {
|
|
1270
|
+
return ArgumentError();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// ensure any sort argument pairs are valid... I guess they
|
|
1274
|
+
// need to be the same length? what happens if not? [A: error]
|
|
1275
|
+
|
|
1276
|
+
const rows = ref[0]?.length || 0;
|
|
1277
|
+
const orders: number[] = [];
|
|
1278
|
+
const values: CellValue[][] = [];
|
|
1279
|
+
|
|
1280
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
1281
|
+
|
|
1282
|
+
const target = i/2;
|
|
1283
|
+
|
|
1284
|
+
let sort_range = args[i];
|
|
1285
|
+
if (!Array.isArray(sort_range)) {
|
|
1286
|
+
sort_range = [[sort_range]];
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const check = sort_range[0]?.length || 0;
|
|
1290
|
+
if (check !== rows) {
|
|
1291
|
+
return ArgumentError();
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
let order = 1;
|
|
1295
|
+
const arg = args[i+1];
|
|
1296
|
+
if (typeof arg === 'number' && arg < 0) {
|
|
1297
|
+
order = -1;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
orders[target] = order;
|
|
1301
|
+
values[target] = sort_range[0]; // (sort_range[0]).slice(0);
|
|
1302
|
+
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const mapped = ref[0]?.map((value, index) => (index));
|
|
1306
|
+
mapped.sort((a, b) => {
|
|
1307
|
+
|
|
1308
|
+
for (let i = 0; i < orders.length; i++) {
|
|
1309
|
+
const order = orders[i];
|
|
1310
|
+
const value_set = values[i];
|
|
1311
|
+
const result = SortHelper(order, value_set[a], value_set[b]);
|
|
1312
|
+
if (result) {
|
|
1313
|
+
return result;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
return 0;
|
|
1318
|
+
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
// output is same shape
|
|
1322
|
+
|
|
1323
|
+
const columns = ref.length;
|
|
1324
|
+
const result: UnionValue[][] = [];
|
|
1325
|
+
|
|
1326
|
+
for (let c = 0; c < columns; c++) {
|
|
1327
|
+
const column: UnionValue[] = [];
|
|
1328
|
+
for (const index of mapped) {
|
|
1329
|
+
column.push(Box(ref[c][index]));
|
|
1330
|
+
}
|
|
1331
|
+
result.push(column);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return { type: ValueType.array, value: result };
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
},
|
|
1338
|
+
},
|
|
1339
|
+
|
|
1179
1340
|
/**
|
|
1180
1341
|
* sort arguments, but ensure we return empty strings to
|
|
1181
1342
|
* fill up the result array
|
|
1182
1343
|
*
|
|
1183
1344
|
* FIXME: instead of boxing all the values, why not pass them in boxed?
|
|
1184
1345
|
* was this function just written at the wrong time?
|
|
1346
|
+
*
|
|
1347
|
+
* UPDATE: rewriting to match Excel args
|
|
1348
|
+
*
|
|
1185
1349
|
*/
|
|
1186
1350
|
Sort: {
|
|
1187
1351
|
arguments: [
|
|
1188
|
-
{ name: '
|
|
1352
|
+
{ name: 'array', },
|
|
1353
|
+
{ name: 'index', },
|
|
1354
|
+
{ name: 'order', description: 'Set to -1 to sort in descending order', default: 1 }
|
|
1189
1355
|
],
|
|
1190
|
-
fn: (
|
|
1356
|
+
fn: (ref: CellValue|CellValue[][], index = 1, order = 1): UnionValue => {
|
|
1191
1357
|
|
|
1192
|
-
|
|
1358
|
+
if (!Array.isArray(ref)) {
|
|
1359
|
+
ref = [[ref]];
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// FIXME: transpose for column sort
|
|
1193
1363
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1364
|
+
const sort_column = ref[index - 1];
|
|
1365
|
+
if (!sort_column) {
|
|
1366
|
+
return ArgumentError();
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// clean (and be lenient)
|
|
1370
|
+
|
|
1371
|
+
if (order < 0) {
|
|
1372
|
+
order = -1;
|
|
1196
1373
|
}
|
|
1197
1374
|
else {
|
|
1198
|
-
|
|
1375
|
+
order = 1;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const mapped = sort_column.map((value, index) => ({value, index}));
|
|
1379
|
+
mapped.sort((a, b) => SortHelper(order, a.value, b.value));
|
|
1380
|
+
|
|
1381
|
+
// output is same shape
|
|
1382
|
+
|
|
1383
|
+
const columns = ref.length;
|
|
1384
|
+
const result: UnionValue[][] = [];
|
|
1385
|
+
|
|
1386
|
+
for (let c = 0; c < columns; c++) {
|
|
1387
|
+
const column: UnionValue[] = [];
|
|
1388
|
+
for (const { index } of mapped) {
|
|
1389
|
+
column.push(Box(ref[c][index]));
|
|
1390
|
+
}
|
|
1391
|
+
result.push(column);
|
|
1199
1392
|
}
|
|
1200
1393
|
|
|
1201
|
-
return { type: ValueType.array, value:
|
|
1394
|
+
return { type: ValueType.array, value: result };
|
|
1202
1395
|
|
|
1203
1396
|
},
|
|
1204
1397
|
},
|
|
@@ -636,8 +636,13 @@ export class Parser {
|
|
|
636
636
|
// function. in that case we don't want a space in front of the
|
|
637
637
|
// operator.
|
|
638
638
|
|
|
639
|
+
// UPDATE: for aesthetic reasons, also remove spaces around a
|
|
640
|
+
// power operator (caret, "^")
|
|
641
|
+
|
|
642
|
+
// FIXME: parameterize?
|
|
643
|
+
|
|
639
644
|
{
|
|
640
|
-
const separator = (unit.operator === ':' ? '': ' ');
|
|
645
|
+
const separator = ((unit.operator === ':' || unit.operator === '^') ? '': ' ');
|
|
641
646
|
|
|
642
647
|
return (
|
|
643
648
|
this.Render(unit.left, options) +
|