diff --git a/src/DynamicExpresso.Core/Parsing/Parser.cs b/src/DynamicExpresso.Core/Parsing/Parser.cs index faa0445..6ef2aec 100644 --- a/src/DynamicExpresso.Core/Parsing/Parser.cs +++ b/src/DynamicExpresso.Core/Parsing/Parser.cs @@ -1996,7 +1996,7 @@ private Expression GenerateNotEqual(Expression left, Expression right) private Expression GenerateGreaterThan(Expression left, Expression right) { - if (left.Type == typeof(string)) + if (left.Type == typeof(string) && right.Type == typeof(string)) { return Expression.GreaterThan( GenerateStaticMethodCall("Compare", left, right), @@ -2008,7 +2008,7 @@ private Expression GenerateGreaterThan(Expression left, Expression right) private Expression GenerateGreaterThanEqual(Expression left, Expression right) { - if (left.Type == typeof(string)) + if (left.Type == typeof(string) && right.Type == typeof(string)) { return Expression.GreaterThanOrEqual( GenerateStaticMethodCall("Compare", left, right), @@ -2020,7 +2020,7 @@ private Expression GenerateGreaterThanEqual(Expression left, Expression right) private Expression GenerateLessThan(Expression left, Expression right) { - if (left.Type == typeof(string)) + if (left.Type == typeof(string) && right.Type == typeof(string)) { return Expression.LessThan( GenerateStaticMethodCall("Compare", left, right), @@ -2033,7 +2033,7 @@ private Expression GenerateLessThan(Expression left, Expression right) private Expression GenerateLessThanEqual(Expression left, Expression right) { - if (left.Type == typeof(string)) + if (left.Type == typeof(string) && right.Type == typeof(string)) { return Expression.LessThanOrEqual( GenerateStaticMethodCall("Compare", left, right), @@ -2081,6 +2081,10 @@ private Expression GenerateBinary(ExpressionType binaryType, Expression left, Ex } var applicableMethod = FindBinaryOperator(opName, left, right); + if (applicableMethod == null) + { + applicableMethod = FindBinaryOperatorWithImplicitConversion(opName, ref left, ref right); + } MethodInfo operatorMethod = null; if (applicableMethod != null) @@ -2175,6 +2179,64 @@ private MethodData FindBinaryOperator(string operatorName, Expression left, Expr return userDefinedOperator; } + private MethodData FindBinaryOperatorWithImplicitConversion(string operatorName, ref Expression left, ref Expression right) + { + if (operatorName == null) + return null; + + var leftType = left.Type; + var rightType = right.Type; + var convertedLeft = TryGenerateImplicitConversion(left, rightType); + var leftConvertedOperator = convertedLeft != null + ? FindBinaryOperator(operatorName, convertedLeft, right) + : null; + + var convertedRight = TryGenerateImplicitConversion(right, leftType); + var rightConvertedOperator = convertedRight != null + ? FindBinaryOperator(operatorName, left, convertedRight) + : null; + + if (leftConvertedOperator != null && rightConvertedOperator != null && !ReferenceEquals(leftConvertedOperator.MethodBase, rightConvertedOperator.MethodBase)) + { + throw ParseException.Create(_token.pos, ErrorMessages.AmbiguousBinaryOperatorInvocation, operatorName, TypeUtils.GetTypeName(leftType), TypeUtils.GetTypeName(rightType)); + } + + if (leftConvertedOperator != null) + { + left = convertedLeft; + return leftConvertedOperator; + } + + if (rightConvertedOperator != null) + { + right = convertedRight; + return rightConvertedOperator; + } + + return null; + } + + private static Expression TryGenerateImplicitConversion(Expression expression, Type type) + { + if (!HasImplicitConversion(expression.Type, type)) + return null; + + return Expression.Convert(expression, type); + } + + private static bool HasImplicitConversion(Type sourceType, Type targetType) + { + return HasImplicitConversionOperator(sourceType, sourceType, targetType) || + HasImplicitConversionOperator(targetType, sourceType, targetType); + } + + private static bool HasImplicitConversionOperator(Type declaringType, Type sourceType, Type targetType) + { + return declaringType.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Any(method => method.Name == "op_Implicit" && method.ReturnType == targetType && + method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == sourceType); + } + private static Expression GenerateStringConcat(Expression left, Expression right) { var leftObj = GenerateStringConcatOperand(left); diff --git a/test/DynamicExpresso.UnitTest/OperatorsTest.cs b/test/DynamicExpresso.UnitTest/OperatorsTest.cs index 6989320..418ee7c 100644 --- a/test/DynamicExpresso.UnitTest/OperatorsTest.cs +++ b/test/DynamicExpresso.UnitTest/OperatorsTest.cs @@ -858,6 +858,202 @@ public void Can_mix_overloaded_operators() Assert.That(target.Eval("(x + z) == \"13\"", new Parameter("z", z)), Is.True); } + [TestCase("m < \"60\"", true)] + [TestCase("m <= \"50\"", true)] + [TestCase("m > \"40\"", true)] + [TestCase("m >= \"50\"", true)] + [TestCase("\"40\" < m", true)] + [TestCase("\"50\" <= m", true)] + [TestCase("\"60\" > m", true)] + [TestCase("\"50\" >= m", true)] + public void Can_use_string_relational_operators_on_custom_type(string expression, bool expected) + { + var target = new Interpreter() + .SetVariable("m", new MoneyWithStringRelationalOperators(50)); + + Assert.That(target.Eval(expression), Is.EqualTo(expected)); + } + + [TestCase("m < \"60\"", true)] + [TestCase("m <= \"50\"", true)] + [TestCase("m > \"40\"", true)] + [TestCase("m >= \"50\"", true)] + [TestCase("\"40\" < m", true)] + [TestCase("\"50\" <= m", true)] + [TestCase("\"60\" > m", true)] + [TestCase("\"50\" >= m", true)] + public void Can_use_implicit_conversion_fallback_for_string_relational_operators_on_custom_type(string expression, bool expected) + { + var target = new Interpreter() + .SetVariable("m", new MoneyWithImplicitConversionOnlyRelationalOperators(50)); + + Assert.That(target.Eval(expression), Is.EqualTo(expected)); + } + + [TestCase("m == \"50\"", true)] + [TestCase("m != \"10\"", true)] + [TestCase("\"50\" == m", true)] + [TestCase("\"10\" != m", true)] + public void Can_use_string_equality_operators_on_custom_type(string expression, bool expected) + { + var target = new Interpreter() + .SetVariable("m", new MoneyWithStringRelationalOperators(50)); + + Assert.That(target.Eval(expression), Is.EqualTo(expected)); + } + + private sealed class MoneyWithStringRelationalOperators + { + private readonly decimal _amount; + + public MoneyWithStringRelationalOperators(decimal amount) + { + _amount = amount; + } + + public static implicit operator MoneyWithStringRelationalOperators(string value) + { + return new MoneyWithStringRelationalOperators(decimal.Parse(value)); + } + + public static bool operator <(MoneyWithStringRelationalOperators left, string right) + { + return left._amount < decimal.Parse(right); + } + + public static bool operator >(MoneyWithStringRelationalOperators left, string right) + { + return left._amount > decimal.Parse(right); + } + + public static bool operator <=(MoneyWithStringRelationalOperators left, string right) + { + return left._amount <= decimal.Parse(right); + } + + public static bool operator >=(MoneyWithStringRelationalOperators left, string right) + { + return left._amount >= decimal.Parse(right); + } + + public static bool operator <(string left, MoneyWithStringRelationalOperators right) + { + return decimal.Parse(left) < right._amount; + } + + public static bool operator >(string left, MoneyWithStringRelationalOperators right) + { + return decimal.Parse(left) > right._amount; + } + + public static bool operator <=(string left, MoneyWithStringRelationalOperators right) + { + return decimal.Parse(left) <= right._amount; + } + + public static bool operator >=(string left, MoneyWithStringRelationalOperators right) + { + return decimal.Parse(left) >= right._amount; + } + + public static bool operator ==(MoneyWithStringRelationalOperators left, string right) + { + return !ReferenceEquals(left, null) && left._amount == decimal.Parse(right); + } + + public static bool operator !=(MoneyWithStringRelationalOperators left, string right) + { + return !(left == right); + } + + public static bool operator ==(string left, MoneyWithStringRelationalOperators right) + { + return !ReferenceEquals(right, null) && decimal.Parse(left) == right._amount; + } + + public static bool operator !=(string left, MoneyWithStringRelationalOperators right) + { + return !(left == right); + } + + public override bool Equals(object obj) + { + var money = obj as MoneyWithStringRelationalOperators; + return money != null && _amount == money._amount; + } + + public override int GetHashCode() + { + return _amount.GetHashCode(); + } + } + + private sealed class MoneyWithImplicitConversionOnlyRelationalOperators + { + private readonly decimal _amount; + + public MoneyWithImplicitConversionOnlyRelationalOperators(decimal amount) + { + _amount = amount; + } + + public static implicit operator MoneyWithImplicitConversionOnlyRelationalOperators(string value) + { + return new MoneyWithImplicitConversionOnlyRelationalOperators(decimal.Parse(value)); + } + + public static bool operator <(MoneyWithImplicitConversionOnlyRelationalOperators left, string right) + { + return left._amount < decimal.Parse(right); + } + + public static bool operator >(MoneyWithImplicitConversionOnlyRelationalOperators left, string right) + { + return left._amount > decimal.Parse(right); + } + + public static bool operator <=(MoneyWithImplicitConversionOnlyRelationalOperators left, string right) + { + return left._amount <= decimal.Parse(right); + } + + public static bool operator >=(MoneyWithImplicitConversionOnlyRelationalOperators left, string right) + { + return left._amount >= decimal.Parse(right); + } + + public static bool operator <(MoneyWithImplicitConversionOnlyRelationalOperators left, MoneyWithImplicitConversionOnlyRelationalOperators right) + { + return left._amount < right._amount; + } + + public static bool operator >(MoneyWithImplicitConversionOnlyRelationalOperators left, MoneyWithImplicitConversionOnlyRelationalOperators right) + { + return left._amount > right._amount; + } + + public static bool operator <=(MoneyWithImplicitConversionOnlyRelationalOperators left, MoneyWithImplicitConversionOnlyRelationalOperators right) + { + return left._amount <= right._amount; + } + + public static bool operator >=(MoneyWithImplicitConversionOnlyRelationalOperators left, MoneyWithImplicitConversionOnlyRelationalOperators right) + { + return left._amount >= right._amount; + } + + public override bool Equals(object obj) + { + var money = obj as MoneyWithImplicitConversionOnlyRelationalOperators; + return money != null && _amount == money._amount; + } + + public override int GetHashCode() + { + return _amount.GetHashCode(); + } + } + [Test] public void Throw_an_exception_if_a_custom_type_doesnt_define_equal_operator()