eakl
eakl

Reputation: 571

How to parse an arbitrary mathematical expressions containing arrays in javascript

I want to store arbitrary mathematical expression with basic operations (+, -, *, /, ^, sqrt, grouping, etc…) and id placeholders as a string.

const json = {
  “formula”: “{{a45bc2a1-ed82-4ccd-a455-f7959e875aad}}+({{f6c2ef2b-a4fa-4cfb-b62d-d0d7c3e266d9}}*{{335563ad-a715-47b9-8e54-2b8553768168}})”
}

Ids are mapped to arrays such as:

const map = {
  “a45bc2a1-ed82-4ccd-a455-f7959e875aad”: [1, 2, 3, 4, 5],
  “f6c2ef2b-a4fa-4cfb-b62d-d0d7c3e266d9”: [10, 20, 30, 40, 50],
  “335563ad-a715-47b9-8e54-2b8553768168”: [1, 2, 3, 4, 5]
}

How can I achieve this? eval() doesn’t work with vectors and I don’t know how to parse arbitrary formula to do element-wise operations

Result should be:

[11, 42, 93, 164, 255]

Upvotes: 1

Views: 175

Answers (3)

Chris Welton
Chris Welton

Reputation: 106

I played around with this a while today and got a version working that does not require defining a formal AST, and only uses eval() to instantiate an arrow function that calculates each index of the output based on the input lists and an incrementing input index.

'use strict';
const json={
    formula: '{{a45bc2a1-ed82-4ccd-a455-f7959e875aad}}+({{f6c2ef2b-a4fa-4cfb-b62d-d0d7c3e266d9}}*{{335563ad-a715-47b9-8e54-2b8553768168}})'
}

const map = {
    'a45bc2a1-ed82-4ccd-a455-f7959e875aad': [1, 2, 3, 4, 5],
    'f6c2ef2b-a4fa-4cfb-b62d-d0d7c3e266d9': [10, 20, 30, 40, 50],
    '335563ad-a715-47b9-8e54-2b8553768168': [1, 2, 3, 4, 5]
}

function parseFormula (formula) {
    const template = /{{([^}]*)}}/;
    const idList = [];
    for (let i = 0; template.test(formula); i++) {
        idList.push(formula.match(template)[0].replace(/[{}]/g, ''));
        formula = formula.replace(template, `input[${i}][i]`);
    }
    return {idList, formula};
}

function calculateFormula(map, parsed) {
    const input = [];
    const result = [];
    const lambda = eval(`(input, i) => ${parsed.formula}`)
    parsed.idList.forEach(id => input.push(map[id]));
    for (let i = 0; i < input[0].length; i++) {
        result.push(lambda(input, i));
    }
    return result;
}

const parsed = parseFormula(json.formula);
console.log(parsed);
console.log(calculateFormula(map, parsed));

Output:

{
  idList: [
    'a45bc2a1-ed82-4ccd-a455-f7959e875aad',       
    'f6c2ef2b-a4fa-4cfb-b62d-d0d7c3e266d9',       
    '335563ad-a715-47b9-8e54-2b8553768168'        
  ],
  formula: 'input[0][i]+(input[1][i]*input[2][i])'
}
[ 11, 42, 93, 164, 255 ]

Upvotes: 0

georg
georg

Reputation: 215029

Since you're, in fact, developing a programming language, you'll going to need a compiler (=translate string expressions into an AST) and a runtime (=evaluate AST given inputs and predefined bindings). Here's some code to get you started. It only supports a limited grammar expression = term | term op expression and doesn't handle any errors:

// "compiler"

function parse(str) {
    return expr([...str])
}

function expr(chars) {
    let node = term(chars)
    if (chars.length)
        node = {
            op: chars.shift(),
            left: node,
            right: expr(chars)
        }
    return node
}

function term(chars) {
    let str = ''
    while (chars.length && chars[0].match(/\w/))
        str += chars.shift()
    return {value: str}
}

// "runtime"

ops = {
    '+': (a, b) => a + b,
    '-': (a, b) => a - b,
}

function evaluate(node, bindings) {
    if (node.value)
        return bindings[node.value]
    return eval_op(
        node.op,
        evaluate(node.left, bindings),
        evaluate(node.right, bindings))
}

function eval_op(op, left, right) {
    let fn = ops[op]
    return left.map((_, i) => fn(left[i], right[i]))
}

// demo

input = 'abc+def+xyz'
bindings = {
    'abc': [1, 2, 3],
    'def': [4, 5, 6],
    'xyz': [7, 8, 9],
}

ast = parse(input)
console.log('AST', ast)
res = evaluate(ast, bindings)
console.log('RESULT', res)

Of course, there's a lot more work involved to turn this sketch into real code. I'd suggest you learn a bit about formal grammars, parsers and parser generators.

Upvotes: 1

Barmar
Barmar

Reputation: 782166

Loop over the array indexes, and replace the placeholders with the current index in the corresponding element of map. Then call eval().

let len = Object.values(json.formula)[0].length;
for (let i = 0; i < len; i++) {
    let formula = json.formula.replace(/{{([-\w]+)}}/g, (m, placeholder) => map[placeholder][i];
    console.log(eval(formula));
}

Upvotes: 0

Related Questions