@trebco/treb 32.10.0 → 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/dist/treb.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /*! API v32.10. Copyright 2018-2025 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
1
+ /*! API v32.11. Copyright 2018-2025 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
2
2
  /*
3
3
  * This file is part of TREB.
4
4
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "32.10.0",
3
+ "version": "32.11.0",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "homepage": "https://treb.app",
6
6
  "repository": {
@@ -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: 'values' }
1352
+ { name: 'array', },
1353
+ { name: 'index', },
1354
+ { name: 'order', description: 'Set to -1 to sort in descending order', default: 1 }
1189
1355
  ],
1190
- fn: (...args: CellValue[]): UnionValue => {
1356
+ fn: (ref: CellValue|CellValue[][], index = 1, order = 1): UnionValue => {
1191
1357
 
1192
- args = Utils.FlattenCellValues(args);
1358
+ if (!Array.isArray(ref)) {
1359
+ ref = [[ref]];
1360
+ }
1361
+
1362
+ // FIXME: transpose for column sort
1193
1363
 
1194
- if(args.every(test => typeof test === 'number')) {
1195
- (args as number[]).sort((a, b) => a - b);
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
- args.sort(); // lexical
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: [args.map(value => Box(value))] };
1394
+ return { type: ValueType.array, value: result };
1202
1395
 
1203
1396
  },
1204
1397
  },