From a7296a717d1385b5ad04d3a1c9c136cf8db51b69 Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Thu, 19 Sep 2024 18:14:57 +1200 Subject: [PATCH] Add additonal functions * Added full functions mentioned on https://www.odata.org/documentation/odata-version-2-0/uri-conventions/ * Added features to allow function overloading * Included documentation of functions and operations supported to readme --- README.md | 56 +++++++ .../FunctionArgumentCountException.cs | 10 +- .../FunctionOverloadNotFoundException.cs | 32 ++++ .../FunctionCallDefinition.cs | 139 ++++++++++++---- .../Languages/ODataFilterLanguage.cs | 157 +++++++++++++++++- .../Util/ExpressionConversions.cs | 22 +++ .../ODataFilter/ODataArithmeticTests.cs | 31 ---- .../Languages/ODataFilter/ODataTests.cs | 74 +++++++++ .../Parser/ParserErrorTests.cs | 2 +- 9 files changed, 448 insertions(+), 75 deletions(-) create mode 100644 src/StringToExpression/Exceptions/FunctionOverloadNotFoundException.cs delete mode 100644 tests/StringToExpression.Test/Languages/ODataFilter/ODataArithmeticTests.cs create mode 100644 tests/StringToExpression.Test/Languages/ODataFilter/ODataTests.cs diff --git a/README.md b/README.md index ec0f38d..30ae10a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,62 @@ public async Task GetDoohickies([FromUri(Name = "$filter")] s `StringToExpression` has the advantage of being configurable; if the OData parser doesnt support methods you want, (or it supports methods you dont want) it is very easy to extend `ODataFilterLanguage` and modify the configuration +### Supported Operations + +| Operators | Name | Example | +|----------------------|-----------------------|------------------------------------| +| eq | Equal | City eq 'Redmond' | +| ne | Not equal | City ne 'London' | +| gt | Greater than | Price gt 20 | +| ge | Greater than or equal | Price ge 10 | +| lt | Less than | Price lt 20 | +| le | Less than or equal | Price le 100 | +| and | Logical and | Price le 200 and Price gt 3.5 | +| or | Logical or | Price le 3.5 or Price gt 200 | +| not | Logical negation | not endswith(Description,'milk') | +| add | Addition | Price add 5 gt 10 | +| sub | Subtraction | Price sub 5 gt 10 | +| mul | Multiplication | Price mul 2 gt 2000 | +| div | Division | Price div 2 gt 4 | +| mod | Modulo | Price mod 2 eq 0 | +| ( ) | Precedence grouping | (Price sub 5) gt 10 | +| / | Property access | Address/City eq 'Redmond' | + +### Supported Functions + +| String Functions | Example | +|-----------------------------------------------------------|---------------------------------------------------| +| bool substringof(string po, string p1) | substringof('day', 'Monday') eq true | +| bool endswith(string p0, string p1) | endswith('Monday', 'day') eq true | +| bool startswith(string p0, string p1) | startswith('Monday', 'Mon') eq true | +| int length(string p0) | length('Monday') eq 6 | Price ge 10 | +| int indexof(string p0, string p1) | indexof('Monday', 'n') eq 2 | +| string replace(string p0, string find, string replace) | replace('Monday', 'Mon', 'Satur') eq 'Saturday' | +| string substring(string p0, int pos) | substring('Monday', 3) eq 'day' | +| string substring(string p0, int pos, int length) | substring('Monday', 3, 2) eq 'da' | +| string tolower(string p0) | tolower('Monday') eq 'monday' | +| string toupper(string p0) | toupper('Monday') eq 'MONDAY' | +| string trim(string p0) | trim(' Monday ') eq 'Monday' | +| string concat(string p0, string p1) | concat('Mon', 'day') eq 'Monday' | + +| Date Functions | Example | +|-----------------------------------------------------------|---------------------------------------------------| +| int day(DateTime p0) | day(datetime'2000-01-02T03:04:05') eq 2 | +| int hour(DateTime p0) | hour(datetime'2000-01-02T03:04:05') eq 3 | +| int minute(DateTime p0) | minute(datetime'2000-01-02T03:04:05') eq 4 | +| int month(DateTime p0) | month(datetime'2000-01-02T03:04:05') eq 1 | +| int second(DateTime p0) | second(datetime'2000-01-02T03:04:05') eq 5 | +| int year(DateTime p0) | year(datetime'2000-01-02T03:04:05') eq 2000 | + +| Math Functions | Example | +|-----------------------------------------------------------|---------------------------------------------------| +| double round(double p0) | round(10.4) eq 10
round(10.6) eq 11
round(10.5) eq 10
round(11.5) eq 12 +| double floor(double p0) | floor(10.6) eq 10 +| decimal floor(decimal p0) | month(datetime'2000-01-02T03:04:05') eq 1 +| double ceiling(double p0) | ceiling(10.4) eq 11 + + + ## Custom languages Languages are defined by a set of `GrammerDefintions`. These define both how the string is broken up into tokens as well as the behaviour of each token. There are many subclasses of `GrammerDefinition` that makes implementing standard language features very easy. diff --git a/src/StringToExpression/Exceptions/FunctionArgumentCountException.cs b/src/StringToExpression/Exceptions/FunctionArgumentCountException.cs index da4c409..6e518d2 100644 --- a/src/StringToExpression/Exceptions/FunctionArgumentCountException.cs +++ b/src/StringToExpression/Exceptions/FunctionArgumentCountException.cs @@ -11,7 +11,7 @@ public class FunctionArgumentCountException : ParseException /// /// String segment that contains the bracket that contains the incorrect number of operands. /// - public readonly StringSegment BracketStringSegment; + public readonly StringSegment FunctionStringSegment; /// /// Expected number of operands. @@ -26,13 +26,13 @@ public class FunctionArgumentCountException : ParseException /// /// Initializes a new instance of the class. /// - /// The location of the function arguments. + /// The location of the function arguments. /// The Expected number of operands. /// The actual number of operands. - public FunctionArgumentCountException(StringSegment bracketStringSegment, int expectedOperandCount, int actualOperandCount) - : base(bracketStringSegment, $"Bracket '{bracketStringSegment.Value}' contains {actualOperandCount} operand{(actualOperandCount > 1 ? "s" : "")} but was expecting {expectedOperandCount} operand{(expectedOperandCount > 1 ? "s" : "")}") + public FunctionArgumentCountException(StringSegment functionStringSegment, int expectedOperandCount, int actualOperandCount) + : base(functionStringSegment, $"Function '{functionStringSegment.Value}' contains {actualOperandCount} operand{(actualOperandCount > 1 ? "s" : "")} but was expecting {expectedOperandCount} operand{(expectedOperandCount > 1 ? "s" : "")}") { - BracketStringSegment = bracketStringSegment; + FunctionStringSegment = functionStringSegment; ExpectedOperandCount = expectedOperandCount; ActualOperandCount = actualOperandCount; } diff --git a/src/StringToExpression/Exceptions/FunctionOverloadNotFoundException.cs b/src/StringToExpression/Exceptions/FunctionOverloadNotFoundException.cs new file mode 100644 index 0000000..7987e6a --- /dev/null +++ b/src/StringToExpression/Exceptions/FunctionOverloadNotFoundException.cs @@ -0,0 +1,32 @@ +using StringToExpression.Util; +using System; + +namespace StringToExpression.Exceptions +{ + /// + /// Exception when a cannot find correct overlaod for function + /// + public class FunctionOverlaodNotFoundException : ParseException + { + /// + /// String segment that contains the bracket that contains the incorrect number of operands. + /// + public readonly StringSegment FunctionStringSegment; + + /// + /// Actual type of operands. + /// + public readonly Type[] ActualArgumentTypes; + + /// + /// Initializes a new instance of the class. + /// + /// The location of the function + public FunctionOverlaodNotFoundException(StringSegment functionStringSegment) + : base(functionStringSegment, $"Function '{functionStringSegment.Value}' is not defiend wtih those input types") + { + FunctionStringSegment = functionStringSegment; + + } + } +} diff --git a/src/StringToExpression/GrammerDefinitions/FunctionCallDefinition.cs b/src/StringToExpression/GrammerDefinitions/FunctionCallDefinition.cs index ab305ce..de235d1 100644 --- a/src/StringToExpression/GrammerDefinitions/FunctionCallDefinition.cs +++ b/src/StringToExpression/GrammerDefinitions/FunctionCallDefinition.cs @@ -17,15 +17,33 @@ namespace StringToExpression.GrammerDefinitions /// public class FunctionCallDefinition : BracketOpenDefinition { - /// - /// Argument types that the function accepts. - /// - public readonly IReadOnlyList ArgumentTypes; + + public class Overload + { + /// + /// Argument types that the function accepts. + /// + public readonly IReadOnlyList ArgumentTypes; + + /// + /// A function given the arguments, outputs a new operand. + /// + public readonly Func ExpressionBuilder; + + public Overload( + IEnumerable argumentTypes, + Func expressionBuilder) + { + this.ArgumentTypes = argumentTypes?.ToList(); + this.ExpressionBuilder = expressionBuilder; + } + } + /// - /// A function given the arguments, outputs a new operand. + /// Function overlaods /// - public readonly Func ExpressionBuilder; + public readonly IReadOnlyList Overloads; /// /// Initializes a new instance of the class. @@ -39,10 +57,29 @@ public FunctionCallDefinition( string regex, IEnumerable argumentTypes, Func expressionBuilder) + : this(name, regex, new[] { new Overload(argumentTypes, expressionBuilder) }) + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the definition. + /// The regex to match tokens. + /// list of overloads avilable for function + public FunctionCallDefinition( + string name, + string regex, + IEnumerable overloads) : base(name, regex) { - this.ArgumentTypes = argumentTypes?.ToList(); - this.ExpressionBuilder = expressionBuilder; + var overloadList = overloads?.ToList(); + if (overloadList.Count == 0) + { + throw new ArgumentException("Must specify at least one overlaod", nameof(overloads)); + } + this.Overloads = overloadList; } /// @@ -56,6 +93,58 @@ public FunctionCallDefinition(string name, string regex,Func bracketOperands, out IEnumerable typedArguments) + { + var possibleOverloads = Overloads + .Where(x => x.ArgumentTypes == null || x.ArgumentTypes.Count == bracketOperands.Count) + .OrderBy(x=>x.ArgumentTypes == null); + + // No viable overloads, user has probably inputted wrong number of arguments + if (!possibleOverloads.Any()) + { + throw new FunctionArgumentCountException( + StringSegment.Encompass(bracketOperands.Select(x => x.SourceMap)), + Overloads.First().ArgumentTypes.Count, + bracketOperands.Count); + } + + foreach(var possibleOverload in possibleOverloads) + { + //null argument types is treated as a I can accept anything + if (possibleOverload.ArgumentTypes == null) + { + typedArguments = bracketOperands.Select(x => x.Expression); + return possibleOverload; + } + + var argumentMatch = bracketOperands.Zip(possibleOverload.ArgumentTypes, (o, t) => { + var canConvert = ExpressionConversions.TryConvert(o.Expression, t, out var result); + return new { CanConvert = canConvert, Operand = o, ArgumentType = t, ConvertedOperand = result }; + }); + + + if (argumentMatch.All(x => x.CanConvert)) + { + typedArguments = argumentMatch.Select(x => x.ConvertedOperand); + return possibleOverload; + } + + // If we have only a single possible overlaod but we arguement types dont align + // we will throw an error as though they had the wrong types + if (possibleOverloads.Count() == 1) + { + var badConvert = argumentMatch.First(x => !x.CanConvert); + throw new FunctionArgumentTypeException(badConvert.Operand.SourceMap, badConvert.ArgumentType, badConvert.Operand.Expression.Type); + } + } + + //We had multiple overloads, but none of them matched + throw new FunctionOverlaodNotFoundException(StringSegment.Encompass(bracketOperands.Select(x => x.SourceMap))); + + + } + /// /// Applies the bracket operands. Executes the expressionBuilder with all the operands in the brackets. /// @@ -68,39 +157,19 @@ public FunctionCallDefinition(string name, string regex,FuncWhen an error occured while executing the expressionBuilder public override void ApplyBracketOperands(Operator bracketOpen, Stack bracketOperands, Operator bracketClose, ParseState state) { - var operandSource = StringSegment.Encompass(bracketOperands.Select(x => x.SourceMap)); - var functionArguments = bracketOperands.Select(x => x.Expression); - //if we have been given specific argument types validate them - if (ArgumentTypes != null) - { - var expectedArgumentCount = ArgumentTypes.Count; - if (expectedArgumentCount != bracketOperands.Count) - throw new FunctionArgumentCountException( - operandSource, - expectedArgumentCount, - bracketOperands.Count); - - functionArguments = bracketOperands.Zip(ArgumentTypes, (o, t) => { - try - { - return ExpressionConversions.Convert(o.Expression, t); - } - catch(InvalidOperationException) - { - //if we cant convert to the argument type then something is wrong with the argument - //so we will throw it up - throw new FunctionArgumentTypeException(o.SourceMap, t, o.Expression.Type); - } - }); + var functionSourceMap = StringSegment.Encompass( + bracketOpen.SourceMap, + StringSegment.Encompass(bracketOperands.Select(x => x.SourceMap)), + bracketClose.SourceMap); - } + var overload = MatchOverload(bracketOperands, out var functionArguments); - var functionSourceMap = StringSegment.Encompass(bracketOpen.SourceMap, operandSource); + var functionArgumentsArray = functionArguments.ToArray(); Expression output; try { - output = ExpressionBuilder(functionArgumentsArray); + output = overload.ExpressionBuilder(functionArgumentsArray); } catch(Exception ex) { diff --git a/src/StringToExpression/Languages/ODataFilterLanguage.cs b/src/StringToExpression/Languages/ODataFilterLanguage.cs index f1533a4..c92eff8 100644 --- a/src/StringToExpression/Languages/ODataFilterLanguage.cs +++ b/src/StringToExpression/Languages/ODataFilterLanguage.cs @@ -31,6 +31,11 @@ protected static class StringMembers /// public static MethodInfo EndsWith = Type.Method(x => x.EndsWith(default(string))); + /// + /// The MemberInfo for the Length property + /// + public static MemberInfo Length = Type.Member(x => x.Length); + /// /// The MethodInfo for the Contains method /// @@ -45,6 +50,36 @@ protected static class StringMembers /// The MethodInfo for the ToUpper method /// public static MethodInfo ToUpper = Type.Method(x => x.ToUpper()); + + /// + /// The MethodInfo for the IndexOf method + /// + public static MethodInfo IndexOf = Type.Method(x => x.IndexOf(default(string))); + + /// + /// The MethodInfo for the Replace method + /// + public static MethodInfo Replace = Type.Method(x => x.Replace(default(string), default(string))); + + /// + /// The MethodInfo for the Substring method + /// + public static MethodInfo Substring = Type.Method(x => x.Substring(default(int))); + + /// + /// The MethodInfo for the Trim method + /// + public static MethodInfo Trim = Type.Method(x => x.Trim()); + + /// + /// The MethodInfo for the Trim method + /// + public static MethodInfo Concat = Type.Method(x => string.Concat(default(string), default(string))); + + /// + /// The MethodInfo for the Substring method that contains a length parameter + /// + public static MethodInfo SubstringWithLength = Type.Method(x => x.Substring(default(int), default(int))); } /// @@ -83,6 +118,24 @@ protected static class DateTimeMembers public static MemberInfo Second = Type.Member(x => x.Second); } + protected static class MathMembers + { + /// + /// The MethodInfo for the Round method + /// + public static MethodInfo Round = Type.Method(x => Math.Round(default(double))); + + /// + /// The MethodInfo for the Floor method + /// + public static MethodInfo Floor = Type.Method(x => Math.Floor(default(double))); + + /// + /// The MethodInfo for the Ceil method + /// + public static MethodInfo Ceiling = Type.Method(x => Math.Ceiling(default(double))); + } + private readonly Language language; /// @@ -379,6 +432,76 @@ protected virtual IEnumerable FunctionDefinitions() instance:parameters[0], method:StringMembers.ToUpper); }), + new FunctionCallDefinition( + name:"FN_LENGTH", + regex: @"length\(", + argumentTypes: new[] {typeof(string) }, + expressionBuilder: (parameters) => { + return Expression.MakeMemberAccess( + expression:parameters[0], + member:StringMembers.Length); + }), + new FunctionCallDefinition( + name:"FN_INDEXOF", + regex: @"indexof\(", + argumentTypes: new[] {typeof(string), typeof(string) }, + expressionBuilder: (parameters) => { + return Expression.Call( + instance:parameters[0], + method:StringMembers.IndexOf, + arguments: new [] { parameters[1] }); + }), + new FunctionCallDefinition( + name:"FN_REPLACE", + regex: @"replace\(", + argumentTypes: new[] {typeof(string), typeof(string), typeof(string) }, + expressionBuilder: (parameters) => { + return Expression.Call( + instance:parameters[0], + method:StringMembers.Replace, + arguments: new [] { parameters[1], parameters[2] }); + }), + new FunctionCallDefinition( + name:"FN_SUBSTRING", + regex: @"substring\(", + overloads: new[] + { + new FunctionCallDefinition.Overload( + argumentTypes: new[] {typeof(string), typeof(int)}, + expressionBuilder: (parameters) => { + return Expression.Call( + instance:parameters[0], + method:StringMembers.Substring, + arguments: new [] { parameters[1]}); + }), + new FunctionCallDefinition.Overload( + argumentTypes: new[] {typeof(string), typeof(int), typeof(int)}, + expressionBuilder: (parameters) => { + return Expression.Call( + instance:parameters[0], + method:StringMembers.SubstringWithLength, + arguments: new [] { parameters[1], parameters[2]}); + }) + }), + new FunctionCallDefinition( + name:"FN_TRIM", + regex: @"trim\(", + argumentTypes: new []{typeof(string)}, + expressionBuilder: (parameters) => { + return Expression.Call( + instance:parameters[0], + method:StringMembers.Trim); + }), + new FunctionCallDefinition( + name:"FN_CONCAT", + regex: @"concat\(", + argumentTypes: new[] {typeof(string), typeof(string)}, + expressionBuilder: (parameters) => { + return Expression.Call( + method:StringMembers.Concat, + arguments: new [] { parameters[0], parameters[1]}); + }), + new FunctionCallDefinition( name:"FN_DAY", @@ -398,7 +521,7 @@ protected virtual IEnumerable FunctionDefinitions() parameters[0], DateTimeMembers.Hour); }), - new FunctionCallDefinition( + new FunctionCallDefinition( name:"FN_MINUTE", regex: @"minute\(", argumentTypes: new[] {typeof(DateTime) }, @@ -407,7 +530,7 @@ protected virtual IEnumerable FunctionDefinitions() parameters[0], DateTimeMembers.Minute); }), - new FunctionCallDefinition( + new FunctionCallDefinition( name:"FN_MONTH", regex: @"month\(", argumentTypes: new[] {typeof(DateTime) }, @@ -425,7 +548,7 @@ protected virtual IEnumerable FunctionDefinitions() parameters[0], DateTimeMembers.Year); }), - new FunctionCallDefinition( + new FunctionCallDefinition( name:"FN_SECOND", regex: @"second\(", argumentTypes: new[] {typeof(DateTime) }, @@ -434,6 +557,34 @@ protected virtual IEnumerable FunctionDefinitions() parameters[0], DateTimeMembers.Second); }), + + new FunctionCallDefinition( + name:"FN_ROUND", + regex: @"round\(", + argumentTypes: new[] {typeof(double) }, + expressionBuilder: (parameters) => { + return Expression.Call( + method:MathMembers.Round, + arguments: new [] { parameters[0]}); + }), + new FunctionCallDefinition( + name:"FN_FLOOR", + regex: @"floor\(", + argumentTypes: new[] {typeof(double) }, + expressionBuilder: (parameters) => { + return Expression.Call( + method:MathMembers.Floor, + arguments: new [] { parameters[0]}); + }), + new FunctionCallDefinition( + name:"FN_CEILING", + regex: @"ceiling\(", + argumentTypes: new[] {typeof(double) }, + expressionBuilder: (parameters) => { + return Expression.Call( + method:MathMembers.Ceiling, + arguments: new [] { parameters[0]}); + }), }; } diff --git a/src/StringToExpression/Util/ExpressionConversions.cs b/src/StringToExpression/Util/ExpressionConversions.cs index ba829be..fe64caa 100644 --- a/src/StringToExpression/Util/ExpressionConversions.cs +++ b/src/StringToExpression/Util/ExpressionConversions.cs @@ -239,6 +239,28 @@ public static Expression Convert(Expression exp, Type type) : Expression.Convert(exp, type); } + /// + /// Try to converts an expression to the given type only if it is not that type already. + /// + /// + /// + /// Expression of the given type + /// true if a common type exists; otherwise, false. + public static bool TryConvert(Expression exp, Type type, out Expression result) + { + try + { + result = Convert(exp, type); + return true; + } + + catch (InvalidOperationException) + { + result = default(Expression); + return false; + } + } + /// /// Determines if expression is a null constant. /// diff --git a/tests/StringToExpression.Test/Languages/ODataFilter/ODataArithmeticTests.cs b/tests/StringToExpression.Test/Languages/ODataFilter/ODataArithmeticTests.cs deleted file mode 100644 index ee36f4b..0000000 --- a/tests/StringToExpression.Test/Languages/ODataFilter/ODataArithmeticTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -using StringToExpression.LanguageDefinitions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; - -namespace StringToExpression.Test.Languages.ODataFilter -{ - public class ODataArithmeticTests - { - [Theory] - [InlineData("(1 add 1) eq 2")] - [InlineData("(2 add 2 mul 5) eq 12")] - [InlineData("((2 add 2) mul 5) eq 20")] - [InlineData("(4 sub 2 mul 5) eq -6")] - [InlineData("((4 sub 2) mul 5) eq 10")] - [InlineData("(2.5 mul 4) eq 10")] - [InlineData("(2.5 mul 3) eq 7.5")] - [InlineData("(9m div 10) eq 0.9")] - [InlineData("(22.5 div 9) eq 2.5")] - [InlineData("(10 div 5 mul 2) eq 4")] - public void When_arithmetic_should_evaluate(string query) - { - var filter = new ODataFilterLanguage().Parse(query); - var stringParserCompiled = filter.Compile(); - Assert.True(stringParserCompiled(null)); - } - } -} diff --git a/tests/StringToExpression.Test/Languages/ODataFilter/ODataTests.cs b/tests/StringToExpression.Test/Languages/ODataFilter/ODataTests.cs new file mode 100644 index 0000000..1a25073 --- /dev/null +++ b/tests/StringToExpression.Test/Languages/ODataFilter/ODataTests.cs @@ -0,0 +1,74 @@ +using StringToExpression.LanguageDefinitions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace StringToExpression.Test.Languages.ODataFilter +{ + public class ODataTests + { + [Theory] + [InlineData("(1 add 1) eq 2")] + [InlineData("(2 add 2 mul 5) eq 12")] + [InlineData("((2 add 2) mul 5) eq 20")] + [InlineData("(4 sub 2 mul 5) eq -6")] + [InlineData("((4 sub 2) mul 5) eq 10")] + [InlineData("(2.5 mul 4) eq 10")] + [InlineData("(2.5 mul 3) eq 7.5")] + [InlineData("(9m div 10) eq 0.9")] + [InlineData("(22.5 div 9) eq 2.5")] + [InlineData("(10 div 5 mul 2) eq 4")] + public void When_arithmetic_should_evaluate(string query) + { + var filter = new ODataFilterLanguage().Parse(query); + var stringParserCompiled = filter.Compile(); + Assert.True(stringParserCompiled(null)); + } + + + [Theory] + [InlineData("substringof('day', 'Monday') eq true", true)] + [InlineData("endswith('Monday', 'day') eq true", true)] + [InlineData("startswith('Monday', 'Mon') eq true", true)] + [InlineData("length('Monday') eq 6", true)] + [InlineData("indexof('Monday', 'n') eq 2", true)] + [InlineData("replace('Monday', 'Mon', 'Satur') eq 'Saturday'", true)] + [InlineData("substring('Monday', 3) eq 'day'", true)] + [InlineData("substring('Monday', 3, 2) eq 'da'", true)] + [InlineData("tolower('Monday') eq 'monday'", true)] + [InlineData("toupper('Monday') eq 'MONDAY'", true)] + [InlineData("trim(' Monday ') eq 'Monday'", true)] + [InlineData("concat('Mon', 'day') eq 'Monday'", true)] + + [InlineData("day(datetime'2000-01-02T03:04:05') eq 2", true)] + [InlineData("hour(datetime'2000-01-02T03:04:05') eq 3", true)] + [InlineData("minute(datetime'2000-01-02T03:04:05') eq 4", true)] + [InlineData("month(datetime'2000-01-02T03:04:05') eq 1", true)] + [InlineData("second(datetime'2000-01-02T03:04:05') eq 5", true)] + [InlineData("year(datetime'2000-01-02T03:04:05') eq 2000", true)] + + [InlineData("round(10.4) eq 10", true)] + [InlineData("round(10.6) eq 11", true)] + [InlineData("round(10.5) eq 10", true)] + [InlineData("round(11.5) eq 12", true)] + + [InlineData("floor(10.4) eq 10", true)] + [InlineData("floor(10.6) eq 10", true)] + [InlineData("floor(10.5) eq 10", true)] + [InlineData("floor(11.5) eq 11", true)] + + [InlineData("ceiling(10.4) eq 11", true)] + [InlineData("ceiling(10.6) eq 11", true)] + [InlineData("ceiling(10.5) eq 11", true)] + [InlineData("ceiling(11.5) eq 12", true)] + public void When_functions_should_evaluate(string query, bool expectedMatch) + { + var compiled = new ODataFilterLanguage().Parse(query).Compile(); + Assert.Equal(expectedMatch, compiled(null)); + + } + } +} diff --git a/tests/StringToExpression.Test/Parser/ParserErrorTests.cs b/tests/StringToExpression.Test/Parser/ParserErrorTests.cs index 47cbd08..84f9a5c 100644 --- a/tests/StringToExpression.Test/Parser/ParserErrorTests.cs +++ b/tests/StringToExpression.Test/Parser/ParserErrorTests.cs @@ -99,7 +99,7 @@ public ParserErrorTests() [InlineData("Log(1024)", typeof(FunctionArgumentCountException), 4,8)] [InlineData("Log(1024,'2')", typeof(FunctionArgumentTypeException), 9,12)] [InlineData("2 + '2'", typeof(OperationInvalidException), 0,7)] - [InlineData("2 + error(2,3)", typeof(OperationInvalidException), 4,13)] + [InlineData("2 + error(2,3)", typeof(OperationInvalidException), 4,14)] [InlineData("2 + 💩 * 3", typeof(OperationInvalidException), 4,6)] //also interesting as double-byte unicode have length 2