Coder Social home page Coder Social logo

ncalc2's Introduction

NCalc

Build status NuGet NuGet

⚠️ This repository is currently only passively maintained. If there are fully fledged PRs, I will occasional merge those in and publish new versions. If you would like to become a maintainer and work on some of the open issues, please reply here #61

A clone of NCalc from http://ncalc.codeplex.com/ with the following changes:

  • added support for .NET Standard 2.0+
  • added compilation of expressions to actual CLR lambdas

Installation

Simply install the package via NuGet

PM> Install-Package CoreCLR-NCalc

Creating Lambdas

Simple Expressions

var expr = new Expression("1 + 2");
Func<int> f = expr.ToLambda<int>();
Console.WriteLine(f()); // will print 3

Expressions with Functions and Parameters

class ExpressionContext
{
  public int Param1 { get; set; }
  public string Param2 { get; set; }
  
  public int Foo(int a, int b)
  {
    return a + b;
  }
}

var expr = new Expression("Foo([Param1], 2) = 4 && [Param2] = 'test'");
Func<ExpressionContext, bool> f = expr.ToLambda<ExpressionContext, bool>();

var context = new ExpressionContext { Param1 = 2, Param2 = "test" };
Console.WriteLine(f(context)); // will print True

Performance Comparison

The measurements were done during CI runs on AppVeyor and fluctuate a lot in between runs, but the speedup is consistently in the thousands of percent range. The speedup measured on actual hardware was even higher (between 10,000% and 35,000%).

Formula Description Expression Evaluations / sec Lambda Evaluations / sec Speedup
(4 * 12 / 7) + ((9 * 2) % 8) Simple Arithmetics 474,247.87 32,691,490.41 6,793.33%
5 * 2 = 2 * 5 && (1 / 3.0) * 3 = 1 Simple Arithmetics 276,226.31 93,222,709.05 33,648.67%
[Param1] * 7 + [Param2] Constant Values 707,493.27 21,766,101.47 2,976.51%
[Param1] * 7 + [Param2] Dynamic Values 582,832.10 21,400,445.13 3,571.80%
Foo([Param1] * 7, [Param2]) Dynamic Values and Function Call 594,259.69 17,209,334.34 2,795.93%

ncalc2's People

Contributors

altso avatar andrewhanley2 avatar bradtglass avatar bykiev avatar david-brink-talogy avatar deltex80 avatar dependabot[bot] avatar dgeller-ouhsc avatar eero-dev avatar eugenca avatar fadulalla avatar kfrancis avatar lg2de avatar lukas-ais avatar malukuseito avatar randbrown avatar rexcfnghk avatar sebastienros avatar sklose avatar tyrone-sudeium avatar victoriatolls avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ncalc2's Issues

C# like handling of `null` in comparation operators

In C# when you write code alike (int?)0 == null result is false but in NCalc2 because of LambdaExpressionVistor.UnwrapNullable all null are coveted to 0.

This in effect cause NullInt() == 0 return true.

As some user expressions could relying on this behavior I think best would be new option flag for this behavior.
When this flag is enable then in LambdaExpressionVistor.WithCommonNumericType add code like:

        private bool IsNullable(L.Expression expression)
        {
            var ti = expression.Type.GetTypeInfo();
            return ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(Nullable<>);
        }

        private L.Expression HaveValueExpression(L.Expression expression)
        {
            if (IsNullable(expression))
            {
                return L.Expression.Property(expression, "HasValue");
            }
            else
            {
                return L.Expression.Constant(true);
            }
        }

        private Func<L.Expression, L.Expression, L.Expression> UnwrapNullableAction(L.Expression left, L.Expression rigth, Func<L.Expression, L.Expression, L.Expression> action, BinaryExpressionType expressiontype)
        {
            if (/*TODO should we use null like C#? */ && (IsNullable(left) || IsNullable(rigth)))
            {
                if (/*TODO is comparison expressiontype */)
                {
                    return (l, r) => L.Expression.Condition(
                        L.Expression.Equal(HaveValueExpression(left), HaveValueExpression(rigth)),
                        action(l, r), // will compare `0 == 0` instead of `null == null` but this is fine
                        L.Expression.Constant(false));
                }
                else
                {
                    return (l, r) => L.Expression.Condition(
                        L.Expression.And(HaveValueExpression(left), HaveValueExpression(rigth)),
                        action(l, r),
                        L.Expression.Constant(null));
                }
            }
            else
            {
                return action;
            }
        }

        private L.Expression WithCommonNumericType(L.Expression left, L.Expression right,
            Func<L.Expression, L.Expression, L.Expression> action, BinaryExpressionType expressiontype = BinaryExpressionType.Unknown)
        {
            action = UnwrapNullableAction(left, right, action, expressiontype);
            left = UnwrapNullable(left);
            right = UnwrapNullable(right);

            /*TODO rest of logic */
        }

null+null+null... evaluate never ends

I have a test that executes very long or never ends:
` [Test]
public void Nulls_Test()
{
var expression = new NCalc.Expression("null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null" +
"+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null" +
"+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null" +
"+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null" +
"+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null" +
"+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null+null")
{
Options = NCalc.EvaluateOptions.AllowNullParameter
};

  var res = expression.Evaluate();
  Assert.AreEqual(null, res);
}`

Support implicit conversion for primitive-type arguments.

Currently, in order for a method to be found in a lambda context, the arguments must match the parameters exactly.

For example:

public class Context {
   int Sum(double a, double b) => a + b;
}

Sum(1.0, 1.0); // returns 2
Sum(1, 1);      // throws a "method not found" exception, but ideally it should return 2.

Even though an int can be implicitly cast to a double, the method fails.
This is similar to issue #37 but deals with primitive types only.

[REQ] Easily skimmable release history

I am making the request to have a more easily available and skimmable changelog history.
I tried determining what the breaking changes were from 2.x to 3.0 but was unable to find the release history documenting the changes.
Having these readily available makes upgrading a legacy codebase much less cumbersome.

Expressions with big Integers evaluate either to wrongs results or they throw a System.OverflowException

  1. In the first example the results a supposed to be the same, but expression2 evaluates to : -3478141659,48 instead of 816825636.52
    <TestMethod()>
    Public Sub TestNCalc()

        Dim expression1 As String = "(-1413174363.48)+200000000+2030000000"
        Dim expression2 As String = "200000000+2030000000-1413174363.48"

        Dim exp1 As Func(Of Decimal) = New Expression(expression1).ToLambda(Of Decimal)
        Dim exp2 As Func(Of Decimal) = New Expression(expression2).ToLambda(Of Decimal)

        Dim calculatedValue1 As Decimal = exp1()
        Dim calculatedValue2 As Decimal = exp2()

        Assert.AreEqual(816825636.52D, exp1())
        Assert.AreEqual(816825636.52D, exp2())

    End Sub
  1. The second example does not work at all. Here as System.OverflowException is thrown.
    <TestMethod()>
    Public Sub TestNCalc1()

        Dim expression As String = "if(2080000000>2200321782,0,2200321782-2080000000)"
        Dim calculatedValue As Decimal = New Expression(expression).Evaluate()

        Assert.AreEqual(120321782, calculatedValue)

    End Sub

==========

I am using the latest version of NCoreCalc 2.2.70.

Allow to operate on System.Linq.Expressions.Expression

Currently Expression.ToLambda returns Func<>
Intermediate expression is hidden and not avaiable to touch
LambdaExpressionVistor is internal

I need a way to use generated System.Linq.Expressions.Expression for my needs
Thank you.

NullReferenceException when expression contains null parameter

I found a bug, which should be discussed: when expression contains null parameter and null parameters are not allowed a NullReferenceException is thrown. We don't need to get the most precise type if any of parameters is null

{
var allowNull = (_options & EvaluateOptions.AllowNullParameter) == EvaluateOptions.AllowNullParameter;
Type mpt = allowNull ? GetMostPreciseType(a?.GetType(), b?.GetType()) ?? typeof(object) : GetMostPreciseType(a.GetType(), b.GetType());
if (a == null && b == null)
{
return 0;
}
if (a == null || b == null)
{
return -1;
}

If the null parameters are not allowed we should throw ArgumentNullException instead, but it'll lead to breaking change. I think the best choice will be check first if any of parameters is null and don't throw any exceptions like EvaluateOptions.AllowNullParameter is specified. What do you think?

Upd: since if one or both null parameters in expression will cause NullReferenceException it will not be a breaking change and it's safe to throw ArgumentNullException, but a minor version increase should be done

Cannot evaluate formula

Consider the following code:

public static InputValidation ValidCustomFormula = delegate (string val) {
            if (String.IsNullOrWhiteSpace(val))
                return "";
            else
            {

                var expr = new Expression(val);

                decimal x = 5m;
                decimal a = 6m;
                decimal b = 7m;

                expr.Parameters["x"] = x;
                expr.Parameters["a"] = a;
                expr.Parameters["b"] = b;

                try
                {
                    Func<float> f = expr.ToLambda<float>(); // Here it throws System.ArgumentNullException. Parameter name: expression

                    float res = f();
                }
                catch(Exception e)
                {
                    return "There was an error while parsing the custom formula: " + e.Message;
                }
          }
    }

    var res = ValidCustomFormula("2 + 2 - a - b - x");

As long as I don't include any parameter in the expression itselt (i.e.: "2 + 2 + 2") it works, but it doesn't when I include some parameter. Am I missing something? Looks like a bug.

Thanks.

NCalc101

Not really an issue, but discussions are not enabled...

NCalc fans might like to know that NCalc101 is free to use and provides an NCalc development environment in the browser. Those that have used Regex101 will know how useful such a utility is!

All code is executed client side through the magic of Blazor and WASM.

The PanoramicData.NCalcExtensions (github) are built in, so you can do things like this with ease:

select(list(list(1, 2, 3), list(4, 5, 6), list(7, 8, 9)), 'x', 'skip(x, 1)')

This is the equivalent of C#:
new List { new List { 1, 2, 3 }, new List { 4, 5, 6 }, new List { 7, 8, 9 } }.Select(x => x.Skip(1)).ToList();

... and gives visual output like:
2 3
5 6
8 9

Decimals support depending on culture?

Hi,

I would like to know if expressions can be handled depending on the thread culture or if these expressions are fixed?

For example:
In french culture, the decimal value of 123.45 is 123,45.

Thanks

ToLambda: ArgumentNullException if method not found in context

Update LambdaExpressionVistor to throw more specific exception if method was not found in context class.

Example:
class ExpressionContext { }
expression Get([Param]) == 0' .

'ToLambda<ExpressionContext,bool>()' throws System.ArgumentNullException : Value cannot be null. Parameter name: method - no method name (Get) provided in exception

Why calculating with boolean values is prohibited?

In the current version calculating with boolean values is prohibited.
There is a Unit Test explicitly preventing such calculation: Fixtures.ShouldDisplayErrorIfUncompatibleTypes.

We need such calculation.
Would you accept a PR to implement this?

Wrong calculation

var expr = new Expression("2747474 * 63663");		
Console.WriteLine(expr.Evaluate());

It prints -1 181 221 874, when the right answer is 174 912 437 262.

Allocation optimizations

As we're using NCalc in a Unity game where allocations are critical I added a few optimizations to reduce them a bit. Not sure if you'd like those to get adopted into mainline, especially the instance caching might not be thread safe.

Changes basically consist of two parts:

  • (Optionally) Reusing instances of FunctionArgs and EvaluationVisitors instead of creating new ones on each evaluation
  • Replacing ToLower calls on strings for case insensitive tests with Equals(, OrdinalIgnoreCase) calls
  • Also as a really minor one I replaced an empty array creation with reusing a single instance of it

0001-Added-instance-caches.txt

Cheers,
Chris

Fix overloading when parameters are non-primitive.

Hello, it's me again! :)

After using #20 for 6 months, I've come to realise that when searching for a method, if its parameters are of type object, it's treated as equal to any other overload with object parameters. Even if the objects' internal types are different. This caused mismatching types exceptions.

This stems from using ToTypeCode() instead of just the type when searching for a method.
(I don't actually know if there's a need to use TypeCode when comparing parameters for methods.

I've made a PR ( #30 ) that addresses this issue, would appreciate if you could take a look and tell me what you think. Cheers.

Faris.

Added support for overloading and the params keyword.

Enhancement
Hey!

I needed support for overloading and the params keyword so I went ahead and added them in a fork.
I opened a PR in case others have similar needs, here: #19 ..

I am not sure if I was meant to open an issue before creating a PR, so here's an issue, just in case.

Index was outside the bounds of the array. at NCalc.Expression.Evaluate()

In any random and rare scenario, Getting Index was outside the bounds of the array. at NCalc.Expression.Evaluate() while only evaluating very simple expression, or boolean conversions. Using .net core 3.0.

Issue doesn't seem to appear while using EvaluationOptions.NoCache.

Any pointers on if I may be doing something wrong? Any way to identify the detailed trace atleast?

Null handling changed from version 2 to 3

I have this code in my application, and it worked properly with every version of NCalc prior to 3.0. Now, this same code throws an exception when evaluating the expression.

It seems to be ok if I remove the {} brackets. Was something changed to make curly braces treated differently?

            var expression = "{null} = 'WrongValue'";
            var eap = new NCalc.Expression(expression
    // need to indicate NoCache in order to avoid threading issues
    , NCalc.EvaluateOptions.NoCache);

            eap.EvaluateParameter += delegate (string name, NCalc.ParameterArgs args)
            {
                if (name == "null")
                {
                    // if 'null' appears in an expression, it is considered a parameter, so must replace it with
                    // a string.  This will be an issue if comparing "null = 'null'", as it would evaluate to true, instead
                    // of false.  will need to investigate if there is a better way to handle null
                    args.Result = "null";
                }
            };
            var evaluated = eap.Evaluate();

Exception:

  Message: 
NCalc.EvaluationException : token recognition error at: '{':1:0
token recognition error at: '}':1:5
---- NCalc.EvaluationException : token recognition error at: '{':1:0
token recognition error at: '}':1:5

  Stack Trace: 
Expression.Evaluate()
ExpressionTests.DemonstrateNullException() line 92
----- Inner Stack Trace -----
Expression.Compile(String expression, Boolean nocache)
Expression.HasErrors()

``

Support inheritance in lambda context.

I'd like to add support for inheritance when using lambda contexts. For example, if I have derived class B inheriting from class A, I'd like to be able to use all functions in B as well as those in the base class A.

public class A {
   public int Sum(int a, int b) {
      return a + b;
   }
}

public class B : A {
   public int Sum(int a, int b, int c) {
      return a + b + c;
   }
}

[Fact]
public void ShouldSupportInheritance()
{
      var lambda = new Expression("Sum(1,1) + Sum(1,1,1)").ToLambda<B, int>();
      var context = new B();
      Assert.Equal(5, lambda1(context));
}

Can't compare null parameter to a constant

The following test fails with an exception below. As a user, I would expect it to pass. I can create a PR with a fix if you agree it's a bug.

[Fact]
public void ShouldCompareNullableNullToNonNullable()
{
    var e = new Expression("[x] = 5", EvaluateOptions.AllowNullParameter);

    e.Parameters["x"] = null;
    Assert.False((bool)e.Evaluate());
}
System.InvalidCastException
Null object cannot be converted to a value type.
   at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
   at NCalc.Domain.EvaluationVisitor.CompareUsingMostPreciseType(Object a, Object b) in \src\NCalc\Domain\EvaluationVisitor.cs:line 65
   at NCalc.Domain.EvaluationVisitor.Visit(BinaryExpression expression) in \src\NCalc\Domain\EvaluationVisitor.cs:line 160
   at NCalc.Domain.BinaryExpression.Accept(LogicalExpressionVisitor visitor) in \src\NCalc\Domain\BinaryExpression.cs:line 20
   at NCalc.Expression.Evaluate() in \src\NCalc\Expression.cs:line 323
   at NCalc.Tests.Fixtures.ShouldCompareNullableNullToNonNullable() in \test\NCalc.Tests\Fixtures.cs:line 96

Please allow bracket in parameter name

I wish I could write parameter name with bracket like this

var array = new float[] { 1,2,3,4,5,6 };
var e = new Expression("1 + (2 * [array[0]])");
e.EvaluateParameter += delegate(string name, ParameterArgs args) {
      // got "array[0]" as name
      if (name.StartsWith("array[") && name.EndsWith("]"))
      {
          int i = int.Parse(name.Replace("array[","").Replace("]",""));
          args.Result = array[i];
      }
};

If possible I would like to have it allow arbitrary nested bracket

var keys = new string[] { key0,key1,key2 };
var dict = new Dictionary<string,float>();
var e = new Expression("1 + (2 * [dict[key[0]]])");
e.EvaluateParameter += delegate(string name, ParameterArgs args) {
      // got "dict[key[0]]" as name for doing lookup
};

Expression.HasErrors() not flagging error with if function

var expr = new Expression("if (1 = 1, 'office')");
Console.WriteLine($"has error:{expr.HasErrors()}"); //false
Console.WriteLine(expr.Evaluate()); //throws error!

Running the above code with the latest version (2.2.80), the expression will not show as having errors ( the second line), but when evaluating it. it willk throw an argument exception (third line).

Why is this happening? is there a better way to ensure a given expression does not have errors?

BigInteger context functions not working

This piece of code throws System.MissingMethodException: 'method not found: Abs'

public class ExpressionContext
{
    public BigInteger Abs(BigInteger x)
    {
        return BigInteger.Abs(x);
    }
}

private static void Test()
{
    Expression expression = new Expression("Abs(-1)");
    var func = expression.ToLambda<ExpressionContext, BigInteger>();
    Console.WriteLine(func(new ExpressionContext()).ToString());
}

License file?

There seems to be no license file for the project.
Is there plan to add one?
Thanks

add "AND", "OR", "NOT" to grammar

The boolean operators "and", "or", and "not" are case sensitive in the grammar. To help with readability of ncalc expressions, we are interested in adding uppercase equivalent operators.

NCalc does have some options related to ignoring case, but these appear to all apply to functions or variables, not operators in the grammar.

I did some experimenting with ANTLR3 and was able to use a slightly modified NCalc.g grammar file to add this support by generating a new parser and lexer, so I know it's technically possible. I can put together a pull request for this if this is something that would be OK.

List of Functions

Hi,

I've been testing NCalc and I'm very impressed by the capabilities.

Is it possible to get a list of the available functions in NCalc? I'm trying to write an editor where a user would input an expression that is evaluated by NCalc. I would like to highlight all the function names that are accepted by NCalc but am not sure how to get that list.

Kind Regards
Fredrik Dahlberg

Comparing null to custom function crashes

Expression like "F(1) == null", where F(1) is custom function, throws exception:
System.NullReferenceException : Object reference not set to an instance of an object. at System.Object.GetType() at NCalc.Domain.EvaluationVisitor.CompareUsingMostPreciseType(Object a, Object b) at NCalc.Domain.EvaluationVisitor.Visit(BinaryExpression expression) at NCalc.Expression.Evaluate()

F(1) can return null or not null objects.

This is an issue with all known Ncalc clones, even with the original, except Ncalc-Edge 1.4.1.000. We are forced to use that version and I'd like to use your much faster one.

if function with int and double parameters

When using the if function in compiled mode I get the error:
'cannot be evaluated due this error: Argument types do not match'
with expressions like
if(true, 1, 0.0)
While in interpreted mode this is evaluated correctly.
I tried to write an overload of the function in my context object that takes an int and double as parameters but that didn't do the trick.
Is it possible to add support for expressions like this?
Thanks

Namespace Error (Serializable)

I cannot add [Serializable] to any class because the compiler can't decide if it wants to use NCalc or NetStandard.

The Error-Message says something like this (translated):

"SerializableAttribute" is present in
"NCalc, Version=3.5.0.2, Culture=neutral, PublicKeyToken=null" and
"netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51"

Does anyone have any idea how to fix this?

Antlr is not parsing specific numbers correctly

This unit test fails unexpectedly:

[Fact]
public void IfTest()
{
    // Arrange
    const long expected = 9999999999L;
    var expression = $"if(true, {expected}, 0)";
    var e = new Expression(expression);

    // Act
    var actual = e.Evaluate();

    // Assert
    Assert.Equal(expected, actual);
}

Xunit.Sdk.EqualException
Assert.Equal() Failure
Expected: 9999999999
Actual: 1E+10

Strangely the test passes if you remove one 9 from the long constant. The same thing happens for the lambda version:

[Fact]
public void IfTest2()
{
    // Arrange
    const long expected = 9999999999L;
    var expression = $"if(true, {expected}, 0)";
    var e = new Expression(expression);
    var context = new object();

    var lambda = e.ToLambda<object, long>();

    // Act
    var actual = lambda(context);

    // Assert
    Assert.Equal(expected, actual);
}

Override operators

I cannot find anything on this but can someone please tell me if it's possible to override the operators? I want to be able to check when + - / * > >= < <= are performed if one of the parameters is null and thus handle it.

Request mechanism to enable/disable Debug.Writeline

The caching mechanism writes unconditional messages to Debug.Writeline.

Debug.WriteLine("Cache entry released: " + key);
Debug.WriteLine("Expression retrieved from cache: " + expression);
Debug.WriteLine("Expression added to cache: " + expression);

Could we add a

boolean DebugLogging;

property (default = true). If DebugLogging = false, don't write these messages.

Or alternately a more general mechanism to specify

DebugVerbosity = (None, Low, Medium, High, Garrulous etc)

to allow for additional logging later.

I'm happy to do this, but wanted feedback on the preferred approach.

A bit of background as to why this matters:-

In an example I'm working with there are over 10,000 calls to Debug.WriteLine in NCalc due to cached expressions. We are using NCalc via a DLL, which itself plugs in to UnrealEngine and the SDK is swamped with these messages, making development excruciatingly slow. All solutions to remove these messages at the host end are complex and slow.

Comment support

Hi, I need a comment support (like in C# /**/ and //), is anybody tried to add support directly in NCalc? Of course I can prepare the data before passing to NCalc, but I believe antlr will be faster and more suitable for it.

Looking for a Maintainer

Unfortunately, I do not have enough time to spare to actively maintain this library and address all the open feature requests and bug reports. Therefore, I am looking for somebody interested in becoming a maintainer for this project. Please reply to this issue if you are interested.

Caching doesn't work in a multi threaded context

Hi,

I noticed that NCalc often hangs when multiple threads are using it. I only use an ncalc expression object that is local to the method so I thought it should be thread safe.
My application often freezes on the line:
expression.HasErrors()
When i looked in the debug window the error was:
"Recursive read lock acquisitions not allowed in this mode"

I realized that this could have something to do with the caching in NCalc, so I turned it off by doing:
Expression.CacheEnabled = false;

When I turn caching off everything works fine.

I realize this might be a tough one to get the bottom of, but I thought it was worth mentioning it.

Let me know if you want any more info.

Kind Regards
Fredrik Dahlberg

Adding custom functions to shared function libraries can unexpectedly change the behaviour of a consuming system

TLDR;
Adding custom functions to published NCalc function libraries, or NCalc itself, can cause unexpected breaking changes to the way an expression is evaluated if the names of the functions clash. It is unexpected that the addition of new functions would be a breaking change, especially as it does not generate any error message, either at compile time or runtime.

Detailed Explanation
We publish and use an open source library of functions to make NCalc even more useful, namely PanoramicData.NCalcExtensions. PanoramicData.NCalcExtensions extends NCalc by inheriting from the Expression type and adding an event handler to the EvaluateFunction event handler.

One of our internal systems provides even more, system-specific extensions by inheriting from the derived ExtendedExpression class from PanoramicData.NCalcExtensions - adding a third set/layer of functions. Within this system we can therefore evaluate expressions containing functions from NCalc, from PanoramicData.NCalcExtensions and from our system itself.

Recently, a new function was added to PanoramicData.NCalcExtensions with the same name as one of the custom functions in our own system. Suddenly, the function in the PanoramicData.NCalcExtensions library is being used in preference to the one in our own system, breaking our consuming system's functionality. It turns out that adding new functions to a shared, custom functions library can be a breaking change, and this was unexpected.

Possible changes to resolve the issue (not exclusive):

  1. Ideally, I think NCalc would follow the principals of overriding, namely that a function in a derived class was used in preference to the function with the same name in the base class. This makes adding functions to NCalc, and to function libraries, somewhat safer.
  2. An alternative, although not as ideal, is to add a check for a clash of function naming in multiple extensions so that developers are at least made aware that this might cause them a problem.

Fully resolving this issue using my preferred solution (change 1 above) is probably a breaking change, as I think the behaviour can only be changed by replacing the EvaluateFunction event handler with another mechanism that has the ability to guarantee - and control - the order of evaluating which function to use.

I am aware that this library is not actively maintained, but I thought I would post my discovery so that other people were aware of the problem - this might help other people track down an issue they have. Furthermore, should any changes be made to NCalc in the future, this change can be considered as part of those efforts.

Disclaimer: I work for Panoramic Data. I don't see a conflict of interest because of this, but I mention it in the spirit of full disclosure.

Prioritise lambda context functions over built-in ones.

Improvement

Prioritise functions defined in lambda context over built in functions when evaluating an expression.


Original comment

Originally posted by @fadulalla in #35 (comment)

Hi all,

This broke some functionality for us because we have our own implementations of Min and Max in our context, which have now been overshadowed by these built-in min and max.

As a solution, I propose we move them after the keywords and after the context lookup, something like the snippet at the end of this comment.

My reasoning:

  • Min, Max and Pow are not reserved C# keywords. So we shouldn't short-circuit them like we do with if and in (we can't expect context users not to override them).
  • Context method lookup should have precedence to give users freedom. If context had precedence, those that want the built-in Min/Max have the options of either removing any implementation in their context, and it'd default to the built-in ones. Or they can re-implement the functions themselves. Whereas the way it is now restricts everyone to using the built in function with no way to override it with their own implementation.
  • The built-in ones only support double and they cannot be overridden. (Consider something like Min(pointA, pointB) where points A and B are custom coordinate objects with custom Min implementation -- this is our case).
  • Context method look-up is more strict and meticulous (it also considers arguments and their types in addition to the method's name) so it would make more sense to have that first before loosening/broadening our search.
  • This change would be nonbreaking for people already using the newly introduced built-in Min/Max functions because their contexts wouldn't have had it in the first place (and if it did, they wouldn't have been able to use it anyway).

My proposal is something like:

string functionName = function.Identifier.Name.ToLowerInvariant();
if (functionName == "if") {
    _result = L.Expression.Condition(args[0], args[1], args[2]);
    return;
} else if (functionName == "in") {
    var items = L.Expression.NewArrayInit(args[0].Type, new ArraySegment<L.Expression>(args, 1, args.Length - 1));
    var smi = typeof (Array).GetRuntimeMethod("IndexOf", new[] { typeof(Array), typeof(object) });
    var r = L.Expression.Call(smi, L.Expression.Convert(items, typeof(Array)), L.Expression.Convert(args[0], typeof(object)));
    _result = L.Expression.GreaterThanOrEqual(r, L.Expression.Constant(0));
    return;
}

//"FindMethod" would be changed so it would just return null instead of throwing "method not found".
var mi = FindMethod(function.Identifier.Name, args);
if (mi != null) {
    _result = L.Expression.Call(_context, mi.BaseMethodInfo, mi.PreparedArguments);
    return;
}

// default function implementation like min, max, pow, and any other in the future.
switch (functionName) {
    case "min":
        var min_arg0 = L.Expression.Convert(args[0], typeof(double));
        var min_arg1 = L.Expression.Convert(args[1], typeof(double));
        _result = L.Expression.Condition(L.Expression.LessThan(min_arg0, min_arg1), min_arg0, min_arg1);
        break;
    case "max":
        var max_arg0 = L.Expression.Convert(args[0], typeof(double));
        var max_arg1 = L.Expression.Convert(args[1], typeof(double));
        _result = L.Expression.Condition(L.Expression.GreaterThan(max_arg0, max_arg1), max_arg0, max_arg1);
        break;
    case "pow":
        var pow_arg0 = L.Expression.Convert(args[0], typeof(double));
        var pow_arg1 = L.Expression.Convert(args[1], typeof(double));
        _result = L.Expression.Power(pow_arg0, pow_arg1);
        break;
    default:
        throw new Exception($"method not found: {functionName}");
}

@sklose I understand you're only passively maintaining the repo at the moment so I'm happy to prepare this, add tests and open a PR if you concur with the above.

We've only just found out about this because we've only recently upgraded to the latest version. We had to rollback to a version just before this one though because of the breaking change.

Thanks all,

fadulalla

Why ToLambda<T> does not evaluate parameters dynamically

I have a problem similar to issue #6.
I have analyzed the code trying to solve the issue.

I'm wondering why the parameters are referenced as constant and not used dynamically.
Is there any use case to

  1. use parameters
  2. create a lambda
  3. call lambda function multiple times with same parameters?

If so, we need an option.
Otherwise I would like to rewrite the handling of parameters introduced with fixing #1.

if function with decimal and double

var expresion = new Expression("if([param1] > 5, [param2], 0.00)");
expresion.Parameters.Add("param1", 5);
expresion.Parameters.Add("param2", 1.5m);

var lambda = expresion.ToLambda<decimal>();
var result = lambda();

I am getting an ArgumentException with the following message: "Argument types do not match".
If I change the value of param2 to be a double, it works. My understanding is that 0.00 in this context is interpreted as a double and the 2 arguments of the if function must be of the same type.

Operator '*' can't be applied to operands of types 'double' and 'decimal'

I receive the error in the title when evaluating expressions with literal values. I believe the Parser is converting literals to double's.

For example;

  • 10 * Volume - The 10 parses as a double

My challenge is that I cannot change the data type of the Volume parameter in the above example which is a decimal and therefore resulting in the error. Any suggestion on how I can get around this issue?

A suggestion would be to add another option to the EvaluationOption class like LiteralsAsDecimal. Then either in the parser use the option to convert to decimal if set, or in the EvaluationVistor convert the value as it's passed to Numbers. Another option would be to add another method like CompareUsingMostPerciseType for the Multiplication and Division operators to use.

Check "if" expression for errors such as undefined formulas.

Hi,

I have an expression of the following type:
if(a > 3, MyFunction(34), 45)
The problem is that MyFunction doesn't exist and I would like to know this somehow. Is that possible? If I evaluate the formula with a > 3 I will get an exception of course. Problem is that i don't know what the formulas will be like and I can't test all different input combinations.

As far as I understand HasErrors only checks that the syntax is correct, not that the functions actually are implemented.

Kind Regards
Fredrik Dahlberg

Assembly Signature Requirements

你好,很感谢提供了这么好用的类库,在使用过程中,我一般会下载源代码加已分析,然后直接从 NuGet 中获取到版本,并添加到项目引用当中,但由于我的项目都是使用了强签名的,而在 NuGet 中获取到的并没有签名,虽然我可以手动加上一个签名,但这样会造成二次操作,容易在更新时忘记,所以,还是希望从 NuGet 中获致到的版本本身就是带签名的,谢谢。

Hello, thank you for providing such a useful library, in the process of using it, I usually download the source code plus analysis, and then get the version directly from NuGet and add it to the project reference, but because my projects are using a strong signature, and the one obtained in NuGet does not have a signature, although I can add a signature manually, but this will cause a second operation, easy to forget when updating, so, I still hope that the version obtained from NuGet itself is signed, thank you.

Translated with www.DeepL.com/Translator (free version)

Natural logarithms.

Hi. I'm not able to figure out how to solve equations with natural logarithms.
Have not been able to find E ( Log(x, E) ) or find any Ln(x).
Any suggestions?

NCalc 101

Struggling to debug your NCalcs? Check out https://ncalc101.magicsuite.net/ !

Features:

  • Create, update, export, import and delete "Workspaces"
  • Workspaces have their own set of variables
  • Workspaces are stored in browser local storage
  • Import/Export to share between users
  • Mobile friendly

Your feedback welcome.

Performance after upgrade to antlr4

Hi, there is a performance hit after upgrade to Antlr 4.
You can compare performance tests on AppVeyor:

2.2.111: https://ci.appveyor.com/project/sklose/ncalc2/builds/46365044/tests
3.0.119: https://ci.appveyor.com/project/sklose/ncalc2/build/tests

test 3.0.119 2.2.111
NCalc.Tests.Performance.ParameterAccess(formula: "[Param1] * 7 + [Param2]") 176 ms 95 ms
NCalc.Tests.Performance.Arithmetics(formula: "(4 * 12 / 7) + ((9 * 2) % 8)") 424 ms 102 ms
NCalc.Tests.Performance.DynamicParameterAccess(formula: "[Param1] * 7 + [Param2]") 156 ms 89 ms
NCalc.Tests.Performance.Arithmetics(formula: "5 * 2 = 2 * 5 && (1 / 3.0) * 3 = 1") 805 ms 379 ms
NCalc.Tests.Performance.FunctionWithDynamicParameterAccess(formula: "Foo([Param1] * 7, [Param2])") 265 ms 87 ms

@psiservices-dbrink, is any ideas how to improve the performance?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.