fable 3.1.63 → 3.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/services/expression-parser-functions/README.md +9 -0
- package/docs/services/expression-parser-functions/beziercurvefit.md +57 -0
- package/docs/services/expression-parser-functions/bezierpoint.md +55 -0
- package/docs/services/expression-parser-functions/intercept.md +59 -0
- package/docs/services/expression-parser-functions/setvalue.md +55 -0
- package/docs/services/expression-parser-functions/slope.md +51 -0
- package/docs/services/expression-parser-functions/ternary.md +180 -0
- package/docs/services/expression-parser.md +131 -0
- package/example_applications/mathematical_playground/AppData.json +36 -1
- package/example_applications/mathematical_playground/Math-Solver-Harness.js +215 -0
- package/example_applications/mathematical_playground/TestSuite-AcidTest.json +178 -0
- package/example_applications/mathematical_playground/TestSuite-Identities.json +147 -0
- package/example_applications/mathematical_playground/TestSuite-Precision.json +127 -0
- package/example_applications/mathematical_playground/TestSuite-Spreadsheet.json +198 -0
- package/package.json +3 -2
- package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-ExpressionTokenizer-DirectiveMutation.js +299 -2
- package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-ExpressionTokenizer.js +1 -1
- package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-FunctionMap.json +5 -4
- package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-TokenMap.json +65 -1
- package/source/services/Fable-Service-ExpressionParser.js +144 -0
- package/source/services/Fable-Service-Logic.js +33 -0
- package/source/services/Fable-Service-Math.js +78 -0
- package/test/ExpressionParser_tests.js +1059 -0
|
@@ -112,6 +112,7 @@ class FableServiceExpressionParser extends libFableServiceBase
|
|
|
112
112
|
|
|
113
113
|
// Wire each sub service into this instance of the solver.
|
|
114
114
|
this.Tokenizer.connectExpressionParser(this);
|
|
115
|
+
this.Tokenizer.TokenizerDirectiveMutation.connectExpressionParser(this);
|
|
115
116
|
this.Linter.connectExpressionParser(this);
|
|
116
117
|
this.Postfix.connectExpressionParser(this);
|
|
117
118
|
this.ValueMarshal.connectExpressionParser(this);
|
|
@@ -456,6 +457,149 @@ class FableServiceExpressionParser extends libFableServiceBase
|
|
|
456
457
|
|
|
457
458
|
return tmpValueArray;
|
|
458
459
|
}
|
|
460
|
+
else if (tmpResultsObject.SolverDirectives.Code == 'MULTIROWMAP')
|
|
461
|
+
{
|
|
462
|
+
// MULTIROWMAP iterates over an array of objects (rows), allowing each expression to reference
|
|
463
|
+
// the current row and any number of previous/future rows via configurable offsets.
|
|
464
|
+
// Optional SERIESSTART and SERIESSTEP control which rows are iterated and in what direction.
|
|
465
|
+
const tmpDirectiveValues = tmpResultsObject.SolverDirectives.Values;
|
|
466
|
+
const tmpDirectiveValueKeys = tmpResultsObject.SolverDirectives.ValueKeys;
|
|
467
|
+
const tmpRowsAddress = tmpResultsObject.SolverDirectives.RowsAddress;
|
|
468
|
+
let tmpValueArray = [];
|
|
469
|
+
|
|
470
|
+
// Resolve the rows array from the data source
|
|
471
|
+
let tmpRows = null;
|
|
472
|
+
if (tmpRowsAddress)
|
|
473
|
+
{
|
|
474
|
+
tmpRows = tmpManifest.getValueByHash(tmpDataSourceObject, tmpRowsAddress);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (!Array.isArray(tmpRows) || tmpRows.length < 1)
|
|
478
|
+
{
|
|
479
|
+
tmpResultsObject.ExpressionParserLog.push(`ExpressionParser.solve detected invalid MULTIROWMAP directive parameters. ROWS FROM address must resolve to a non-empty array.`);
|
|
480
|
+
this.log.warn(tmpResultsObject.ExpressionParserLog[tmpResultsObject.ExpressionParserLog.length-1]);
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Resolve SeriesStart: default to 0 (first row). Negative values count from end.
|
|
485
|
+
let tmpSeriesStart = 0;
|
|
486
|
+
if (tmpResultsObject.SolverDirectives.SeriesStart !== null)
|
|
487
|
+
{
|
|
488
|
+
tmpSeriesStart = parseInt(tmpResultsObject.SolverDirectives.SeriesStart);
|
|
489
|
+
if (isNaN(tmpSeriesStart))
|
|
490
|
+
{
|
|
491
|
+
// Try to resolve as a variable from the data source
|
|
492
|
+
let tmpStartToken = this.fable.ExpressionParser.Postfix.getTokenContainerObject(tmpResultsObject.SolverDirectives.SeriesStart, 'Token.Symbol');
|
|
493
|
+
this.substituteValuesInTokenizedObjects([tmpStartToken], tmpDataSourceObject, tmpResultsObject, tmpManifest);
|
|
494
|
+
if (tmpStartToken.Resolved)
|
|
495
|
+
{
|
|
496
|
+
tmpSeriesStart = parseInt(tmpStartToken.Value);
|
|
497
|
+
}
|
|
498
|
+
if (isNaN(tmpSeriesStart))
|
|
499
|
+
{
|
|
500
|
+
tmpSeriesStart = 0;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Handle negative start (count from end)
|
|
505
|
+
if (tmpSeriesStart < 0)
|
|
506
|
+
{
|
|
507
|
+
tmpSeriesStart = tmpRows.length + tmpSeriesStart;
|
|
508
|
+
}
|
|
509
|
+
// Clamp to valid range
|
|
510
|
+
tmpSeriesStart = Math.max(0, Math.min(tmpSeriesStart, tmpRows.length - 1));
|
|
511
|
+
|
|
512
|
+
// Resolve SeriesStep: default to 1
|
|
513
|
+
let tmpSeriesStep = 1;
|
|
514
|
+
if (tmpResultsObject.SolverDirectives.SeriesStep !== null)
|
|
515
|
+
{
|
|
516
|
+
tmpSeriesStep = parseInt(tmpResultsObject.SolverDirectives.SeriesStep);
|
|
517
|
+
if (isNaN(tmpSeriesStep))
|
|
518
|
+
{
|
|
519
|
+
// Try to resolve as a variable from the data source
|
|
520
|
+
let tmpStepToken = this.fable.ExpressionParser.Postfix.getTokenContainerObject(tmpResultsObject.SolverDirectives.SeriesStep, 'Token.Symbol');
|
|
521
|
+
this.substituteValuesInTokenizedObjects([tmpStepToken], tmpDataSourceObject, tmpResultsObject, tmpManifest);
|
|
522
|
+
if (tmpStepToken.Resolved)
|
|
523
|
+
{
|
|
524
|
+
tmpSeriesStep = parseInt(tmpStepToken.Value);
|
|
525
|
+
}
|
|
526
|
+
if (isNaN(tmpSeriesStep))
|
|
527
|
+
{
|
|
528
|
+
tmpSeriesStep = 1;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (tmpSeriesStep === 0)
|
|
533
|
+
{
|
|
534
|
+
tmpResultsObject.ExpressionParserLog.push(`ExpressionParser.solve detected invalid MULTIROWMAP directive parameters. SERIESSTEP cannot be zero.`);
|
|
535
|
+
this.log.warn(tmpResultsObject.ExpressionParserLog[tmpResultsObject.ExpressionParserLog.length-1]);
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
let tmpStepCounter = 0;
|
|
540
|
+
for (let i = tmpSeriesStart; (tmpSeriesStep > 0) ? (i < tmpRows.length) : (i >= 0); i += tmpSeriesStep)
|
|
541
|
+
{
|
|
542
|
+
// Build the data source for this iteration with the mapped variables
|
|
543
|
+
let tmpRowStepDataSourceObject = Object.assign({}, tmpDataSourceObject);
|
|
544
|
+
tmpRowStepDataSourceObject.stepIndex = tmpStepCounter;
|
|
545
|
+
tmpRowStepDataSourceObject.rowIndex = i;
|
|
546
|
+
|
|
547
|
+
for (let j = 0; j < tmpDirectiveValueKeys.length; j++)
|
|
548
|
+
{
|
|
549
|
+
const tmpVariableKey = tmpDirectiveValueKeys[j];
|
|
550
|
+
const tmpVariableDescription = tmpDirectiveValues[tmpVariableKey];
|
|
551
|
+
const tmpTargetRowIndex = i + tmpVariableDescription.RowOffset;
|
|
552
|
+
|
|
553
|
+
// Check if the target row index is within bounds
|
|
554
|
+
if (tmpTargetRowIndex < 0 || tmpTargetRowIndex >= tmpRows.length)
|
|
555
|
+
{
|
|
556
|
+
// Out of bounds -- use the default value
|
|
557
|
+
tmpRowStepDataSourceObject[tmpVariableKey] = tmpVariableDescription.Default;
|
|
558
|
+
}
|
|
559
|
+
else
|
|
560
|
+
{
|
|
561
|
+
const tmpTargetRow = tmpRows[tmpTargetRowIndex];
|
|
562
|
+
if (tmpVariableDescription.Property === null)
|
|
563
|
+
{
|
|
564
|
+
// No property specified, use the whole row
|
|
565
|
+
tmpRowStepDataSourceObject[tmpVariableKey] = tmpTargetRow;
|
|
566
|
+
}
|
|
567
|
+
else
|
|
568
|
+
{
|
|
569
|
+
// Look up the property on the target row
|
|
570
|
+
let tmpValue = tmpManifest.getValueByHash(tmpTargetRow, tmpVariableDescription.Property);
|
|
571
|
+
if (tmpValue == null)
|
|
572
|
+
{
|
|
573
|
+
tmpValue = tmpVariableDescription.Default;
|
|
574
|
+
}
|
|
575
|
+
tmpRowStepDataSourceObject[tmpVariableKey] = tmpValue;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
let tmpMutatedValues = this.substituteValuesInTokenizedObjects(tmpResultsObject.PostfixTokenObjects, tmpRowStepDataSourceObject, tmpResultsObject, tmpManifest);
|
|
581
|
+
|
|
582
|
+
tmpValueArray.push( this.solvePostfixedExpression( tmpResultsObject.PostfixSolveList, tmpDataDestinationObject, tmpResultsObject, tmpManifest) );
|
|
583
|
+
|
|
584
|
+
for (let j = 0; j < tmpMutatedValues.length; j++)
|
|
585
|
+
{
|
|
586
|
+
tmpMutatedValues[j].Resolved = false;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
tmpStepCounter++;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Do the assignment
|
|
593
|
+
let tmpAssignmentManifestHash = tmpResultsObject.PostfixedAssignmentAddress;
|
|
594
|
+
if ((tmpResultsObject.OriginalRawTokens[1] === '=') && (typeof(tmpResultsObject.OriginalRawTokens[0]) === 'string') && (tmpResultsObject.OriginalRawTokens[0].length > 0))
|
|
595
|
+
{
|
|
596
|
+
tmpAssignmentManifestHash = tmpResultsObject.OriginalRawTokens[0];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
tmpManifest.setValueByHash(tmpDataDestinationObject, tmpAssignmentManifestHash, tmpValueArray);
|
|
600
|
+
|
|
601
|
+
return tmpValueArray;
|
|
602
|
+
}
|
|
459
603
|
else if (tmpResultsObject.SolverDirectives.Code == 'MONTECARLO')
|
|
460
604
|
{
|
|
461
605
|
const [ tmpSampleCount ] = this._prepareDirectiveParameters([
|
|
@@ -126,6 +126,39 @@ class FableServiceLogic extends libFableServiceBase
|
|
|
126
126
|
}
|
|
127
127
|
return pOnTrue;
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Numeric-aware ternary selection for the expression parser.
|
|
132
|
+
* Treats 0, "0", "", null, undefined, false, NaN, empty arrays, and empty objects as falsy.
|
|
133
|
+
* Used by the ternary operator desugaring (condition ? trueVal :: falseVal).
|
|
134
|
+
*
|
|
135
|
+
* @param {any} pCondition - The condition to evaluate
|
|
136
|
+
* @param {any} pOnTrue - The value to return if the condition is truthy
|
|
137
|
+
* @param {any} [pOnFalse = ''] - The value to return if the condition is falsy
|
|
138
|
+
* @return {any} - The selected value
|
|
139
|
+
*/
|
|
140
|
+
ternary(pCondition, pOnTrue, pOnFalse = '')
|
|
141
|
+
{
|
|
142
|
+
// Standard JS falsy check
|
|
143
|
+
if (!pCondition)
|
|
144
|
+
{
|
|
145
|
+
return pOnFalse;
|
|
146
|
+
}
|
|
147
|
+
// Numeric zero as a string (from comparison operators returning "0")
|
|
148
|
+
if (pCondition === '0')
|
|
149
|
+
{
|
|
150
|
+
return pOnFalse;
|
|
151
|
+
}
|
|
152
|
+
if (Array.isArray(pCondition) && pCondition.length < 1)
|
|
153
|
+
{
|
|
154
|
+
return pOnFalse;
|
|
155
|
+
}
|
|
156
|
+
if (typeof pCondition === 'object' && Object.keys(pCondition).length < 1)
|
|
157
|
+
{
|
|
158
|
+
return pOnFalse;
|
|
159
|
+
}
|
|
160
|
+
return pOnTrue;
|
|
161
|
+
}
|
|
129
162
|
}
|
|
130
163
|
|
|
131
164
|
module.exports = FableServiceLogic;
|
|
@@ -460,6 +460,84 @@ class FableServiceMath extends libFableServiceBase
|
|
|
460
460
|
return tmpLeftArbitraryValue.lte(tmpRightValue);
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Comparison operator: returns "1" if left > right, "0" otherwise.
|
|
465
|
+
* For use as an infix operator in the expression parser.
|
|
466
|
+
*
|
|
467
|
+
* @param {number|string} pLeftValue - The left value to compare.
|
|
468
|
+
* @param {number|string} pRightValue - The right value to compare.
|
|
469
|
+
* @returns {string} - "1" if left > right, "0" otherwise.
|
|
470
|
+
*/
|
|
471
|
+
greaterThanOperator(pLeftValue, pRightValue)
|
|
472
|
+
{
|
|
473
|
+
return this.gtPrecise(pLeftValue, pRightValue) ? '1' : '0';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Comparison operator: returns "1" if left >= right, "0" otherwise.
|
|
478
|
+
* For use as an infix operator in the expression parser.
|
|
479
|
+
*
|
|
480
|
+
* @param {number|string} pLeftValue - The left value to compare.
|
|
481
|
+
* @param {number|string} pRightValue - The right value to compare.
|
|
482
|
+
* @returns {string} - "1" if left >= right, "0" otherwise.
|
|
483
|
+
*/
|
|
484
|
+
greaterThanOrEqualOperator(pLeftValue, pRightValue)
|
|
485
|
+
{
|
|
486
|
+
return this.gtePrecise(pLeftValue, pRightValue) ? '1' : '0';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Comparison operator: returns "1" if left < right, "0" otherwise.
|
|
491
|
+
* For use as an infix operator in the expression parser.
|
|
492
|
+
*
|
|
493
|
+
* @param {number|string} pLeftValue - The left value to compare.
|
|
494
|
+
* @param {number|string} pRightValue - The right value to compare.
|
|
495
|
+
* @returns {string} - "1" if left < right, "0" otherwise.
|
|
496
|
+
*/
|
|
497
|
+
lessThanOperator(pLeftValue, pRightValue)
|
|
498
|
+
{
|
|
499
|
+
return this.ltPrecise(pLeftValue, pRightValue) ? '1' : '0';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Comparison operator: returns "1" if left <= right, "0" otherwise.
|
|
504
|
+
* For use as an infix operator in the expression parser.
|
|
505
|
+
*
|
|
506
|
+
* @param {number|string} pLeftValue - The left value to compare.
|
|
507
|
+
* @param {number|string} pRightValue - The right value to compare.
|
|
508
|
+
* @returns {string} - "1" if left <= right, "0" otherwise.
|
|
509
|
+
*/
|
|
510
|
+
lessThanOrEqualOperator(pLeftValue, pRightValue)
|
|
511
|
+
{
|
|
512
|
+
return this.ltePrecise(pLeftValue, pRightValue) ? '1' : '0';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Comparison operator: returns "1" if left == right, "0" otherwise.
|
|
517
|
+
* For use as an infix operator in the expression parser.
|
|
518
|
+
*
|
|
519
|
+
* @param {number|string} pLeftValue - The left value to compare.
|
|
520
|
+
* @param {number|string} pRightValue - The right value to compare.
|
|
521
|
+
* @returns {string} - "1" if left == right, "0" otherwise.
|
|
522
|
+
*/
|
|
523
|
+
equalOperator(pLeftValue, pRightValue)
|
|
524
|
+
{
|
|
525
|
+
return this.comparePrecise(pLeftValue, pRightValue) == 0 ? '1' : '0';
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Comparison operator: returns "1" if left != right, "0" otherwise.
|
|
530
|
+
* For use as an infix operator in the expression parser.
|
|
531
|
+
*
|
|
532
|
+
* @param {number|string} pLeftValue - The left value to compare.
|
|
533
|
+
* @param {number|string} pRightValue - The right value to compare.
|
|
534
|
+
* @returns {string} - "1" if left != right, "0" otherwise.
|
|
535
|
+
*/
|
|
536
|
+
notEqualOperator(pLeftValue, pRightValue)
|
|
537
|
+
{
|
|
538
|
+
return this.comparePrecise(pLeftValue, pRightValue) != 0 ? '1' : '0';
|
|
539
|
+
}
|
|
540
|
+
|
|
463
541
|
/**
|
|
464
542
|
* Converts degrees to radians with arbitrary precision.
|
|
465
543
|
*
|