mathjs icon indicating copy to clipboard operation
mathjs copied to clipboard

Overriding function using math.import does not override that function when using evaluate function

Open Liam-OShea opened this issue 3 years ago • 2 comments

UPDATE - I made some progress and am able to avoid the error described in this comment, however I am still receiving incorrect output when using evaluate()

--

Hi josdejong! I am enjoying using your library.

Background

I am working on adding the ability to create user-defined calculated columns to a grid of data. I am using MathJS to evaluate an expression in the form of a string representing the calculated column calculation. This string is defined by the user.

Ex: scope = {PropertyA = 1, PropertyB = 2, PropertyC = 3} // Values in scope defined by data in row being calculated math.evaluate('PropertyA + max(PropertyA, PropertyB, PropertyC)', scope)

I am having an issue regarding the treatment of null values causing errors when evaluating the expression. Some rows of data may not have a property which is included in the expression, causing an error when evaluate() is called.

I did some research on MathJS treatment of null values and I did find https://github.com/josdejong/mathjs/issues/830 so I understand why I am encountering these issues.

My goal is to have expressions with null values in them treated as excel treats them. For example during simple mathematical operations I would like a null value to be treated as 0 (eg 5 + null -> 5 + 0 = 5), and in functions I would like them to be omitted (eg mean(5, 10, null) = 7.5 rather than 5)

Question

I found https://github.com/josdejong/mathjs/issues/1829 where someone had a similar issue regarding the treatment of null in functions. I followed the pattern laid out by @Big-Gremlin (https://github.com/josdejong/mathjs/issues/1829#issuecomment-623087203) but it seems to only work when explicitly calling the overridden function, and does not work when that function comes up in .evaluate(expression, scope).

eg. const scope = {a:1, b:null} math.evaluate(sum(a, b), scope) // error math.sum([1, null]) // works, returns 1

  • Is there a way to override a function that is being executed through the evaluate() function?
  • Is there a way to treat a null value as 0 when being worked on by operators, but treat it as null (so that it can possibly be omitted if overriding the function as described above is possible) within a function? (emulate the behaviour of excel)

My current solution is to detect if the value is null, and if so set it as 0 in the evaluate() scope. This handles the math operator side of the problem (+, -, /, *, etc). Then my next step if I cannot figure this out is to parse the string expression for each function and modify the string to remove the null property from the function parameters. This seems a little hacky to me so I am trying to avoid it.

Thank you!

Liam-OShea avatar Nov 09 '21 19:11 Liam-OShea

UPDATE

I was able to get the function override to run in the evaluate() function without causing errors by modifying a part of the import command in the solution created by Big-Gremlin

const defaultMaxFunc = mathjs.max;
const newMaxFunc = factory('max', [], () => {
  return (x) => {
    x = Array.isArray(x) ?  x.filter(value => typeof(value) === 'number') : x;
    return defaultMaxFunc(x);
  };
});

// math.import([newMaxFunc], { override: true }); // Old line
math.import(newMaxFunc, { override: true });  // Pass in new function rather than array with new function

However, the output is not correct. After applying the override, evaluate() always returns the first parameter rather than the correct calculated output:

math.max([1,null,3]) // returns 3
math.evaluate('max(a,b,c)',{a=1,b=null,c=3}) // returns 1

In the following code I placed a breakpoint and noticed that when using math.max() x is an array with the values of the parameters inside. When using evaluate('max()') x is a singular value representing only the first parameter of the max function. Is there a different technique I need to use to gain access to all the parameters and filter out the null values when using evaluate()?

const defaultMaxFunc = mathjs.max;
const newMaxFunc = factory('max', [], () => {
  return (x) => {
    console.log(x) // array with math.max(), singular value with math.evaluate('max(..)')
    x = Array.isArray(x) ?  x.filter(value => typeof(value) === 'number') : x;
    return defaultMaxFunc(x);
  };
});

Liam-OShea avatar Nov 09 '21 20:11 Liam-OShea

The max function accepts both an array like max([1, 2, 3]) or variable arguments like max(1, 2, 3). In your example you use both notations. Your custom version works for max([a, b, c]), where you pass one argument which is an array, but what your custom version is missing is handling the case where a user enters max(a, b, c): you only look at the first argument. To fix this, your implementation should do something like:

const defaultMaxFunc = mathjs.max;
const newMaxFunc = factory('max', [], () => {
  return (...args) => {
    // args is always an array, and can hold either a list with singular values, or a single array or Matrix

    // ... adjust null values here...

    return defaultMaxFunc.apply(null, args);
  };
});

josdejong avatar Dec 29 '21 13:12 josdejong