@oat-sa/tao-calculator
v0.7.1
Published
A calculator's engine for TAO
Downloads
1,328
Readme
tao-calculator
A calculator's engine for TAO.
- Calculator's engine overview
- Project's structure
- Third-party libraries
- Development
- Known issues and drawbacks
- History
- License
Calculator's engine overview
The calculator's engine relies on a formal parser for evaluating mathematical expressions. It respects the mathematical order of operations. Mathematical precision is handled by an arbitrary precision library which avoid the well known round-off issue.
Requirements
- node.js version 14 and above
Usage
Add the dependency by running
npm i @oat-sa/tao-calculator
Then import the engine:
import { engineFactory } from '@oat-sa/tao-calculator';
const calculator = engineFactory({
expression: '', // The current expression
position: 0, // The current position in the expression
instant: false, // Should the engine compute the expression as soon as an operation is complete?
corrector: false, // Should the engine auto-correct incomplete expression?
variables: {}, // A list of variables
commands: {}, // A list of commands that can be invoked
plugins: {}, // A list of plugins to load
maths: {} // Additional config for the internal maths evaluator
});
See the Configuration section for more info.
API
The API is described in the dedicated page.
The engine emits events for each noticeable operation applied to the calculator. In other words, each time a modification is applied, a related event is emitted.
The expression is managed internally through a list of extracted tokens. For easing the expression management, the numbers are tokenized digit by digit. This helps adding or removing elements up to a single digit or symbol.
The engine can be extended through two distinct mechanisms: commands and plugins.
It is also accompanied by a set of helpers for manipulating expression terms.
Project's structure
├── .github
│ └── workflows # github action workflows
├── build # standalone package generated
├── dist # bundle generated from the source code
├── sandbox # sandbox for playing with the engine locally
├── src # root of the source files
│ ├── core # source files for the core engine
│ │ └── strategies # a set of strategies for internal features
│ ├── plugins # a set of plugins that can be added on top of the engine
│ ├── utils # a set of utilities
│ └── index.js # the engine entry point
├── *.config.js # shared tools configurations
├── API.md
├── HISTORY.md
├── LICENSE
├── package.json
├── package-lock.json
└── README.md
Third-party libraries
The calculator engine relies on several third-party libraries, which are listed here.
Parser
The expression is parsed and evaluated by expr-eval, which was forked in a OAT repository as we needed to extend its abilities to ease the implementation of the Nth root function.
The purpose of this fork was to bring the ability to use any two-entry functions as a regular binary operator, with the same operator precedence than the function. The feature is relying on a prefix that transforms the function into an operator.
This parser accepts mathematical expressions, including functions, and is able to process them. It also provides a way to add custom functions, and, last but not least, it accepts to replace the existing implementation of built-in operators and functions by an arbitrary version.
This library is used to parse and evaluate the expressions built inside the calculator, so it is a very important piece in the architecture. The precedence of operators is managed by the parser, as well as the operations are evaluated and computed. The parser can be seen as a function accepting expressions and returning the result of this expression once evaluated.
Expressions syntax
The parser expects expressions to be ASCII strings, with the following rules:
- Numerical values are expressed with digits, the decimal separator being the dot
.
:3.1415
. - Scientific notation using exponent part is allowed:
3.1415e10
. - Regular arithmetic operators are supported, with natural precedence:
+
,-
,*
,/
,!
. Operators can be either unary or binary, depending on their meaning. - Expressions can be wrapped in parenthesis:
3*(5+2)
. - Terms are separated by spaces, however regular operators and parenthesis also act as separators.
- Functions and constants are supported:
cos PI
. - Variables are supported, but require to be defined:
x=10, 3 * x
. - Any functions can be turned into a binary operator by prefixing them with a
@
:4 @nthrt 16
is equivalent tonthrt(4, 16)
. Obviously, only functions that accept two parameters can work properly with this trick.
Numbers
In order to avoid the well known round-off issue, the default number representation has been replaced in the parser by an arbitrary precision implementation. We chose Decimal.js, which is a good compromise regarding precision and performances. It exposes a comprehensive API to manipulate and compute decimal numbers.
As the parser in use is not resilient against the round-off issue, every built-in operation is replaced by the equivalent provided by Decimal.js
. This allows us to support a better numerical precision, but it also means the result of the parser is now a Decimal
object instead of a native Number
.
To ensure the parser is still working well after having replaced its built-in computation functions, some wrappers have been added. They convert the data type on the fly if required. The benefit is that we can feed the parser with native types and rely on the wrappers to always ensure a compatible format. See the implementation in mathsEvaluator.
Lexer
Manipulating the expression requires to be able to recognize its elements, and to do so the calculator component is relying on a lexer, that is able to cut the expression into atomic and clearly identifiable parts.
The tokenization relies on moo, which is a well supported and offers an optimized tokenizer. It accepts rational expressions to define the patterns it is meant to recognize.
It is worth mentioning this tokenizer is not utilized by the parser to recognize the expression, as the parser is bringing its own way to do that. The purpose of this tokenizer, inside the calculator, is to bring the ability to identify the elements of the expression, in order to manipulates them separately, up to each digit of a number. Among other things, this allows to:
- display properly each element with respect to their meaning (mathematical symbols or special tokens)
- navigate freely between elements, and add or remove them easily, including each digit of a number
- apply some business logic on top of them (for instance change the sign of the current operand, or recognize exponents)
Development
Setup
- Clone the repository
git clone [email protected]:oat-sa/tao-calculator-fe.git
- Install the package with
npm
:
cd tao-calculator-fe
npm ci
Useful Commands
To run the sandbox:
npm run dev
To run unit tests:
npm test
To update the test snapshots:
npm run test:update
To run unit tests while developing:
npm run test:watch
To run unit tests with coverage report:
npm run test:cov
All commands
npm run dev
: run a sandbox and watch for changes. It also opens it in the browsernpm run test
: run the test suitenpm run test <testname>
: run a test suitetestname
(optional): Specific test to run. If it is not provided, all will be ran.
npm run test:update
: run tests and update the snapshotsnpm run test:watch
: run tests after each change in the codenpm run test:cov
: run tests and report the code coverage in the terminalnpm run coverage:html
: show coverage report in browsernpm run coverage:clover
: build a code coverage report for continuous integrationnpm run build
: build for production intodist
directorynpm run build:watch
: build for production intodist
directory and watch for changesnpm run lint
: check syntax of codenpm run lint:report
: build a syntax check reportnpm run format
: correct the code style
Known issues and drawbacks
Thanks to the numbers representation engine, the calculator is able to give a good computation precision. However, due to the nature of computation, some known issues are still there, and cannot be simply addressed.
Loss of precision of inlined variables
Internally, the computed value have hundreds of decimal digits, and only a few is displayed. So, if a variable is inlined, i.e. its displayed value is used instead of the variable itself, the result won't be accurate. For instance, compute the square root of 2. If you immediately elevate it at a power of 2, you will retrieve the former value, said 2. However, if you take the displayed value and elevate it to the power of 2, the result will be different, and could be considered wrong if you expected to retrieve the former value.
Loss of precision in some irrational numbers
Mathematically, it is impossible to have a bijective computation with the inverse of 3: 1/3
gives 0.3333333333333
, and we can continue indefinitely with the 3
after the decimal point. Now if you multiply this value by 3, no matter the amount of 3
you will add after the decimal point, you will never retrieve 1
, but 0.9999999999
instead. A mathematical trick is to add a 4
as a last digit, but unfortunately this won't work with the calculator's engine. In fact, it can only work by doing: 0.333333333 + 0.333333333 + 0.333333334
. Which is not the same.
History
Changes are detailed in the history page.
This repository has been migrated on 2023/01/31 from:
Please consult those repositories for history prior to this date.
License
Copyright (c) 2018-2023 Open Assessment Technologies SA
Licensed under the terms of the GNU GPL v2