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 ?
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))