From 2368e6da5e46d71f96995a082d785b76a4bb6395 Mon Sep 17 00:00:00 2001 From: Alexander Schenkel Date: Sat, 24 Aug 2024 17:58:26 +0200 Subject: [PATCH] Adding unit tests for all readme examples --- README.md | 54 ++++++++--- package.json | 1 + spec/specs/readmeDemosSpec.js | 167 ++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 11 deletions(-) create mode 100644 spec/specs/readmeDemosSpec.js diff --git a/README.md b/README.md index 77c71f7..dc3bba8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,35 @@ One can then provide values for all unknown variables / functions and evaluate a For an example application, see https://fparser.alexi.ch/. +- [Features](#features) +- [Usage](#usage) +- [More options](#more-options) + - [Using multiple variables](#using-multiple-variables) + - [Using named variables](#using-named-variables) + - [Using named object path variables](#using-named-object-path-variables) + - [Using user-defined functions](#using-user-defined-functions) + - [Using strings](#using-strings) + - [Using logical operators](#using-logical-operators) + - [Conditional evaluation](#conditional-evaluation) + - [Re-use a Formula object](#re-use-a-formula-object) + - [Memoization](#memoization) + - [Blacklisted functions](#blacklisted-functions) + - [Get all used variables](#get-all-used-variables) + - [Get the parsed formula string](#get-the-parsed-formula-string) +- [Changelog](#changelog) + - [3.1.0](#anchor-310) + - [3.0.1](#anchor-301) + - [3.0.0](#anchor-300) + - [2.1.0](#anchor-210) + - [2.0.2](#anchor-202) + - [2.0.0](#anchor-200) + - [1.4.0](#anchor-140) + - [1.3.0](#anchor-130) +- [Contributors](#contributors) +- [TODOs, Whishlist](#todos-whishlist) +- [License](#license) + + ## Features Parses a mathematical formula from a string. Known expressions: @@ -127,7 +156,7 @@ const fObj = new Formula('sin(inverse(x))'); //Define the function(s) on the Formula object, then use it multiple times: fObj.inverse = (value) => 1/value; -let results = fObj.evaluate({x: 1,x:2,x:3}); +let results = fObj.evaluate([{ x: 1 }, { x: 2 }, { x: 3 }]); // Or pass it in the value object, and OVERRIDE an existing function: let result = fObj.evaluate({ @@ -135,22 +164,23 @@ let result = fObj.evaluate({ inverse: (value) => (-1*value) }); -If defined in the value object AND on the formula object, the Value object has the precedence ``` +If the function is defined in the value object AND on the formula object, the Value object has precedence + Functions also support the object path syntax: ```javascript // in an evaluate() value object: -const fObj = new Formula('sin(lib.inverse(x))'); +const fObj = new Formula('sin(lib.inverse([lib.x]))'); const res = fObj.evaluate({ - lib: { inverse: (value) => 1/value } + lib: { inverse: (value) => 1 / value, x: Math.PI } }); // or set it on the Formula instance: const fObj2 = new Formula('sin(lib.inverse(x))'); -fObj2.lib = { inverse: (value) => 1/value }; -const res2 = fObj.evaluate(); +fObj2.lib = { inverse: (value) => 1 / value }; +const res2 = fObj2.evaluate({ x: Math.PI }); ``` ### Using strings @@ -176,7 +206,7 @@ let result = fObj.evaluate({ var1: 'FooBar', longer: (s1, s2) => s1.length > s2. // --> 14 ``` -### Using of logical operators +### Using logical operators Logical operators allow for conditional logic. The result of the evaluation is always `0` (expression is false) or `1` (expression is true). @@ -185,7 +215,7 @@ Example: Calculate a percentage value based on a variable `x`, but only if `x` is between 0 and 1: ```javascript -const fObj = new Formula('x >= 0 * x <= 1 * x * 100'); +const fObj = new Formula('(x >= 0) * (x <= 1) * x * 100'); let result = fObj.evaluate([{ x: 0.5 }, { x: 0.7 }, { x: 1.5 }, { x: -0.5 }, { x: -1.7 }]); // --> [50, 70, 0, 0, 0] ``` @@ -199,6 +229,8 @@ let result = fObj.evaluate([{ x: 0.5 }, { x: 0.7 }, { x: 1.5 }, { x: -0.5 }, { // --> [50, 70, 0, 0, 0] ``` +**NOTE**: Logical operators have the LEAST precedence: `3 > 1 + 4 < 2` is evaluated as `3 > (1+4) < 2`. + ### Conditional evaluation The previous chapter introduced logical operators. This can be used to implement a conditional function, or `if` function: @@ -295,9 +327,9 @@ Now, `evaluate` in your formula uses your own version of this function. ### Get all used variables ```javascript -// Get all used variables in the order of their appereance: +// Get all used variables in the order of their appearance: const f4 = new Formula('x*sin(PI*y) + y / (2-x*[var1]) + [var2]'); -console.log(f4.getVariables()); // ['x','y','var1','var2'] +console.log(f4.getVariables()); // ['x','PI','y','var1','var2'] ``` ### Get the parsed formula string @@ -305,7 +337,7 @@ console.log(f4.getVariables()); // ['x','y','var1','var2'] After parsing, get the formula string as parsed: ```javascript -// Get all used variables in the order of their appereance: +// Get all used variables in the order of their appearance: const f = new Formula('x * ( y + 9 )'); console.log(f.getExpressionString()); // 'x * (y + 9)' ``` diff --git a/package.json b/package.json index a88922c..fd94601 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "docker-demopage": "docker run --rm -ti -v \"$PWD\":/usr/src/app -w /usr/src/app/demopage -p 3000:8080 fparser npm run dev", "docker-test": "docker run --rm -ti -v \"$PWD\":/usr/src/app -w /usr/src/app fparse npm run test", "build-dev": "NODE_ENV=development tsc --noEmit && vite build --mode=development && tsc --emitDeclarationOnly --declaration", + "prepublishOnly": "npm run build", "build": "NODE_ENV=production tsc --noEmit && vite build --minify --mode=production && tsc --emitDeclarationOnly --declaration", "build-demopage-image": "docker build --pull -t fparser-demopage .", "test": "NODE_ENV=development npm run build-dev && jasmine && karma start" diff --git a/spec/specs/readmeDemosSpec.js b/spec/specs/readmeDemosSpec.js new file mode 100644 index 0000000..0f118b8 --- /dev/null +++ b/spec/specs/readmeDemosSpec.js @@ -0,0 +1,167 @@ +import Formula from '../../dist/fparser.js'; + +describe('Demos from the readme', function () { + it('using multiple variables', () => { + const fObj = new Formula('a*x^2 + b*x + c'); + + // Just pass a value object containing a value for each unknown variable: + let result = fObj.evaluate({ a: 2, b: -1, c: 3, x: 3 }); // result = 18 + expect(result).toEqual(18); + }); + + it('Using named variables', () => { + const fObj = new Formula('2*[var1] + sin([var2]+PI)'); + + // Just pass a value object containing a value for each named variable: + let result = fObj.evaluate({ var1: 5, var2: 0.7 }); + expect(result).toBeCloseTo(2 * 5 + Math.sin(0.7 + Math.PI), 6); + }); + + describe('Using named object path variables', () => { + it('Example 1', () => { + const fObj = new Formula('2*[var1.propertyA] + 3*[var2.propertyB.propertyC]'); + + // Just pass a value object containing a value for each named variable: + let result = fObj.evaluate({ var1: { propertyA: 3 }, var2: { propertyB: { propertyC: 9 } } }); + expect(result).toEqual(2 * 3 + 3 * 9); + }); + it('Example 2', () => { + // var2.propertyB is an array, so we can use an index for the 3rd entry of propertyB: + const fObj = new Formula('2*[var1.propertyA] + 3*[var2.propertyB.2]'); + let result = fObj.evaluate({ var1: { propertyA: 3 }, var2: { propertyB: [2, 4, 6] } }); + expect(result).toEqual(2 * 3 + 3 * 6); + }); + }); + + describe('Using user-defined functions', () => { + it('Example 1', () => { + const fObj = new Formula('sin(inverse(x))'); + + //Define the function(s) on the Formula object, then use it multiple times: + fObj.inverse = (value) => 1 / value; + let results = fObj.evaluate([{ x: 1 }, { x: 2 }, { x: 3 }]); + expect(results).toEqual([Math.sin(fObj.inverse(1)), Math.sin(fObj.inverse(2)), Math.sin(fObj.inverse(3))]); + + // Or pass it in the value object, and OVERRIDE an existing function: + let result = fObj.evaluate({ + x: 2 / Math.PI, + inverse: (value) => -1 * value + }); + expect(result).toEqual(Math.sin(-1 * (2 / Math.PI))); + }); + + it('Example 2', () => { + // in an evaluate() value object: + const fObj = new Formula('sin(lib.inverse([lib.x]))'); + let res = fObj.evaluate({ + lib: { inverse: (value) => 1 / value, x: Math.PI } + }); + expect(res).toEqual(Math.sin(1 / Math.PI)); + + // or set it on the Formula instance: + const fObj2 = new Formula('sin(lib.inverse(x))'); + fObj2.lib = { inverse: (value) => 1 / value }; + const res2 = fObj2.evaluate({ x: Math.PI }); + expect(res2).toEqual(Math.sin(1 / Math.PI)); + }); + }); + + describe('Using strings', () => { + it('Example 1', () => { + const fObj = new Formula('concat([var1], "Bar")'); + let result = fObj.evaluate({ var1: 'Foo', concat: (s1, s2) => s1 + s2 }); + expect(result).toEqual('FooBar'); + }); + + it('Example 2', () => { + const fObj = new Formula('20 - longer([var1], "Bar")'); + let result = fObj.evaluate({ + var1: 'FooBar', + longer: (s1, s2) => (s1.length > s2.length ? s1.length : s2.length) + }); + expect(result).toEqual(14); + }); + }); + + describe('Using logical operators', () => { + it('Example 1', () => { + const fObj = new Formula('(x >= 0) * (x <= 1) * x * 100'); + let result = fObj.evaluate([{ x: 0.5 }, { x: 0.7 }, { x: 1.5 }, { x: -0.5 }, { x: -1.7 }]); + expect(result).toEqual([50, 70, 0, -0, -0]); + }); + + it('Example 2', () => { + const fObj = new Formula('withinOne(x) * 100'); + fObj.withinOne = (x) => (x >= 0 && x <= 1 ? x : 0); + let result = fObj.evaluate([{ x: 0.5 }, { x: 0.7 }, { x: 1.5 }, { x: -0.5 }, { x: -1.7 }]); + expect(result).toEqual([50, 70, 0, 0, 0]); + }); + }); + + describe('Conditional evaluation', () => { + it('Example 1', () => { + const fObj = new Formula('ifElse([age] < 18, [price]*0.5, [price])'); + fObj.ifElse = (predicate, trueValue, falseValue) => (predicate ? trueValue : falseValue); + const res = fObj.evaluate([ + { price: 100, age: 17 }, + { price: 100, age: 20 } + ]); + expect(res).toEqual([50, 100]); + }); + }); + + describe('Re-use a Formula object', () => { + it('Example 1', () => { + const fObj = new Formula(); + // ... + fObj.setFormula('2*x^2 + 5*x + 3'); + let res = fObj.evaluate({ x: 3 }); + expect(res).toEqual(2 * Math.pow(3, 2) + 5 * 3 + 3); + // ... + fObj.setFormula('x*y'); + res = fObj.evaluate({ x: 2, y: 4 }); + expect(res).toEqual(2 * 4); + }); + }); + + describe('Memoization', () => { + it('Example 1', () => { + const fObj = new Formula('x * y', { memoization: true }); + let res1 = fObj.evaluate({ x: 2, y: 3 }); // 6, evaluated by calculating x*y + expect(res1).toEqual(2 * 3); + let res2 = fObj.evaluate({ x: 2, y: 3 }); // 6, from memory + expect(res2).toEqual(2 * 3); + }); + }); + + describe('Blacklisted Functions', () => { + it('Example 1', () => { + // Internal functions cannot be used in formulas: + let fObj = new Formula('evaluate(x)'); + expect(() => fObj.evaluate({ x: 5 })).toThrow(); + + // This also counts for function aliases / references to internal functions: + fObj = new Formula('ex(x)'); + fObj.ex = fObj.evaluate; + expect(() => fObj.evaluate({ x: 5 })).toThrow(); + }); + }); + + describe('Get all used variables', () => { + it('Example 1', () => { + // Get all used variables in the order of their appearance: + const f4 = new Formula('x*sin(PI*y) + y / (2-x*[var1]) + [var2]'); + let res = f4.getVariables(); + expect(res).toEqual(['x', 'PI', 'y', 'var1', 'var2']); + }); + }); + + describe('Get the parsed formula string', () => { + it('Example 1', () => { + // Get all used variables in the order of their appearance: + const f = new Formula('x * ( y + 9 )'); + let res = f.getExpressionString(); + expect(res).toEqual('x * (y + 9)'); + }); + }); +});