Skip to content

Commit

Permalink
Add additonal functions (#25)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
alex-davies and Alex Davies authored Sep 24, 2024
1 parent 18342e6 commit 229e016
Show file tree
Hide file tree
Showing 9 changed files with 448 additions and 75 deletions.
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,62 @@ public async Task<IHttpActionResult> 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 <br/> round(10.6) eq 11 </br> round(10.5) eq 10 <br/> 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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class FunctionArgumentCountException : ParseException
/// <summary>
/// String segment that contains the bracket that contains the incorrect number of operands.
/// </summary>
public readonly StringSegment BracketStringSegment;
public readonly StringSegment FunctionStringSegment;

/// <summary>
/// Expected number of operands.
Expand All @@ -26,13 +26,13 @@ public class FunctionArgumentCountException : ParseException
/// <summary>
/// Initializes a new instance of the <see cref="FunctionArgumentCountException"/> class.
/// </summary>
/// <param name="bracketStringSegment">The location of the function arguments.</param>
/// <param name="functionStringSegment">The location of the function arguments.</param>
/// <param name="expectedOperandCount">The Expected number of operands.</param>
/// <param name="actualOperandCount">The actual number of operands.</param>
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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using StringToExpression.Util;
using System;

namespace StringToExpression.Exceptions
{
/// <summary>
/// Exception when a cannot find correct overlaod for function
/// </summary>
public class FunctionOverlaodNotFoundException : ParseException
{
/// <summary>
/// String segment that contains the bracket that contains the incorrect number of operands.
/// </summary>
public readonly StringSegment FunctionStringSegment;

/// <summary>
/// Actual type of operands.
/// </summary>
public readonly Type[] ActualArgumentTypes;

/// <summary>
/// Initializes a new instance of the <see cref="FunctionOverlaodNotFoundException"/> class.
/// </summary>
/// <param name="bracketStringSegment">The location of the function</param>
public FunctionOverlaodNotFoundException(StringSegment functionStringSegment)
: base(functionStringSegment, $"Function '{functionStringSegment.Value}' is not defiend wtih those input types")
{
FunctionStringSegment = functionStringSegment;

}
}
}
139 changes: 104 additions & 35 deletions src/StringToExpression/GrammerDefinitions/FunctionCallDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,33 @@ namespace StringToExpression.GrammerDefinitions
/// <seealso cref="StringToExpression.GrammerDefinitions.BracketOpenDefinition" />
public class FunctionCallDefinition : BracketOpenDefinition
{
/// <summary>
/// Argument types that the function accepts.
/// </summary>
public readonly IReadOnlyList<Type> ArgumentTypes;

public class Overload
{
/// <summary>
/// Argument types that the function accepts.
/// </summary>
public readonly IReadOnlyList<Type> ArgumentTypes;

/// <summary>
/// A function given the arguments, outputs a new operand.
/// </summary>
public readonly Func<Expression[], Expression> ExpressionBuilder;

public Overload(
IEnumerable<Type> argumentTypes,
Func<Expression[], Expression> expressionBuilder)
{
this.ArgumentTypes = argumentTypes?.ToList();
this.ExpressionBuilder = expressionBuilder;
}
}


/// <summary>
/// A function given the arguments, outputs a new operand.
/// Function overlaods
/// </summary>
public readonly Func<Expression[], Expression> ExpressionBuilder;
public readonly IReadOnlyList<Overload> Overloads;

/// <summary>
/// Initializes a new instance of the <see cref="FunctionCallDefinition"/> class.
Expand All @@ -39,10 +57,29 @@ public FunctionCallDefinition(
string regex,
IEnumerable<Type> argumentTypes,
Func<Expression[], Expression> expressionBuilder)
: this(name, regex, new[] { new Overload(argumentTypes, expressionBuilder) })
{

}

/// <summary>
/// Initializes a new instance of the <see cref="FunctionCallDefinition"/> class.
/// </summary>
/// <param name="name">The name of the definition.</param>
/// <param name="regex">The regex to match tokens.</param>
/// <param name="overloads">list of overloads avilable for function</param>
public FunctionCallDefinition(
string name,
string regex,
IEnumerable<Overload> 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;
}

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


public Overload MatchOverload(Stack<Operand> bracketOperands, out IEnumerable<Expression> 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)));


}

/// <summary>
/// Applies the bracket operands. Executes the expressionBuilder with all the operands in the brackets.
/// </summary>
Expand All @@ -68,39 +157,19 @@ public FunctionCallDefinition(string name, string regex,Func<Expression[], Expre
/// <exception cref="OperationInvalidException">When an error occured while executing the expressionBuilder</exception>
public override void ApplyBracketOperands(Operator bracketOpen, Stack<Operand> 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)
{
Expand Down
Loading

0 comments on commit 229e016

Please sign in to comment.