Skip to content

Commit

Permalink
Check for null when comparing using the most precise type. (#16)
Browse files Browse the repository at this point in the history
* Check for null when comparing using the most precise type. This allows null value parameters to be used in expressions such as "'a string' == null" where null is defined as e.Parameters["null"] = null.

* Added AllowNullParameter option to define a null parameter and allow comparison of values to null. Set as an option to not affecting existing use of NCalc.

* Added some additional null checks from #3

* Remove .GetType() call from loop, fix typo.

* Removed unused overload.
  • Loading branch information
spudstuff authored and pitermarx committed Aug 30, 2018
1 parent 6b2e52a commit 3dbd6c1
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 5 deletions.
77 changes: 76 additions & 1 deletion Evaluant.Calculator.Tests/Fixtures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,81 @@ public void ExpressionShouldEvaluateParameters()
Assert.AreEqual(117.07, e.Evaluate());
}

[Test]
public void ExpressionShouldHandleNullRightParameters()
{
var e = new Expression("'a string' == null", EvaluateOptions.AllowNullParameter);

Assert.AreEqual(false, e.Evaluate());
}

[Test]
public void ExpressionShouldHandleNullLeftParameters()
{
var e = new Expression("null == 'a string'", EvaluateOptions.AllowNullParameter);

Assert.AreEqual(false, e.Evaluate());
}

[Test]
public void ExpressionShouldHandleNullBothParameters()
{
var e = new Expression("null == null", EvaluateOptions.AllowNullParameter);

Assert.AreEqual(true, e.Evaluate());
}

[Test]
public void ShouldCompareNullToNull()
{
var e = new Expression("[x] = null", EvaluateOptions.AllowNullParameter);

e.Parameters["x"] = null;

Assert.AreEqual(true, e.Evaluate());
}

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

e.Parameters["x"] = (int?)5;
Assert.AreEqual(true, e.Evaluate());

e.Parameters["x"] = (int?)6;
Assert.AreEqual(false, e.Evaluate());
}

[Test]
public void ShouldCompareNullToString()
{
var e = new Expression("[x] = 'foo'", EvaluateOptions.AllowNullParameter);

e.Parameters["x"] = null;

Assert.AreEqual(false, e.Evaluate());
}

[Test]
public void ExpressionDoesNotDefineNullParameterWithoutNullOption()
{
var e = new Expression("'a string' == null");

var ex = Assert.Throws<ArgumentException>(() => e.Evaluate());
Assert.IsTrue(ex.Message.Contains("Parameter name: null"));
}

[Test]
public void ExpressionThrowsNullReferenceExceptionWithoutNullOption()
{
var e = new Expression("'a string' == null");

e.Parameters["null"] = null;

Assert.Throws<NullReferenceException>(() => e.Evaluate());
}

[Test]
public void ShouldEvaluateConditionnal()
{
Expand Down Expand Up @@ -260,7 +335,7 @@ public void ShouldNotLoosePrecision()
}

[Test]
public void ShouldThrowAnExpcetionWhenInvalidNumber()
public void ShouldThrowAnExceptionWhenInvalidNumber()
{
try
{
Expand Down
10 changes: 7 additions & 3 deletions Evaluant.Calculator/Domain/EvaluationVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public override void Visit(LogicalExpression expression)
private static readonly Type[] CommonTypes = { typeof(Int64), typeof(Double), typeof(Boolean), typeof(String), typeof(Decimal) };

/// <summary>
/// Gets the the most precise type.
/// Gets the the most precise type of two types.
/// </summary>
/// <param name="a">Type a.</param>
/// <param name="b">Type b.</param>
Expand All @@ -44,12 +44,16 @@ private static Type GetMostPreciseType(Type a, Type b)
}
}

return a;
return a ?? b;
}

public int CompareUsingMostPreciseType(object a, object b)
{
Type mpt = GetMostPreciseType(a.GetType(), b.GetType());
Type mpt = options.AllowNullParameter()
// Allow nulls to be compared with other values
? GetMostPreciseType(a?.GetType(), b?.GetType()) ?? typeof(object)
// No breaking changes fallback
: GetMostPreciseType(a.GetType(), b.GetType());
return Comparer.Default.Compare(Convert.ChangeType(a, mpt), Convert.ChangeType(b, mpt));
}

Expand Down
12 changes: 11 additions & 1 deletion Evaluant.Calculator/EvaluationOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ public enum EvaluateOptions
//
// Summary:
// When using Round(), if a number is halfway between two others, it is rounded toward the nearest number that is away from zero.
RoundAwayFromZero = 16
RoundAwayFromZero = 16,

//
// Summary:
// Defines a "null" parameter and allows comparison of values to null.
AllowNullParameter = 32
}

internal static class EvaluateOptionsExtensions
Expand Down Expand Up @@ -58,5 +63,10 @@ public static bool RoundAwayFromZero(this EvaluateOptions opts)
{
return opts.Has(EvaluateOptions.RoundAwayFromZero);
}

public static bool AllowNullParameter(this EvaluateOptions opts)
{
return opts.Has(EvaluateOptions.AllowNullParameter);
}
}
}
7 changes: 7 additions & 0 deletions Evaluant.Calculator/Expression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ public object Evaluate()
visitor.EvaluateParameter += EvaluateParameter;
visitor.Parameters = Parameters;

// Add a "null" parameter which returns null if configured to do so
// Configured as an option to ensure no breaking changes for historical use
if (Options.AllowNullParameter() && !visitor.Parameters.ContainsKey("null"))
{
visitor.Parameters["null"] = null;
}

// if array evaluation, execute the same expression multiple times
if (Options.IterateParameters())
{
Expand Down

0 comments on commit 3dbd6c1

Please sign in to comment.