fable 3.1.62 → 3.1.64
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.md +59 -0
- package/example_applications/mathematical_playground/AppData.json +36 -1
- package/example_applications/mathematical_playground/Math-Solver-Harness.js +107 -0
- package/package.json +12 -11
- package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-ExpressionTokenizer-DirectiveMutation.js +156 -2
- package/source/services/Fable-Service-ExpressionParser.js +143 -0
- package/test/ExpressionParser_tests.js +769 -0
- package/.babelrc +0 -3
- package/.browserslistrc +0 -1
- package/dist/fable.js +0 -5210
- package/dist/fable.js.map +0 -1
- package/dist/fable.min.js +0 -12
- package/dist/fable.min.js.map +0 -1
- package/dist/indoctrinate_content_staging/Indoctrinate-Catalog-AppData.json +0 -10514
- /package/docs/{cover.md → _cover.md} +0 -0
|
@@ -264,6 +264,65 @@ parser.solve('Result = MONTECARLO SAMPLECOUNT 5000 VAR x PT x -3 PT x 3 VAR y PT
|
|
|
264
264
|
{}, {}, manifest, dest);
|
|
265
265
|
```
|
|
266
266
|
|
|
267
|
+
### MULTIROWMAP Expressions
|
|
268
|
+
|
|
269
|
+
Iterate over an array of objects (rows) with multi-row lookback and lookahead. Each variable can reference the current row or any row at a relative offset, with configurable defaults for out-of-bounds access.
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
// Syntax:
|
|
273
|
+
// Result = MULTIROWMAP ROWS FROM <address>
|
|
274
|
+
// [SERIESSTART <n>] [SERIESSTEP <n>]
|
|
275
|
+
// VAR <name> FROM <property> [OFFSET <n>] [DEFAULT <value>]
|
|
276
|
+
// ... : <expression>
|
|
277
|
+
|
|
278
|
+
// Basic: compute area from each row's Width and Height
|
|
279
|
+
parser.solve('Areas = MULTIROWMAP ROWS FROM Rows VAR w FROM Width VAR h FROM Height : w * h',
|
|
280
|
+
{ Rows: [{Width:10,Height:20}, {Width:30,Height:40}] }, {}, manifest, dest);
|
|
281
|
+
// dest.Areas === ['200', '1200']
|
|
282
|
+
|
|
283
|
+
// Previous row lookback (day-over-day change)
|
|
284
|
+
parser.solve('Change = MULTIROWMAP ROWS FROM Prices VAR Today FROM Close VAR Yesterday FROM Close OFFSET -1 DEFAULT 0 : Today - Yesterday',
|
|
285
|
+
data, {}, manifest, dest);
|
|
286
|
+
|
|
287
|
+
// Two-row lookback (Fibonacci verification)
|
|
288
|
+
parser.solve('Check = MULTIROWMAP ROWS FROM FibRows VAR Cur FROM Fib VAR P1 FROM Fib OFFSET -1 DEFAULT 0 VAR P2 FROM Fib OFFSET -2 DEFAULT 0 : Cur - (P1 + P2)',
|
|
289
|
+
data, {}, manifest, dest);
|
|
290
|
+
|
|
291
|
+
// Forward lookback (next row reference)
|
|
292
|
+
parser.solve('NextDiff = MULTIROWMAP ROWS FROM Rows VAR Cur FROM Value VAR Next FROM Value OFFSET 1 DEFAULT 0 : Next - Cur',
|
|
293
|
+
data, {}, manifest, dest);
|
|
294
|
+
|
|
295
|
+
// Central difference (both directions)
|
|
296
|
+
parser.solve('Deriv = MULTIROWMAP ROWS FROM Rows VAR Prev FROM Value OFFSET -1 DEFAULT 0 VAR Next FROM Value OFFSET 1 DEFAULT 0 : (Next - Prev) / 2',
|
|
297
|
+
data, {}, manifest, dest);
|
|
298
|
+
|
|
299
|
+
// SERIESSTART: skip initial rows (start from row 2)
|
|
300
|
+
parser.solve('MA = MULTIROWMAP ROWS FROM Prices SERIESSTART 2 VAR c0 FROM Close VAR c1 FROM Close OFFSET -1 DEFAULT 0 VAR c2 FROM Close OFFSET -2 DEFAULT 0 : (c0 + c1 + c2) / 3',
|
|
301
|
+
data, {}, manifest, dest);
|
|
302
|
+
|
|
303
|
+
// Negative SERIESSTART: start from 2 rows before end
|
|
304
|
+
parser.solve('LastTwo = MULTIROWMAP ROWS FROM Rows SERIESSTART -2 VAR v FROM Value : v + 0',
|
|
305
|
+
data, {}, manifest, dest);
|
|
306
|
+
|
|
307
|
+
// SERIESSTEP -1: iterate backwards
|
|
308
|
+
parser.solve('Reversed = MULTIROWMAP ROWS FROM Rows SERIESSTART -1 SERIESSTEP -1 VAR v FROM Value : v + 0',
|
|
309
|
+
data, {}, manifest, dest);
|
|
310
|
+
|
|
311
|
+
// SERIESSTEP 2: every other row
|
|
312
|
+
parser.solve('Sampled = MULTIROWMAP ROWS FROM Rows SERIESSTEP 2 VAR v FROM Value : v + 0',
|
|
313
|
+
data, {}, manifest, dest);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Available variables in MULTIROWMAP expressions:**
|
|
317
|
+
- `stepIndex` — iteration counter (0, 1, 2, ...)
|
|
318
|
+
- `rowIndex` — actual array index of the current row
|
|
319
|
+
- Any VAR-mapped variable names
|
|
320
|
+
|
|
321
|
+
**OFFSET values:**
|
|
322
|
+
- `0` (default) — current row
|
|
323
|
+
- `-1` — previous row, `-2` — two rows back, `-3` — three back, etc.
|
|
324
|
+
- `1` — next row, `2` — two ahead, etc.
|
|
325
|
+
|
|
267
326
|
### ITERATIVESERIES
|
|
268
327
|
|
|
269
328
|
Run cumulative computations over arrays:
|
|
@@ -22,5 +22,40 @@
|
|
|
22
22
|
"BezierYValues": [0, 1, 4, 9, 16],
|
|
23
23
|
|
|
24
24
|
"SalesMonths": [1, 2, 3, 4, 5, 6],
|
|
25
|
-
"SalesRevenue": [150, 200, 250, 310, 350, 400]
|
|
25
|
+
"SalesRevenue": [150, 200, 250, 310, 350, 400],
|
|
26
|
+
|
|
27
|
+
"FibonacciRows": [
|
|
28
|
+
{ "Fib": 0 },
|
|
29
|
+
{ "Fib": 1 },
|
|
30
|
+
{ "Fib": 1 },
|
|
31
|
+
{ "Fib": 2 },
|
|
32
|
+
{ "Fib": 3 },
|
|
33
|
+
{ "Fib": 5 },
|
|
34
|
+
{ "Fib": 8 },
|
|
35
|
+
{ "Fib": 13 },
|
|
36
|
+
{ "Fib": 21 },
|
|
37
|
+
{ "Fib": 34 }
|
|
38
|
+
],
|
|
39
|
+
|
|
40
|
+
"StockPrices": [
|
|
41
|
+
{ "Date": "2025-01-01", "Close": 100.00, "Volume": 5000 },
|
|
42
|
+
{ "Date": "2025-01-02", "Close": 102.50, "Volume": 6200 },
|
|
43
|
+
{ "Date": "2025-01-03", "Close": 101.00, "Volume": 4800 },
|
|
44
|
+
{ "Date": "2025-01-06", "Close": 105.00, "Volume": 7100 },
|
|
45
|
+
{ "Date": "2025-01-07", "Close": 108.50, "Volume": 8300 },
|
|
46
|
+
{ "Date": "2025-01-08", "Close": 107.00, "Volume": 5500 },
|
|
47
|
+
{ "Date": "2025-01-09", "Close": 110.00, "Volume": 6800 },
|
|
48
|
+
{ "Date": "2025-01-10", "Close": 112.50, "Volume": 7200 }
|
|
49
|
+
],
|
|
50
|
+
|
|
51
|
+
"TemperatureReadings": [
|
|
52
|
+
{ "Hour": 0, "Temp": 58.0 },
|
|
53
|
+
{ "Hour": 3, "Temp": 55.5 },
|
|
54
|
+
{ "Hour": 6, "Temp": 54.0 },
|
|
55
|
+
{ "Hour": 9, "Temp": 60.5 },
|
|
56
|
+
{ "Hour": 12, "Temp": 68.0 },
|
|
57
|
+
{ "Hour": 15, "Temp": 72.5 },
|
|
58
|
+
{ "Hour": 18, "Temp": 67.0 },
|
|
59
|
+
{ "Hour": 21, "Temp": 61.5 }
|
|
60
|
+
]
|
|
26
61
|
}
|
|
@@ -115,4 +115,111 @@ console.log(` Intercept (baseline): ${tmpSlopeInterceptResults.TrendIntercept}`
|
|
|
115
115
|
_ExpressionParser.solve('Month7Prediction = TrendIntercept + TrendSlope * 7', tmpSlopeInterceptResults, tmpSlopeInterceptResults, false, tmpSlopeInterceptResults);
|
|
116
116
|
console.log(` Predicted month 7 revenue: ${tmpSlopeInterceptResults.Month7Prediction}`);
|
|
117
117
|
|
|
118
|
+
/* * * * * * * * * * * * * * * * *
|
|
119
|
+
*
|
|
120
|
+
* MULTIROWMAP: Multi-Row Lookback Series Solver
|
|
121
|
+
*
|
|
122
|
+
* MULTIROWMAP lets you iterate over an array of objects (rows) and reference
|
|
123
|
+
* values from the current row, previous rows, and even future rows.
|
|
124
|
+
*
|
|
125
|
+
* Syntax:
|
|
126
|
+
* Result = MULTIROWMAP ROWS FROM <address>
|
|
127
|
+
* [SERIESSTART <n>]
|
|
128
|
+
* [SERIESSTEP <n>]
|
|
129
|
+
* VAR <name> FROM <property> [OFFSET <n>] [DEFAULT <value>]
|
|
130
|
+
* ...
|
|
131
|
+
* : <expression>
|
|
132
|
+
*
|
|
133
|
+
* OFFSET 0 (default) = current row
|
|
134
|
+
* OFFSET -1 = previous row, OFFSET -2 = two rows back, etc.
|
|
135
|
+
* OFFSET 1 = next row, OFFSET 2 = two rows ahead, etc.
|
|
136
|
+
* DEFAULT provides a fallback when the referenced row is out of bounds.
|
|
137
|
+
* SERIESSTART controls which row index to start iterating from (negative counts from end).
|
|
138
|
+
* SERIESSTEP controls the iteration step (default 1, use -1 to go backwards).
|
|
139
|
+
*
|
|
140
|
+
*/
|
|
141
|
+
_Fable.log.info(`Beginning MULTIROWMAP Exercises....`);
|
|
142
|
+
|
|
143
|
+
let tmpMultiRowResults = {};
|
|
144
|
+
|
|
145
|
+
// --- Example 1: Fibonacci Verification ---
|
|
146
|
+
// Verify that each value in a Fibonacci sequence equals the sum of the two previous values
|
|
147
|
+
_ExpressionParser.solve(
|
|
148
|
+
'FibCheck = MULTIROWMAP ROWS FROM FibonacciRows VAR Current FROM Fib VAR Prev1 FROM Fib OFFSET -1 DEFAULT 0 VAR Prev2 FROM Fib OFFSET -2 DEFAULT 0 : Current - (Prev1 + Prev2)',
|
|
149
|
+
_AppData, {}, false, tmpMultiRowResults);
|
|
150
|
+
console.log(`\nFibonacci verification (should be 0 from row 2 onward):`);
|
|
151
|
+
console.log(` FibCheck: [${tmpMultiRowResults.FibCheck.join(', ')}]`);
|
|
152
|
+
|
|
153
|
+
// --- Example 2: Stock Price Day-over-Day Change ---
|
|
154
|
+
// Calculate the daily price change for a stock
|
|
155
|
+
_ExpressionParser.solve(
|
|
156
|
+
'DailyChange = MULTIROWMAP ROWS FROM StockPrices VAR Today FROM Close VAR Yesterday FROM Close OFFSET -1 DEFAULT 0 : Today - Yesterday',
|
|
157
|
+
_AppData, {}, false, tmpMultiRowResults);
|
|
158
|
+
console.log(`\nStock daily price change:`);
|
|
159
|
+
console.log(` DailyChange: [${tmpMultiRowResults.DailyChange.join(', ')}]`);
|
|
160
|
+
|
|
161
|
+
// --- Example 3: 3-Period Moving Average ---
|
|
162
|
+
// Compute a 3-day moving average of stock closing prices
|
|
163
|
+
_ExpressionParser.solve(
|
|
164
|
+
'MA3 = MULTIROWMAP ROWS FROM StockPrices VAR c0 FROM Close VAR c1 FROM Close OFFSET -1 DEFAULT 0 VAR c2 FROM Close OFFSET -2 DEFAULT 0 : (c0 + c1 + c2) / 3',
|
|
165
|
+
_AppData, {}, false, tmpMultiRowResults);
|
|
166
|
+
console.log(`\n3-period moving average of stock prices:`);
|
|
167
|
+
console.log(` MA3: [${tmpMultiRowResults.MA3.join(', ')}]`);
|
|
168
|
+
|
|
169
|
+
// --- Example 4: Forward-Looking (Next Row Reference) ---
|
|
170
|
+
// For each temperature reading, compute the difference to the next reading
|
|
171
|
+
_ExpressionParser.solve(
|
|
172
|
+
'TempChange = MULTIROWMAP ROWS FROM TemperatureReadings VAR Current FROM Temp VAR Next FROM Temp OFFSET 1 DEFAULT 0 : Next - Current',
|
|
173
|
+
_AppData, {}, false, tmpMultiRowResults);
|
|
174
|
+
console.log(`\nTemperature change to next reading (forward lookback):`);
|
|
175
|
+
console.log(` TempChange: [${tmpMultiRowResults.TempChange.join(', ')}]`);
|
|
176
|
+
|
|
177
|
+
// --- Example 5: Central Difference (Both Directions) ---
|
|
178
|
+
// Approximate the derivative using central difference: (next - prev) / 2
|
|
179
|
+
_ExpressionParser.solve(
|
|
180
|
+
'TempDerivative = MULTIROWMAP ROWS FROM TemperatureReadings VAR Prev FROM Temp OFFSET -1 DEFAULT 0 VAR Next FROM Temp OFFSET 1 DEFAULT 0 : (Next - Prev) / 2',
|
|
181
|
+
_AppData, {}, false, tmpMultiRowResults);
|
|
182
|
+
console.log(`\nTemperature central difference derivative:`);
|
|
183
|
+
console.log(` TempDerivative: [${tmpMultiRowResults.TempDerivative.join(', ')}]`);
|
|
184
|
+
|
|
185
|
+
// --- Example 6: Second Derivative (Two-Row Lookback) ---
|
|
186
|
+
// Approximate the second derivative: current - 2*prev + twoprev
|
|
187
|
+
_ExpressionParser.solve(
|
|
188
|
+
'TempAcceleration = MULTIROWMAP ROWS FROM TemperatureReadings VAR Current FROM Temp VAR Prev FROM Temp OFFSET -1 DEFAULT 0 VAR TwoBack FROM Temp OFFSET -2 DEFAULT 0 : Current - 2 * Prev + TwoBack',
|
|
189
|
+
_AppData, {}, false, tmpMultiRowResults);
|
|
190
|
+
console.log(`\nTemperature second derivative (acceleration):`);
|
|
191
|
+
console.log(` TempAcceleration: [${tmpMultiRowResults.TempAcceleration.join(', ')}]`);
|
|
192
|
+
|
|
193
|
+
// --- Example 7: SERIESSTART - Skip Initial Rows ---
|
|
194
|
+
// Start the moving average from row 2 so all three periods have valid data
|
|
195
|
+
_ExpressionParser.solve(
|
|
196
|
+
'MA3Valid = MULTIROWMAP ROWS FROM StockPrices SERIESSTART 2 VAR c0 FROM Close VAR c1 FROM Close OFFSET -1 DEFAULT 0 VAR c2 FROM Close OFFSET -2 DEFAULT 0 : (c0 + c1 + c2) / 3',
|
|
197
|
+
_AppData, {}, false, tmpMultiRowResults);
|
|
198
|
+
console.log(`\n3-period MA starting from row 2 (all periods valid):`);
|
|
199
|
+
console.log(` MA3Valid: [${tmpMultiRowResults.MA3Valid.join(', ')}]`);
|
|
200
|
+
|
|
201
|
+
// --- Example 8: SERIESSTEP -1 - Iterate Backwards ---
|
|
202
|
+
// Read stock prices in reverse chronological order
|
|
203
|
+
_ExpressionParser.solve(
|
|
204
|
+
'ReversePrices = MULTIROWMAP ROWS FROM StockPrices SERIESSTART -1 SERIESSTEP -1 VAR Price FROM Close : Price + 0',
|
|
205
|
+
_AppData, {}, false, tmpMultiRowResults);
|
|
206
|
+
console.log(`\nStock prices in reverse order:`);
|
|
207
|
+
console.log(` ReversePrices: [${tmpMultiRowResults.ReversePrices.join(', ')}]`);
|
|
208
|
+
|
|
209
|
+
// --- Example 9: SERIESSTEP 2 - Every Other Row ---
|
|
210
|
+
// Sample every other temperature reading
|
|
211
|
+
_ExpressionParser.solve(
|
|
212
|
+
'EveryOtherTemp = MULTIROWMAP ROWS FROM TemperatureReadings SERIESSTEP 2 VAR t FROM Temp : t + 0',
|
|
213
|
+
_AppData, {}, false, tmpMultiRowResults);
|
|
214
|
+
console.log(`\nEvery other temperature reading:`);
|
|
215
|
+
console.log(` EveryOtherTemp: [${tmpMultiRowResults.EveryOtherTemp.join(', ')}]`);
|
|
216
|
+
|
|
217
|
+
// --- Example 10: Three-Row Lookback with Multiple Properties ---
|
|
218
|
+
// Weighted volume change: compare current volume to 3 days ago, scaled by price ratio
|
|
219
|
+
_ExpressionParser.solve(
|
|
220
|
+
'WeightedVolChange = MULTIROWMAP ROWS FROM StockPrices VAR CurVol FROM Volume VAR CurPrice FROM Close VAR OldVol FROM Volume OFFSET -3 DEFAULT 0 VAR OldPrice FROM Close OFFSET -3 DEFAULT 1 : (CurVol - OldVol) * (CurPrice / OldPrice)',
|
|
221
|
+
_AppData, {}, false, tmpMultiRowResults);
|
|
222
|
+
console.log(`\nWeighted volume change (3-day lookback with price ratio):`);
|
|
223
|
+
console.log(` WeightedVolChange: [${tmpMultiRowResults.WeightedVolChange.join(', ')}]`);
|
|
224
|
+
|
|
118
225
|
console.log('QED');
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fable",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.64",
|
|
4
4
|
"description": "A service dependency injection, configuration and logging library.",
|
|
5
5
|
"main": "source/Fable.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node source/Fable.js",
|
|
8
|
-
"coverage": "
|
|
9
|
-
"test": "
|
|
8
|
+
"coverage": "npx quack coverage",
|
|
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",
|
|
14
|
-
"tests": "
|
|
15
|
+
"tests": "npx quack test -g"
|
|
15
16
|
},
|
|
16
17
|
"mocha": {
|
|
17
18
|
"diff": true,
|
|
@@ -50,21 +51,21 @@
|
|
|
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",
|
|
57
58
|
"async.waterfall": "^0.5.2",
|
|
58
59
|
"big.js": "^7.0.1",
|
|
59
|
-
"cachetrax": "^1.0.
|
|
60
|
+
"cachetrax": "^1.0.6",
|
|
60
61
|
"cookie": "^1.1.1",
|
|
61
62
|
"data-arithmatic": "^1.0.7",
|
|
62
63
|
"dayjs": "^1.11.19",
|
|
63
|
-
"fable-log": "^3.0.
|
|
64
|
-
"fable-serviceproviderbase": "^3.0.
|
|
65
|
-
"fable-settings": "^3.0.
|
|
66
|
-
"fable-uuid": "^3.0.
|
|
67
|
-
"manyfest": "^1.0.
|
|
64
|
+
"fable-log": "^3.0.18",
|
|
65
|
+
"fable-serviceproviderbase": "^3.0.19",
|
|
66
|
+
"fable-settings": "^3.0.16",
|
|
67
|
+
"fable-uuid": "^3.0.13",
|
|
68
|
+
"manyfest": "^1.0.48",
|
|
68
69
|
"simple-get": "^4.0.1"
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -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,6 +189,156 @@ 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
|
+
|
|
191
342
|
parseDirectives(pResultObject)
|
|
192
343
|
{
|
|
193
344
|
let tmpResults = (typeof(pResultObject) === 'object') ? pResultObject : { ExpressionParserLog: [] };
|
|
@@ -258,6 +409,9 @@ class ExpressionTokenizerDirectiveMutation extends libExpressionParserOperationB
|
|
|
258
409
|
case 'MAP':
|
|
259
410
|
tmpResults.SolverDirectives = this.parseMapDirective(tmpResults.SolverDirectiveTokens);
|
|
260
411
|
break;
|
|
412
|
+
case 'MULTIROWMAP':
|
|
413
|
+
tmpResults.SolverDirectives = this.parseMultiRowMapDirective(tmpResults.SolverDirectiveTokens);
|
|
414
|
+
break;
|
|
261
415
|
default:
|
|
262
416
|
// No further parsing needed
|
|
263
417
|
break;
|
|
@@ -456,6 +456,149 @@ class FableServiceExpressionParser extends libFableServiceBase
|
|
|
456
456
|
|
|
457
457
|
return tmpValueArray;
|
|
458
458
|
}
|
|
459
|
+
else if (tmpResultsObject.SolverDirectives.Code == 'MULTIROWMAP')
|
|
460
|
+
{
|
|
461
|
+
// MULTIROWMAP iterates over an array of objects (rows), allowing each expression to reference
|
|
462
|
+
// the current row and any number of previous/future rows via configurable offsets.
|
|
463
|
+
// Optional SERIESSTART and SERIESSTEP control which rows are iterated and in what direction.
|
|
464
|
+
const tmpDirectiveValues = tmpResultsObject.SolverDirectives.Values;
|
|
465
|
+
const tmpDirectiveValueKeys = tmpResultsObject.SolverDirectives.ValueKeys;
|
|
466
|
+
const tmpRowsAddress = tmpResultsObject.SolverDirectives.RowsAddress;
|
|
467
|
+
let tmpValueArray = [];
|
|
468
|
+
|
|
469
|
+
// Resolve the rows array from the data source
|
|
470
|
+
let tmpRows = null;
|
|
471
|
+
if (tmpRowsAddress)
|
|
472
|
+
{
|
|
473
|
+
tmpRows = tmpManifest.getValueByHash(tmpDataSourceObject, tmpRowsAddress);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!Array.isArray(tmpRows) || tmpRows.length < 1)
|
|
477
|
+
{
|
|
478
|
+
tmpResultsObject.ExpressionParserLog.push(`ExpressionParser.solve detected invalid MULTIROWMAP directive parameters. ROWS FROM address must resolve to a non-empty array.`);
|
|
479
|
+
this.log.warn(tmpResultsObject.ExpressionParserLog[tmpResultsObject.ExpressionParserLog.length-1]);
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Resolve SeriesStart: default to 0 (first row). Negative values count from end.
|
|
484
|
+
let tmpSeriesStart = 0;
|
|
485
|
+
if (tmpResultsObject.SolverDirectives.SeriesStart !== null)
|
|
486
|
+
{
|
|
487
|
+
tmpSeriesStart = parseInt(tmpResultsObject.SolverDirectives.SeriesStart);
|
|
488
|
+
if (isNaN(tmpSeriesStart))
|
|
489
|
+
{
|
|
490
|
+
// Try to resolve as a variable from the data source
|
|
491
|
+
let tmpStartToken = this.fable.ExpressionParser.Postfix.getTokenContainerObject(tmpResultsObject.SolverDirectives.SeriesStart, 'Token.Symbol');
|
|
492
|
+
this.substituteValuesInTokenizedObjects([tmpStartToken], tmpDataSourceObject, tmpResultsObject, tmpManifest);
|
|
493
|
+
if (tmpStartToken.Resolved)
|
|
494
|
+
{
|
|
495
|
+
tmpSeriesStart = parseInt(tmpStartToken.Value);
|
|
496
|
+
}
|
|
497
|
+
if (isNaN(tmpSeriesStart))
|
|
498
|
+
{
|
|
499
|
+
tmpSeriesStart = 0;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Handle negative start (count from end)
|
|
504
|
+
if (tmpSeriesStart < 0)
|
|
505
|
+
{
|
|
506
|
+
tmpSeriesStart = tmpRows.length + tmpSeriesStart;
|
|
507
|
+
}
|
|
508
|
+
// Clamp to valid range
|
|
509
|
+
tmpSeriesStart = Math.max(0, Math.min(tmpSeriesStart, tmpRows.length - 1));
|
|
510
|
+
|
|
511
|
+
// Resolve SeriesStep: default to 1
|
|
512
|
+
let tmpSeriesStep = 1;
|
|
513
|
+
if (tmpResultsObject.SolverDirectives.SeriesStep !== null)
|
|
514
|
+
{
|
|
515
|
+
tmpSeriesStep = parseInt(tmpResultsObject.SolverDirectives.SeriesStep);
|
|
516
|
+
if (isNaN(tmpSeriesStep))
|
|
517
|
+
{
|
|
518
|
+
// Try to resolve as a variable from the data source
|
|
519
|
+
let tmpStepToken = this.fable.ExpressionParser.Postfix.getTokenContainerObject(tmpResultsObject.SolverDirectives.SeriesStep, 'Token.Symbol');
|
|
520
|
+
this.substituteValuesInTokenizedObjects([tmpStepToken], tmpDataSourceObject, tmpResultsObject, tmpManifest);
|
|
521
|
+
if (tmpStepToken.Resolved)
|
|
522
|
+
{
|
|
523
|
+
tmpSeriesStep = parseInt(tmpStepToken.Value);
|
|
524
|
+
}
|
|
525
|
+
if (isNaN(tmpSeriesStep))
|
|
526
|
+
{
|
|
527
|
+
tmpSeriesStep = 1;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (tmpSeriesStep === 0)
|
|
532
|
+
{
|
|
533
|
+
tmpResultsObject.ExpressionParserLog.push(`ExpressionParser.solve detected invalid MULTIROWMAP directive parameters. SERIESSTEP cannot be zero.`);
|
|
534
|
+
this.log.warn(tmpResultsObject.ExpressionParserLog[tmpResultsObject.ExpressionParserLog.length-1]);
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
let tmpStepCounter = 0;
|
|
539
|
+
for (let i = tmpSeriesStart; (tmpSeriesStep > 0) ? (i < tmpRows.length) : (i >= 0); i += tmpSeriesStep)
|
|
540
|
+
{
|
|
541
|
+
// Build the data source for this iteration with the mapped variables
|
|
542
|
+
let tmpRowStepDataSourceObject = Object.assign({}, tmpDataSourceObject);
|
|
543
|
+
tmpRowStepDataSourceObject.stepIndex = tmpStepCounter;
|
|
544
|
+
tmpRowStepDataSourceObject.rowIndex = i;
|
|
545
|
+
|
|
546
|
+
for (let j = 0; j < tmpDirectiveValueKeys.length; j++)
|
|
547
|
+
{
|
|
548
|
+
const tmpVariableKey = tmpDirectiveValueKeys[j];
|
|
549
|
+
const tmpVariableDescription = tmpDirectiveValues[tmpVariableKey];
|
|
550
|
+
const tmpTargetRowIndex = i + tmpVariableDescription.RowOffset;
|
|
551
|
+
|
|
552
|
+
// Check if the target row index is within bounds
|
|
553
|
+
if (tmpTargetRowIndex < 0 || tmpTargetRowIndex >= tmpRows.length)
|
|
554
|
+
{
|
|
555
|
+
// Out of bounds -- use the default value
|
|
556
|
+
tmpRowStepDataSourceObject[tmpVariableKey] = tmpVariableDescription.Default;
|
|
557
|
+
}
|
|
558
|
+
else
|
|
559
|
+
{
|
|
560
|
+
const tmpTargetRow = tmpRows[tmpTargetRowIndex];
|
|
561
|
+
if (tmpVariableDescription.Property === null)
|
|
562
|
+
{
|
|
563
|
+
// No property specified, use the whole row
|
|
564
|
+
tmpRowStepDataSourceObject[tmpVariableKey] = tmpTargetRow;
|
|
565
|
+
}
|
|
566
|
+
else
|
|
567
|
+
{
|
|
568
|
+
// Look up the property on the target row
|
|
569
|
+
let tmpValue = tmpManifest.getValueByHash(tmpTargetRow, tmpVariableDescription.Property);
|
|
570
|
+
if (tmpValue == null)
|
|
571
|
+
{
|
|
572
|
+
tmpValue = tmpVariableDescription.Default;
|
|
573
|
+
}
|
|
574
|
+
tmpRowStepDataSourceObject[tmpVariableKey] = tmpValue;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let tmpMutatedValues = this.substituteValuesInTokenizedObjects(tmpResultsObject.PostfixTokenObjects, tmpRowStepDataSourceObject, tmpResultsObject, tmpManifest);
|
|
580
|
+
|
|
581
|
+
tmpValueArray.push( this.solvePostfixedExpression( tmpResultsObject.PostfixSolveList, tmpDataDestinationObject, tmpResultsObject, tmpManifest) );
|
|
582
|
+
|
|
583
|
+
for (let j = 0; j < tmpMutatedValues.length; j++)
|
|
584
|
+
{
|
|
585
|
+
tmpMutatedValues[j].Resolved = false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
tmpStepCounter++;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Do the assignment
|
|
592
|
+
let tmpAssignmentManifestHash = tmpResultsObject.PostfixedAssignmentAddress;
|
|
593
|
+
if ((tmpResultsObject.OriginalRawTokens[1] === '=') && (typeof(tmpResultsObject.OriginalRawTokens[0]) === 'string') && (tmpResultsObject.OriginalRawTokens[0].length > 0))
|
|
594
|
+
{
|
|
595
|
+
tmpAssignmentManifestHash = tmpResultsObject.OriginalRawTokens[0];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
tmpManifest.setValueByHash(tmpDataDestinationObject, tmpAssignmentManifestHash, tmpValueArray);
|
|
599
|
+
|
|
600
|
+
return tmpValueArray;
|
|
601
|
+
}
|
|
459
602
|
else if (tmpResultsObject.SolverDirectives.Code == 'MONTECARLO')
|
|
460
603
|
{
|
|
461
604
|
const [ tmpSampleCount ] = this._prepareDirectiveParameters([
|