@trebco/treb 30.1.2 → 30.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "30.1.2",
3
+ "version": "30.1.8",
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
 
@@ -731,6 +750,52 @@ export class ExpressionCalculator {
731
750
  const fn = Primitives.MapOperator(x.operator);
732
751
 
733
752
  if (!fn) {
753
+
754
+ // support dynamically-constructed ranges, as long as the
755
+ // arguments are both addresses (we might see ranges, but
756
+ // if they are 1x1 then we can accept them)
757
+
758
+ if (x.operator === ':') {
759
+ return (expr: UnitBinary) => {
760
+
761
+ const left = this.CalculateExpression(expr.left as ExtendedExpressionUnit, true);
762
+ const right = this.CalculateExpression(expr.right as ExtendedExpressionUnit, true);
763
+
764
+ let start: UnitAddress|undefined;
765
+ let end: UnitAddress|undefined;
766
+
767
+ // console.info({expr, left, right});
768
+
769
+ if (UnionIsExpressionUnit(left) && UnionIsExpressionUnit(right)) {
770
+
771
+ if (left.value.type === 'range') {
772
+ if (left.value.start.row === left.value.end.row && left.value.start.column === left.value.end.column) {
773
+ start = left.value.start;
774
+ }
775
+ }
776
+ else if (left.value.type === 'address') {
777
+ start = left.value;
778
+ }
779
+
780
+ if (right.value.type === 'range') {
781
+ if (right.value.start.row === right.value.end.row && right.value.start.column === right.value.end.column) {
782
+ end = right.value.start;
783
+ }
784
+ }
785
+ else if (right.value.type === 'address') {
786
+ end = right.value;
787
+ }
788
+
789
+ if (start && end) {
790
+ return this.CellFunction4(start, end);
791
+ }
792
+
793
+ }
794
+
795
+ return ExpressionError();
796
+ };
797
+ }
798
+
734
799
  return () => { // expr: UnitBinary) => {
735
800
  console.info(`(unexpected binary operator: ${x.operator})`);
736
801
  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
@@ -6385,6 +6385,14 @@ export class Grid extends GridBase {
6385
6385
  if ((area.start.row === Infinity || area.start.row < this.active_sheet.rows) &&
6386
6386
  (area.start.column === Infinity || area.start.column < this.active_sheet.columns)) {
6387
6387
 
6388
+ if (area.start.spill && area.end.row === area.start.row && area.end.column === area.start.column) {
6389
+ const sheet = this.model.sheets.Find(area.start.sheet_id || -1);
6390
+ const cell = sheet?.CellData(area.start);
6391
+ if (cell?.spill && cell.spill.start.row === area.start.row && cell.spill.start.column === area.start.column) {
6392
+ area.ConsumeArea(cell.spill);
6393
+ }
6394
+ }
6395
+
6388
6396
  area = this.active_sheet.RealArea(area);
6389
6397
  const label = area.spreadsheet_label;
6390
6398