Skip to content

Commit

Permalink
Adding unit tests for all readme examples
Browse files Browse the repository at this point in the history
  • Loading branch information
bylexus committed Aug 24, 2024
1 parent 248bffa commit 2368e6d
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 11 deletions.
54 changes: 43 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -127,30 +156,31 @@ 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({
x: 2/Math.PI,
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
Expand All @@ -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).

Expand All @@ -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]
```
Expand All @@ -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:
Expand Down Expand Up @@ -295,17 +327,17 @@ 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

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)'
```
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
167 changes: 167 additions & 0 deletions spec/specs/readmeDemosSpec.js
Original file line number Diff line number Diff line change
@@ -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)');
});
});
});

0 comments on commit 2368e6d

Please sign in to comment.