Skip to content

Commit 229e016

Browse files
alex-daviesAlex Davies
and
Alex Davies
authored
Add additonal functions (#25)
* 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 Co-authored-by: Alex Davies <[email protected]>
1 parent 18342e6 commit 229e016

File tree

9 files changed

+448
-75
lines changed

9 files changed

+448
-75
lines changed

README.md

+56
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,62 @@ public async Task<IHttpActionResult> GetDoohickies([FromUri(Name = "$filter")] s
3434

3535
`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
3636

37+
### Supported Operations
38+
39+
| Operators | Name | Example |
40+
|----------------------|-----------------------|------------------------------------|
41+
| eq | Equal | City eq 'Redmond' |
42+
| ne | Not equal | City ne 'London' |
43+
| gt | Greater than | Price gt 20 |
44+
| ge | Greater than or equal | Price ge 10 |
45+
| lt | Less than | Price lt 20 |
46+
| le | Less than or equal | Price le 100 |
47+
| and | Logical and | Price le 200 and Price gt 3.5 |
48+
| or | Logical or | Price le 3.5 or Price gt 200 |
49+
| not | Logical negation | not endswith(Description,'milk') |
50+
| add | Addition | Price add 5 gt 10 |
51+
| sub | Subtraction | Price sub 5 gt 10 |
52+
| mul | Multiplication | Price mul 2 gt 2000 |
53+
| div | Division | Price div 2 gt 4 |
54+
| mod | Modulo | Price mod 2 eq 0 |
55+
| ( ) | Precedence grouping | (Price sub 5) gt 10 |
56+
| / | Property access | Address/City eq 'Redmond' |
57+
58+
### Supported Functions
59+
60+
| String Functions | Example |
61+
|-----------------------------------------------------------|---------------------------------------------------|
62+
| bool substringof(string po, string p1) | substringof('day', 'Monday') eq true |
63+
| bool endswith(string p0, string p1) | endswith('Monday', 'day') eq true |
64+
| bool startswith(string p0, string p1) | startswith('Monday', 'Mon') eq true |
65+
| int length(string p0) | length('Monday') eq 6 | Price ge 10 |
66+
| int indexof(string p0, string p1) | indexof('Monday', 'n') eq 2 |
67+
| string replace(string p0, string find, string replace) | replace('Monday', 'Mon', 'Satur') eq 'Saturday' |
68+
| string substring(string p0, int pos) | substring('Monday', 3) eq 'day' |
69+
| string substring(string p0, int pos, int length) | substring('Monday', 3, 2) eq 'da' |
70+
| string tolower(string p0) | tolower('Monday') eq 'monday' |
71+
| string toupper(string p0) | toupper('Monday') eq 'MONDAY' |
72+
| string trim(string p0) | trim(' Monday ') eq 'Monday' |
73+
| string concat(string p0, string p1) | concat('Mon', 'day') eq 'Monday' |
74+
75+
| Date Functions | Example |
76+
|-----------------------------------------------------------|---------------------------------------------------|
77+
| int day(DateTime p0) | day(datetime'2000-01-02T03:04:05') eq 2 |
78+
| int hour(DateTime p0) | hour(datetime'2000-01-02T03:04:05') eq 3 |
79+
| int minute(DateTime p0) | minute(datetime'2000-01-02T03:04:05') eq 4 |
80+
| int month(DateTime p0) | month(datetime'2000-01-02T03:04:05') eq 1 |
81+
| int second(DateTime p0) | second(datetime'2000-01-02T03:04:05') eq 5 |
82+
| int year(DateTime p0) | year(datetime'2000-01-02T03:04:05') eq 2000 |
83+
84+
| Math Functions | Example |
85+
|-----------------------------------------------------------|---------------------------------------------------|
86+
| double round(double p0) | round(10.4) eq 10 <br/> round(10.6) eq 11 </br> round(10.5) eq 10 <br/> round(11.5) eq 12
87+
| double floor(double p0) | floor(10.6) eq 10
88+
| decimal floor(decimal p0) | month(datetime'2000-01-02T03:04:05') eq 1
89+
| double ceiling(double p0) | ceiling(10.4) eq 11
90+
91+
92+
3793
## Custom languages
3894
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.
3995

src/StringToExpression/Exceptions/FunctionArgumentCountException.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public class FunctionArgumentCountException : ParseException
1111
/// <summary>
1212
/// String segment that contains the bracket that contains the incorrect number of operands.
1313
/// </summary>
14-
public readonly StringSegment BracketStringSegment;
14+
public readonly StringSegment FunctionStringSegment;
1515

1616
/// <summary>
1717
/// Expected number of operands.
@@ -26,13 +26,13 @@ public class FunctionArgumentCountException : ParseException
2626
/// <summary>
2727
/// Initializes a new instance of the <see cref="FunctionArgumentCountException"/> class.
2828
/// </summary>
29-
/// <param name="bracketStringSegment">The location of the function arguments.</param>
29+
/// <param name="functionStringSegment">The location of the function arguments.</param>
3030
/// <param name="expectedOperandCount">The Expected number of operands.</param>
3131
/// <param name="actualOperandCount">The actual number of operands.</param>
32-
public FunctionArgumentCountException(StringSegment bracketStringSegment, int expectedOperandCount, int actualOperandCount)
33-
: base(bracketStringSegment, $"Bracket '{bracketStringSegment.Value}' contains {actualOperandCount} operand{(actualOperandCount > 1 ? "s" : "")} but was expecting {expectedOperandCount} operand{(expectedOperandCount > 1 ? "s" : "")}")
32+
public FunctionArgumentCountException(StringSegment functionStringSegment, int expectedOperandCount, int actualOperandCount)
33+
: base(functionStringSegment, $"Function '{functionStringSegment.Value}' contains {actualOperandCount} operand{(actualOperandCount > 1 ? "s" : "")} but was expecting {expectedOperandCount} operand{(expectedOperandCount > 1 ? "s" : "")}")
3434
{
35-
BracketStringSegment = bracketStringSegment;
35+
FunctionStringSegment = functionStringSegment;
3636
ExpectedOperandCount = expectedOperandCount;
3737
ActualOperandCount = actualOperandCount;
3838
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using StringToExpression.Util;
2+
using System;
3+
4+
namespace StringToExpression.Exceptions
5+
{
6+
/// <summary>
7+
/// Exception when a cannot find correct overlaod for function
8+
/// </summary>
9+
public class FunctionOverlaodNotFoundException : ParseException
10+
{
11+
/// <summary>
12+
/// String segment that contains the bracket that contains the incorrect number of operands.
13+
/// </summary>
14+
public readonly StringSegment FunctionStringSegment;
15+
16+
/// <summary>
17+
/// Actual type of operands.
18+
/// </summary>
19+
public readonly Type[] ActualArgumentTypes;
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="FunctionOverlaodNotFoundException"/> class.
23+
/// </summary>
24+
/// <param name="bracketStringSegment">The location of the function</param>
25+
public FunctionOverlaodNotFoundException(StringSegment functionStringSegment)
26+
: base(functionStringSegment, $"Function '{functionStringSegment.Value}' is not defiend wtih those input types")
27+
{
28+
FunctionStringSegment = functionStringSegment;
29+
30+
}
31+
}
32+
}

src/StringToExpression/GrammerDefinitions/FunctionCallDefinition.cs

+104-35
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,33 @@ namespace StringToExpression.GrammerDefinitions
1717
/// <seealso cref="StringToExpression.GrammerDefinitions.BracketOpenDefinition" />
1818
public class FunctionCallDefinition : BracketOpenDefinition
1919
{
20-
/// <summary>
21-
/// Argument types that the function accepts.
22-
/// </summary>
23-
public readonly IReadOnlyList<Type> ArgumentTypes;
20+
21+
public class Overload
22+
{
23+
/// <summary>
24+
/// Argument types that the function accepts.
25+
/// </summary>
26+
public readonly IReadOnlyList<Type> ArgumentTypes;
27+
28+
/// <summary>
29+
/// A function given the arguments, outputs a new operand.
30+
/// </summary>
31+
public readonly Func<Expression[], Expression> ExpressionBuilder;
32+
33+
public Overload(
34+
IEnumerable<Type> argumentTypes,
35+
Func<Expression[], Expression> expressionBuilder)
36+
{
37+
this.ArgumentTypes = argumentTypes?.ToList();
38+
this.ExpressionBuilder = expressionBuilder;
39+
}
40+
}
41+
2442

2543
/// <summary>
26-
/// A function given the arguments, outputs a new operand.
44+
/// Function overlaods
2745
/// </summary>
28-
public readonly Func<Expression[], Expression> ExpressionBuilder;
46+
public readonly IReadOnlyList<Overload> Overloads;
2947

3048
/// <summary>
3149
/// Initializes a new instance of the <see cref="FunctionCallDefinition"/> class.
@@ -39,10 +57,29 @@ public FunctionCallDefinition(
3957
string regex,
4058
IEnumerable<Type> argumentTypes,
4159
Func<Expression[], Expression> expressionBuilder)
60+
: this(name, regex, new[] { new Overload(argumentTypes, expressionBuilder) })
61+
{
62+
63+
}
64+
65+
/// <summary>
66+
/// Initializes a new instance of the <see cref="FunctionCallDefinition"/> class.
67+
/// </summary>
68+
/// <param name="name">The name of the definition.</param>
69+
/// <param name="regex">The regex to match tokens.</param>
70+
/// <param name="overloads">list of overloads avilable for function</param>
71+
public FunctionCallDefinition(
72+
string name,
73+
string regex,
74+
IEnumerable<Overload> overloads)
4275
: base(name, regex)
4376
{
44-
this.ArgumentTypes = argumentTypes?.ToList();
45-
this.ExpressionBuilder = expressionBuilder;
77+
var overloadList = overloads?.ToList();
78+
if (overloadList.Count == 0)
79+
{
80+
throw new ArgumentException("Must specify at least one overlaod", nameof(overloads));
81+
}
82+
this.Overloads = overloadList;
4683
}
4784

4885
/// <summary>
@@ -56,6 +93,58 @@ public FunctionCallDefinition(string name, string regex,Func<Expression[], Expre
5693
{
5794
}
5895

96+
97+
public Overload MatchOverload(Stack<Operand> bracketOperands, out IEnumerable<Expression> typedArguments)
98+
{
99+
var possibleOverloads = Overloads
100+
.Where(x => x.ArgumentTypes == null || x.ArgumentTypes.Count == bracketOperands.Count)
101+
.OrderBy(x=>x.ArgumentTypes == null);
102+
103+
// No viable overloads, user has probably inputted wrong number of arguments
104+
if (!possibleOverloads.Any())
105+
{
106+
throw new FunctionArgumentCountException(
107+
StringSegment.Encompass(bracketOperands.Select(x => x.SourceMap)),
108+
Overloads.First().ArgumentTypes.Count,
109+
bracketOperands.Count);
110+
}
111+
112+
foreach(var possibleOverload in possibleOverloads)
113+
{
114+
//null argument types is treated as a I can accept anything
115+
if (possibleOverload.ArgumentTypes == null)
116+
{
117+
typedArguments = bracketOperands.Select(x => x.Expression);
118+
return possibleOverload;
119+
}
120+
121+
var argumentMatch = bracketOperands.Zip(possibleOverload.ArgumentTypes, (o, t) => {
122+
var canConvert = ExpressionConversions.TryConvert(o.Expression, t, out var result);
123+
return new { CanConvert = canConvert, Operand = o, ArgumentType = t, ConvertedOperand = result };
124+
});
125+
126+
127+
if (argumentMatch.All(x => x.CanConvert))
128+
{
129+
typedArguments = argumentMatch.Select(x => x.ConvertedOperand);
130+
return possibleOverload;
131+
}
132+
133+
// If we have only a single possible overlaod but we arguement types dont align
134+
// we will throw an error as though they had the wrong types
135+
if (possibleOverloads.Count() == 1)
136+
{
137+
var badConvert = argumentMatch.First(x => !x.CanConvert);
138+
throw new FunctionArgumentTypeException(badConvert.Operand.SourceMap, badConvert.ArgumentType, badConvert.Operand.Expression.Type);
139+
}
140+
}
141+
142+
//We had multiple overloads, but none of them matched
143+
throw new FunctionOverlaodNotFoundException(StringSegment.Encompass(bracketOperands.Select(x => x.SourceMap)));
144+
145+
146+
}
147+
59148
/// <summary>
60149
/// Applies the bracket operands. Executes the expressionBuilder with all the operands in the brackets.
61150
/// </summary>
@@ -68,39 +157,19 @@ public FunctionCallDefinition(string name, string regex,Func<Expression[], Expre
68157
/// <exception cref="OperationInvalidException">When an error occured while executing the expressionBuilder</exception>
69158
public override void ApplyBracketOperands(Operator bracketOpen, Stack<Operand> bracketOperands, Operator bracketClose, ParseState state)
70159
{
71-
var operandSource = StringSegment.Encompass(bracketOperands.Select(x => x.SourceMap));
72-
var functionArguments = bracketOperands.Select(x => x.Expression);
73-
//if we have been given specific argument types validate them
74-
if (ArgumentTypes != null)
75-
{
76-
var expectedArgumentCount = ArgumentTypes.Count;
77-
if (expectedArgumentCount != bracketOperands.Count)
78-
throw new FunctionArgumentCountException(
79-
operandSource,
80-
expectedArgumentCount,
81-
bracketOperands.Count);
82-
83-
functionArguments = bracketOperands.Zip(ArgumentTypes, (o, t) => {
84-
try
85-
{
86-
return ExpressionConversions.Convert(o.Expression, t);
87-
}
88-
catch(InvalidOperationException)
89-
{
90-
//if we cant convert to the argument type then something is wrong with the argument
91-
//so we will throw it up
92-
throw new FunctionArgumentTypeException(o.SourceMap, t, o.Expression.Type);
93-
}
94-
});
160+
var functionSourceMap = StringSegment.Encompass(
161+
bracketOpen.SourceMap,
162+
StringSegment.Encompass(bracketOperands.Select(x => x.SourceMap)),
163+
bracketClose.SourceMap);
95164

96-
}
165+
var overload = MatchOverload(bracketOperands, out var functionArguments);
97166

98-
var functionSourceMap = StringSegment.Encompass(bracketOpen.SourceMap, operandSource);
167+
99168
var functionArgumentsArray = functionArguments.ToArray();
100169
Expression output;
101170
try
102171
{
103-
output = ExpressionBuilder(functionArgumentsArray);
172+
output = overload.ExpressionBuilder(functionArgumentsArray);
104173
}
105174
catch(Exception ex)
106175
{

0 commit comments

Comments
 (0)