/TSExpressionEngine

Typescript Math Toolkit Expression Engine

Primary LanguageTypeScriptMIT LicenseMIT

Typescript Math Toolkit Expression Engine

The Typescript function parser provided elsewhere in my repos dates back to some old C++ code. A JS version, btw, is also available in the Javascript Math Toolkit. In 2016, I had need to parse and evaluate more general expressions across a mix of numeric, string, and boolean values. So, I stared working on an extension of the function parser code base.

The TSMT expression engine allows general expressions to be parsed and then evaluated with either numeric, string, and/or boolean values for the independent variables. The expression returns either a number, string, or boolean (although the latter is by far the most common).

My initial use case for an expression engine was a lightweight rules engine. Expressions may also be used to dynamically evaluate what parts of a UI to show, for example, based on either formulas returned from a server (and independent variables from a data object) or those computed on-the-fly as a user interacts with an application.

This version of the engine was created by extending the function parser code base. My current belief is that a refactor is in order before a formal release to the toolkit. The current implementation, however, is useful as I have already built a lightweight rules engine on top of a modified version of this code for a client.

And, just for completeness, I will mention that there really isn't anything new under the sun; the fundamentals of how this and similar codes work are described here.

Also, please note that this code is experimental in the sense that it is a somewhat hacked up version of the TSMT function parser. I peraonally think it's been pushed along that axis about as far as it can go without a full refactor.

NOTE: This repo is obsoleted; the Expression Engine has been upgraded and is now part of the AMYR library.

Author: Jim Armstrong - The Algorithmist

@algorithmist

theAlgorithmist [at] gmail [dot] com

Typescript: 2.3.2

Version: 1.0

Installation

Installation involves all the usual suspects

  • npm and gulp installed globally
  • Clone the repository
  • npm install
  • get coffee (this is the most important step)

Building and running the tests

  1. gulp compile

  2. gulp test

The test suite is in Mocha/Chai and specs reside in the test folder.

Overview

Consider a very simple expression, x + 1. This could be interpreted as a math formula and evaluated for numerical values of the independent variable, x. So, the expression for an input, x = 1, evaluates to a value of 2.0. But, what if x is a string? We would expect a value of x = 'abc' to result in an evaluation of 'abc1'.

Now, what about x + 1 < y where both x and y are independent variables that could take on either string and/or numeric values? The result would depend on the input type as coercion to string invokes a lexicographic comparison vs. a simpler numeric comparison for inputs of type number.

The first principle of the TSMT Expression Engine is that independent variables and the output of an expression may be a number, string, or boolean value. The engine attempts to evaluate the expression to a reasonable result based in input type(s).

The following numeric and boolean operations are supported in the engine: +, -, *, /, <=, >=, <, >, and =

The following math functions are supported: abs, ceil, floor, max, min, round

It is possible to check if a string is contained in a list (array) of strings, i.e.

"x ~ [ab, cd, efg]" or "x ~ [1, 2, 3]"

The independent variable, x should be a string and the list of strings to check against should be inside brackets and without quotes. This expression evaluates to a boolean.

It is possible to check against a string literal. Currently, the literal should be to the right of a comparison operator, i.e. "x = 'abc'". This checks string values of the independent variable, x agains the literal, 'abc' and returns a boolean.

There is currently no enforcement of order of operations, so make liberal use of parentheses to make expressions unambiguous, especially when using combinations of numeric and boolean operations. For example, "(2*x) < (3*y - 2)". Refer to the specs in the test folder for more examples.

In applications, I tend to enforce some order of operations when building expressions, since it is easier to insert proper parentheses at that time. This requirement may be relaxed in a future release.

Public API

The TSMT$ExpressionEngine class contains the following public methods.

set variables(vars: Array<string>)
clear(): void
parse(str: string): boolean
get stack(): Array<string>
eval(variableList: Array<string>, variables: Array<expressionOperand>, stack: Array<string>): expressionValue
evaluate(variables: Array<expressionOperand>): expressionValue

Usage

Typical usage of the TSMT$ExpressionEngine class involves:

1 - Define the list of independent variable, i.e. ['X', 'COLOR', 'LIMIT']

2 - Input an expression involving these variables into the parser (check the return for success)

3 - Evaluate the expression for a list of actual values for each independent variable (in the order defined)

The list of independent variables may be provided at construction or input later with a mutator.

Always call the clear() method before parsing a new expression.

It is possible to access a copy of the expression stack after parsing and then evaluate an expression using the saved stack at a later point in an application. I have personally found this feature very useful in rule engines.

Examples

Evaluate the expression, "x + y" where both x and y are string variables.

const expression: TSMT$ExpressionEngine = new TSMT$ExpressionEngine()
expression.variables = ["x", "y"];

let success: boolean = expression.parse( "x + y" );
expect(success).to.be.true;

let value: expressionValue = expression.evaluate(['a', 'b']);

expect(<string> value).to.equal('ab');

value = expression.evaluate(['DFW', 'SFO'])
expect(<string> value).to.equal('DFWSFO');

Evaluate the expression, "(2*x + 1) - (3*y - 2)" after having evaluated the expression in the above example. x and y are number variables.

expression.clear();
expression.variables = ["x", "y"];

success = expression.parse( "(2*x + 1) - (3*y - 2)" );
expect(success).to.be.true;

value = expression.evaluate([0, 0]);
expect(<number> value).to.equal(3);

value = expression.evaluate([1, 1])
expect(<number> value).to.equal(2);

value = expression.evaluate([-2, 3])
expect(<number> value).to.equal(-10);

Refer to the specs in the test folder for more usage examples.

License

Apache 2.0

Free Software? Yeah, Homey plays that