@trebco/treb 30.1.2 → 30.2.4

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/dist/treb.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /*! API v30.1. Copyright 2018-2024 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
1
+ /*! API v30.2. Copyright 2018-2024 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
2
2
 
3
3
  /**
4
4
  * add our tag to the map
@@ -282,6 +282,17 @@ export interface EmbeddedSpreadsheetOptions {
282
282
  * starting in 30.1.0. set to false to disable.
283
283
  */
284
284
  spill?: boolean;
285
+
286
+ /**
287
+ * language. at the moment this controls spreadsheet function names
288
+ * only; the plan is to expand to the rest of the interface over time.
289
+ * should be an ISO 639-1 language code, like "en", "fr" or "sv" (case
290
+ * insensitive). we only support a limited subset of languages at the
291
+ * moment.
292
+ *
293
+ * leave blank or set to "locale" to use the current locale.
294
+ */
295
+ language?: string;
285
296
  }
286
297
 
287
298
  /**
@@ -393,6 +404,9 @@ export declare class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
393
404
  */
394
405
  ExternalEditor(config?: Partial<ExternalEditorConfig>): void;
395
406
 
407
+ /** dynamically load language module */
408
+ LoadLanguage(language?: string): Promise<void>;
409
+
396
410
  /**
397
411
  * Use this function to batch multiple document changes. Essentially the
398
412
  * grid stops broadcasting events for the duration of the function call,
@@ -5,7 +5,7 @@ import * as esbuild from 'esbuild';
5
5
  import { SassPlugin, WorkerPlugin, NotifyPlugin, HTMLPlugin } from './esbuild-utils.mjs';
6
6
  import { promises as fs } from 'fs';
7
7
 
8
- import pkg from './package.json' assert { type: 'json' };
8
+ import pkg from './package.json' with { type: 'json' };
9
9
 
10
10
 
11
11
  /**
@@ -58,6 +58,26 @@ for (let i = 0; i < process.argv.length; i++) {
58
58
  }
59
59
  }
60
60
 
61
+ /**
62
+ * thanks to
63
+ * https://github.com/evanw/esbuild/issues/3337#issuecomment-2085394950
64
+ */
65
+ function RewriteIgnoredImports() {
66
+ return {
67
+ name: 'RewriteIgnoredImports',
68
+ setup(build) {
69
+ build.onEnd(async (result) => {
70
+ if (result.outputFiles) {
71
+ for (const file of result.outputFiles) {
72
+ const { path, text } = file;
73
+ await fs.writeFile(path, text.replace(/esbuild-ignore-import:/, ''));
74
+ }
75
+ }
76
+ });
77
+ },
78
+ };
79
+ }
80
+
61
81
  /** @type esbuild.BuildOptions */
62
82
  const build_options = {
63
83
  entryPoints: [
@@ -78,7 +98,9 @@ const build_options = {
78
98
  'process.env.BUILD_VERSION': `"${pkg.version}"`,
79
99
  'process.env.BUILD_NAME': `"${pkg.name}"`,
80
100
  },
101
+ write: false,
81
102
  plugins: [
103
+ RewriteIgnoredImports(),
82
104
  NotifyPlugin(),
83
105
  WorkerPlugin(options),
84
106
  HTMLPlugin(options),
package/esbuild-utils.mjs CHANGED
@@ -198,6 +198,7 @@ export const WorkerPlugin = (options) => ({
198
198
  }
199
199
  });
200
200
 
201
+
201
202
  /**
202
203
  * @function
203
204
  * @param {{verbose?: boolean, minify?: boolean}} [options]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "30.1.2",
3
+ "version": "30.2.4",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "homepage": "https://treb.app",
6
6
  "repository": {
@@ -200,7 +200,8 @@ export class Area implements IArea {
200
200
  + (address.absolute_column ? '$' : '')
201
201
  + this.ColumnToLabel(address.column)
202
202
  + (address.absolute_row ? '$' : '')
203
- + (address.row + 1);
203
+ + (address.row + 1)
204
+ + (address.spill ? '#' : '');
204
205
 
205
206
  }
206
207
 
@@ -36,7 +36,8 @@ import * as Utilities from './utilities';
36
36
 
37
37
  import { FunctionLibrary } from './function-library';
38
38
  import type { FunctionMap } from './descriptors';
39
- import { ReturnType } from './descriptors';
39
+ // import * as Utils from './utilities';
40
+
40
41
  import { AltFunctionLibrary, BaseFunctionLibrary } from './functions/base-functions';
41
42
  import { FinanceFunctionLibrary } from './functions/finance-functions';
42
43
  import { TextFunctionLibrary, TextFunctionAliases } from './functions/text-functions';
@@ -666,7 +667,7 @@ export class Calculator extends Graph {
666
667
  name: 'width', },
667
668
 
668
669
  ],
669
- return_type: ReturnType.reference,
670
+ return_type: 'reference',
670
671
  volatile: true,
671
672
  fn: ((reference: UnionValue, rows = 0, columns = 0, height?: number, width?: number): UnionValue => {
672
673
 
@@ -758,7 +759,7 @@ export class Calculator extends Graph {
758
759
  arguments: [
759
760
  { name: 'reference', description: 'Cell reference (string)' },
760
761
  ],
761
- return_type: ReturnType.reference,
762
+ return_type: 'reference',
762
763
  volatile: true, // necessary?
763
764
  fn: ((reference: string): UnionValue => {
764
765
 
@@ -23,9 +23,11 @@ import type { RenderFunction, ClickFunction, UnionValue } from 'treb-base-types'
23
23
 
24
24
  // FIXME: at least some of this could move to base types
25
25
 
26
+ /*
26
27
  export enum ReturnType {
27
28
  value, reference
28
29
  }
30
+ */
29
31
 
30
32
  /**
31
33
  * descriptor for an individual argument
@@ -161,9 +163,9 @@ export interface CompositeFunctionDescriptor {
161
163
  xlfn?: boolean;
162
164
 
163
165
  /**
164
- * support returning references
166
+ * support returning references. 'value' is default. FIXME: could just be a bool?
165
167
  */
166
- return_type?: ReturnType;
168
+ return_type?: 'reference'|'value';
167
169
 
168
170
  /**
169
171
  * @internal
@@ -32,7 +32,6 @@ import type { Parser, ExpressionUnit, UnitBinary, UnitIdentifier,
32
32
  UnitGroup, UnitUnary, UnitAddress, UnitRange, UnitCall, UnitDimensionedQuantity, UnitStructuredReference } from 'treb-parser';
33
33
  import type { DataModel, MacroFunction, Sheet } from 'treb-data-model';
34
34
  import { NameError, ReferenceError, ExpressionError, UnknownError, SpillError } from './function-error';
35
- import { ReturnType } from './descriptors';
36
35
 
37
36
  import * as Primitives from './primitives';
38
37
 
@@ -194,7 +193,27 @@ export class ExpressionCalculator {
194
193
 
195
194
  switch (arg.type) {
196
195
  case 'address':
197
- address = arg;
196
+
197
+ if (arg.spill) {
198
+ let sheet: Sheet|undefined;
199
+ if (arg.sheet_id) {
200
+ sheet = this.data_model.sheets.Find(arg.sheet_id);
201
+ }
202
+
203
+ if (!sheet) {
204
+ console.error('missing sheet [da6]');
205
+ return ReferenceError();
206
+ }
207
+
208
+ const cell_data = sheet.CellData(arg);
209
+ if (cell_data.spill) {
210
+ range = cell_data.spill;
211
+ }
212
+
213
+ }
214
+ else {
215
+ address = arg;
216
+ }
198
217
  break;
199
218
 
200
219
  case 'range':
@@ -541,7 +560,7 @@ export class ExpressionCalculator {
541
560
  return argument_error;
542
561
  }
543
562
 
544
- if (func.return_type === ReturnType.reference) {
563
+ if (func.return_type === 'reference') {
545
564
 
546
565
  const result = func.fn.apply(null, mapped_args);
547
566
 
@@ -714,6 +733,41 @@ export class ExpressionCalculator {
714
733
 
715
734
  }
716
735
 
736
+ /**
737
+ * convert expr to a cell address, possibly calculating the contents.
738
+ * returns single address only (ranges with len > 1 will fail)
739
+ */
740
+ protected AsReference(expr: ExpressionUnit): ICellAddress|undefined {
741
+
742
+ switch (expr.type) {
743
+ case 'address':
744
+ return expr;
745
+
746
+ case 'range':
747
+ if (expr.start.row === expr.end.row && expr.start.column === expr.end.column) {
748
+ return expr.start;
749
+ }
750
+ break;
751
+
752
+ default:
753
+ {
754
+ const union = this.CalculateExpression(expr as ExtendedExpressionUnit, true);
755
+ if (UnionIsExpressionUnit(union)) {
756
+ if (union.value.type === 'address') {
757
+ return union.value;
758
+ }
759
+ if (union.value.type === 'range' && union.value.start.row === union.value.end.row && union.value.start.column === union.value.end.column) {
760
+ return union.value.start;
761
+ }
762
+ }
763
+ }
764
+ break;
765
+ }
766
+
767
+ return undefined;
768
+
769
+ }
770
+
717
771
  protected BinaryExpression(x: UnitBinary): (expr: UnitBinary) => UnionValue /*UnionOrArray*/ {
718
772
 
719
773
  // we are constructing and caching functions for binary expressions.
@@ -731,6 +785,25 @@ export class ExpressionCalculator {
731
785
  const fn = Primitives.MapOperator(x.operator);
732
786
 
733
787
  if (!fn) {
788
+
789
+ // support dynamically-constructed ranges, as long as the
790
+ // arguments are both addresses (we might see ranges, but
791
+ // if they are 1x1 then we can accept them)
792
+
793
+ if (x.operator === ':') {
794
+ return (expr: UnitBinary) => {
795
+
796
+ const start = this.AsReference(expr.left);
797
+ const end = this.AsReference(expr.right);
798
+
799
+ if (start && end) {
800
+ return this.CellFunction4(start, end);
801
+ }
802
+
803
+ return ExpressionError();
804
+ };
805
+ }
806
+
734
807
  return () => { // expr: UnitBinary) => {
735
808
  console.info(`(unexpected binary operator: ${x.operator})`);
736
809
  return ExpressionError();
@@ -24,7 +24,7 @@ import * as Utils from '../utilities';
24
24
  import { ReferenceError, NotImplError, NAError, ArgumentError, DivideByZeroError, ValueError } from '../function-error';
25
25
  import type { UnionValue,
26
26
  RenderFunctionResult, RenderFunctionOptions, Complex, CellValue } from 'treb-base-types';
27
- import { Box, ValueType, GetValueType, ComplexOrReal, IsComplex } from 'treb-base-types';
27
+ import { Box, ValueType, GetValueType, ComplexOrReal, IsComplex, Area } from 'treb-base-types';
28
28
  import { Sparkline } from './sparkline';
29
29
  import { LotusDate, UnlotusDate } from 'treb-format';
30
30
 
@@ -33,6 +33,7 @@ import { UnionIsMetadata } from '../expression-calculator';
33
33
 
34
34
  import { Exp as ComplexExp, Power as ComplexPower, Multiply as ComplexMultply } from '../complex-math';
35
35
  import { CoerceComplex } from './function-utilities';
36
+ import type { UnitAddress, UnitRange } from 'treb-parser';
36
37
 
37
38
  /**
38
39
  * BaseFunctionLibrary is a static object that has basic spreadsheet
@@ -1180,7 +1181,11 @@ export const BaseFunctionLibrary: FunctionMap = {
1180
1181
  },
1181
1182
  */
1182
1183
 
1184
+
1183
1185
  /*
1186
+ * rewrite of xlookup to return a reference. better compatibility.
1187
+ * ---
1188
+ *
1184
1189
  * unsaid anywhere (that I can locate) aboud XLOOKUP is that lookup
1185
1190
  * array must be one-dimensional. it can be either a row or a column,
1186
1191
  * but one dimension must be one. that simplifies things quite a bit.
@@ -1193,21 +1198,331 @@ export const BaseFunctionLibrary: FunctionMap = {
1193
1198
  arguments: [
1194
1199
  { name: 'Lookup value', },
1195
1200
  { name: 'Lookup array', },
1196
- { name: 'Return array', },
1201
+ { name: 'Return array', metadata: true, },
1202
+ { name: 'Not found', boxed: true },
1203
+ { name: 'Match mode', default: 0, },
1204
+ { name: 'Search mode', default: 1, },
1205
+ ],
1206
+ return_type: 'reference',
1207
+ xlfn: true,
1208
+ fn: (
1209
+ lookup_value: IntrinsicValue,
1210
+ lookup_array: IntrinsicValue[][],
1211
+ return_array: UnionValue,
1212
+ not_found?: UnionValue,
1213
+ match_mode = 0,
1214
+ search_mode = 1,
1215
+ ): UnionValue => {
1216
+
1217
+ ////////
1218
+
1219
+ if (!return_array) {
1220
+ return ArgumentError();
1221
+ }
1222
+
1223
+ // const parse_result = this.parser.Parse(reference);
1224
+ // if (parse_result.error || !parse_result.expression) {
1225
+ // return ReferenceError;
1226
+ //}
1227
+
1228
+ let rng: Area|undefined;
1229
+
1230
+ if (return_array.type === ValueType.array) {
1231
+ // console.info({return_array});
1232
+
1233
+ const arr = return_array.value;
1234
+ const r = arr.length;
1235
+ const c = arr[0].length;
1236
+
1237
+ const start = arr[0][0];
1238
+ const end = arr[r-1][c-1];
1239
+
1240
+ if (UnionIsMetadata(start) && UnionIsMetadata(end)) {
1241
+ rng = new Area(start.value.address, end.value.address);
1242
+ }
1243
+
1244
+ }
1245
+
1246
+ if (!rng) {
1247
+ console.info('invalid range');
1248
+ return ReferenceError();
1249
+ }
1250
+
1251
+ // console.info({rng});
1252
+
1253
+ /*
1254
+ if (return_array.type === ValueType.array) {
1255
+
1256
+ // subset array. this is constructed, so we can take ownership
1257
+ // and modify it, although it would be safer to copy. also, what's
1258
+ // the cost of functional vs imperative loops these days?
1259
+
1260
+ const end_row = typeof height === 'number' ? (rows + height) : undefined;
1261
+ const end_column = typeof width === 'number' ? (columns + width) : undefined;
1262
+
1263
+ const result: UnionValue = {
1264
+ type: ValueType.array,
1265
+ value: reference.value.slice(rows, end_row).map(row => row.slice(columns, end_column)),
1266
+ };
1267
+
1268
+ return result;
1269
+
1270
+ }
1271
+ */
1272
+
1273
+ // FIXME: we could I suppose be more graceful about single values
1274
+ // if passed instead of arrays
1275
+
1276
+ if (!Array.isArray(lookup_array)) {
1277
+ console.info("lookup is not an array");
1278
+ return ValueError();
1279
+ }
1280
+
1281
+ const first = lookup_array[0];
1282
+ if (!Array.isArray(first)) {
1283
+ console.info("lookup is not a 2d array");
1284
+ return ValueError();
1285
+ }
1286
+
1287
+ if (lookup_array.length !== 1 && first.length !== 1) {
1288
+ console.info("lookup array has invalid dimensions");
1289
+ return ValueError();
1290
+ }
1291
+
1292
+ // FIXME: is it required that the return array be (at least) the
1293
+ // same size? we can return undefineds, but maybe we should error
1294
+
1295
+ /*
1296
+ if (!Array.isArray(return_array)) {
1297
+ console.info("return array is not an array");
1298
+ return ValueError();
1299
+ }
1300
+ */
1301
+
1302
+ const transpose = (lookup_array.length === 1);
1303
+ if (transpose) {
1304
+ lookup_array = Utils.TransposeArray(lookup_array);
1305
+ // return_array = Utils.TransposeArray(return_array);
1306
+ }
1307
+
1308
+ // maybe reverse...
1309
+
1310
+ if (search_mode < 0) {
1311
+ lookup_array.reverse();
1312
+ // return_array.reverse();
1313
+ }
1314
+
1315
+ //
1316
+ // return value at index, transpose if necessary, and return
1317
+ // an array. we might prefer to return a scalar if there's only
1318
+ // one value, not sure what's the intended behavior
1319
+ //
1320
+ const ReturnIndex = (rng: Area, index: number): UnionValue => {
1321
+
1322
+ // console.info("transpose?", transpose, {rng}, 'shape', rng.rows, rng.columns);
1323
+
1324
+ let start: UnitAddress|undefined;
1325
+ let end: UnitAddress|undefined;
1326
+
1327
+ // I guess "transpose" in this context means "return a row from column(s)"? rename
1328
+
1329
+ if (transpose) {
1330
+
1331
+ if (search_mode < 0) {
1332
+ index = rng.rows - 1 - index; // invert FIXME: test
1333
+ }
1334
+
1335
+ if (index >= 0 && index < rng.rows) {
1336
+ start = {
1337
+ type: 'address',
1338
+ position: 0, id: 1, label: '',
1339
+ row: rng.start.row + index,
1340
+ column: rng.start.column,
1341
+ sheet_id: rng.start.sheet_id,
1342
+ };
1343
+
1344
+ end = {
1345
+ type: 'address',
1346
+ position: 0, id: 2, label: '',
1347
+ row: rng.start.row + index,
1348
+ column: rng.end.column,
1349
+ sheet_id: rng.start.sheet_id,
1350
+ };
1351
+ }
1352
+
1353
+ }
1354
+ else {
1355
+
1356
+ if (search_mode < 0) {
1357
+ index = rng.columns - 1 - index; // invert FIXME: test
1358
+ }
1359
+
1360
+ if (index >= 0 && index < rng.columns) {
1361
+ start = {
1362
+ type: 'address',
1363
+ position: 0, id: 1, label: '',
1364
+ row: rng.start.row,
1365
+ column: rng.start.column + index,
1366
+ sheet_id: rng.start.sheet_id,
1367
+ };
1368
+
1369
+ end = {
1370
+ type: 'address',
1371
+ position: 0, id: 2, label: '',
1372
+ row: rng.end.row,
1373
+ column: rng.start.column + index,
1374
+ sheet_id: rng.start.sheet_id,
1375
+ };
1376
+ }
1377
+ }
1378
+
1379
+ if (start && end) {
1380
+
1381
+ const expr: UnitRange = {
1382
+ type: 'range',
1383
+ position: 0,
1384
+ id: 0,
1385
+ label: '',
1386
+ start, end,
1387
+ };
1388
+
1389
+ // console.info({expr});
1390
+
1391
+ return {
1392
+ type: ValueType.object,
1393
+ value: expr,
1394
+ }
1395
+
1396
+ }
1397
+
1398
+ return { type: ValueType.undefined };
1399
+
1400
+ };
1401
+
1402
+ // if value is not a string, then we can ignore wildcards.
1403
+ // in that case convert to exact match.
1404
+
1405
+ if (match_mode === 2 && typeof lookup_value !== 'string') {
1406
+ match_mode = 0;
1407
+ }
1408
+
1409
+ // what does inexact matching mean in this case if the lookup
1410
+ // value is a string or boolean? (...)
1411
+
1412
+ if ((match_mode === 1 || match_mode === -1) && typeof lookup_value === 'number') {
1413
+
1414
+ let min_delta = 0;
1415
+ let index = -1;
1416
+
1417
+ for (let i = 0; i < lookup_array.length; i++) {
1418
+ const value = lookup_array[i][0];
1419
+
1420
+
1421
+ if (typeof value === 'number') {
1422
+
1423
+ // check for exact match first, just in case
1424
+ if (value === lookup_value) {
1425
+ return ReturnIndex(rng, i);
1426
+ }
1427
+
1428
+ const delta = Math.abs(value - lookup_value);
1429
+
1430
+ if ((match_mode === 1 && value > lookup_value) || (match_mode === -1 && value < lookup_value)){
1431
+ if (index < 0 || delta < min_delta) {
1432
+ min_delta = delta;
1433
+ index = i;
1434
+ }
1435
+ }
1436
+
1437
+ }
1438
+ }
1439
+
1440
+ if (index >= 0) {
1441
+ return ReturnIndex(rng, index);
1442
+ }
1443
+
1444
+ }
1445
+
1446
+ switch (match_mode) {
1447
+
1448
+ case 2:
1449
+ {
1450
+ // wildcard string match. we only handle strings for
1451
+ // this case (see above).
1452
+
1453
+ const pattern = Utils.ParseWildcards(lookup_value?.toString() || '');
1454
+ const regex = new RegExp('^' + pattern + '$', 'i'); //.exec(lookup_value);
1455
+
1456
+ for (let i = 0; i < lookup_array.length; i++) {
1457
+ const value = lookup_array[i][0];
1458
+ if (typeof value === 'string' && regex.exec(value)) {
1459
+ return ReturnIndex(rng, i);
1460
+ }
1461
+ }
1462
+
1463
+ }
1464
+ break;
1465
+
1466
+ case 0:
1467
+ if (typeof lookup_value === 'string') {
1468
+ lookup_value = lookup_value.toLowerCase();
1469
+ }
1470
+ for (let i = 0; i < lookup_array.length; i++) {
1471
+ let value = lookup_array[i][0];
1472
+
1473
+ if (typeof value === 'string') {
1474
+ value = value.toLowerCase();
1475
+ }
1476
+ if (value === lookup_value) {
1477
+ return ReturnIndex(rng, i);
1478
+ }
1479
+ }
1480
+
1481
+ break;
1482
+ }
1483
+
1484
+
1485
+ // FIXME: if we're expecting to return an array maybe we should
1486
+ // pack it up as an array? if it's not already an array? (...)
1487
+
1488
+ return (not_found && not_found.type !== ValueType.undefined) ? not_found : NAError();
1489
+
1490
+ },
1491
+ },
1492
+
1493
+
1494
+ /*
1495
+ * unsaid anywhere (that I can locate) aboud XLOOKUP is that lookup
1496
+ * array must be one-dimensional. it can be either a row or a column,
1497
+ * but one dimension must be one. that simplifies things quite a bit.
1498
+ *
1499
+ * there's a note in the docs about binary search over the data --
1500
+ * that might explain how inexact VLOOKUP works as well. seems an odd
1501
+ * choice but maybe back in the day it made sense
1502
+ * /
1503
+ XLOOKUP: {
1504
+ arguments: [
1505
+ { name: 'Lookup value', },
1506
+ { name: 'Lookup array', },
1507
+ { name: 'Return array', address: true, },
1197
1508
  { name: 'Not found', boxed: true },
1198
1509
  { name: 'Match mode', default: 0, },
1199
1510
  { name: 'Search mode', default: 1, },
1200
1511
  ],
1512
+ return_type: 'reference',
1201
1513
  xlfn: true,
1202
1514
  fn: (
1203
1515
  lookup_value: IntrinsicValue,
1204
1516
  lookup_array: IntrinsicValue[][],
1205
- return_array: IntrinsicValue[][],
1517
+ return_array: string,
1206
1518
  not_found?: UnionValue,
1207
1519
  match_mode = 0,
1208
1520
  search_mode = 1,
1209
1521
  ): UnionValue => {
1210
1522
 
1523
+ console.info({return_array});
1524
+
1525
+
1211
1526
  // FIXME: we could I suppose be more graceful about single values
1212
1527
  // if passed instead of arrays
1213
1528
 
@@ -1368,6 +1683,7 @@ export const BaseFunctionLibrary: FunctionMap = {
1368
1683
 
1369
1684
  },
1370
1685
  },
1686
+ */
1371
1687
 
1372
1688
  /**
1373
1689
  * copied from HLOOKUP, fix that one first