fable 3.1.63 → 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,6 +1,6 @@
1
1
  {
2
2
  "name": "fable",
3
- "version": "3.1.63",
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": {
@@ -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,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([
@@ -1384,5 +1384,774 @@ suite
1384
1384
  );
1385
1385
  }
1386
1386
  );
1387
+ suite
1388
+ (
1389
+ 'Multi-Row Map Expressions',
1390
+ function ()
1391
+ {
1392
+ test
1393
+ (
1394
+ 'Test basic MULTIROWMAP with current row',
1395
+ (fDone) =>
1396
+ {
1397
+ let testFable = new libFable();
1398
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1399
+ let tmpManifest = testFable.newManyfest();
1400
+
1401
+ let tmpDataSourceObject = {
1402
+ Rows: [
1403
+ { Width: 10, Height: 20 },
1404
+ { Width: 30, Height: 40 },
1405
+ { Width: 50, Height: 60 },
1406
+ ]
1407
+ };
1408
+ let tmpDataDestinationObject = {};
1409
+
1410
+ // Simple: reference current row values (OFFSET 0 is default)
1411
+ let tmpResult = _Parser.solve(
1412
+ 'Areas = MULTIROWMAP ROWS FROM Rows VAR w FROM Width VAR h FROM Height : w * h',
1413
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1414
+
1415
+ Expect(tmpResult).to.be.an('array');
1416
+ Expect(tmpResult.length).to.equal(3);
1417
+ Expect(tmpResult[0]).to.equal('200');
1418
+ Expect(tmpResult[1]).to.equal('1200');
1419
+ Expect(tmpResult[2]).to.equal('3000');
1420
+
1421
+ return fDone();
1422
+ }
1423
+ );
1424
+ test
1425
+ (
1426
+ 'Test MULTIROWMAP with previous row lookback (OFFSET -1)',
1427
+ (fDone) =>
1428
+ {
1429
+ let testFable = new libFable();
1430
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1431
+ let tmpManifest = testFable.newManyfest();
1432
+
1433
+ let tmpDataSourceObject = {
1434
+ Rows: [
1435
+ { Value: 100 },
1436
+ { Value: 200 },
1437
+ { Value: 300 },
1438
+ { Value: 400 },
1439
+ ]
1440
+ };
1441
+ let tmpDataDestinationObject = {};
1442
+
1443
+ // Reference the current row and the previous row's value
1444
+ let tmpResult = _Parser.solve(
1445
+ 'Deltas = MULTIROWMAP ROWS FROM Rows VAR Current FROM Value OFFSET 0 VAR Previous FROM Value OFFSET -1 DEFAULT 0 : Current - Previous',
1446
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1447
+
1448
+ Expect(tmpResult).to.be.an('array');
1449
+ Expect(tmpResult.length).to.equal(4);
1450
+ // Row 0: 100 - 0 (default) = 100
1451
+ Expect(tmpResult[0]).to.equal('100');
1452
+ // Row 1: 200 - 100 = 100
1453
+ Expect(tmpResult[1]).to.equal('100');
1454
+ // Row 2: 300 - 200 = 100
1455
+ Expect(tmpResult[2]).to.equal('100');
1456
+ // Row 3: 400 - 300 = 100
1457
+ Expect(tmpResult[3]).to.equal('100');
1458
+
1459
+ return fDone();
1460
+ }
1461
+ );
1462
+ test
1463
+ (
1464
+ 'Test MULTIROWMAP with two-row lookback (OFFSET -2)',
1465
+ (fDone) =>
1466
+ {
1467
+ let testFable = new libFable();
1468
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1469
+ let tmpManifest = testFable.newManyfest();
1470
+
1471
+ let tmpDataSourceObject = {
1472
+ Rows: [
1473
+ { Value: 5 },
1474
+ { Value: 10 },
1475
+ { Value: 20 },
1476
+ { Value: 35 },
1477
+ { Value: 55 },
1478
+ ]
1479
+ };
1480
+ let tmpDataDestinationObject = {};
1481
+
1482
+ // Second derivative: current - 2*previous + two-back
1483
+ let tmpResult = _Parser.solve(
1484
+ 'SecondDeriv = MULTIROWMAP ROWS FROM Rows VAR Current FROM Value OFFSET 0 VAR Prev FROM Value OFFSET -1 DEFAULT 0 VAR TwoBack FROM Value OFFSET -2 DEFAULT 0 : Current - 2 * Prev + TwoBack',
1485
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1486
+
1487
+ Expect(tmpResult).to.be.an('array');
1488
+ Expect(tmpResult.length).to.equal(5);
1489
+ // Row 0: 5 - 2*0 + 0 = 5
1490
+ Expect(tmpResult[0]).to.equal('5');
1491
+ // Row 1: 10 - 2*5 + 0 = 0
1492
+ Expect(tmpResult[1]).to.equal('0');
1493
+ // Row 2: 20 - 2*10 + 5 = 5
1494
+ Expect(tmpResult[2]).to.equal('5');
1495
+ // Row 3: 35 - 2*20 + 10 = 5
1496
+ Expect(tmpResult[3]).to.equal('5');
1497
+ // Row 4: 55 - 2*35 + 20 = 5
1498
+ Expect(tmpResult[4]).to.equal('5');
1499
+
1500
+ return fDone();
1501
+ }
1502
+ );
1503
+ test
1504
+ (
1505
+ 'Test MULTIROWMAP with three-row lookback (OFFSET -3)',
1506
+ (fDone) =>
1507
+ {
1508
+ let testFable = new libFable();
1509
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1510
+ let tmpManifest = testFable.newManyfest();
1511
+
1512
+ let tmpDataSourceObject = {
1513
+ Rows: [
1514
+ { Value: 1 },
1515
+ { Value: 2 },
1516
+ { Value: 4 },
1517
+ { Value: 8 },
1518
+ { Value: 16 },
1519
+ ]
1520
+ };
1521
+ let tmpDataDestinationObject = {};
1522
+
1523
+ // Sum of current + three rows back
1524
+ let tmpResult = _Parser.solve(
1525
+ 'ThreeBackSum = MULTIROWMAP ROWS FROM Rows VAR Current FROM Value VAR ThreeBack FROM Value OFFSET -3 DEFAULT 0 : Current + ThreeBack',
1526
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1527
+
1528
+ Expect(tmpResult).to.be.an('array');
1529
+ Expect(tmpResult.length).to.equal(5);
1530
+ // Row 0: 1 + 0 (default, no row at -3) = 1
1531
+ Expect(tmpResult[0]).to.equal('1');
1532
+ // Row 1: 2 + 0 (default, no row at -2) = 2
1533
+ Expect(tmpResult[1]).to.equal('2');
1534
+ // Row 2: 4 + 0 (default, no row at -1) = 4
1535
+ Expect(tmpResult[2]).to.equal('4');
1536
+ // Row 3: 8 + 1 (row 0) = 9
1537
+ Expect(tmpResult[3]).to.equal('9');
1538
+ // Row 4: 16 + 2 (row 1) = 18
1539
+ Expect(tmpResult[4]).to.equal('18');
1540
+
1541
+ return fDone();
1542
+ }
1543
+ );
1544
+ test
1545
+ (
1546
+ 'Test MULTIROWMAP Fibonacci sequence',
1547
+ (fDone) =>
1548
+ {
1549
+ let testFable = new libFable();
1550
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1551
+ let tmpManifest = testFable.newManyfest();
1552
+
1553
+ // Fibonacci: each row = sum of two previous rows
1554
+ // Seed: first two rows have the starting values
1555
+ let tmpDataSourceObject = {
1556
+ FibRows: [
1557
+ { Fib: 0 },
1558
+ { Fib: 1 },
1559
+ { Fib: 1 },
1560
+ { Fib: 2 },
1561
+ { Fib: 3 },
1562
+ { Fib: 5 },
1563
+ { Fib: 8 },
1564
+ { Fib: 13 },
1565
+ ]
1566
+ };
1567
+ let tmpDataDestinationObject = {};
1568
+
1569
+ // Verify: for each row, check that Current == Prev1 + Prev2 (except seed rows)
1570
+ let tmpResult = _Parser.solve(
1571
+ 'FibCheck = MULTIROWMAP ROWS FROM FibRows VAR Current FROM Fib VAR Prev1 FROM Fib OFFSET -1 DEFAULT 0 VAR Prev2 FROM Fib OFFSET -2 DEFAULT 0 : Current - (Prev1 + Prev2)',
1572
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1573
+
1574
+ Expect(tmpResult).to.be.an('array');
1575
+ Expect(tmpResult.length).to.equal(8);
1576
+ // Row 0: 0 - (0 + 0) = 0 (seed)
1577
+ Expect(tmpResult[0]).to.equal('0');
1578
+ // Row 1: 1 - (0 + 0) = 1 (seed)
1579
+ Expect(tmpResult[1]).to.equal('1');
1580
+ // Row 2 onwards should all be 0 since each is the sum of previous two
1581
+ Expect(tmpResult[2]).to.equal('0');
1582
+ Expect(tmpResult[3]).to.equal('0');
1583
+ Expect(tmpResult[4]).to.equal('0');
1584
+ Expect(tmpResult[5]).to.equal('0');
1585
+ Expect(tmpResult[6]).to.equal('0');
1586
+ Expect(tmpResult[7]).to.equal('0');
1587
+
1588
+ return fDone();
1589
+ }
1590
+ );
1591
+ test
1592
+ (
1593
+ 'Test MULTIROWMAP with custom default values',
1594
+ (fDone) =>
1595
+ {
1596
+ let testFable = new libFable();
1597
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1598
+ let tmpManifest = testFable.newManyfest();
1599
+
1600
+ let tmpDataSourceObject = {
1601
+ Rows: [
1602
+ { Value: 10 },
1603
+ { Value: 20 },
1604
+ { Value: 30 },
1605
+ ]
1606
+ };
1607
+ let tmpDataDestinationObject = {};
1608
+
1609
+ // Use negative default for previous row
1610
+ let tmpResult = _Parser.solve(
1611
+ 'WithDefaults = MULTIROWMAP ROWS FROM Rows VAR Current FROM Value VAR Previous FROM Value OFFSET -1 DEFAULT -1 : Current + Previous',
1612
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1613
+
1614
+ Expect(tmpResult).to.be.an('array');
1615
+ Expect(tmpResult.length).to.equal(3);
1616
+ // Row 0: 10 + (-1) = 9
1617
+ Expect(tmpResult[0]).to.equal('9');
1618
+ // Row 1: 20 + 10 = 30
1619
+ Expect(tmpResult[1]).to.equal('30');
1620
+ // Row 2: 30 + 20 = 50
1621
+ Expect(tmpResult[2]).to.equal('50');
1622
+
1623
+ return fDone();
1624
+ }
1625
+ );
1626
+ test
1627
+ (
1628
+ 'Test MULTIROWMAP with multiple properties from same row offset',
1629
+ (fDone) =>
1630
+ {
1631
+ let testFable = new libFable();
1632
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1633
+ let tmpManifest = testFable.newManyfest();
1634
+
1635
+ let tmpDataSourceObject = {
1636
+ Measurements: [
1637
+ { Width: 10, Height: 5, Depth: 2 },
1638
+ { Width: 20, Height: 10, Depth: 4 },
1639
+ { Width: 30, Height: 15, Depth: 6 },
1640
+ { Width: 40, Height: 20, Depth: 8 },
1641
+ ]
1642
+ };
1643
+ let tmpDataDestinationObject = {};
1644
+
1645
+ // Compare volume change: current volume minus previous volume
1646
+ let tmpResult = _Parser.solve(
1647
+ 'VolumeDelta = MULTIROWMAP ROWS FROM Measurements VAR w FROM Width VAR h FROM Height VAR d FROM Depth VAR pw FROM Width OFFSET -1 DEFAULT 0 VAR ph FROM Height OFFSET -1 DEFAULT 0 VAR pd FROM Depth OFFSET -1 DEFAULT 0 : (w * h * d) - (pw * ph * pd)',
1648
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1649
+
1650
+ Expect(tmpResult).to.be.an('array');
1651
+ Expect(tmpResult.length).to.equal(4);
1652
+ // Row 0: (10*5*2) - (0*0*0) = 100 - 0 = 100
1653
+ Expect(tmpResult[0]).to.equal('100');
1654
+ // Row 1: (20*10*4) - (10*5*2) = 800 - 100 = 700
1655
+ Expect(tmpResult[1]).to.equal('700');
1656
+ // Row 2: (30*15*6) - (20*10*4) = 2700 - 800 = 1900
1657
+ Expect(tmpResult[2]).to.equal('1900');
1658
+ // Row 3: (40*20*8) - (30*15*6) = 6400 - 2700 = 3700
1659
+ Expect(tmpResult[3]).to.equal('3700');
1660
+
1661
+ return fDone();
1662
+ }
1663
+ );
1664
+ test
1665
+ (
1666
+ 'Test MULTIROWMAP with stepIndex variable',
1667
+ (fDone) =>
1668
+ {
1669
+ let testFable = new libFable();
1670
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1671
+ let tmpManifest = testFable.newManyfest();
1672
+
1673
+ let tmpDataSourceObject = {
1674
+ Rows: [
1675
+ { Value: 100 },
1676
+ { Value: 200 },
1677
+ { Value: 300 },
1678
+ ]
1679
+ };
1680
+ let tmpDataDestinationObject = {};
1681
+
1682
+ // Use stepIndex (row index) in the expression
1683
+ let tmpResult = _Parser.solve(
1684
+ 'Indexed = MULTIROWMAP ROWS FROM Rows VAR v FROM Value : v + stepIndex * 1000',
1685
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1686
+
1687
+ Expect(tmpResult).to.be.an('array');
1688
+ Expect(tmpResult.length).to.equal(3);
1689
+ Expect(tmpResult[0]).to.equal('100');
1690
+ Expect(tmpResult[1]).to.equal('1200');
1691
+ Expect(tmpResult[2]).to.equal('2300');
1692
+
1693
+ return fDone();
1694
+ }
1695
+ );
1696
+ test
1697
+ (
1698
+ 'Test MULTIROWMAP moving average',
1699
+ (fDone) =>
1700
+ {
1701
+ let testFable = new libFable();
1702
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1703
+ let tmpManifest = testFable.newManyfest();
1704
+
1705
+ let tmpDataSourceObject = {
1706
+ Prices: [
1707
+ { Close: 100 },
1708
+ { Close: 110 },
1709
+ { Close: 105 },
1710
+ { Close: 115 },
1711
+ { Close: 120 },
1712
+ ]
1713
+ };
1714
+ let tmpDataDestinationObject = {};
1715
+
1716
+ // 3-period moving average: (current + prev + two-back) / 3
1717
+ let tmpResult = _Parser.solve(
1718
+ 'MA3 = MULTIROWMAP ROWS FROM Prices VAR c0 FROM Close VAR c1 FROM Close OFFSET -1 DEFAULT 0 VAR c2 FROM Close OFFSET -2 DEFAULT 0 : (c0 + c1 + c2) / 3',
1719
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1720
+
1721
+ Expect(tmpResult).to.be.an('array');
1722
+ Expect(tmpResult.length).to.equal(5);
1723
+ // Row 2 onwards has three valid values for a true 3-period MA
1724
+ // Row 2: (105 + 110 + 100) / 3 = 105
1725
+ Expect(tmpResult[2]).to.equal('105');
1726
+ // Row 3: (115 + 105 + 110) / 3 = 110
1727
+ Expect(tmpResult[3]).to.equal('110');
1728
+ // Row 4: (120 + 115 + 105) / 3 = 113.33...
1729
+ Expect(Number(tmpResult[4])).to.be.closeTo(113.333, 0.01);
1730
+
1731
+ return fDone();
1732
+ }
1733
+ );
1734
+ test
1735
+ (
1736
+ 'Test MULTIROWMAP returns undefined for invalid rows address',
1737
+ (fDone) =>
1738
+ {
1739
+ let testFable = new libFable();
1740
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1741
+ let tmpManifest = testFable.newManyfest();
1742
+
1743
+ let tmpDataSourceObject = {};
1744
+ let tmpDataDestinationObject = {};
1745
+
1746
+ // No valid rows
1747
+ let tmpResult = _Parser.solve(
1748
+ 'Bad = MULTIROWMAP ROWS FROM NonExistent VAR v FROM Value : v + 1',
1749
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1750
+
1751
+ Expect(tmpResult).to.be.undefined;
1752
+
1753
+ return fDone();
1754
+ }
1755
+ );
1756
+ test
1757
+ (
1758
+ 'Test MULTIROWMAP with forward row lookback (positive OFFSET)',
1759
+ (fDone) =>
1760
+ {
1761
+ let testFable = new libFable();
1762
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1763
+ let tmpManifest = testFable.newManyfest();
1764
+
1765
+ let tmpDataSourceObject = {
1766
+ Rows: [
1767
+ { Value: 10 },
1768
+ { Value: 20 },
1769
+ { Value: 30 },
1770
+ { Value: 40 },
1771
+ { Value: 50 },
1772
+ ]
1773
+ };
1774
+ let tmpDataDestinationObject = {};
1775
+
1776
+ // Look ahead: current value minus next row's value
1777
+ let tmpResult = _Parser.solve(
1778
+ 'ForwardDelta = MULTIROWMAP ROWS FROM Rows VAR Current FROM Value VAR Next FROM Value OFFSET 1 DEFAULT 0 : Next - Current',
1779
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1780
+
1781
+ Expect(tmpResult).to.be.an('array');
1782
+ Expect(tmpResult.length).to.equal(5);
1783
+ // Row 0: 20 - 10 = 10
1784
+ Expect(tmpResult[0]).to.equal('10');
1785
+ // Row 1: 30 - 20 = 10
1786
+ Expect(tmpResult[1]).to.equal('10');
1787
+ // Row 2: 40 - 30 = 10
1788
+ Expect(tmpResult[2]).to.equal('10');
1789
+ // Row 3: 50 - 40 = 10
1790
+ Expect(tmpResult[3]).to.equal('10');
1791
+ // Row 4: 0 (default) - 50 = -50
1792
+ Expect(tmpResult[4]).to.equal('-50');
1793
+
1794
+ return fDone();
1795
+ }
1796
+ );
1797
+ test
1798
+ (
1799
+ 'Test MULTIROWMAP with forward lookback two rows ahead (OFFSET 2)',
1800
+ (fDone) =>
1801
+ {
1802
+ let testFable = new libFable();
1803
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1804
+ let tmpManifest = testFable.newManyfest();
1805
+
1806
+ let tmpDataSourceObject = {
1807
+ Rows: [
1808
+ { Value: 1 },
1809
+ { Value: 2 },
1810
+ { Value: 3 },
1811
+ { Value: 4 },
1812
+ { Value: 5 },
1813
+ ]
1814
+ };
1815
+ let tmpDataDestinationObject = {};
1816
+
1817
+ // Look two ahead
1818
+ let tmpResult = _Parser.solve(
1819
+ 'TwoAhead = MULTIROWMAP ROWS FROM Rows VAR Current FROM Value VAR Future FROM Value OFFSET 2 DEFAULT 99 : Future + Current',
1820
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1821
+
1822
+ Expect(tmpResult).to.be.an('array');
1823
+ Expect(tmpResult.length).to.equal(5);
1824
+ // Row 0: 3 + 1 = 4
1825
+ Expect(tmpResult[0]).to.equal('4');
1826
+ // Row 1: 4 + 2 = 6
1827
+ Expect(tmpResult[1]).to.equal('6');
1828
+ // Row 2: 5 + 3 = 8
1829
+ Expect(tmpResult[2]).to.equal('8');
1830
+ // Row 3: 99 (default) + 4 = 103
1831
+ Expect(tmpResult[3]).to.equal('103');
1832
+ // Row 4: 99 (default) + 5 = 104
1833
+ Expect(tmpResult[4]).to.equal('104');
1834
+
1835
+ return fDone();
1836
+ }
1837
+ );
1838
+ test
1839
+ (
1840
+ 'Test MULTIROWMAP with SERIESSTART to skip initial rows',
1841
+ (fDone) =>
1842
+ {
1843
+ let testFable = new libFable();
1844
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1845
+ let tmpManifest = testFable.newManyfest();
1846
+
1847
+ let tmpDataSourceObject = {
1848
+ Rows: [
1849
+ { Value: 100 },
1850
+ { Value: 200 },
1851
+ { Value: 300 },
1852
+ { Value: 400 },
1853
+ { Value: 500 },
1854
+ ]
1855
+ };
1856
+ let tmpDataDestinationObject = {};
1857
+
1858
+ // Start from row 2
1859
+ let tmpResult = _Parser.solve(
1860
+ 'FromRow2 = MULTIROWMAP ROWS FROM Rows SERIESSTART 2 VAR v FROM Value : v + 0',
1861
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1862
+
1863
+ Expect(tmpResult).to.be.an('array');
1864
+ Expect(tmpResult.length).to.equal(3);
1865
+ Expect(tmpResult[0]).to.equal('300');
1866
+ Expect(tmpResult[1]).to.equal('400');
1867
+ Expect(tmpResult[2]).to.equal('500');
1868
+
1869
+ return fDone();
1870
+ }
1871
+ );
1872
+ test
1873
+ (
1874
+ 'Test MULTIROWMAP with negative SERIESSTART (count from end)',
1875
+ (fDone) =>
1876
+ {
1877
+ let testFable = new libFable();
1878
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1879
+ let tmpManifest = testFable.newManyfest();
1880
+
1881
+ let tmpDataSourceObject = {
1882
+ Rows: [
1883
+ { Value: 10 },
1884
+ { Value: 20 },
1885
+ { Value: 30 },
1886
+ { Value: 40 },
1887
+ { Value: 50 },
1888
+ ]
1889
+ };
1890
+ let tmpDataDestinationObject = {};
1891
+
1892
+ // Start from 2 rows before the end
1893
+ let tmpResult = _Parser.solve(
1894
+ 'LastTwo = MULTIROWMAP ROWS FROM Rows SERIESSTART -2 VAR v FROM Value : v + 0',
1895
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1896
+
1897
+ Expect(tmpResult).to.be.an('array');
1898
+ Expect(tmpResult.length).to.equal(2);
1899
+ Expect(tmpResult[0]).to.equal('40');
1900
+ Expect(tmpResult[1]).to.equal('50');
1901
+
1902
+ return fDone();
1903
+ }
1904
+ );
1905
+ test
1906
+ (
1907
+ 'Test MULTIROWMAP with SERIESSTEP -1 (iterate backwards)',
1908
+ (fDone) =>
1909
+ {
1910
+ let testFable = new libFable();
1911
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1912
+ let tmpManifest = testFable.newManyfest();
1913
+
1914
+ let tmpDataSourceObject = {
1915
+ Rows: [
1916
+ { Value: 10 },
1917
+ { Value: 20 },
1918
+ { Value: 30 },
1919
+ { Value: 40 },
1920
+ { Value: 50 },
1921
+ ]
1922
+ };
1923
+ let tmpDataDestinationObject = {};
1924
+
1925
+ // Iterate backwards from the last row
1926
+ let tmpResult = _Parser.solve(
1927
+ 'Reversed = MULTIROWMAP ROWS FROM Rows SERIESSTART -1 SERIESSTEP -1 VAR v FROM Value : v + 0',
1928
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1929
+
1930
+ Expect(tmpResult).to.be.an('array');
1931
+ Expect(tmpResult.length).to.equal(5);
1932
+ Expect(tmpResult[0]).to.equal('50');
1933
+ Expect(tmpResult[1]).to.equal('40');
1934
+ Expect(tmpResult[2]).to.equal('30');
1935
+ Expect(tmpResult[3]).to.equal('20');
1936
+ Expect(tmpResult[4]).to.equal('10');
1937
+
1938
+ return fDone();
1939
+ }
1940
+ );
1941
+ test
1942
+ (
1943
+ 'Test MULTIROWMAP with SERIESSTEP 2 (skip every other row)',
1944
+ (fDone) =>
1945
+ {
1946
+ let testFable = new libFable();
1947
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1948
+ let tmpManifest = testFable.newManyfest();
1949
+
1950
+ let tmpDataSourceObject = {
1951
+ Rows: [
1952
+ { Value: 10 },
1953
+ { Value: 20 },
1954
+ { Value: 30 },
1955
+ { Value: 40 },
1956
+ { Value: 50 },
1957
+ { Value: 60 },
1958
+ ]
1959
+ };
1960
+ let tmpDataDestinationObject = {};
1961
+
1962
+ // Every other row
1963
+ let tmpResult = _Parser.solve(
1964
+ 'EveryOther = MULTIROWMAP ROWS FROM Rows SERIESSTEP 2 VAR v FROM Value : v + 0',
1965
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
1966
+
1967
+ Expect(tmpResult).to.be.an('array');
1968
+ Expect(tmpResult.length).to.equal(3);
1969
+ Expect(tmpResult[0]).to.equal('10');
1970
+ Expect(tmpResult[1]).to.equal('30');
1971
+ Expect(tmpResult[2]).to.equal('50');
1972
+
1973
+ return fDone();
1974
+ }
1975
+ );
1976
+ test
1977
+ (
1978
+ 'Test MULTIROWMAP backwards with lookback still works',
1979
+ (fDone) =>
1980
+ {
1981
+ let testFable = new libFable();
1982
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
1983
+ let tmpManifest = testFable.newManyfest();
1984
+
1985
+ let tmpDataSourceObject = {
1986
+ Rows: [
1987
+ { Value: 10 },
1988
+ { Value: 20 },
1989
+ { Value: 30 },
1990
+ { Value: 40 },
1991
+ ]
1992
+ };
1993
+ let tmpDataDestinationObject = {};
1994
+
1995
+ // Going backwards, previous row (OFFSET -1) means the row with a LOWER index
1996
+ // which is actually the next row in reverse iteration order
1997
+ let tmpResult = _Parser.solve(
1998
+ 'BackwardsWithLook = MULTIROWMAP ROWS FROM Rows SERIESSTART -1 SERIESSTEP -1 VAR Current FROM Value VAR Prev FROM Value OFFSET -1 DEFAULT 0 : Current - Prev',
1999
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
2000
+
2001
+ Expect(tmpResult).to.be.an('array');
2002
+ Expect(tmpResult.length).to.equal(4);
2003
+ // Row 3 (first in reverse): 40 - 30 = 10
2004
+ Expect(tmpResult[0]).to.equal('10');
2005
+ // Row 2: 30 - 20 = 10
2006
+ Expect(tmpResult[1]).to.equal('10');
2007
+ // Row 1: 20 - 10 = 10
2008
+ Expect(tmpResult[2]).to.equal('10');
2009
+ // Row 0: 10 - 0 (default) = 10
2010
+ Expect(tmpResult[3]).to.equal('10');
2011
+
2012
+ return fDone();
2013
+ }
2014
+ );
2015
+ test
2016
+ (
2017
+ 'Test MULTIROWMAP with SERIESSTART and SERIESSTEP combined',
2018
+ (fDone) =>
2019
+ {
2020
+ let testFable = new libFable();
2021
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
2022
+ let tmpManifest = testFable.newManyfest();
2023
+
2024
+ let tmpDataSourceObject = {
2025
+ Rows: [
2026
+ { Value: 10 },
2027
+ { Value: 20 },
2028
+ { Value: 30 },
2029
+ { Value: 40 },
2030
+ { Value: 50 },
2031
+ { Value: 60 },
2032
+ { Value: 70 },
2033
+ { Value: 80 },
2034
+ ]
2035
+ };
2036
+ let tmpDataDestinationObject = {};
2037
+
2038
+ // Start at row 1, step by 3
2039
+ let tmpResult = _Parser.solve(
2040
+ 'SteppedStart = MULTIROWMAP ROWS FROM Rows SERIESSTART 1 SERIESSTEP 3 VAR v FROM Value : v + 0',
2041
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
2042
+
2043
+ Expect(tmpResult).to.be.an('array');
2044
+ // Rows: 1, 4, 7 => Values: 20, 50, 80
2045
+ Expect(tmpResult.length).to.equal(3);
2046
+ Expect(tmpResult[0]).to.equal('20');
2047
+ Expect(tmpResult[1]).to.equal('50');
2048
+ Expect(tmpResult[2]).to.equal('80');
2049
+
2050
+ return fDone();
2051
+ }
2052
+ );
2053
+ test
2054
+ (
2055
+ 'Test MULTIROWMAP with both forward and backward lookback simultaneously',
2056
+ (fDone) =>
2057
+ {
2058
+ let testFable = new libFable();
2059
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
2060
+ let tmpManifest = testFable.newManyfest();
2061
+
2062
+ let tmpDataSourceObject = {
2063
+ Rows: [
2064
+ { Value: 10 },
2065
+ { Value: 20 },
2066
+ { Value: 30 },
2067
+ { Value: 40 },
2068
+ { Value: 50 },
2069
+ ]
2070
+ };
2071
+ let tmpDataDestinationObject = {};
2072
+
2073
+ // Central difference: (next - previous) / 2
2074
+ let tmpResult = _Parser.solve(
2075
+ 'CentralDiff = MULTIROWMAP ROWS FROM Rows VAR Prev FROM Value OFFSET -1 DEFAULT 0 VAR Next FROM Value OFFSET 1 DEFAULT 0 : (Next - Prev) / 2',
2076
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
2077
+
2078
+ Expect(tmpResult).to.be.an('array');
2079
+ Expect(tmpResult.length).to.equal(5);
2080
+ // Row 0: (20 - 0) / 2 = 10
2081
+ Expect(tmpResult[0]).to.equal('10');
2082
+ // Row 1: (30 - 10) / 2 = 10
2083
+ Expect(tmpResult[1]).to.equal('10');
2084
+ // Row 2: (40 - 20) / 2 = 10
2085
+ Expect(tmpResult[2]).to.equal('10');
2086
+ // Row 3: (50 - 30) / 2 = 10
2087
+ Expect(tmpResult[3]).to.equal('10');
2088
+ // Row 4: (0 - 40) / 2 = -20
2089
+ Expect(tmpResult[4]).to.equal('-20');
2090
+
2091
+ return fDone();
2092
+ }
2093
+ );
2094
+ test
2095
+ (
2096
+ 'Test MULTIROWMAP with rowIndex and stepIndex variables',
2097
+ (fDone) =>
2098
+ {
2099
+ let testFable = new libFable();
2100
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
2101
+ let tmpManifest = testFable.newManyfest();
2102
+
2103
+ let tmpDataSourceObject = {
2104
+ Rows: [
2105
+ { Value: 100 },
2106
+ { Value: 200 },
2107
+ { Value: 300 },
2108
+ { Value: 400 },
2109
+ { Value: 500 },
2110
+ ]
2111
+ };
2112
+ let tmpDataDestinationObject = {};
2113
+
2114
+ // With SERIESSTART, rowIndex is the actual array index, stepIndex is the iteration count
2115
+ let tmpResult = _Parser.solve(
2116
+ 'IndexTest = MULTIROWMAP ROWS FROM Rows SERIESSTART 2 VAR v FROM Value : v + rowIndex * 1000 + stepIndex',
2117
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
2118
+
2119
+ Expect(tmpResult).to.be.an('array');
2120
+ Expect(tmpResult.length).to.equal(3);
2121
+ // stepIndex=0, rowIndex=2: 300 + 2000 + 0 = 2300
2122
+ Expect(tmpResult[0]).to.equal('2300');
2123
+ // stepIndex=1, rowIndex=3: 400 + 3000 + 1 = 3401
2124
+ Expect(tmpResult[1]).to.equal('3401');
2125
+ // stepIndex=2, rowIndex=4: 500 + 4000 + 2 = 4502
2126
+ Expect(tmpResult[2]).to.equal('4502');
2127
+
2128
+ return fDone();
2129
+ }
2130
+ );
2131
+ test
2132
+ (
2133
+ 'Test MULTIROWMAP SERIESSTEP zero returns undefined',
2134
+ (fDone) =>
2135
+ {
2136
+ let testFable = new libFable();
2137
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
2138
+ let tmpManifest = testFable.newManyfest();
2139
+
2140
+ let tmpDataSourceObject = {
2141
+ Rows: [{ Value: 1 }]
2142
+ };
2143
+ let tmpDataDestinationObject = {};
2144
+
2145
+ let tmpResult = _Parser.solve(
2146
+ 'BadStep = MULTIROWMAP ROWS FROM Rows SERIESSTEP 0 VAR v FROM Value : v + 0',
2147
+ tmpDataSourceObject, {}, tmpManifest, tmpDataDestinationObject);
2148
+
2149
+ Expect(tmpResult).to.be.undefined;
2150
+
2151
+ return fDone();
2152
+ }
2153
+ );
2154
+ }
2155
+ );
1387
2156
  }
1388
2157
  );