@marktiderman/genesis-forms 0.4.0 → 0.4.1

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/index.cjs CHANGED
@@ -64,6 +64,7 @@ function tokenize(source) {
64
64
  i++;
65
65
  while (isDigit(source[i] ?? "")) i++;
66
66
  }
67
+ if (isIdStart(source[i] ?? "")) throw new FormulaError("Write a number in digits only, like 1000000.", at);
67
68
  const v = Number(source.slice(at, i));
68
69
  if (!Number.isFinite(v)) throw new FormulaError("This number is too large.", at);
69
70
  tokens.push({ t: "num", v, at });
@@ -293,6 +294,27 @@ function numbersIn(values, out = []) {
293
294
  }
294
295
  return out;
295
296
  }
297
+ var listTotals = /* @__PURE__ */ new WeakMap();
298
+ var ownLists = /* @__PURE__ */ new WeakSet();
299
+ function ownFormulaList(list) {
300
+ Object.freeze(list);
301
+ ownLists.add(list);
302
+ return list;
303
+ }
304
+ function totalsOf(list) {
305
+ const own2 = ownLists.has(list);
306
+ let t = own2 ? listTotals.get(list) : void 0;
307
+ if (t) return t;
308
+ const nums = numbersIn(list);
309
+ t = { sum: 0, count: nums.length, min: Infinity, max: -Infinity };
310
+ for (const n of nums) {
311
+ t.sum += n;
312
+ if (n < t.min) t.min = n;
313
+ if (n > t.max) t.max = n;
314
+ }
315
+ if (own2) listTotals.set(list, t);
316
+ return t;
317
+ }
296
318
  function compare(op, a, b) {
297
319
  const blank2 = (v) => v === null || Array.isArray(v) || typeof v === "string" && v.trim() === "";
298
320
  if (blank2(a) || blank2(b)) return op === "!=";
@@ -317,7 +339,7 @@ function evaluateFormula(ast, read) {
317
339
  case "text":
318
340
  return node.value;
319
341
  case "ref":
320
- return read(node.column !== void 0 ? { key: node.key, column: node.column } : { key: node.key });
342
+ return read(node);
321
343
  case "unary": {
322
344
  const n = formulaNumber(scalar(run(node.arg)));
323
345
  return n === null ? null : -n;
@@ -325,12 +347,31 @@ function evaluateFormula(ast, read) {
325
347
  case "if":
326
348
  return truthy(run(node.test)) ? run(node.then) : run(node.else);
327
349
  case "call": {
328
- const nums = numbersIn(node.args.map(run));
329
- const total = nums.reduce((a, b) => a + b, 0);
350
+ let total = 0;
351
+ let count = 0;
352
+ let min = Infinity;
353
+ let max = -Infinity;
354
+ for (const arg of node.args) {
355
+ const v = run(arg);
356
+ if (Array.isArray(v)) {
357
+ const t = totalsOf(v);
358
+ total += t.sum;
359
+ count += t.count;
360
+ if (t.min < min) min = t.min;
361
+ if (t.max > max) max = t.max;
362
+ continue;
363
+ }
364
+ const n = formulaNumber(v);
365
+ if (n === null) continue;
366
+ total += n;
367
+ count++;
368
+ if (n < min) min = n;
369
+ if (n > max) max = n;
370
+ }
330
371
  if (node.fn === "sum") return finite(total);
331
- if (!nums.length) return null;
332
- if (node.fn === "avg") return finite(total / nums.length);
333
- return nums.reduce((a, b) => node.fn === "min" ? Math.min(a, b) : Math.max(a, b));
372
+ if (!count) return null;
373
+ if (node.fn === "avg") return finite(total / count);
374
+ return node.fn === "min" ? min : max;
334
375
  }
335
376
  case "binary": {
336
377
  const a = run(node.left);
@@ -358,6 +399,7 @@ function evaluateFormula(ast, read) {
358
399
  // src/calculated.ts
359
400
  var CALCULATED_TYPE = "calculated";
360
401
  var COMPUTED_FIELD_TYPES = [CALCULATED_TYPE];
402
+ var MAX_FORM_FORMULA_LENGTH = 2e4;
361
403
  var MAX_CALCULATED_DECIMALS = 10;
362
404
  function calculatedProps(field2) {
363
405
  const p = field2.props ?? {};
@@ -396,23 +438,45 @@ function formatCalculated(value, field2) {
396
438
  return typeof value === "string" ? value : "";
397
439
  }
398
440
  var MAX_FORMULA_ITEMS = 1e4;
441
+ var MAX_SETTLE_ITEMS = 5e4;
442
+ var extract = (value, column) => column !== void 0 ? value.flatMap((row) => {
443
+ if (!row || typeof row !== "object" || Array.isArray(row)) return [];
444
+ const cell = row[column];
445
+ return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
446
+ }) : value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
447
+ var extracted = /* @__PURE__ */ new WeakMap();
448
+ function cachedList(value, column) {
449
+ let byColumn = extracted.get(value);
450
+ if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
451
+ const key = column ?? "";
452
+ let list = byColumn.get(key);
453
+ if (!list) byColumn.set(key, list = ownFormulaList(extract(value, column)));
454
+ return list;
455
+ }
456
+ function readAnswer(value, column) {
457
+ if (Array.isArray(value)) return cachedList(value, column);
458
+ return column !== void 0 ? null : formulaInput(value);
459
+ }
399
460
  function formulaInput(value, column) {
400
461
  if (value === void 0 || value === null) return null;
401
462
  if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
402
463
  if (column !== void 0) {
403
464
  if (!Array.isArray(value)) return null;
404
- return value.flatMap((row) => {
405
- if (!row || typeof row !== "object" || Array.isArray(row)) return [];
406
- const cell = row[column];
407
- return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
408
- });
409
- }
410
- if (Array.isArray(value)) {
411
- return value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
465
+ return extract(value, column);
412
466
  }
467
+ if (Array.isArray(value)) return extract(value, void 0);
413
468
  return typeof value === "object" ? null : value;
414
469
  }
470
+ function refNodes(node, out = []) {
471
+ if (node.kind === "ref") out.push(node);
472
+ else if (node.kind === "unary") refNodes(node.arg, out);
473
+ else if (node.kind === "binary") refNodes(node.left, out), refNodes(node.right, out);
474
+ else if (node.kind === "if") refNodes(node.test, out), refNodes(node.then, out), refNodes(node.else, out);
475
+ else if (node.kind === "call") for (const a of node.args) refNodes(a, out);
476
+ return out;
477
+ }
415
478
  var UNREAD = /* @__PURE__ */ Symbol("unread");
479
+ var OVER = /* @__PURE__ */ Symbol("over");
416
480
  function calculator(definition) {
417
481
  const compiled = [];
418
482
  const idByKey = /* @__PURE__ */ new Map();
@@ -423,14 +487,39 @@ function calculator(definition) {
423
487
  for (const block of definition.blocks) {
424
488
  if (block.kind !== "field" || !isCalculated(block)) continue;
425
489
  const parsed = parseFormula(calculatedProps(block).formula);
426
- const reads = parsed.ok ? parsed.refs.map((r) => idByKey.get(r.key)) : [];
427
- compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, last: null });
490
+ const reads = [];
491
+ const slots = /* @__PURE__ */ new Map();
492
+ const byKey = /* @__PURE__ */ new Map();
493
+ for (const ref of parsed.ok ? refNodes(parsed.ast) : []) {
494
+ const id = idByKey.get(ref.key);
495
+ const key = `${id ?? ""}\0${ref.column ?? ""}`;
496
+ let slot = byKey.get(key);
497
+ if (slot === void 0) {
498
+ slot = reads.push({ id, column: ref.column, key }) - 1;
499
+ byKey.set(key, slot);
500
+ }
501
+ slots.set(ref, slot);
502
+ }
503
+ compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, slots, last: null });
428
504
  }
429
505
  const withCalculated = (values, path) => {
430
506
  if (!compiled.length) return values;
431
507
  const visible = new Set(path);
432
508
  let next = values;
433
- const input = (id) => id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id) ? UNREAD : next[id];
509
+ let budget = MAX_SETTLE_ITEMS;
510
+ const within = /* @__PURE__ */ new Map();
511
+ const input = ({ id, key }) => {
512
+ if (id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id)) return UNREAD;
513
+ const value = next[id];
514
+ if (!Array.isArray(value)) return value;
515
+ let ok = within.get(key);
516
+ if (ok === void 0) {
517
+ ok = value.length <= MAX_FORMULA_ITEMS && value.length <= budget;
518
+ if (ok) budget -= value.length;
519
+ within.set(key, ok);
520
+ }
521
+ return ok ? value : OVER;
522
+ };
434
523
  for (const entry of compiled) {
435
524
  const { field: field2, ast } = entry;
436
525
  const inputs = entry.reads.map(input);
@@ -438,11 +527,18 @@ function calculator(definition) {
438
527
  if (entry.last && entry.last.inputs.length === inputs.length && entry.last.inputs.every((v, i) => v === inputs[i])) {
439
528
  value = entry.last.value;
440
529
  } else {
530
+ const resolved = new Array(inputs.length);
441
531
  const read = (ref) => {
442
- const raw = input(idByKey.get(ref.key));
443
- return raw === UNREAD ? null : formulaInput(raw, ref.column);
532
+ const slot = entry.slots.get(ref);
533
+ if (slot === void 0) return null;
534
+ let v = resolved[slot];
535
+ if (v === void 0) {
536
+ const raw = inputs[slot];
537
+ v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, entry.reads[slot].column);
538
+ }
539
+ return v;
444
540
  };
445
- value = ast ? calculatedValue(evaluateFormula(ast, read), field2) : void 0;
541
+ value = ast && !inputs.includes(OVER) ? calculatedValue(evaluateFormula(ast, read), field2) : void 0;
446
542
  entry.last = { inputs, value };
447
543
  }
448
544
  const current = Object.prototype.hasOwnProperty.call(next, field2.id) ? next[field2.id] : void 0;
@@ -1423,9 +1519,15 @@ function parseForm(input) {
1423
1519
  issue(`${path}.kind`, "Unknown block kind.");
1424
1520
  }
1425
1521
  });
1522
+ let formulaChars = 0;
1426
1523
  input.blocks.forEach((block, i) => {
1427
1524
  if (!isObject2(block) || block.kind !== "field" || block.type !== CALCULATED_TYPE) return;
1428
1525
  const path = `blocks[${i}].props.formula`;
1526
+ const source = isObject2(block.props) && typeof block.props.formula === "string" ? block.props.formula : "";
1527
+ formulaChars += source.length;
1528
+ if (formulaChars > MAX_FORM_FORMULA_LENGTH && formulaChars - source.length <= MAX_FORM_FORMULA_LENGTH) {
1529
+ issue(path, `The form's formulas together are over ${MAX_FORM_FORMULA_LENGTH} characters; shorten some, or work part out once in an earlier calculated field.`);
1530
+ }
1429
1531
  if (block.required === true) {
1430
1532
  warnings.push({ path: `blocks[${i}].required`, message: "A calculated field is worked out, not answered, so it is never required." });
1431
1533
  }
@@ -2267,9 +2369,11 @@ exports.HIGHLIGHT_NOTE_MESSAGE = HIGHLIGHT_NOTE_MESSAGE;
2267
2369
  exports.MAX_CALCULATED_DECIMALS = MAX_CALCULATED_DECIMALS;
2268
2370
  exports.MAX_FORMULA_ITEMS = MAX_FORMULA_ITEMS;
2269
2371
  exports.MAX_FORMULA_LENGTH = MAX_FORMULA_LENGTH;
2372
+ exports.MAX_FORM_FORMULA_LENGTH = MAX_FORM_FORMULA_LENGTH;
2270
2373
  exports.MAX_HIDDEN_STRING_LENGTH = MAX_HIDDEN_STRING_LENGTH;
2271
2374
  exports.MAX_HIGHLIGHT_NOTE_LENGTH = MAX_HIGHLIGHT_NOTE_LENGTH;
2272
2375
  exports.MAX_HIGHLIGHT_QUOTE_LENGTH = MAX_HIGHLIGHT_QUOTE_LENGTH;
2376
+ exports.MAX_SETTLE_ITEMS = MAX_SETTLE_ITEMS;
2273
2377
  exports.MAX_TABLE_CELL_LENGTH = MAX_TABLE_CELL_LENGTH;
2274
2378
  exports.MAX_TABLE_COLUMNS = MAX_TABLE_COLUMNS;
2275
2379
  exports.MAX_TIMESTAMP_SECONDS = MAX_TIMESTAMP_SECONDS;