Search code examples
javascriptnode.jsjsoneval

Alternative to calculating a value in a string format in js


Recently I was task to create a calculation mechanism for products into the backend. There are many products that are unique in terms of their calculations. Since there will be an admin panel for adding new products, I wanted to make it as dynamic as possible, so that I don't have to edit the backend to add simple formulas. in the database, in product table , I wanted to dedicate a column as type of JSON to store the blueprint of the calculation:

calculation: {
      bottom: {
        length: "%v + 100",
        width: "%v + 100",
        UC: "%v * 12",
      },
      side: {
        length: "%v - 50",
        width: "%v + 4",
        UC: "%v * 11",
      },
    },

I couldn't come up with better idea that to inset a placeholder %v for my incoming variables.

And the structure is going to be the same thing. Meaning, the only dynamic part is the could of attributes (in the example is bottom and side).

now the issue is that with this idea is that evaluating the blueprint is really dangerous since I have to use .eval(). For example, I created this code:

    // Example 
    const products = [
      {
        id: 1,
        name: "something",
        calculation: {
          bottom: {
            length: "%v + 100",
            width: "%v + 100",
            UC: "%v * 12",
          },
          side: {
            length: "%v - 50",
            width: "%v + 4",
            UC: "%v * 11",
          },
        },
      },
    ];
    // calculation function
    function handleCalculation(calcObj, initialValues) {
      const updatedCalcObj = {};
    
      for (let prop in calcObj) {
        updatedCalcObj[prop] = {};
    
        for (let attr in calcObj[prop]) {
          const expression = calcObj[prop][attr].replace(
            "%v",
            initialValues[prop][attr]
          );
          // !!! DANGER IS HERE.
          updatedCalcObj[prop][attr] = eval(expression);
        }
      }
    
      return updatedCalcObj;
    }
    
    // example of body of a request
    const initialValues = {
      bottom: {
        length: 100,
        width: 200,
        UC: 1,
      },
      side: {
        length: 120,
        width: 50,
        UC: 1,
      },
    };
    
    // seeing the result
    console.log(handleCalculation(products[0].calculation, initialValues))

Have you encountered a similar situation ? Is there a better way to approach this ?


Solution

  • Take advantage of the fact that JSON and mathmatical expressions can both be parsed as trees and define a simple parser:

    // Example 
    const products = [
        {
            id: 1,
            name: "something",
            calculation: {
                bottom: {
                    length: {add: ["%v", 100]},
                    width: {add: ["%v", 100]},
                    UC: {mult: ["%v", 12]},
                },
                side: {
                    length: {sub: ["%v", 50]},
                    width: {add: ["%v", 4]},
                    UC: {mult: ["%v", 11]},
                },
                nested_example: {
                    // (v + 100) * (v / 2 - 20)
                    result: {mult: [
                        {add: ["%v", 100]},
                        {sub: [
                            {div:["%v", 2]}, 
                            20
                        ]},
                    ]},
                },
            },
        },
    ];
    const operations = {
        add: (a, b) => a + b,
        sub: (a, b) => a - b,
        mult: (a, b) => a * b,
        div: (a, b) => a / b,
    };
    function calculate(calcObj, initialValue) {
        if (calcObj === "%v") return initialValue;
        let [operation, [left, right]] = Object.entries(calcObj)[0];
        if (typeof left !== "number") {
            left = calculate(left, initialValue);
        }
        if (typeof right !== "number") {
            right = calculate(right, initialValue);
        }
        return operations[operation](left, right);
    }
    // calculation function
    function handleCalculation(calcObj, initialValues) {
        const updatedCalcObj = {};
    
        for (let prop in calcObj) {
            updatedCalcObj[prop] = {};
    
            for (let attr in calcObj[prop]) {
                const calculation = calcObj[prop][attr];
                const value = initialValues[prop][attr];
                updatedCalcObj[prop][attr] = calculate(calculation, value);
            }
        }
    
        return updatedCalcObj;
    }
    
    // example of body of a request
    const initialValues = {
        bottom: {
            length: 100,
            width: 200,
            UC: 1,
        },
        side: {
            length: 120,
            width: 50,
            UC: 1,
        },
        nested_example: {
            result: 100,
        },
    };
    
    // seeing the result
    console.log(handleCalculation(products[0].calculation, initialValues))