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