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.
- package/docs/services/expression-parser-functions/README.md +9 -0
- package/docs/services/expression-parser-functions/beziercurvefit.md +57 -0
- package/docs/services/expression-parser-functions/bezierpoint.md +55 -0
- package/docs/services/expression-parser-functions/intercept.md +59 -0
- package/docs/services/expression-parser-functions/setvalue.md +55 -0
- package/docs/services/expression-parser-functions/slope.md +51 -0
- package/docs/services/expression-parser-functions/ternary.md +180 -0
- package/docs/services/expression-parser.md +131 -0
- package/example_applications/mathematical_playground/AppData.json +36 -1
- package/example_applications/mathematical_playground/Math-Solver-Harness.js +215 -0
- package/example_applications/mathematical_playground/TestSuite-AcidTest.json +178 -0
- package/example_applications/mathematical_playground/TestSuite-Identities.json +147 -0
- package/example_applications/mathematical_playground/TestSuite-Precision.json +127 -0
- package/example_applications/mathematical_playground/TestSuite-Spreadsheet.json +198 -0
- package/package.json +3 -2
- package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-ExpressionTokenizer-DirectiveMutation.js +299 -2
- package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-ExpressionTokenizer.js +1 -1
- package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-FunctionMap.json +5 -4
- package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-TokenMap.json +65 -1
- package/source/services/Fable-Service-ExpressionParser.js +144 -0
- package/source/services/Fable-Service-Logic.js +33 -0
- package/source/services/Fable-Service-Math.js +78 -0
- 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.
|
|
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.
|
|
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(
|
|
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"
|
package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-TokenMap.json
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"Name": "Set Concatenate",
|
|
47
47
|
"Token": ",",
|
|
48
48
|
"Function": "fable.Math.setConcatenate",
|
|
49
|
-
"Precedence":
|
|
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
|
}
|