@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/testing.cjs CHANGED
@@ -34,6 +34,7 @@ function tokenize(source) {
34
34
  i++;
35
35
  while (isDigit(source[i] ?? "")) i++;
36
36
  }
37
+ if (isIdStart(source[i] ?? "")) throw new FormulaError("Write a number in digits only, like 1000000.", at);
37
38
  const v = Number(source.slice(at, i));
38
39
  if (!Number.isFinite(v)) throw new FormulaError("This number is too large.", at);
39
40
  tokens.push({ t: "num", v, at });
@@ -232,6 +233,27 @@ function numbersIn(values, out = []) {
232
233
  }
233
234
  return out;
234
235
  }
236
+ var listTotals = /* @__PURE__ */ new WeakMap();
237
+ var ownLists = /* @__PURE__ */ new WeakSet();
238
+ function ownFormulaList(list) {
239
+ Object.freeze(list);
240
+ ownLists.add(list);
241
+ return list;
242
+ }
243
+ function totalsOf(list) {
244
+ const own2 = ownLists.has(list);
245
+ let t = own2 ? listTotals.get(list) : void 0;
246
+ if (t) return t;
247
+ const nums = numbersIn(list);
248
+ t = { sum: 0, count: nums.length, min: Infinity, max: -Infinity };
249
+ for (const n of nums) {
250
+ t.sum += n;
251
+ if (n < t.min) t.min = n;
252
+ if (n > t.max) t.max = n;
253
+ }
254
+ if (own2) listTotals.set(list, t);
255
+ return t;
256
+ }
235
257
  function compare(op, a, b) {
236
258
  const blank2 = (v) => v === null || Array.isArray(v) || typeof v === "string" && v.trim() === "";
237
259
  if (blank2(a) || blank2(b)) return op === "!=";
@@ -256,7 +278,7 @@ function evaluateFormula(ast, read) {
256
278
  case "text":
257
279
  return node.value;
258
280
  case "ref":
259
- return read(node.column !== void 0 ? { key: node.key, column: node.column } : { key: node.key });
281
+ return read(node);
260
282
  case "unary": {
261
283
  const n = formulaNumber(scalar(run(node.arg)));
262
284
  return n === null ? null : -n;
@@ -264,12 +286,31 @@ function evaluateFormula(ast, read) {
264
286
  case "if":
265
287
  return truthy(run(node.test)) ? run(node.then) : run(node.else);
266
288
  case "call": {
267
- const nums = numbersIn(node.args.map(run));
268
- const total = nums.reduce((a, b) => a + b, 0);
289
+ let total = 0;
290
+ let count = 0;
291
+ let min = Infinity;
292
+ let max = -Infinity;
293
+ for (const arg of node.args) {
294
+ const v = run(arg);
295
+ if (Array.isArray(v)) {
296
+ const t = totalsOf(v);
297
+ total += t.sum;
298
+ count += t.count;
299
+ if (t.min < min) min = t.min;
300
+ if (t.max > max) max = t.max;
301
+ continue;
302
+ }
303
+ const n = formulaNumber(v);
304
+ if (n === null) continue;
305
+ total += n;
306
+ count++;
307
+ if (n < min) min = n;
308
+ if (n > max) max = n;
309
+ }
269
310
  if (node.fn === "sum") return finite(total);
270
- if (!nums.length) return null;
271
- if (node.fn === "avg") return finite(total / nums.length);
272
- return nums.reduce((a, b) => node.fn === "min" ? Math.min(a, b) : Math.max(a, b));
311
+ if (!count) return null;
312
+ if (node.fn === "avg") return finite(total / count);
313
+ return node.fn === "min" ? min : max;
273
314
  }
274
315
  case "binary": {
275
316
  const a = run(node.left);
@@ -296,6 +337,7 @@ function evaluateFormula(ast, read) {
296
337
 
297
338
  // src/calculated.ts
298
339
  var CALCULATED_TYPE = "calculated";
340
+ var MAX_FORM_FORMULA_LENGTH = 2e4;
299
341
  var MAX_CALCULATED_DECIMALS = 10;
300
342
  function calculatedProps(field) {
301
343
  const p = field.props ?? {};
@@ -334,23 +376,41 @@ function formatCalculated(value, field) {
334
376
  return typeof value === "string" ? value : "";
335
377
  }
336
378
  var MAX_FORMULA_ITEMS = 1e4;
379
+ var MAX_SETTLE_ITEMS = 5e4;
380
+ var extract = (value, column) => column !== void 0 ? value.flatMap((row) => {
381
+ if (!row || typeof row !== "object" || Array.isArray(row)) return [];
382
+ const cell = row[column];
383
+ return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
384
+ }) : value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
385
+ var extracted = /* @__PURE__ */ new WeakMap();
386
+ function cachedList(value, column) {
387
+ let byColumn = extracted.get(value);
388
+ if (!byColumn) extracted.set(value, byColumn = /* @__PURE__ */ new Map());
389
+ const key = column ?? "";
390
+ let list = byColumn.get(key);
391
+ if (!list) byColumn.set(key, list = ownFormulaList(extract(value, column)));
392
+ return list;
393
+ }
394
+ function readAnswer(value, column) {
395
+ if (Array.isArray(value)) return cachedList(value, column);
396
+ return column !== void 0 ? null : formulaInput(value);
397
+ }
337
398
  function formulaInput(value, column) {
338
399
  if (value === void 0 || value === null) return null;
339
400
  if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
340
- if (column !== void 0) {
341
- if (!Array.isArray(value)) return null;
342
- return value.flatMap((row) => {
343
- if (!row || typeof row !== "object" || Array.isArray(row)) return [];
344
- const cell = row[column];
345
- return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
346
- });
347
- }
348
- if (Array.isArray(value)) {
349
- return value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
350
- }
401
+ if (Array.isArray(value)) return extract(value, void 0);
351
402
  return typeof value === "object" ? null : value;
352
403
  }
404
+ function refNodes(node, out = []) {
405
+ if (node.kind === "ref") out.push(node);
406
+ else if (node.kind === "unary") refNodes(node.arg, out);
407
+ else if (node.kind === "binary") refNodes(node.left, out), refNodes(node.right, out);
408
+ else if (node.kind === "if") refNodes(node.test, out), refNodes(node.then, out), refNodes(node.else, out);
409
+ else if (node.kind === "call") for (const a of node.args) refNodes(a, out);
410
+ return out;
411
+ }
353
412
  var UNREAD = /* @__PURE__ */ Symbol("unread");
413
+ var OVER = /* @__PURE__ */ Symbol("over");
354
414
  function calculator(definition) {
355
415
  const compiled = [];
356
416
  const idByKey = /* @__PURE__ */ new Map();
@@ -361,14 +421,39 @@ function calculator(definition) {
361
421
  for (const block of definition.blocks) {
362
422
  if (block.kind !== "field" || !isCalculated(block)) continue;
363
423
  const parsed = parseFormula(calculatedProps(block).formula);
364
- const reads = parsed.ok ? parsed.refs.map((r) => idByKey.get(r.key)) : [];
365
- compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, last: null });
424
+ const reads = [];
425
+ const slots = /* @__PURE__ */ new Map();
426
+ const byKey = /* @__PURE__ */ new Map();
427
+ for (const ref of parsed.ok ? refNodes(parsed.ast) : []) {
428
+ const id = idByKey.get(ref.key);
429
+ const key = `${id ?? ""}\0${ref.column ?? ""}`;
430
+ let slot = byKey.get(key);
431
+ if (slot === void 0) {
432
+ slot = reads.push({ id, column: ref.column, key }) - 1;
433
+ byKey.set(key, slot);
434
+ }
435
+ slots.set(ref, slot);
436
+ }
437
+ compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, slots, last: null });
366
438
  }
367
439
  const withCalculated = (values, path) => {
368
440
  if (!compiled.length) return values;
369
441
  const visible = new Set(path);
370
442
  let next = values;
371
- const input = (id) => id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id) ? UNREAD : next[id];
443
+ let budget = MAX_SETTLE_ITEMS;
444
+ const within = /* @__PURE__ */ new Map();
445
+ const input = ({ id, key }) => {
446
+ if (id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id)) return UNREAD;
447
+ const value = next[id];
448
+ if (!Array.isArray(value)) return value;
449
+ let ok = within.get(key);
450
+ if (ok === void 0) {
451
+ ok = value.length <= MAX_FORMULA_ITEMS && value.length <= budget;
452
+ if (ok) budget -= value.length;
453
+ within.set(key, ok);
454
+ }
455
+ return ok ? value : OVER;
456
+ };
372
457
  for (const entry of compiled) {
373
458
  const { field, ast } = entry;
374
459
  const inputs = entry.reads.map(input);
@@ -376,11 +461,18 @@ function calculator(definition) {
376
461
  if (entry.last && entry.last.inputs.length === inputs.length && entry.last.inputs.every((v, i) => v === inputs[i])) {
377
462
  value = entry.last.value;
378
463
  } else {
464
+ const resolved = new Array(inputs.length);
379
465
  const read = (ref) => {
380
- const raw = input(idByKey.get(ref.key));
381
- return raw === UNREAD ? null : formulaInput(raw, ref.column);
466
+ const slot = entry.slots.get(ref);
467
+ if (slot === void 0) return null;
468
+ let v = resolved[slot];
469
+ if (v === void 0) {
470
+ const raw = inputs[slot];
471
+ v = resolved[slot] = raw === UNREAD ? null : readAnswer(raw, entry.reads[slot].column);
472
+ }
473
+ return v;
382
474
  };
383
- value = ast ? calculatedValue(evaluateFormula(ast, read), field) : void 0;
475
+ value = ast && !inputs.includes(OVER) ? calculatedValue(evaluateFormula(ast, read), field) : void 0;
384
476
  entry.last = { inputs, value };
385
477
  }
386
478
  const current = Object.prototype.hasOwnProperty.call(next, field.id) ? next[field.id] : void 0;
@@ -1782,9 +1874,15 @@ function parseForm(input) {
1782
1874
  issue(`${path}.kind`, "Unknown block kind.");
1783
1875
  }
1784
1876
  });
1877
+ let formulaChars = 0;
1785
1878
  input.blocks.forEach((block, i) => {
1786
1879
  if (!isObject2(block) || block.kind !== "field" || block.type !== CALCULATED_TYPE) return;
1787
1880
  const path = `blocks[${i}].props.formula`;
1881
+ const source = isObject2(block.props) && typeof block.props.formula === "string" ? block.props.formula : "";
1882
+ formulaChars += source.length;
1883
+ if (formulaChars > MAX_FORM_FORMULA_LENGTH && formulaChars - source.length <= MAX_FORM_FORMULA_LENGTH) {
1884
+ 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.`);
1885
+ }
1788
1886
  if (block.required === true) {
1789
1887
  warnings.push({ path: `blocks[${i}].required`, message: "A calculated field is worked out, not answered, so it is never required." });
1790
1888
  }