@revisium/formula 0.7.0 → 0.9.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.
Files changed (76) hide show
  1. package/README.md +27 -0
  2. package/dist/__tests__/array-context.spec.d.ts +2 -0
  3. package/dist/__tests__/array-context.spec.d.ts.map +1 -0
  4. package/dist/__tests__/dependency-graph.spec.d.ts +2 -0
  5. package/dist/__tests__/dependency-graph.spec.d.ts.map +1 -0
  6. package/dist/__tests__/evaluate-complex.spec.d.ts +2 -0
  7. package/dist/__tests__/evaluate-complex.spec.d.ts.map +1 -0
  8. package/dist/__tests__/parse-formula.spec.d.ts +2 -0
  9. package/dist/__tests__/parse-formula.spec.d.ts.map +1 -0
  10. package/dist/__tests__/parser.spec.d.ts +2 -0
  11. package/dist/__tests__/parser.spec.d.ts.map +1 -0
  12. package/dist/__tests__/replace-dependencies.spec.d.ts +2 -0
  13. package/dist/__tests__/replace-dependencies.spec.d.ts.map +1 -0
  14. package/dist/__tests__/serialize-ast.spec.d.ts +2 -0
  15. package/dist/__tests__/serialize-ast.spec.d.ts.map +1 -0
  16. package/dist/__tests__/validate-syntax.spec.d.ts +2 -0
  17. package/dist/__tests__/validate-syntax.spec.d.ts.map +1 -0
  18. package/dist/dependency-graph.d.ts +17 -0
  19. package/dist/dependency-graph.d.ts.map +1 -0
  20. package/dist/editor/index.cjs +4 -19
  21. package/dist/editor/index.d.ts +8 -1
  22. package/dist/editor/index.d.ts.map +1 -0
  23. package/dist/editor/index.mjs +3 -0
  24. package/dist/formula-spec.cjs +808 -535
  25. package/dist/formula-spec.cjs.map +1 -1
  26. package/dist/formula-spec.d.ts +10 -5
  27. package/dist/formula-spec.d.ts.map +1 -0
  28. package/dist/formula-spec.mjs +1039 -0
  29. package/dist/formula-spec.mjs.map +1 -0
  30. package/dist/index.cjs +121 -123
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.ts +10 -20
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.mjs +130 -0
  35. package/dist/index.mjs.map +1 -0
  36. package/dist/ohm/core/parser.d.ts +30 -0
  37. package/dist/ohm/core/parser.d.ts.map +1 -0
  38. package/dist/ohm/core/replace-dependencies.d.ts +3 -0
  39. package/dist/ohm/core/replace-dependencies.d.ts.map +1 -0
  40. package/dist/ohm/core/serialize-ast.d.ts +3 -0
  41. package/dist/ohm/core/serialize-ast.d.ts.map +1 -0
  42. package/dist/ohm/core/types.d.ts +78 -0
  43. package/dist/ohm/core/types.d.ts.map +1 -0
  44. package/dist/ohm/grammar/index.d.ts +3 -0
  45. package/dist/ohm/grammar/index.d.ts.map +1 -0
  46. package/dist/ohm/index.d.ts +8 -0
  47. package/dist/ohm/index.d.ts.map +1 -0
  48. package/dist/ohm/semantics/index.d.ts +2 -0
  49. package/dist/ohm/semantics/index.d.ts.map +1 -0
  50. package/dist/parse-formula.d.ts +9 -0
  51. package/dist/parse-formula.d.ts.map +1 -0
  52. package/dist/types.d.ts +52 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/validate-syntax-BGrLewnG.cjs +1502 -0
  55. package/dist/validate-syntax-BGrLewnG.cjs.map +1 -0
  56. package/dist/validate-syntax-CmqnFrsc.mjs +1421 -0
  57. package/dist/validate-syntax-CmqnFrsc.mjs.map +1 -0
  58. package/dist/validate-syntax.d.ts +9 -0
  59. package/dist/validate-syntax.d.ts.map +1 -0
  60. package/package.json +12 -11
  61. package/dist/chunk-5TM76LGI.js +0 -1216
  62. package/dist/chunk-5TM76LGI.js.map +0 -1
  63. package/dist/chunk-IM22W645.cjs +0 -1244
  64. package/dist/chunk-IM22W645.cjs.map +0 -1
  65. package/dist/editor/index.cjs.map +0 -1
  66. package/dist/editor/index.d.cts +0 -1
  67. package/dist/editor/index.js +0 -3
  68. package/dist/editor/index.js.map +0 -1
  69. package/dist/formula-spec.d.cts +0 -68
  70. package/dist/formula-spec.js +0 -765
  71. package/dist/formula-spec.js.map +0 -1
  72. package/dist/index-CPsOPCQ6.d.cts +0 -166
  73. package/dist/index-CPsOPCQ6.d.ts +0 -166
  74. package/dist/index.d.cts +0 -20
  75. package/dist/index.js +0 -111
  76. package/dist/index.js.map +0 -1
@@ -1,517 +1,653 @@
1
- 'use strict';
2
-
3
- // src/formula-spec.ts
4
- var formulaSpec = {
5
- version: "1.1",
6
- description: "Formula expressions for computed fields. Formulas reference other fields and calculate values automatically.",
7
- syntax: {
8
- fieldReferences: [
9
- "Simple field: fieldName (e.g., price, quantity)",
10
- "Nested path: object.property (e.g., stats.damage)",
11
- "Array index: array[0] or array[-1] for last element",
12
- 'Bracket notation: ["field-name"] for field names containing hyphens',
13
- " - Required when field name contains hyphen (-)",
14
- ' - Without brackets: field-name is parsed as "field minus name"',
15
- ' - With brackets: ["field-name"] references the field correctly',
16
- 'Combined: items[0].price, user.addresses[-1].city, obj["field-name"].value'
17
- ],
18
- arithmeticOperators: [
19
- { operator: "+", description: "Addition or string concatenation" },
20
- { operator: "-", description: "Subtraction" },
21
- { operator: "*", description: "Multiplication" },
22
- { operator: "/", description: "Division" },
23
- { operator: "%", description: "Modulo (remainder)" }
24
- ],
25
- comparisonOperators: [
26
- { operator: "==", description: "Equal" },
27
- { operator: "!=", description: "Not equal" },
28
- { operator: ">", description: "Greater than" },
29
- { operator: "<", description: "Less than" },
30
- { operator: ">=", description: "Greater or equal" },
31
- { operator: "<=", description: "Less or equal" }
32
- ],
33
- logicalOperators: [
34
- { operator: "&&", description: "Logical AND" },
35
- { operator: "||", description: "Logical OR" },
36
- { operator: "!", description: "Logical NOT" }
37
- ],
38
- other: ["Parentheses: (a + b) * c", "Unary minus: -value, a + -b"]
39
- },
40
- functions: {
41
- string: [
42
- {
43
- name: "concat",
44
- description: "Concatenate multiple values into a single string",
45
- signature: "concat(value1, value2, ...)",
46
- returnType: "string",
47
- examples: [
48
- 'concat(firstName, " ", lastName) // "John Doe"',
49
- 'concat("Price: ", price, " USD") // "Price: 100 USD"'
50
- ]
51
- },
52
- {
53
- name: "upper",
54
- description: "Convert string to uppercase",
55
- signature: "upper(text)",
56
- returnType: "string",
57
- examples: ['upper(name) // "HELLO"']
58
- },
59
- {
60
- name: "lower",
61
- description: "Convert string to lowercase",
62
- signature: "lower(text)",
63
- returnType: "string",
64
- examples: ['lower(name) // "hello"']
65
- },
66
- {
67
- name: "trim",
68
- description: "Remove whitespace from both ends of a string",
69
- signature: "trim(text)",
70
- returnType: "string",
71
- examples: ['trim(name) // "hello" from " hello "']
72
- },
73
- {
74
- name: "left",
75
- description: "Extract characters from the beginning of a string",
76
- signature: "left(text, count)",
77
- returnType: "string",
78
- examples: ['left(name, 3) // "hel" from "hello"']
79
- },
80
- {
81
- name: "right",
82
- description: "Extract characters from the end of a string",
83
- signature: "right(text, count)",
84
- returnType: "string",
85
- examples: ['right(name, 3) // "llo" from "hello"']
86
- },
87
- {
88
- name: "replace",
89
- description: "Replace first occurrence of a substring",
90
- signature: "replace(text, search, replacement)",
91
- returnType: "string",
92
- examples: ['replace(name, "o", "0") // "hell0" from "hello"']
93
- },
94
- {
95
- name: "join",
96
- description: "Join array elements into a string",
97
- signature: "join(array, separator?)",
98
- returnType: "string",
99
- examples: ['join(tags) // "a,b,c"', 'join(tags, " | ") // "a | b | c"']
100
- }
101
- ],
102
- numeric: [
103
- {
104
- name: "round",
105
- description: "Round a number to specified decimal places",
106
- signature: "round(number, decimals?)",
107
- returnType: "number",
108
- examples: ["round(3.14159, 2) // 3.14", "round(3.5) // 4"]
109
- },
110
- {
111
- name: "floor",
112
- description: "Round down to the nearest integer",
113
- signature: "floor(number)",
114
- returnType: "number",
115
- examples: ["floor(3.7) // 3"]
116
- },
117
- {
118
- name: "ceil",
119
- description: "Round up to the nearest integer",
120
- signature: "ceil(number)",
121
- returnType: "number",
122
- examples: ["ceil(3.2) // 4"]
123
- },
124
- {
125
- name: "abs",
126
- description: "Get the absolute value",
127
- signature: "abs(number)",
128
- returnType: "number",
129
- examples: ["abs(-5) // 5"]
130
- },
131
- {
132
- name: "sqrt",
133
- description: "Calculate the square root",
134
- signature: "sqrt(number)",
135
- returnType: "number",
136
- examples: ["sqrt(16) // 4"]
137
- },
138
- {
139
- name: "pow",
140
- description: "Raise a number to a power",
141
- signature: "pow(base, exponent)",
142
- returnType: "number",
143
- examples: ["pow(2, 3) // 8"]
144
- },
145
- {
146
- name: "min",
147
- description: "Get the minimum of multiple values",
148
- signature: "min(value1, value2, ...)",
149
- returnType: "number",
150
- examples: ["min(a, b, c) // smallest value"]
151
- },
152
- {
153
- name: "max",
154
- description: "Get the maximum of multiple values",
155
- signature: "max(value1, value2, ...)",
156
- returnType: "number",
157
- examples: ["max(a, b, c) // largest value"]
158
- },
159
- {
160
- name: "log",
161
- description: "Calculate the natural logarithm",
162
- signature: "log(number)",
163
- returnType: "number",
164
- examples: ["log(10) // 2.302..."]
165
- },
166
- {
167
- name: "log10",
168
- description: "Calculate the base-10 logarithm",
169
- signature: "log10(number)",
170
- returnType: "number",
171
- examples: ["log10(100) // 2"]
172
- },
173
- {
174
- name: "exp",
175
- description: "Calculate e raised to a power",
176
- signature: "exp(number)",
177
- returnType: "number",
178
- examples: ["exp(1) // 2.718..."]
179
- },
180
- {
181
- name: "sign",
182
- description: "Get the sign of a number (-1, 0, or 1)",
183
- signature: "sign(number)",
184
- returnType: "number",
185
- examples: ["sign(-5) // -1", "sign(0) // 0", "sign(5) // 1"]
186
- },
187
- {
188
- name: "length",
189
- description: "Get the length of a string or array",
190
- signature: "length(value)",
191
- returnType: "number",
192
- examples: ['length(name) // 5 from "hello"', "length(items) // 3"]
193
- }
194
- ],
195
- boolean: [
196
- {
197
- name: "and",
198
- description: "Logical AND of two values",
199
- signature: "and(a, b)",
200
- returnType: "boolean",
201
- examples: ["and(isActive, hasPermission) // true if both true"]
202
- },
203
- {
204
- name: "or",
205
- description: "Logical OR of two values",
206
- signature: "or(a, b)",
207
- returnType: "boolean",
208
- examples: ["or(isAdmin, isOwner) // true if either true"]
209
- },
210
- {
211
- name: "not",
212
- description: "Logical NOT of a value",
213
- signature: "not(value)",
214
- returnType: "boolean",
215
- examples: ["not(isDeleted) // true if false"]
216
- },
217
- {
218
- name: "contains",
219
- description: "Check if a string contains a substring",
220
- signature: "contains(text, search)",
221
- returnType: "boolean",
222
- examples: ['contains(name, "ell") // true for "hello"']
223
- },
224
- {
225
- name: "startswith",
226
- description: "Check if a string starts with a prefix",
227
- signature: "startswith(text, prefix)",
228
- returnType: "boolean",
229
- examples: ['startswith(name, "hel") // true for "hello"']
230
- },
231
- {
232
- name: "endswith",
233
- description: "Check if a string ends with a suffix",
234
- signature: "endswith(text, suffix)",
235
- returnType: "boolean",
236
- examples: ['endswith(name, "llo") // true for "hello"']
237
- },
238
- {
239
- name: "isnull",
240
- description: "Check if a value is null or undefined",
241
- signature: "isnull(value)",
242
- returnType: "boolean",
243
- examples: ["isnull(optionalField) // true if null/undefined"]
244
- },
245
- {
246
- name: "includes",
247
- description: "Check if an array contains a value",
248
- signature: "includes(array, value)",
249
- returnType: "boolean",
250
- examples: [
251
- 'includes(tags, "featured") // true if array contains value'
252
- ]
253
- }
254
- ],
255
- array: [
256
- {
257
- name: "sum",
258
- description: "Calculate the sum of array elements",
259
- signature: "sum(array)",
260
- returnType: "number",
261
- examples: ["sum(prices) // total of all prices"]
262
- },
263
- {
264
- name: "avg",
265
- description: "Calculate the average of array elements",
266
- signature: "avg(array)",
267
- returnType: "number",
268
- examples: ["avg(scores) // average score"]
269
- },
270
- {
271
- name: "count",
272
- description: "Get the number of elements in an array",
273
- signature: "count(array)",
274
- returnType: "number",
275
- examples: ["count(items) // number of items"]
276
- },
277
- {
278
- name: "first",
279
- description: "Get the first element of an array",
280
- signature: "first(array)",
281
- returnType: "any",
282
- examples: ["first(items) // first item"]
283
- },
284
- {
285
- name: "last",
286
- description: "Get the last element of an array",
287
- signature: "last(array)",
288
- returnType: "any",
289
- examples: ["last(items) // last item"]
290
- }
291
- ],
292
- conversion: [
293
- {
294
- name: "tostring",
295
- description: "Convert a value to string",
296
- signature: "tostring(value)",
297
- returnType: "string",
298
- examples: ['tostring(42) // "42"']
299
- },
300
- {
301
- name: "tonumber",
302
- description: "Convert a value to number",
303
- signature: "tonumber(value)",
304
- returnType: "number",
305
- examples: ['tonumber("42") // 42']
306
- },
307
- {
308
- name: "toboolean",
309
- description: "Convert a value to boolean",
310
- signature: "toboolean(value)",
311
- returnType: "boolean",
312
- examples: ["toboolean(1) // true", "toboolean(0) // false"]
313
- }
314
- ],
315
- conditional: [
316
- {
317
- name: "if",
318
- description: "Return one of two values based on a condition",
319
- signature: "if(condition, valueIfTrue, valueIfFalse)",
320
- returnType: "any",
321
- examples: [
322
- 'if(stock > 0, "Available", "Out of Stock")',
323
- "if(price > 100, price * 0.9, price)"
324
- ]
325
- },
326
- {
327
- name: "coalesce",
328
- description: "Return the first non-null value",
329
- signature: "coalesce(value1, value2, ...)",
330
- returnType: "any",
331
- examples: ['coalesce(nickname, name, "Anonymous")']
332
- }
333
- ]
334
- },
335
- features: [
336
- {
337
- name: "simple_refs",
338
- description: "Reference top-level fields by name",
339
- minVersion: "1.0",
340
- examples: ["price", "quantity", "baseDamage"],
341
- dependenciesExtracted: ['["price"]', '["quantity"]', '["baseDamage"]']
342
- },
343
- {
344
- name: "arithmetic",
345
- description: "Basic math operations (+, -, *, /)",
346
- minVersion: "1.0",
347
- examples: ["price * 1.1", "a + b - c", "quantity * price"]
348
- },
349
- {
350
- name: "comparison",
351
- description: "Compare values (>, <, >=, <=, ==, !=)",
352
- minVersion: "1.0",
353
- examples: ["price > 100", "x == 10", "quantity >= 5"]
354
- },
355
- {
356
- name: "nested_path",
357
- description: "Access nested object properties using dot notation",
358
- minVersion: "1.1",
359
- examples: ["stats.damage", "user.profile.name", "item.metadata.category"],
360
- dependenciesExtracted: ['["stats.damage"]']
361
- },
362
- {
363
- name: "array_index",
364
- description: "Access array elements by numeric index. Negative indices access from the end",
365
- minVersion: "1.1",
366
- examples: [
367
- "items[0].price",
368
- "inventory[1].quantity",
369
- "items[-1].name // last element",
370
- "items[-2].price // second to last"
371
- ],
372
- dependenciesExtracted: ['["items[0].price"]', '["items[-1].name"]']
373
- },
374
- {
375
- name: "root_path",
376
- description: "Absolute path reference starting with /. Always resolves from root data, even inside array item formulas",
377
- minVersion: "1.1",
378
- examples: [
379
- "/taxRate",
380
- "/config.tax",
381
- "price * (1 + /taxRate)",
382
- "price * /config.multiplier"
383
- ],
384
- dependenciesExtracted: ['["/taxRate"]', '["/config.tax"]']
385
- },
386
- {
387
- name: "relative_path",
388
- description: "Relative path reference starting with ../. Each ../ goes up one level in the path hierarchy. Works with nested objects, arrays, and combinations. Supports accessing nested properties after the relative prefix (e.g., ../config.value)",
389
- minVersion: "1.1",
390
- examples: [
391
- "../discount",
392
- "../../rootRate",
393
- "../config.multiplier",
394
- "price * (1 - ../discount)",
395
- "price * ../../globalRate",
396
- "price * ../settings.tax.rate"
397
- ],
398
- dependenciesExtracted: [
399
- '["../discount"]',
400
- '["../../rootRate"]',
401
- '["../config.multiplier"]'
402
- ]
403
- },
404
- {
405
- name: "function_named_fields",
406
- description: "Fields can have the same name as built-in functions (max, min, sum, etc.). Built-in functions are always checked first when a function call is made",
407
- minVersion: "1.0",
408
- examples: [
409
- "max(max, 0)",
410
- "min(min, 100)",
411
- "max(max - field.min, 0)",
412
- "round(round * 2)"
413
- ]
414
- },
415
- {
416
- name: "bracket_notation",
417
- description: 'Access fields containing hyphens using bracket notation with quotes. Required because "field-name" would be parsed as "field minus name" (subtraction). Works like JavaScript object["key"] syntax.',
418
- minVersion: "1.1",
419
- examples: [
420
- '["field-name"] // Without brackets: field - name (subtraction!)',
421
- "['field-name'] // Single quotes also work",
422
- '["field-one"]["field-two"]',
423
- 'obj["field-name"].value',
424
- '["items-list"][0]["val"]',
425
- '["price-new"] * 2'
426
- ],
427
- dependenciesExtracted: ['["field-name"]', "['field-name']"]
428
- }
429
- ],
430
- versionDetection: [
431
- { feature: "Simple refs, arithmetic, comparisons", minVersion: "1.0" },
432
- { feature: "Function-named fields (max(max, 0))", minVersion: "1.0" },
433
- { feature: "Nested paths (a.b)", minVersion: "1.1" },
434
- { feature: "Array index ([0], [-1])", minVersion: "1.1" },
435
- { feature: "Absolute paths (/field)", minVersion: "1.1" },
436
- { feature: "Relative paths (../field)", minVersion: "1.1" },
437
- { feature: 'Bracket notation (["field-name"])', minVersion: "1.1" }
438
- ],
439
- parseResult: {
440
- description: "The parser automatically detects the minimum required version",
441
- interface: `interface ParseResult {
1
+
2
+ //#region src/formula-spec.ts
3
+ const formulaSpec = {
4
+ version: "1.2",
5
+ description: "Formula expressions for computed fields. Formulas reference other fields and calculate values automatically.",
6
+ syntax: {
7
+ fieldReferences: [
8
+ "Simple field: fieldName (e.g., price, quantity)",
9
+ "Nested path: object.property (e.g., stats.damage)",
10
+ "Array index: array[0] or array[-1] for last element",
11
+ "Bracket notation: [\"field-name\"] for field names containing hyphens",
12
+ " - Required when field name contains hyphen (-)",
13
+ " - Without brackets: field-name is parsed as \"field minus name\"",
14
+ " - With brackets: [\"field-name\"] references the field correctly",
15
+ "Combined: items[0].price, user.addresses[-1].city, obj[\"field-name\"].value"
16
+ ],
17
+ arithmeticOperators: [
18
+ {
19
+ operator: "+",
20
+ description: "Addition or string concatenation"
21
+ },
22
+ {
23
+ operator: "-",
24
+ description: "Subtraction"
25
+ },
26
+ {
27
+ operator: "*",
28
+ description: "Multiplication"
29
+ },
30
+ {
31
+ operator: "/",
32
+ description: "Division"
33
+ },
34
+ {
35
+ operator: "%",
36
+ description: "Modulo (remainder)"
37
+ }
38
+ ],
39
+ comparisonOperators: [
40
+ {
41
+ operator: "==",
42
+ description: "Equal"
43
+ },
44
+ {
45
+ operator: "!=",
46
+ description: "Not equal"
47
+ },
48
+ {
49
+ operator: ">",
50
+ description: "Greater than"
51
+ },
52
+ {
53
+ operator: "<",
54
+ description: "Less than"
55
+ },
56
+ {
57
+ operator: ">=",
58
+ description: "Greater or equal"
59
+ },
60
+ {
61
+ operator: "<=",
62
+ description: "Less or equal"
63
+ }
64
+ ],
65
+ logicalOperators: [
66
+ {
67
+ operator: "&&",
68
+ description: "Logical AND"
69
+ },
70
+ {
71
+ operator: "||",
72
+ description: "Logical OR"
73
+ },
74
+ {
75
+ operator: "!",
76
+ description: "Logical NOT"
77
+ }
78
+ ],
79
+ other: ["Parentheses: (a + b) * c", "Unary minus: -value, a + -b"]
80
+ },
81
+ functions: {
82
+ string: [
83
+ {
84
+ name: "concat",
85
+ description: "Concatenate multiple values into a single string",
86
+ signature: "concat(value1, value2, ...)",
87
+ returnType: "string",
88
+ examples: ["concat(firstName, \" \", lastName) // \"John Doe\"", "concat(\"Price: \", price, \" USD\") // \"Price: 100 USD\""]
89
+ },
90
+ {
91
+ name: "upper",
92
+ description: "Convert string to uppercase",
93
+ signature: "upper(text)",
94
+ returnType: "string",
95
+ examples: ["upper(name) // \"HELLO\""]
96
+ },
97
+ {
98
+ name: "lower",
99
+ description: "Convert string to lowercase",
100
+ signature: "lower(text)",
101
+ returnType: "string",
102
+ examples: ["lower(name) // \"hello\""]
103
+ },
104
+ {
105
+ name: "trim",
106
+ description: "Remove whitespace from both ends of a string",
107
+ signature: "trim(text)",
108
+ returnType: "string",
109
+ examples: ["trim(name) // \"hello\" from \" hello \""]
110
+ },
111
+ {
112
+ name: "left",
113
+ description: "Extract characters from the beginning of a string",
114
+ signature: "left(text, count)",
115
+ returnType: "string",
116
+ examples: ["left(name, 3) // \"hel\" from \"hello\""]
117
+ },
118
+ {
119
+ name: "right",
120
+ description: "Extract characters from the end of a string",
121
+ signature: "right(text, count)",
122
+ returnType: "string",
123
+ examples: ["right(name, 3) // \"llo\" from \"hello\""]
124
+ },
125
+ {
126
+ name: "replace",
127
+ description: "Replace first occurrence of a substring",
128
+ signature: "replace(text, search, replacement)",
129
+ returnType: "string",
130
+ examples: ["replace(name, \"o\", \"0\") // \"hell0\" from \"hello\""]
131
+ },
132
+ {
133
+ name: "join",
134
+ description: "Join array elements into a string",
135
+ signature: "join(array, separator?)",
136
+ returnType: "string",
137
+ examples: ["join(tags) // \"a,b,c\"", "join(tags, \" | \") // \"a | b | c\""]
138
+ }
139
+ ],
140
+ numeric: [
141
+ {
142
+ name: "round",
143
+ description: "Round a number to specified decimal places",
144
+ signature: "round(number, decimals?)",
145
+ returnType: "number",
146
+ examples: ["round(3.14159, 2) // 3.14", "round(3.5) // 4"]
147
+ },
148
+ {
149
+ name: "floor",
150
+ description: "Round down to the nearest integer",
151
+ signature: "floor(number)",
152
+ returnType: "number",
153
+ examples: ["floor(3.7) // 3"]
154
+ },
155
+ {
156
+ name: "ceil",
157
+ description: "Round up to the nearest integer",
158
+ signature: "ceil(number)",
159
+ returnType: "number",
160
+ examples: ["ceil(3.2) // 4"]
161
+ },
162
+ {
163
+ name: "abs",
164
+ description: "Get the absolute value",
165
+ signature: "abs(number)",
166
+ returnType: "number",
167
+ examples: ["abs(-5) // 5"]
168
+ },
169
+ {
170
+ name: "sqrt",
171
+ description: "Calculate the square root",
172
+ signature: "sqrt(number)",
173
+ returnType: "number",
174
+ examples: ["sqrt(16) // 4"]
175
+ },
176
+ {
177
+ name: "pow",
178
+ description: "Raise a number to a power",
179
+ signature: "pow(base, exponent)",
180
+ returnType: "number",
181
+ examples: ["pow(2, 3) // 8"]
182
+ },
183
+ {
184
+ name: "min",
185
+ description: "Get the minimum of multiple values",
186
+ signature: "min(value1, value2, ...)",
187
+ returnType: "number",
188
+ examples: ["min(a, b, c) // smallest value"]
189
+ },
190
+ {
191
+ name: "max",
192
+ description: "Get the maximum of multiple values",
193
+ signature: "max(value1, value2, ...)",
194
+ returnType: "number",
195
+ examples: ["max(a, b, c) // largest value"]
196
+ },
197
+ {
198
+ name: "log",
199
+ description: "Calculate the natural logarithm",
200
+ signature: "log(number)",
201
+ returnType: "number",
202
+ examples: ["log(10) // 2.302..."]
203
+ },
204
+ {
205
+ name: "log10",
206
+ description: "Calculate the base-10 logarithm",
207
+ signature: "log10(number)",
208
+ returnType: "number",
209
+ examples: ["log10(100) // 2"]
210
+ },
211
+ {
212
+ name: "exp",
213
+ description: "Calculate e raised to a power",
214
+ signature: "exp(number)",
215
+ returnType: "number",
216
+ examples: ["exp(1) // 2.718..."]
217
+ },
218
+ {
219
+ name: "sign",
220
+ description: "Get the sign of a number (-1, 0, or 1)",
221
+ signature: "sign(number)",
222
+ returnType: "number",
223
+ examples: [
224
+ "sign(-5) // -1",
225
+ "sign(0) // 0",
226
+ "sign(5) // 1"
227
+ ]
228
+ },
229
+ {
230
+ name: "length",
231
+ description: "Get the length of a string or array",
232
+ signature: "length(value)",
233
+ returnType: "number",
234
+ examples: ["length(name) // 5 from \"hello\"", "length(items) // 3"]
235
+ }
236
+ ],
237
+ boolean: [
238
+ {
239
+ name: "and",
240
+ description: "Logical AND of two values",
241
+ signature: "and(a, b)",
242
+ returnType: "boolean",
243
+ examples: ["and(isActive, hasPermission) // true if both true"]
244
+ },
245
+ {
246
+ name: "or",
247
+ description: "Logical OR of two values",
248
+ signature: "or(a, b)",
249
+ returnType: "boolean",
250
+ examples: ["or(isAdmin, isOwner) // true if either true"]
251
+ },
252
+ {
253
+ name: "not",
254
+ description: "Logical NOT of a value",
255
+ signature: "not(value)",
256
+ returnType: "boolean",
257
+ examples: ["not(isDeleted) // true if false"]
258
+ },
259
+ {
260
+ name: "contains",
261
+ description: "Check if a string contains a substring",
262
+ signature: "contains(text, search)",
263
+ returnType: "boolean",
264
+ examples: ["contains(name, \"ell\") // true for \"hello\""]
265
+ },
266
+ {
267
+ name: "startswith",
268
+ description: "Check if a string starts with a prefix",
269
+ signature: "startswith(text, prefix)",
270
+ returnType: "boolean",
271
+ examples: ["startswith(name, \"hel\") // true for \"hello\""]
272
+ },
273
+ {
274
+ name: "endswith",
275
+ description: "Check if a string ends with a suffix",
276
+ signature: "endswith(text, suffix)",
277
+ returnType: "boolean",
278
+ examples: ["endswith(name, \"llo\") // true for \"hello\""]
279
+ },
280
+ {
281
+ name: "isnull",
282
+ description: "Check if a value is null or undefined",
283
+ signature: "isnull(value)",
284
+ returnType: "boolean",
285
+ examples: ["isnull(optionalField) // true if null/undefined"]
286
+ },
287
+ {
288
+ name: "includes",
289
+ description: "Check if an array contains a value",
290
+ signature: "includes(array, value)",
291
+ returnType: "boolean",
292
+ examples: ["includes(tags, \"featured\") // true if array contains value"]
293
+ }
294
+ ],
295
+ array: [
296
+ {
297
+ name: "sum",
298
+ description: "Calculate the sum of array elements",
299
+ signature: "sum(array)",
300
+ returnType: "number",
301
+ examples: ["sum(prices) // total of all prices"]
302
+ },
303
+ {
304
+ name: "avg",
305
+ description: "Calculate the average of array elements",
306
+ signature: "avg(array)",
307
+ returnType: "number",
308
+ examples: ["avg(scores) // average score"]
309
+ },
310
+ {
311
+ name: "count",
312
+ description: "Get the number of elements in an array",
313
+ signature: "count(array)",
314
+ returnType: "number",
315
+ examples: ["count(items) // number of items"]
316
+ },
317
+ {
318
+ name: "first",
319
+ description: "Get the first element of an array",
320
+ signature: "first(array)",
321
+ returnType: "any",
322
+ examples: ["first(items) // first item"]
323
+ },
324
+ {
325
+ name: "last",
326
+ description: "Get the last element of an array",
327
+ signature: "last(array)",
328
+ returnType: "any",
329
+ examples: ["last(items) // last item"]
330
+ }
331
+ ],
332
+ conversion: [
333
+ {
334
+ name: "tostring",
335
+ description: "Convert a value to string",
336
+ signature: "tostring(value)",
337
+ returnType: "string",
338
+ examples: ["tostring(42) // \"42\""]
339
+ },
340
+ {
341
+ name: "tonumber",
342
+ description: "Convert a value to number",
343
+ signature: "tonumber(value)",
344
+ returnType: "number",
345
+ examples: ["tonumber(\"42\") // 42"]
346
+ },
347
+ {
348
+ name: "toboolean",
349
+ description: "Convert a value to boolean",
350
+ signature: "toboolean(value)",
351
+ returnType: "boolean",
352
+ examples: ["toboolean(1) // true", "toboolean(0) // false"]
353
+ }
354
+ ],
355
+ conditional: [{
356
+ name: "if",
357
+ description: "Return one of two values based on a condition",
358
+ signature: "if(condition, valueIfTrue, valueIfFalse)",
359
+ returnType: "any",
360
+ examples: ["if(stock > 0, \"Available\", \"Out of Stock\")", "if(price > 100, price * 0.9, price)"]
361
+ }, {
362
+ name: "coalesce",
363
+ description: "Return the first non-null value",
364
+ signature: "coalesce(value1, value2, ...)",
365
+ returnType: "any",
366
+ examples: ["coalesce(nickname, name, \"Anonymous\")"]
367
+ }]
368
+ },
369
+ features: [
370
+ {
371
+ name: "simple_refs",
372
+ description: "Reference top-level fields by name",
373
+ minVersion: "1.0",
374
+ examples: [
375
+ "price",
376
+ "quantity",
377
+ "baseDamage"
378
+ ],
379
+ dependenciesExtracted: [
380
+ "[\"price\"]",
381
+ "[\"quantity\"]",
382
+ "[\"baseDamage\"]"
383
+ ]
384
+ },
385
+ {
386
+ name: "arithmetic",
387
+ description: "Basic math operations (+, -, *, /)",
388
+ minVersion: "1.0",
389
+ examples: [
390
+ "price * 1.1",
391
+ "a + b - c",
392
+ "quantity * price"
393
+ ]
394
+ },
395
+ {
396
+ name: "comparison",
397
+ description: "Compare values (>, <, >=, <=, ==, !=)",
398
+ minVersion: "1.0",
399
+ examples: [
400
+ "price > 100",
401
+ "x == 10",
402
+ "quantity >= 5"
403
+ ]
404
+ },
405
+ {
406
+ name: "nested_path",
407
+ description: "Access nested object properties using dot notation",
408
+ minVersion: "1.1",
409
+ examples: [
410
+ "stats.damage",
411
+ "user.profile.name",
412
+ "item.metadata.category"
413
+ ],
414
+ dependenciesExtracted: ["[\"stats.damage\"]"]
415
+ },
416
+ {
417
+ name: "array_index",
418
+ description: "Access array elements by numeric index. Negative indices access from the end",
419
+ minVersion: "1.1",
420
+ examples: [
421
+ "items[0].price",
422
+ "inventory[1].quantity",
423
+ "items[-1].name // last element",
424
+ "items[-2].price // second to last"
425
+ ],
426
+ dependenciesExtracted: ["[\"items[0].price\"]", "[\"items[-1].name\"]"]
427
+ },
428
+ {
429
+ name: "root_path",
430
+ description: "Absolute path reference starting with /. Always resolves from root data, even inside array item formulas",
431
+ minVersion: "1.1",
432
+ examples: [
433
+ "/taxRate",
434
+ "/config.tax",
435
+ "price * (1 + /taxRate)",
436
+ "price * /config.multiplier"
437
+ ],
438
+ dependenciesExtracted: ["[\"/taxRate\"]", "[\"/config.tax\"]"]
439
+ },
440
+ {
441
+ name: "relative_path",
442
+ description: "Relative path reference starting with ../. Each ../ goes up one level in the path hierarchy. Works with nested objects, arrays, and combinations. Supports accessing nested properties after the relative prefix (e.g., ../config.value)",
443
+ minVersion: "1.1",
444
+ examples: [
445
+ "../discount",
446
+ "../../rootRate",
447
+ "../config.multiplier",
448
+ "price * (1 - ../discount)",
449
+ "price * ../../globalRate",
450
+ "price * ../settings.tax.rate"
451
+ ],
452
+ dependenciesExtracted: [
453
+ "[\"../discount\"]",
454
+ "[\"../../rootRate\"]",
455
+ "[\"../config.multiplier\"]"
456
+ ]
457
+ },
458
+ {
459
+ name: "function_named_fields",
460
+ description: "Fields can have the same name as built-in functions (max, min, sum, etc.). Built-in functions are always checked first when a function call is made",
461
+ minVersion: "1.0",
462
+ examples: [
463
+ "max(max, 0)",
464
+ "min(min, 100)",
465
+ "max(max - field.min, 0)",
466
+ "round(round * 2)"
467
+ ]
468
+ },
469
+ {
470
+ name: "bracket_notation",
471
+ description: "Access fields containing hyphens using bracket notation with quotes. Required because \"field-name\" would be parsed as \"field minus name\" (subtraction). Works like JavaScript object[\"key\"] syntax.",
472
+ minVersion: "1.1",
473
+ examples: [
474
+ "[\"field-name\"] // Without brackets: field - name (subtraction!)",
475
+ "['field-name'] // Single quotes also work",
476
+ "[\"field-one\"][\"field-two\"]",
477
+ "obj[\"field-name\"].value",
478
+ "[\"items-list\"][0][\"val\"]",
479
+ "[\"price-new\"] * 2"
480
+ ],
481
+ dependenciesExtracted: ["[\"field-name\"]", "['field-name']"]
482
+ },
483
+ {
484
+ name: "context_token",
485
+ description: "Array context tokens provide information about current position and neighboring elements when evaluating formulas inside arrays. # prefix for scalar metadata (number/boolean), @ prefix for object references.",
486
+ minVersion: "1.2",
487
+ examples: [
488
+ "#index // Current array index (0-based)",
489
+ "#length // Array length",
490
+ "#first // true if first element",
491
+ "#last // true if last element",
492
+ "@prev // Previous element (null if first)",
493
+ "@next // Next element (null if last)",
494
+ "#parent.index // Index in parent array (nested arrays)",
495
+ "#parent.length // Length of parent array",
496
+ "#root.index // Index in topmost array",
497
+ "@root.prev // Previous element in topmost array",
498
+ "if(#first, value, @prev.total + value) // Running total",
499
+ "concat(#parent.index + 1, \".\", #index + 1) // \"1.1\", \"1.2\" numbering"
500
+ ]
501
+ }
502
+ ],
503
+ versionDetection: [
504
+ {
505
+ feature: "Simple refs, arithmetic, comparisons",
506
+ minVersion: "1.0"
507
+ },
508
+ {
509
+ feature: "Function-named fields (max(max, 0))",
510
+ minVersion: "1.0"
511
+ },
512
+ {
513
+ feature: "Nested paths (a.b)",
514
+ minVersion: "1.1"
515
+ },
516
+ {
517
+ feature: "Array index ([0], [-1])",
518
+ minVersion: "1.1"
519
+ },
520
+ {
521
+ feature: "Absolute paths (/field)",
522
+ minVersion: "1.1"
523
+ },
524
+ {
525
+ feature: "Relative paths (../field)",
526
+ minVersion: "1.1"
527
+ },
528
+ {
529
+ feature: "Bracket notation ([\"field-name\"])",
530
+ minVersion: "1.1"
531
+ },
532
+ {
533
+ feature: "Context tokens (#index, @prev)",
534
+ minVersion: "1.2"
535
+ }
536
+ ],
537
+ parseResult: {
538
+ description: "The parser automatically detects the minimum required version",
539
+ interface: `interface ParseResult {
442
540
  ast: ASTNode; // Abstract syntax tree
443
541
  dependencies: string[]; // List of field dependencies
444
542
  features: string[]; // List of detected features
445
543
  minVersion: string; // Minimum required version ("1.0" or "1.1")
446
544
  }`
447
- },
448
- examples: [
449
- {
450
- expression: "price * quantity",
451
- description: "Calculate total from price and quantity",
452
- result: "number"
453
- },
454
- {
455
- expression: 'firstName + " " + lastName',
456
- description: "Concatenate strings with space",
457
- result: "string"
458
- },
459
- {
460
- expression: "quantity > 0",
461
- description: "Check if in stock",
462
- result: "boolean"
463
- },
464
- {
465
- expression: 'if(stock > 0, "Available", "Out of Stock")',
466
- description: "Conditional text based on stock",
467
- result: "string"
468
- },
469
- {
470
- expression: "price * (1 + taxRate)",
471
- description: "Price with tax",
472
- result: "number"
473
- },
474
- {
475
- expression: "items[0].price + items[1].price",
476
- description: "Sum first two item prices (v1.1)",
477
- result: "number"
478
- }
479
- ],
480
- apiExamples: [
481
- {
482
- name: "Simple Expression (v1.0)",
483
- description: "Parse a basic arithmetic expression",
484
- code: `parseExpression('price * 1.1')
545
+ },
546
+ astUtilities: [{
547
+ name: "serializeAst",
548
+ description: "Convert an AST back to a formula string. Useful for debugging or displaying parsed formulas.",
549
+ signature: "serializeAst(ast: ASTNode): string",
550
+ code: `import { parseFormula, serializeAst } from '@revisium/formula';
551
+
552
+ const { ast } = parseFormula('price * (1 + taxRate)');
553
+ serializeAst(ast)
554
+ // "price * (1 + taxRate)"
555
+
556
+ // After modifying AST nodes, serialize back to string
557
+ const { ast: ast2 } = parseFormula('a + b');
558
+ serializeAst(ast2)
559
+ // "a + b"`
560
+ }, {
561
+ name: "replaceDependencies",
562
+ description: "Replace field references in an AST with new names. Useful for renaming fields or migrating formulas.",
563
+ signature: "replaceDependencies(ast: ASTNode, replacements: Record<string, string>): ASTNode",
564
+ code: `import { parseFormula, replaceDependencies, serializeAst } from '@revisium/formula';
565
+
566
+ // Rename a field in a formula
567
+ const { ast } = parseFormula('oldPrice * quantity');
568
+ const newAst = replaceDependencies(ast, { oldPrice: 'price' });
569
+ serializeAst(newAst)
570
+ // "price * quantity"
571
+
572
+ // Rename multiple fields
573
+ const { ast: ast2 } = parseFormula('a + b * c');
574
+ const newAst2 = replaceDependencies(ast2, { a: 'x', b: 'y', c: 'z' });
575
+ serializeAst(newAst2)
576
+ // "x + y * z"
577
+
578
+ // Works with nested paths
579
+ const { ast: ast3 } = parseFormula('stats.damage * multiplier');
580
+ const newAst3 = replaceDependencies(ast3, { 'stats.damage': 'stats.power' });
581
+ serializeAst(newAst3)
582
+ // "stats.power * multiplier"`
583
+ }],
584
+ examples: [
585
+ {
586
+ expression: "price * quantity",
587
+ description: "Calculate total from price and quantity",
588
+ result: "number"
589
+ },
590
+ {
591
+ expression: "firstName + \" \" + lastName",
592
+ description: "Concatenate strings with space",
593
+ result: "string"
594
+ },
595
+ {
596
+ expression: "quantity > 0",
597
+ description: "Check if in stock",
598
+ result: "boolean"
599
+ },
600
+ {
601
+ expression: "if(stock > 0, \"Available\", \"Out of Stock\")",
602
+ description: "Conditional text based on stock",
603
+ result: "string"
604
+ },
605
+ {
606
+ expression: "price * (1 + taxRate)",
607
+ description: "Price with tax",
608
+ result: "number"
609
+ },
610
+ {
611
+ expression: "items[0].price + items[1].price",
612
+ description: "Sum first two item prices (v1.1)",
613
+ result: "number"
614
+ }
615
+ ],
616
+ apiExamples: [
617
+ {
618
+ name: "Simple Expression (v1.0)",
619
+ description: "Parse a basic arithmetic expression",
620
+ code: `parseExpression('price * 1.1')
485
621
  // {
486
622
  // minVersion: "1.0",
487
623
  // features: [],
488
624
  // dependencies: ["price"]
489
625
  // }`
490
- },
491
- {
492
- name: "Nested Path (v1.1)",
493
- description: "Parse expression with nested object access",
494
- code: `parseExpression('stats.damage * multiplier')
626
+ },
627
+ {
628
+ name: "Nested Path (v1.1)",
629
+ description: "Parse expression with nested object access",
630
+ code: `parseExpression('stats.damage * multiplier')
495
631
  // {
496
632
  // minVersion: "1.1",
497
633
  // features: ["nested_path"],
498
634
  // dependencies: ["stats.damage", "multiplier"]
499
635
  // }`
500
- },
501
- {
502
- name: "Array Access (v1.1)",
503
- description: "Parse expression with array index access",
504
- code: `parseExpression('items[0].price + items[1].price')
636
+ },
637
+ {
638
+ name: "Array Access (v1.1)",
639
+ description: "Parse expression with array index access",
640
+ code: `parseExpression('items[0].price + items[1].price')
505
641
  // {
506
642
  // minVersion: "1.1",
507
643
  // features: ["array_index", "nested_path"],
508
644
  // dependencies: ["items[0].price", "items[1].price"]
509
645
  // }`
510
- },
511
- {
512
- name: "Evaluate expressions",
513
- description: "Execute formulas with context data",
514
- code: `evaluate('price * 1.1', { price: 100 })
646
+ },
647
+ {
648
+ name: "Evaluate expressions",
649
+ description: "Execute formulas with context data",
650
+ code: `evaluate('price * 1.1', { price: 100 })
515
651
  // 110
516
652
 
517
653
  evaluate('stats.damage', { stats: { damage: 50 } })
@@ -525,11 +661,11 @@ evaluate('price > 100', { price: 150 })
525
661
 
526
662
  evaluate('a + b * c', { a: 1, b: 2, c: 3 })
527
663
  // 7`
528
- },
529
- {
530
- name: "Function-named fields",
531
- description: "Fields can have the same name as built-in functions",
532
- code: `// Built-in functions take precedence in function calls
664
+ },
665
+ {
666
+ name: "Function-named fields",
667
+ description: "Fields can have the same name as built-in functions",
668
+ code: `// Built-in functions take precedence in function calls
533
669
  evaluate('max(max, 0)', { max: 10 })
534
670
  // 10 (max() function, then max field)
535
671
 
@@ -542,11 +678,11 @@ evaluate('round(round * 2)', { round: 3.7 })
542
678
  // Field named "sum" doesn't conflict with sum() function
543
679
  evaluate('sum(values) + sum', { values: [1, 2, 3], sum: 10 })
544
680
  // 16`
545
- },
546
- {
547
- name: "Evaluate with context (array items)",
548
- description: "Use evaluateWithContext() for array item formulas with path resolution",
549
- code: `// Absolute path: /field always resolves from root
681
+ },
682
+ {
683
+ name: "Evaluate with context (array items)",
684
+ description: "Use evaluateWithContext() for array item formulas with path resolution",
685
+ code: `// Absolute path: /field always resolves from root
550
686
  evaluateWithContext('price * (1 + /taxRate)', {
551
687
  rootData: { taxRate: 0.1, items: [{ price: 100 }] },
552
688
  itemData: { price: 100 },
@@ -577,11 +713,11 @@ evaluateWithContext('value + 10', {
577
713
  currentPath: 'items[0]'
578
714
  })
579
715
  // 60`
580
- },
581
- {
582
- name: "Relative paths - path resolution",
583
- description: "Understanding how ../ resolves based on currentPath. Each ../ goes up one segment (object property or array element counts as one segment)",
584
- code: `// Path structure explanation:
716
+ },
717
+ {
718
+ name: "Relative paths - path resolution",
719
+ description: "Understanding how ../ resolves based on currentPath. Each ../ goes up one segment (object property or array element counts as one segment)",
720
+ code: `// Path structure explanation:
585
721
  // currentPath splits by "." (dots), keeping array indices attached to field names
586
722
  // "items[0]" = 1 segment
587
723
  // "items[0].inner" = 2 segments: ["items[0]", "inner"]
@@ -619,11 +755,11 @@ evaluateWithContext('price * ../../rootRate', {
619
755
  })
620
756
  // Resolves ../../rootRate to root.rootRate = 2
621
757
  // Result: 5 * 2 = 10`
622
- },
623
- {
624
- name: "Relative paths - nested arrays",
625
- description: "How relative paths work with arrays inside objects and nested arrays",
626
- code: `// Array inside nested object
758
+ },
759
+ {
760
+ name: "Relative paths - nested arrays",
761
+ description: "How relative paths work with arrays inside objects and nested arrays",
762
+ code: `// Array inside nested object
627
763
  // currentPath: "container.items[0]" (2 segments: ["container", "items[0]"])
628
764
  // ../ goes up 1 level -> "container"
629
765
  evaluateWithContext('price * ../containerRate', {
@@ -665,11 +801,11 @@ evaluateWithContext('qty * ../itemPrice', {
665
801
  })
666
802
  // Resolves ../itemPrice to items[0].itemPrice = 10
667
803
  // Result: 3 * 10 = 30`
668
- },
669
- {
670
- name: "Relative paths - accessing nested properties",
671
- description: "Relative paths can include nested property access after the ../ prefix",
672
- code: `// ../sibling.nested accesses a sibling with nested property
804
+ },
805
+ {
806
+ name: "Relative paths - accessing nested properties",
807
+ description: "Relative paths can include nested property access after the ../ prefix",
808
+ code: `// ../sibling.nested accesses a sibling with nested property
673
809
  // currentPath: "items[0].products[0]" (2 segments)
674
810
  // ../ goes to "items[0]", then accesses .config.discount
675
811
  evaluateWithContext('price * ../config.discount', {
@@ -696,11 +832,11 @@ evaluateWithContext('amount * ../../settings.tax.rate', {
696
832
  })
697
833
  // Resolves ../../settings.tax.rate to root.settings.tax.rate = 0.1
698
834
  // Result: 200 * 0.1 = 20`
699
- },
700
- {
701
- name: "Relative paths - complex nesting",
702
- description: "Complex scenarios with arrays inside objects inside arrays",
703
- code: `// Array inside object inside array
835
+ },
836
+ {
837
+ name: "Relative paths - complex nesting",
838
+ description: "Complex scenarios with arrays inside objects inside arrays",
839
+ code: `// Array inside object inside array
704
840
  // Structure: items[].container.subItems[]
705
841
  // currentPath: "items[0].container.subItems[0]" (3 segments)
706
842
  evaluateWithContext('val * ../containerMultiplier', {
@@ -748,20 +884,157 @@ evaluateWithContext('val * ../../../rootFactor', {
748
884
  // ../../../ goes to root
749
885
  // Resolves ../../../rootFactor to root.rootFactor = 3
750
886
  // Result: 7 * 3 = 21`
751
- }
752
- ],
753
- schemaUsage: {
754
- structure: '{ "x-formula": { "version": 1, "expression": "..." }, "readOnly": true }',
755
- fieldTypes: ["string", "number", "boolean"],
756
- rules: [
757
- "Add x-formula to string, number, or boolean field schema",
758
- "readOnly: true is REQUIRED for fields with x-formula",
759
- "Expression must reference existing fields in the same table",
760
- "Circular dependencies are not allowed (a references b, b references a)"
887
+ },
888
+ {
889
+ name: "Array context tokens - basic",
890
+ description: "Use #index, #length, #first, #last for position info; @prev, @next for neighbor access",
891
+ code: `// arrayContext provides position info for array item formulas
892
+ const arrayContext = {
893
+ levels: [{
894
+ index: 2, // current position
895
+ length: 5, // array length
896
+ prev: { value: 20 }, // previous element
897
+ next: { value: 40 }, // next element
898
+ }]
899
+ };
900
+
901
+ evaluateWithContext('#index', { rootData: {}, arrayContext })
902
+ // 2
903
+
904
+ evaluateWithContext('#length', { rootData: {}, arrayContext })
905
+ // 5
906
+
907
+ evaluateWithContext('#first', { rootData: {}, arrayContext })
908
+ // false (index !== 0)
909
+
910
+ evaluateWithContext('#last', { rootData: {}, arrayContext })
911
+ // false (index !== length - 1)
912
+
913
+ evaluateWithContext('@prev.value', { rootData: {}, arrayContext })
914
+ // 20
915
+
916
+ evaluateWithContext('@next.value', { rootData: {}, arrayContext })
917
+ // 40
918
+
919
+ // At first element, @prev is null
920
+ evaluateWithContext('@prev', {
921
+ rootData: {},
922
+ arrayContext: { levels: [{ index: 0, length: 3, prev: null, next: {} }] }
923
+ })
924
+ // null`
925
+ },
926
+ {
927
+ name: "Array context tokens - nested arrays",
928
+ description: "Access parent array context with #parent.*, #root.*, @parent.*, @root.*",
929
+ code: `// For nested arrays like orders[].items[]:
930
+ // levels[0] = innermost (items), levels[1] = parent (orders)
931
+ const arrayContext = {
932
+ levels: [
933
+ { index: 1, length: 3, prev: {}, next: {} }, // items[1]
934
+ { index: 2, length: 5, prev: {}, next: {} }, // orders[2]
935
+ ]
936
+ };
937
+
938
+ evaluateWithContext('#index', { rootData: {}, arrayContext })
939
+ // 1 (current item index)
940
+
941
+ evaluateWithContext('#parent.index', { rootData: {}, arrayContext })
942
+ // 2 (parent order index)
943
+
944
+ evaluateWithContext('#parent.length', { rootData: {}, arrayContext })
945
+ // 5 (number of orders)
946
+
947
+ // #root.* is shortcut for topmost array (same as #parent.* for 2 levels)
948
+ evaluateWithContext('#root.index', { rootData: {}, arrayContext })
949
+ // 2
950
+
951
+ // For 3+ levels, #root always points to outermost array
952
+ const threeLevel = {
953
+ levels: [
954
+ { index: 0, length: 2, prev: null, next: {} }, // innermost
955
+ { index: 1, length: 3, prev: {}, next: {} }, // middle
956
+ { index: 2, length: 4, prev: {}, next: null }, // outermost (root)
957
+ ]
958
+ };
959
+
960
+ evaluateWithContext('#parent.parent.index', { rootData: {}, arrayContext: threeLevel })
961
+ // 2 (outermost)
962
+
963
+ evaluateWithContext('#root.index', { rootData: {}, arrayContext: threeLevel })
964
+ // 2 (same as #parent.parent.index)`
965
+ },
966
+ {
967
+ name: "Array context tokens - practical examples",
968
+ description: "Common patterns: running total, numbering, delta calculation",
969
+ code: `// Running total pattern (like Excel)
970
+ // rows[].runningTotal = if(#first, value, @prev.runningTotal + value)
971
+ const rows = [
972
+ { value: 10 },
973
+ { value: 20 },
974
+ { value: 15 },
975
+ ];
976
+
977
+ // For rows[2]:
978
+ evaluateWithContext('if(#first, value, @prev.value + value)', {
979
+ rootData: {},
980
+ itemData: { value: 15 },
981
+ arrayContext: {
982
+ levels: [{
983
+ index: 2,
984
+ length: 3,
985
+ prev: { value: 20 }, // Note: use non-computed field from prev
986
+ next: null,
987
+ }]
988
+ }
989
+ })
990
+ // 35 (20 + 15)
991
+
992
+ // Nested numbering like "1.1", "1.2", "2.1"
993
+ // sections[].questions[].number
994
+ evaluateWithContext('concat(#parent.index + 1, ".", #index + 1)', {
995
+ rootData: {},
996
+ arrayContext: {
997
+ levels: [
998
+ { index: 1, length: 3, prev: {}, next: {} }, // question index
999
+ { index: 0, length: 2, prev: null, next: {} }, // section index
761
1000
  ]
762
1001
  }
1002
+ })
1003
+ // "1.2"
1004
+
1005
+ // Delta from previous
1006
+ // measurements[].delta = if(#first, 0, value - @prev.value)
1007
+ evaluateWithContext('if(#first, 0, value - @prev.value)', {
1008
+ rootData: {},
1009
+ itemData: { value: 105 },
1010
+ arrayContext: {
1011
+ levels: [{
1012
+ index: 1,
1013
+ length: 3,
1014
+ prev: { value: 100 },
1015
+ next: { value: 102 },
1016
+ }]
1017
+ }
1018
+ })
1019
+ // 5`
1020
+ }
1021
+ ],
1022
+ schemaUsage: {
1023
+ structure: "{ \"x-formula\": { \"version\": 1, \"expression\": \"...\" }, \"readOnly\": true }",
1024
+ fieldTypes: [
1025
+ "string",
1026
+ "number",
1027
+ "boolean"
1028
+ ],
1029
+ rules: [
1030
+ "Add x-formula to string, number, or boolean field schema",
1031
+ "readOnly: true is REQUIRED for fields with x-formula",
1032
+ "Expression must reference existing fields in the same table",
1033
+ "Circular dependencies are not allowed (a references b, b references a)"
1034
+ ]
1035
+ }
763
1036
  };
764
1037
 
1038
+ //#endregion
765
1039
  exports.formulaSpec = formulaSpec;
766
- //# sourceMappingURL=formula-spec.cjs.map
767
1040
  //# sourceMappingURL=formula-spec.cjs.map