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.
@@ -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.62",
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": "./node_modules/.bin/nyc --reporter=lcov --reporter=text-lcov ./node_modules/mocha/bin/_mocha -- -u tdd -R spec",
9
- "test": "./node_modules/.bin/mocha -u tdd -R spec",
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": "./node_modules/mocha/bin/_mocha -u tdd --exit -R spec --grep"
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.56"
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.5",
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.17",
64
- "fable-serviceproviderbase": "^3.0.18",
65
- "fable-settings": "^3.0.15",
66
- "fable-uuid": "^3.0.12",
67
- "manyfest": "^1.0.47",
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([