fable 3.1.63 → 3.1.65

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 (23) hide show
  1. package/docs/services/expression-parser-functions/README.md +9 -0
  2. package/docs/services/expression-parser-functions/beziercurvefit.md +57 -0
  3. package/docs/services/expression-parser-functions/bezierpoint.md +55 -0
  4. package/docs/services/expression-parser-functions/intercept.md +59 -0
  5. package/docs/services/expression-parser-functions/setvalue.md +55 -0
  6. package/docs/services/expression-parser-functions/slope.md +51 -0
  7. package/docs/services/expression-parser-functions/ternary.md +180 -0
  8. package/docs/services/expression-parser.md +131 -0
  9. package/example_applications/mathematical_playground/AppData.json +36 -1
  10. package/example_applications/mathematical_playground/Math-Solver-Harness.js +215 -0
  11. package/example_applications/mathematical_playground/TestSuite-AcidTest.json +178 -0
  12. package/example_applications/mathematical_playground/TestSuite-Identities.json +147 -0
  13. package/example_applications/mathematical_playground/TestSuite-Precision.json +127 -0
  14. package/example_applications/mathematical_playground/TestSuite-Spreadsheet.json +198 -0
  15. package/package.json +3 -2
  16. package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-ExpressionTokenizer-DirectiveMutation.js +299 -2
  17. package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-ExpressionTokenizer.js +1 -1
  18. package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-FunctionMap.json +5 -4
  19. package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-TokenMap.json +65 -1
  20. package/source/services/Fable-Service-ExpressionParser.js +144 -0
  21. package/source/services/Fable-Service-Logic.js +33 -0
  22. package/source/services/Fable-Service-Math.js +78 -0
  23. package/test/ExpressionParser_tests.js +1059 -0
@@ -0,0 +1,198 @@
1
+ {
2
+ "Name": "Spreadsheet Function Conformance",
3
+ "Description": "Excel/OpenFormula-compatible function tests. Verifies statistical, rounding, and regression functions against known reference values.",
4
+ "Expressions":
5
+ [
6
+ {
7
+ "Category": "Aggregation",
8
+ "Description": "SUM of five values",
9
+ "Equation": "Result = SUM(Vals)",
10
+ "ExpectedResult": "150",
11
+ "Data": { "Vals": [10, 20, 30, 40, 50] }
12
+ },
13
+ {
14
+ "Category": "Aggregation",
15
+ "Description": "AVG of five values",
16
+ "Equation": "Result = AVG(Vals)",
17
+ "ExpectedResult": "30",
18
+ "Data": { "Vals": [10, 20, 30, 40, 50] }
19
+ },
20
+ {
21
+ "Category": "Aggregation",
22
+ "Description": "MEAN is equivalent to AVG",
23
+ "Equation": "Result = MEAN(Vals)",
24
+ "ExpectedResult": "30",
25
+ "Data": { "Vals": [10, 20, 30, 40, 50] }
26
+ },
27
+ {
28
+ "Category": "Aggregation",
29
+ "Description": "MEDIAN of odd-count dataset",
30
+ "Equation": "Result = MEDIAN(Vals)",
31
+ "ExpectedResult": "30",
32
+ "Data": { "Vals": [10, 20, 30, 40, 50] }
33
+ },
34
+ {
35
+ "Category": "Aggregation",
36
+ "Description": "MEDIAN of even-count dataset (average of middle two)",
37
+ "Equation": "Result = MEDIAN(Vals)",
38
+ "ExpectedResult": "25",
39
+ "Data": { "Vals": [10, 20, 30, 40] }
40
+ },
41
+ {
42
+ "Category": "Aggregation",
43
+ "Description": "MEDIAN of single element",
44
+ "Equation": "Result = MEDIAN(Vals)",
45
+ "ExpectedResult": "42",
46
+ "Data": { "Vals": [42] }
47
+ },
48
+ {
49
+ "Category": "Aggregation",
50
+ "Description": "MIN of dataset",
51
+ "Equation": "Result = MIN(Vals)",
52
+ "ExpectedResult": "10",
53
+ "Data": { "Vals": [10, 20, 30, 40, 50] }
54
+ },
55
+ {
56
+ "Category": "Aggregation",
57
+ "Description": "MAX of dataset",
58
+ "Equation": "Result = MAX(Vals)",
59
+ "ExpectedResult": "50",
60
+ "Data": { "Vals": [10, 20, 30, 40, 50] }
61
+ },
62
+ {
63
+ "Category": "Aggregation",
64
+ "Description": "MAX of identical values",
65
+ "Equation": "Result = MAX(Vals)",
66
+ "ExpectedResult": "5",
67
+ "Data": { "Vals": [5, 5, 5] }
68
+ },
69
+ {
70
+ "Category": "Aggregation",
71
+ "Description": "MIN of single element",
72
+ "Equation": "Result = MIN(Vals)",
73
+ "ExpectedResult": "42",
74
+ "Data": { "Vals": [42] }
75
+ },
76
+ {
77
+ "Category": "Aggregation",
78
+ "Description": "COUNT of dataset",
79
+ "Equation": "Result = COUNT(Vals)",
80
+ "ExpectedResult": "5",
81
+ "Data": { "Vals": [10, 20, 30, 40, 50] }
82
+ },
83
+ {
84
+ "Category": "Variance and Standard Deviation",
85
+ "Description": "Sample variance (VAR) — Excel reference: 2,4,4,4,5,5,7,9 = 4.571429",
86
+ "Equation": "Result = ROUND(VAR(Vals), 4)",
87
+ "ExpectedResult": "4.5714",
88
+ "Data": { "Vals": [2, 4, 4, 4, 5, 5, 7, 9] }
89
+ },
90
+ {
91
+ "Category": "Variance and Standard Deviation",
92
+ "Description": "Population variance (VARP) — Excel reference: 2,4,4,4,5,5,7,9 = 4",
93
+ "Equation": "Result = VARP(Vals)",
94
+ "ExpectedResult": "4",
95
+ "Data": { "Vals": [2, 4, 4, 4, 5, 5, 7, 9] }
96
+ },
97
+ {
98
+ "Category": "Variance and Standard Deviation",
99
+ "Description": "Sample standard deviation (STDEV) — Excel reference: 2.138090",
100
+ "Equation": "Result = ROUND(STDEV(Vals), 4)",
101
+ "ExpectedResult": "2.1381",
102
+ "Data": { "Vals": [2, 4, 4, 4, 5, 5, 7, 9] }
103
+ },
104
+ {
105
+ "Category": "Variance and Standard Deviation",
106
+ "Description": "Population standard deviation (STDEVP) — Excel reference: 2",
107
+ "Equation": "Result = STDEVP(Vals)",
108
+ "ExpectedResult": "2",
109
+ "Data": { "Vals": [2, 4, 4, 4, 5, 5, 7, 9] }
110
+ },
111
+ {
112
+ "Category": "Rounding",
113
+ "Description": "ROUND to nearest integer",
114
+ "Equation": "Result = ROUND(3.14159)",
115
+ "ExpectedResult": "3"
116
+ },
117
+ {
118
+ "Category": "Rounding",
119
+ "Description": "ROUND half-up: 2.5 rounds to 3",
120
+ "Equation": "Result = ROUND(2.5)",
121
+ "ExpectedResult": "3"
122
+ },
123
+ {
124
+ "Category": "Rounding",
125
+ "Description": "ROUND to 1 decimal place",
126
+ "Equation": "Result = ROUND(2.55, 1)",
127
+ "ExpectedResult": "2.6"
128
+ },
129
+ {
130
+ "Category": "Rounding",
131
+ "Description": "ROUND to 2 decimal places",
132
+ "Equation": "Result = ROUND(3.14159, 2)",
133
+ "ExpectedResult": "3.14"
134
+ },
135
+ {
136
+ "Category": "Rounding",
137
+ "Description": "ROUND to 4 decimal places",
138
+ "Equation": "Result = ROUND(3.14159, 4)",
139
+ "ExpectedResult": "3.1416"
140
+ },
141
+ {
142
+ "Category": "Rounding",
143
+ "Description": "TOFIXED pads with zeros",
144
+ "Equation": "Result = TOFIXED(3.1, 4)",
145
+ "ExpectedResult": "3.1000"
146
+ },
147
+ {
148
+ "Category": "Rounding",
149
+ "Description": "ABS of negative number",
150
+ "Equation": "Result = ABS(-5)",
151
+ "ExpectedResult": "5"
152
+ },
153
+ {
154
+ "Category": "Rounding",
155
+ "Description": "ABS of positive number (identity)",
156
+ "Equation": "Result = ABS(5)",
157
+ "ExpectedResult": "5"
158
+ },
159
+ {
160
+ "Category": "Rounding",
161
+ "Description": "FLOOR rounds down",
162
+ "Equation": "Result = FLOOR(3.7)",
163
+ "ExpectedResult": "3"
164
+ },
165
+ {
166
+ "Category": "Rounding",
167
+ "Description": "FLOOR of negative rounds toward negative infinity",
168
+ "Equation": "Result = FLOOR(-3.2)",
169
+ "ExpectedResult": "-4"
170
+ },
171
+ {
172
+ "Category": "Rounding",
173
+ "Description": "CEIL rounds up",
174
+ "Equation": "Result = CEIL(3.2)",
175
+ "ExpectedResult": "4"
176
+ },
177
+ {
178
+ "Category": "Rounding",
179
+ "Description": "CEIL of negative rounds toward zero",
180
+ "Equation": "Result = CEIL(-3.7)",
181
+ "ExpectedResult": "-3"
182
+ },
183
+ {
184
+ "Category": "Linear Regression",
185
+ "Description": "SLOPE of perfect linear data y=2x (slope=2)",
186
+ "Equation": "Result = SLOPE(Y, X)",
187
+ "ExpectedResult": "2",
188
+ "Data": { "Y": [2, 4, 6, 8, 10], "X": [1, 2, 3, 4, 5] }
189
+ },
190
+ {
191
+ "Category": "Linear Regression",
192
+ "Description": "INTERCEPT of perfect linear data y=2x (intercept=0)",
193
+ "Equation": "Result = INTERCEPT(Y, X)",
194
+ "ExpectedResult": "0",
195
+ "Data": { "Y": [2, 4, 6, 8, 10], "X": [1, 2, 3, 4, 5] }
196
+ }
197
+ ]
198
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fable",
3
- "version": "3.1.63",
3
+ "version": "3.1.65",
4
4
  "description": "A service dependency injection, configuration and logging library.",
5
5
  "main": "source/Fable.js",
6
6
  "scripts": {
@@ -8,6 +8,7 @@
8
8
  "coverage": "npx quack coverage",
9
9
  "test": "npx quack test",
10
10
  "build": "npx quack build",
11
+ "prepublishOnly": "npx quack build",
11
12
  "docker-dev-build": "docker build ./ -f Dockerfile_LUXURYCode -t fable-image:local",
12
13
  "docker-dev-run": "docker run -it -d --name fable-dev -p 30001:8080 -p 38086:8086 -v \"$PWD/.config:/home/coder/.config\" -v \"$PWD:/home/coder/fable\" -u \"$(id -u):$(id -g)\" -e \"DOCKER_USER=$USER\" fable-image:local",
13
14
  "docker-dev-shell": "docker exec -it fable-dev /bin/bash",
@@ -50,7 +51,7 @@
50
51
  },
51
52
  "homepage": "https://github.com/stevenvelozo/fable",
52
53
  "devDependencies": {
53
- "quackage": "^1.0.58"
54
+ "quackage": "^1.0.59"
54
55
  },
55
56
  "dependencies": {
56
57
  "async.eachlimit": "^0.5.2",
@@ -13,6 +13,7 @@ class ExpressionTokenizerDirectiveMutation extends libExpressionParserOperationB
13
13
  'SERIES': { Name: 'Series', Code: 'SERIES', From: null, To: null, Step: null },
14
14
  'MONTECARLO': { Name: 'Monte Carlo Simulation', SampleCount: '1', Code: 'MONTECARLO', Values: {} },
15
15
  'MAP': { Name: 'Map', Code: 'MAP', Values: {}, ValueKeys: [] },
16
+ 'MULTIROWMAP': { Name: 'Multi-Row Map', Code: 'MULTIROWMAP', RowsAddress: null, SeriesStart: null, SeriesStep: null, Values: {}, ValueKeys: [] },
16
17
  });
17
18
 
18
19
  this.defaultDirective = this.directiveTypes.SOLVE;
@@ -84,7 +85,7 @@ class ExpressionTokenizerDirectiveMutation extends libExpressionParserOperationB
84
85
  let tmpVariableToken = pTokens[i + 1];
85
86
  if (typeof(tmpVariableToken) === 'string' && (tmpVariableToken.length > 0))
86
87
  {
87
- tmpNewMonteCarloDirectiveDescription.Values[tmpVariableToken] =
88
+ tmpNewMonteCarloDirectiveDescription.Values[tmpVariableToken] =
88
89
  {
89
90
  Token: tmpVariableToken,
90
91
  Easing: 'Linear', // could be parametric, logarithmic, bezier, uniform, normal, etc.
@@ -169,7 +170,7 @@ class ExpressionTokenizerDirectiveMutation extends libExpressionParserOperationB
169
170
  if (typeof(tmpVariableToken) === 'string' && (tmpVariableToken.length > 0))
170
171
  {
171
172
  tmpNewMapDirectiveDescription.ValueKeys.push(tmpVariableToken);
172
- tmpNewMapDirectiveDescription.Values[tmpVariableToken] =
173
+ tmpNewMapDirectiveDescription.Values[tmpVariableToken] =
173
174
  {
174
175
  Token: tmpVariableToken,
175
176
  Address: pTokens[i + 3],
@@ -188,10 +189,303 @@ class ExpressionTokenizerDirectiveMutation extends libExpressionParserOperationB
188
189
  return tmpNewMapDirectiveDescription;
189
190
  }
190
191
 
192
+ parseMultiRowMapDirective(pTokens)
193
+ {
194
+ // Parse MULTIROWMAP directive tokens.
195
+ // Syntax: MULTIROWMAP ROWS FROM <address> [SERIESSTART <n>] [SERIESSTEP <n>] VAR <name> FROM <property> OFFSET <n> DEFAULT <defaultValue> ...
196
+ // OFFSET defaults to 0 (current row) if not specified.
197
+ // DEFAULT defaults to '0' if not specified.
198
+ // OFFSET 0 = current row, OFFSET -1 = previous row, OFFSET -2 = two rows back, etc.
199
+ // Positive OFFSET values look forward (e.g., OFFSET 1 = next row, OFFSET 2 = two rows ahead).
200
+ // SERIESSTART defaults to 0 (first row). Negative values count from the end (e.g., -2 = second-to-last row).
201
+ // SERIESSTEP defaults to 1. Use -1 to iterate backwards through rows.
202
+ let tmpNewDirectiveDescription = JSON.parse(JSON.stringify(this.directiveTypes.MULTIROWMAP));
203
+
204
+ for (let i = 0; i < pTokens.length; i++)
205
+ {
206
+ let tmpToken = pTokens[i].toUpperCase();
207
+ switch(tmpToken)
208
+ {
209
+ case 'ROWS':
210
+ // Expect ROWS FROM <address>
211
+ if (((i + 2) < pTokens.length) && (pTokens[i + 1].toUpperCase() === 'FROM'))
212
+ {
213
+ tmpNewDirectiveDescription.RowsAddress = pTokens[i + 2];
214
+ i = i + 2;
215
+ }
216
+ break;
217
+
218
+ case 'SERIESSTART':
219
+ if ((i + 1) < pTokens.length)
220
+ {
221
+ // Handle negative values which get tokenized as separate - and number tokens
222
+ let tmpStartValue = pTokens[i + 1];
223
+ if ((tmpStartValue === '-') && ((i + 2) < pTokens.length))
224
+ {
225
+ tmpStartValue = '-' + pTokens[i + 2];
226
+ i = i + 2;
227
+ }
228
+ else
229
+ {
230
+ i = i + 1;
231
+ }
232
+ tmpNewDirectiveDescription.SeriesStart = tmpStartValue;
233
+ }
234
+ break;
235
+
236
+ case 'SERIESSTEP':
237
+ if ((i + 1) < pTokens.length)
238
+ {
239
+ // Handle negative values which get tokenized as separate - and number tokens
240
+ let tmpStepValue = pTokens[i + 1];
241
+ if ((tmpStepValue === '-') && ((i + 2) < pTokens.length))
242
+ {
243
+ tmpStepValue = '-' + pTokens[i + 2];
244
+ i = i + 2;
245
+ }
246
+ else
247
+ {
248
+ i = i + 1;
249
+ }
250
+ tmpNewDirectiveDescription.SeriesStep = tmpStepValue;
251
+ }
252
+ break;
253
+
254
+ case 'VARIABLE':
255
+ case 'VAR':
256
+ case 'V':
257
+ // Expect: VAR <name> FROM <property> [OFFSET <n>] [DEFAULT <defaultValue>]
258
+ if (((i + 3) < pTokens.length) && (pTokens[i + 2].toUpperCase() === 'FROM'))
259
+ {
260
+ let tmpVariableName = pTokens[i + 1];
261
+ let tmpProperty = pTokens[i + 3];
262
+
263
+ if (typeof(tmpVariableName) === 'string' && (tmpVariableName.length > 0))
264
+ {
265
+ let tmpVariableDescriptor = {
266
+ Token: tmpVariableName,
267
+ Property: tmpProperty,
268
+ RowOffset: 0,
269
+ Default: '0',
270
+ };
271
+
272
+ let tmpCurrentIndex = i + 3;
273
+
274
+ // Scan forward for OFFSET and DEFAULT in any order
275
+ while (tmpCurrentIndex + 1 < pTokens.length)
276
+ {
277
+ let tmpNextToken = pTokens[tmpCurrentIndex + 1].toUpperCase();
278
+ if (tmpNextToken === 'OFFSET')
279
+ {
280
+ if (tmpCurrentIndex + 2 < pTokens.length)
281
+ {
282
+ // Handle negative offsets which get tokenized as separate - and number tokens
283
+ let tmpOffsetValue = pTokens[tmpCurrentIndex + 2];
284
+ if ((tmpOffsetValue === '-') && (tmpCurrentIndex + 3 < pTokens.length))
285
+ {
286
+ tmpOffsetValue = '-' + pTokens[tmpCurrentIndex + 3];
287
+ tmpCurrentIndex = tmpCurrentIndex + 3;
288
+ }
289
+ else
290
+ {
291
+ tmpCurrentIndex = tmpCurrentIndex + 2;
292
+ }
293
+ tmpVariableDescriptor.RowOffset = parseInt(tmpOffsetValue);
294
+ if (isNaN(tmpVariableDescriptor.RowOffset))
295
+ {
296
+ tmpVariableDescriptor.RowOffset = 0;
297
+ }
298
+ }
299
+ }
300
+ else if (tmpNextToken === 'DEFAULT')
301
+ {
302
+ if (tmpCurrentIndex + 2 < pTokens.length)
303
+ {
304
+ // Handle negative defaults which get tokenized as separate - and number tokens
305
+ let tmpDefaultValue = pTokens[tmpCurrentIndex + 2];
306
+ if ((tmpDefaultValue === '-') && (tmpCurrentIndex + 3 < pTokens.length))
307
+ {
308
+ tmpDefaultValue = '-' + pTokens[tmpCurrentIndex + 3];
309
+ tmpCurrentIndex = tmpCurrentIndex + 3;
310
+ }
311
+ else
312
+ {
313
+ tmpCurrentIndex = tmpCurrentIndex + 2;
314
+ }
315
+ tmpVariableDescriptor.Default = tmpDefaultValue;
316
+ }
317
+ }
318
+ else
319
+ {
320
+ // Not a recognized sub-keyword for this variable; stop scanning
321
+ break;
322
+ }
323
+ }
324
+
325
+ i = tmpCurrentIndex;
326
+
327
+ tmpNewDirectiveDescription.ValueKeys.push(tmpVariableName);
328
+ tmpNewDirectiveDescription.Values[tmpVariableName] = tmpVariableDescriptor;
329
+ }
330
+ }
331
+ break;
332
+
333
+ default:
334
+ // Ignore other tokens
335
+ break;
336
+ }
337
+ }
338
+
339
+ return tmpNewDirectiveDescription;
340
+ }
341
+
342
+ rewriteTernaryOperators(pResultObject)
343
+ {
344
+ let tmpTokens = pResultObject.RawTokens;
345
+
346
+ // Scan right-to-left so nested ternaries (innermost first) are handled correctly.
347
+ for (let i = tmpTokens.length - 1; i >= 0; i--)
348
+ {
349
+ if (tmpTokens[i] !== '?')
350
+ {
351
+ continue;
352
+ }
353
+
354
+ // Found a ? token at index i.
355
+ // Walk left to find the start of the condition expression.
356
+ // The condition starts after the previous comma, open parenthesis, assignment operator, or beginning of tokens.
357
+ let tmpConditionStart = 0;
358
+ let tmpParenDepth = 0;
359
+ for (let j = i - 1; j >= 0; j--)
360
+ {
361
+ if (tmpTokens[j] === ')')
362
+ {
363
+ tmpParenDepth++;
364
+ }
365
+ else if (tmpTokens[j] === '(')
366
+ {
367
+ if (tmpParenDepth > 0)
368
+ {
369
+ tmpParenDepth--;
370
+ }
371
+ else
372
+ {
373
+ // We hit an unmatched open paren — condition starts after it
374
+ tmpConditionStart = j + 1;
375
+ break;
376
+ }
377
+ }
378
+ else if (tmpParenDepth === 0)
379
+ {
380
+ let tmpTokenDescriptor = this.ExpressionParser.tokenMap[tmpTokens[j]];
381
+ if (tmpTokenDescriptor && (tmpTokenDescriptor.Type === 'Assignment' || tmpTokens[j] === ','))
382
+ {
383
+ tmpConditionStart = j + 1;
384
+ break;
385
+ }
386
+ }
387
+ }
388
+
389
+ // Walk right from ? to find the matching :: at the same parenthesis depth.
390
+ let tmpSeparatorIndex = -1;
391
+ tmpParenDepth = 0;
392
+ for (let j = i + 1; j < tmpTokens.length; j++)
393
+ {
394
+ if (tmpTokens[j] === '(')
395
+ {
396
+ tmpParenDepth++;
397
+ }
398
+ else if (tmpTokens[j] === ')')
399
+ {
400
+ if (tmpParenDepth > 0)
401
+ {
402
+ tmpParenDepth--;
403
+ }
404
+ else
405
+ {
406
+ // Hit closing paren at our depth — no :: found in this group
407
+ break;
408
+ }
409
+ }
410
+ else if (tmpTokens[j] === '::' && tmpParenDepth === 0)
411
+ {
412
+ tmpSeparatorIndex = j;
413
+ break;
414
+ }
415
+ }
416
+
417
+ if (tmpSeparatorIndex === -1)
418
+ {
419
+ pResultObject.ExpressionParserLog.push(`ExpressionParser.rewriteTernaryOperators found a ? at token index ${i} with no matching :: separator.`);
420
+ this.log.warn(pResultObject.ExpressionParserLog[pResultObject.ExpressionParserLog.length - 1]);
421
+ continue;
422
+ }
423
+
424
+ // Walk right from :: to find the end of the false branch.
425
+ // The false branch ends at the next comma, close parenthesis, or end of tokens at the same depth.
426
+ let tmpFalseBranchEnd = tmpTokens.length;
427
+ tmpParenDepth = 0;
428
+ for (let j = tmpSeparatorIndex + 1; j < tmpTokens.length; j++)
429
+ {
430
+ if (tmpTokens[j] === '(')
431
+ {
432
+ tmpParenDepth++;
433
+ }
434
+ else if (tmpTokens[j] === ')')
435
+ {
436
+ if (tmpParenDepth > 0)
437
+ {
438
+ tmpParenDepth--;
439
+ }
440
+ else
441
+ {
442
+ // Unmatched close paren — false branch ends here
443
+ tmpFalseBranchEnd = j;
444
+ break;
445
+ }
446
+ }
447
+ else if (tmpParenDepth === 0)
448
+ {
449
+ if (tmpTokens[j] === ',')
450
+ {
451
+ tmpFalseBranchEnd = j;
452
+ break;
453
+ }
454
+ }
455
+ }
456
+
457
+ // Extract the three segments
458
+ let tmpConditionTokens = tmpTokens.slice(tmpConditionStart, i);
459
+ let tmpTrueBranchTokens = tmpTokens.slice(i + 1, tmpSeparatorIndex);
460
+ let tmpFalseBranchTokens = tmpTokens.slice(tmpSeparatorIndex + 1, tmpFalseBranchEnd);
461
+
462
+ // Build the replacement: ternary((condition), trueBranch, falseBranch)
463
+ // The condition is wrapped in its own parentheses to ensure correct
464
+ // precedence grouping when arithmetic appears on both sides of a comparison.
465
+ let tmpReplacementTokens = ['ternary', '(', '('];
466
+ tmpReplacementTokens = tmpReplacementTokens.concat(tmpConditionTokens);
467
+ tmpReplacementTokens.push(')');
468
+ tmpReplacementTokens.push(',');
469
+ tmpReplacementTokens = tmpReplacementTokens.concat(tmpTrueBranchTokens);
470
+ tmpReplacementTokens.push(',');
471
+ tmpReplacementTokens = tmpReplacementTokens.concat(tmpFalseBranchTokens);
472
+ tmpReplacementTokens.push(')');
473
+
474
+ // Splice the replacement into the token array
475
+ tmpTokens.splice(tmpConditionStart, tmpFalseBranchEnd - tmpConditionStart, ...tmpReplacementTokens);
476
+
477
+ // Adjust i to re-scan from the start of what we just inserted (in case of nested ternaries in the condition)
478
+ i = tmpConditionStart;
479
+ }
480
+ }
481
+
191
482
  parseDirectives(pResultObject)
192
483
  {
193
484
  let tmpResults = (typeof(pResultObject) === 'object') ? pResultObject : { ExpressionParserLog: [] };
194
485
 
486
+ // Rewrite ternary operators before directive parsing
487
+ this.rewriteTernaryOperators(tmpResults);
488
+
195
489
  tmpResults.SolverDirectives = this.defaultDirective;
196
490
  tmpResults.SolverDirectiveTokens = [];
197
491
 
@@ -258,6 +552,9 @@ class ExpressionTokenizerDirectiveMutation extends libExpressionParserOperationB
258
552
  case 'MAP':
259
553
  tmpResults.SolverDirectives = this.parseMapDirective(tmpResults.SolverDirectiveTokens);
260
554
  break;
555
+ case 'MULTIROWMAP':
556
+ tmpResults.SolverDirectives = this.parseMultiRowMapDirective(tmpResults.SolverDirectiveTokens);
557
+ break;
261
558
  default:
262
559
  // No further parsing needed
263
560
  break;
@@ -215,7 +215,7 @@ class ExpressionTokenizer extends libExpressionParserOperationBase
215
215
  {
216
216
  if (tmpCurrentToken.length > 0)
217
217
  {
218
- tmpResults.RawTokens.push(tmpTokenKey);
218
+ tmpResults.RawTokens.push(tmpCurrentToken);
219
219
  }
220
220
  tmpCurrentToken = '';
221
221
  tmpCurrentTokenType = false;
@@ -285,6 +285,11 @@
285
285
  "Address": "fable.Logic.when"
286
286
  },
287
287
 
288
+ "ternary": {
289
+ "Name": "numeric-aware ternary selection (used by ? :: operator desugaring)",
290
+ "Address": "fable.Logic.ternary"
291
+ },
292
+
288
293
  "entryinset": {
289
294
  "Name": "Entry in Set",
290
295
  "Address": "fable.Math.entryInSet"
@@ -465,10 +470,6 @@
465
470
  "Address": "fable.Math.interceptPrecise"
466
471
  },
467
472
 
468
- "polynomialregression": {
469
- "Name": "Perform an nth degree Polynomial Regression on a Set of X and Y Values",
470
- "Address": "fable.Math.polynomialRegression"
471
- },
472
473
  "leastsquares": {
473
474
  "Name": "Perform a Least Squares Regression on a Set of Independent Variable Vectors and a Dependent Variable Vector",
474
475
  "Address": "fable.Math.leastSquares"
@@ -46,7 +46,7 @@
46
46
  "Name": "Set Concatenate",
47
47
  "Token": ",",
48
48
  "Function": "fable.Math.setConcatenate",
49
- "Precedence": 5,
49
+ "Precedence": 6,
50
50
  "Type": "Operator"
51
51
  },
52
52
 
@@ -99,5 +99,69 @@
99
99
  "Function": "fable.Math.subtractPrecise",
100
100
  "Precedence": 4,
101
101
  "Type": "Operator"
102
+ },
103
+
104
+ ">":
105
+ {
106
+ "Name": "Greater Than",
107
+ "Token": ">",
108
+ "Function": "fable.Math.greaterThanOperator",
109
+ "Precedence": 5,
110
+ "Type": "Operator"
111
+ },
112
+ ">=":
113
+ {
114
+ "Name": "Greater Than or Equal",
115
+ "Token": ">=",
116
+ "Function": "fable.Math.greaterThanOrEqualOperator",
117
+ "Precedence": 5,
118
+ "Type": "Operator"
119
+ },
120
+ "<":
121
+ {
122
+ "Name": "Less Than",
123
+ "Token": "<",
124
+ "Function": "fable.Math.lessThanOperator",
125
+ "Precedence": 5,
126
+ "Type": "Operator"
127
+ },
128
+ "<=":
129
+ {
130
+ "Name": "Less Than or Equal",
131
+ "Token": "<=",
132
+ "Function": "fable.Math.lessThanOrEqualOperator",
133
+ "Precedence": 5,
134
+ "Type": "Operator"
135
+ },
136
+ "==":
137
+ {
138
+ "Name": "Equal",
139
+ "Token": "==",
140
+ "Function": "fable.Math.equalOperator",
141
+ "Precedence": 5,
142
+ "Type": "Operator"
143
+ },
144
+ "!=":
145
+ {
146
+ "Name": "Not Equal",
147
+ "Token": "!=",
148
+ "Function": "fable.Math.notEqualOperator",
149
+ "Precedence": 5,
150
+ "Type": "Operator"
151
+ },
152
+
153
+ "?":
154
+ {
155
+ "Name": "Ternary Condition",
156
+ "Token": "?",
157
+ "Precedence": 0,
158
+ "Type": "Ternary"
159
+ },
160
+ "::":
161
+ {
162
+ "Name": "Ternary Separator",
163
+ "Token": "::",
164
+ "Precedence": 0,
165
+ "Type": "Ternary"
102
166
  }
103
167
  }