Skip to content

Initial implementation of user defined functions #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions StringMath.Tests/BooleanExprTests.cs
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ public void Setup()
_context.RegisterLogical(">", (a, b) => a > b, Precedence.Power);
_context.RegisterLogical("<", (a, b) => a < b, Precedence.Power);
_context.RegisterLogical("!", (a) => !a);
_context.RegisterFunction("if", (args) => args[0] > 0 ? args[1] : args[2]);
}

[Test]
@@ -56,6 +57,12 @@ public void Evaluate_Binary_Operation()
_context.RegisterBinary("+", (a, b) => a + b);
Assert.IsTrue("(3 + 5) > 7".EvalBoolean(_context));
}

[Test]
public void Evaluate_Ternary_Operation()
{
Assert.IsTrue("if(5 > 2, {true}, {false})".EvalBoolean(_context));
}
}

static class BooleanMathExtensions
27 changes: 27 additions & 0 deletions StringMath.Tests/MathExprTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using NUnit.Framework;
using System;
using System.Linq;

namespace StringMath.Tests
{
@@ -15,6 +16,9 @@ public void Setup()
MathExpr.AddOperator("<>", (a, b) => double.Parse($"{a}{b}"), Precedence.Prefix);
MathExpr.AddOperator("e", (a, b) => double.Parse($"{a}e{b}"), Precedence.Power);
MathExpr.AddOperator("sind", a => Math.Sin(a * (Math.PI / 180)));
MathExpr.AddFunction("add", args => args.Sum());
MathExpr.AddFunction("sqrt", args => Math.Sqrt(args[0]));
MathExpr.AddFunction("if", args => args[0] > 1 ? args[1] : args[2]);
}

[Test]
@@ -79,6 +83,7 @@ public void Double_Conversion_Should_Evaluate_Expression()
[TestCase("{a}", new[] { "a" })]
[TestCase("2 * {a} - {PI}", new[] { "a" })]
[TestCase("({a} - 5) * 4 + {E}", new[] { "a" })]
[TestCase("if({a} - 2, 1, 0) + {E}", new[] { "a" })]
public void LocalVariables_Should_Exclude_Global_Variables(string input, string[] expected)
{
MathExpr expr = input;
@@ -90,6 +95,7 @@ public void LocalVariables_Should_Exclude_Global_Variables(string input, string[
[TestCase("{a}", new[] { "a" })]
[TestCase("2 * {a} - {PI}", new[] { "a", "PI" })]
[TestCase("({a} - 5) * 4 + {E}", new[] { "a", "E" })]
[TestCase("if({a} / 2, 1, 0) + {E}", new[] { "a", "E" })]
public void Variables_Should_Include_Global_Variables(string input, string[] expected)
{
MathExpr expr = input;
@@ -190,6 +196,17 @@ public void Evaluate(string input, double variable, double expected)
Assert.AreEqual(expected, expr.Result);
}

[Test]
[TestCase("add({a}, 2)", 1, 3)]
[TestCase("sqrt({a})", 4, 2)]
[TestCase("3 + sqrt({a})", 4, 5)]
public void Evaluate_Function(string input, double variable, double expected)
{
MathExpr expr = input.Substitute("a", variable);

Assert.AreEqual(expected, expr.Result);
}

[Test]
[TestCase("abs -5", 5)]
[TestCase("abs(-1)", 1)]
@@ -368,5 +385,15 @@ public void Compile_Resolves_Global_Variables()

Assert.AreEqual(1 + Math.PI, result);
}

[Test]
public void Compile_Resolves_Function()
{
var expr = "1 + add(2, 3)".ToMathExpr();
var fn = expr.Compile();
double result = fn();

Assert.AreEqual(6, result);
}
}
}
2 changes: 2 additions & 0 deletions StringMath/Expressions/IExpression.cs
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ internal enum ExpressionType
VariableExpression,
/// <summary><see cref="Expressions.ConstantExpression"/>.</summary>
ConstantExpression,
/// <summary><see cref="Expressions.InvocationExpression"/>.</summary>
Invocation,
}

/// <summary>Contract for expressions.</summary>
26 changes: 26 additions & 0 deletions StringMath/Expressions/InvocationExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Linq;

namespace StringMath.Expressions
{
internal class InvocationExpression : IExpression
{
public InvocationExpression(string name, IReadOnlyCollection<IExpression> arguments)
{
Name = name;
Arguments = arguments;
}

public ExpressionType Type => ExpressionType.Invocation;

public string Name { get; }
public IReadOnlyCollection<IExpression> Arguments { get; }

/// <inheritdoc />
public override string ToString()
=> ToString(MathContext.Default);

public string ToString(IMathContext context)
=> $"{Name}({string.Join(", ", Arguments.Select(x => x.ToString()))})";
}
}
16 changes: 16 additions & 0 deletions StringMath/IMathContext.cs
Original file line number Diff line number Diff line change
@@ -19,6 +19,11 @@ public interface IMathContext
/// <param name="operation">The implementation of the operator.</param>
void RegisterUnary(string operatorName, Func<double, double> operation);

/// <summary>Registers a function implementation.</summary>
/// <param name="functionName">The name of the function.</param>
/// <param name="body">The implementation of the function.</param>
void RegisterFunction(string functionName, Func<double[], double> body);

/// <summary>Evaluates a binary operation.</summary>
/// <param name="op">The operator.</param>
/// <param name="a">Left value.</param>
@@ -32,6 +37,12 @@ public interface IMathContext
/// <returns>The result.</returns>
double EvaluateUnary(string op, double a);

/// <summary>Invokes a function by name and returns the result.</summary>
/// <param name="name">The function name.</param>
/// <param name="args">The function arguments.</param>
/// <returns>The result.</returns>
double InvokeFunction(string name, params double[] args);

/// <summary>Returns the precedence of a binary operator. Unary operators have <see cref="Precedence.Prefix"/> precedence.</summary>
/// <param name="operatorName">The operator.</param>
/// <returns>A <see cref="Precedence"/> value.</returns>
@@ -46,5 +57,10 @@ public interface IMathContext
/// <param name="operatorName">The operator.</param>
/// <returns>True if the operator is unary, false if it does not exist or it is binary.</returns>
bool IsUnary(string operatorName);

/// <summary>Tells whether an identifier is a function.</summary>
/// <param name="functionName"></param>
/// <returns></returns>
bool IsFunction(string functionName);
}
}
25 changes: 25 additions & 0 deletions StringMath/MathContext.cs
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ namespace StringMath
/// <remarks>Inherits operators from <see cref="Parent"/>.</remarks>
public sealed class MathContext : IMathContext
{
private readonly Dictionary<string, Func<double[], double>> _functions = new Dictionary<string, Func<double[], double>>(StringComparer.Ordinal);
private readonly Dictionary<string, Func<double, double, double>> _binaryEvaluators = new Dictionary<string, Func<double, double, double>>(StringComparer.Ordinal);
private readonly Dictionary<string, Func<double, double>> _unaryEvaluators = new Dictionary<string, Func<double, double>>(StringComparer.Ordinal);
private readonly Dictionary<string, Precedence> _binaryPrecedence = new Dictionary<string, Precedence>(StringComparer.Ordinal);
@@ -66,6 +67,10 @@ public bool IsUnary(string operatorName)
public bool IsBinary(string operatorName)
=> _binaryEvaluators.ContainsKey(operatorName) || (Parent?.IsBinary(operatorName) ?? false);

/// <inheritdoc />
public bool IsFunction(string functionName)
=> _functions.ContainsKey(functionName) || (Parent?.IsFunction(functionName) ?? false);

/// <inheritdoc />
public Precedence GetBinaryPrecedence(string operatorName)
{
@@ -85,6 +90,15 @@ public void RegisterBinary(string operatorName, Func<double, double, double> ope
_binaryPrecedence[operatorName] = precedence ?? Precedence.UserDefined;
}

/// <inheritdoc />
public void RegisterFunction(string functionName, Func<double[], double> body)
{
functionName.EnsureNotNull(nameof(functionName));
body.EnsureNotNull(nameof(body));

_functions[functionName] = body;
}

/// <inheritdoc />
public void RegisterUnary(string operatorName, Func<double, double> operation)
{
@@ -116,6 +130,17 @@ public double EvaluateUnary(string op, double a)
return result;
}

/// <inheritdoc />
public double InvokeFunction(string name, params double[] args)
{
double result = _functions.TryGetValue(name, out var fn)
? fn(args)
: Parent?.InvokeFunction(name, args)
?? throw MathException.MissingFunction(name);

return result;
}

#region Factorial

private static readonly Dictionary<double, double> _factorials = new Dictionary<double, double>
5 changes: 5 additions & 0 deletions StringMath/MathException.cs
Original file line number Diff line number Diff line change
@@ -19,6 +19,8 @@ public enum ErrorCode
UNEXISTING_VARIABLE = 4,
/// <summary>Readonly variable.</summary>
READONLY_VARIABLE = 8,
/// <summary>Missing function.</summary>
UNDEFINED_FUNCTION = 16,
}

/// <summary>Initializes a new instance of a <see cref="MathException"/>.</summary>
@@ -64,5 +66,8 @@ internal static Exception MissingVariable(string variable)

internal static Exception ReadonlyVariable(string name)
=> new MathException(ErrorCode.READONLY_VARIABLE, $"Variable '{name}' is read-only.");

internal static Exception MissingFunction(string name)
=> new MathException(ErrorCode.UNDEFINED_FUNCTION, $"Undefined function '{name}'.");
}
}
17 changes: 17 additions & 0 deletions StringMath/MathExpr.cs
Original file line number Diff line number Diff line change
@@ -194,6 +194,17 @@ public MathExpr SetOperator(string name, Func<double, double> operation)
return this;
}

/// <summary>Adds a new function or overwrites and existing one.</summary>
/// <param name="name">The name of the function.</param>
/// <param name="body">The code to execute for this function.</param>
/// <returns>The current math expression.</returns>
public MathExpr SetFunction(string name, Func<double[], double> body)
{
_cachedResult = null;
Context.RegisterFunction(name, body);
return this;
}

/// <summary>Add a new binary operator or overwrite an existing operator implementation.</summary>
/// <param name="name">The operator's string representation.</param>
/// <param name="operation">The operation to execute for this operator.</param>
@@ -210,6 +221,12 @@ public static void AddOperator(string name, Func<double, double, double> operati
public static void AddOperator(string name, Func<double, double> operation)
=> MathContext.Default.RegisterUnary(name, operation);

/// <summary>Adds a new function or overwrites and existing one.</summary>
/// <param name="name">The name of the function.</param>
/// <param name="body">The code to execute for this function.</param>
public static void AddFunction(string name, Func<double[], double> body)
=> MathContext.Default.RegisterFunction(name, body);

/// <inheritdoc cref="Substitute(string, double)"/>
/// <remarks>Variables will be available in all <see cref="MathExpr"/> expressions.</remarks>
public static void AddVariable(string name, double value)
52 changes: 31 additions & 21 deletions StringMath/Parser/Parser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using StringMath.Expressions;
using System.Collections.Generic;

namespace StringMath
{
@@ -70,22 +71,36 @@ private IExpression ParseBinaryExpression(IExpression? left = default, Precedenc
return left;
}

private IExpression ParsePrimaryExpression()
private IExpression ParsePrimaryExpression() => _currentToken.Type switch
{
switch (_currentToken.Type)
{
case TokenType.Number:
return new ConstantExpression(Take().Text);
TokenType.Number => new ConstantExpression(Take().Text),
TokenType.Operator when _mathContext.IsFunction(_currentToken.Text) => ParseInvocationExpression(),
TokenType.Identifier => new VariableExpression(Take().Text),
TokenType.OpenParen => ParseGroupingExpression(),
_ => throw MathException.UnexpectedToken(_currentToken),
};

private IExpression ParseInvocationExpression()
{
var fnName = Take().Text;

Match(TokenType.OpenParen);

case TokenType.Identifier:
return new VariableExpression(Take().Text);
var arguments = new List<IExpression>(2);

case TokenType.OpenParen:
return ParseGroupingExpression();
do
{
if (_currentToken.Text == ",")
Take();

default:
throw MathException.UnexpectedToken(_currentToken);
var argExpr = ParseBinaryExpression();
arguments.Add(argExpr);
}
while (_currentToken.Text == ",");

Match(TokenType.CloseParen);

return new InvocationExpression(fnName, arguments);
}

private IExpression ParseGroupingExpression()
@@ -98,12 +113,9 @@ private IExpression ParseGroupingExpression()
return expr;
}

private Token Match(TokenType tokenType)
{
return _currentToken.Type == tokenType
? Take()
: throw MathException.UnexpectedToken(_currentToken, tokenType);
}
private Token Match(TokenType tokenType) => _currentToken.Type == tokenType
? Take()
: throw MathException.UnexpectedToken(_currentToken, tokenType);

private Token Take()
{
@@ -112,9 +124,7 @@ private Token Take()
return previous;
}

private bool IsEndOfStatement()
{
return _currentToken.Type == TokenType.EndOfCode;
}
private bool IsEndOfStatement()
=> _currentToken.Type == TokenType.EndOfCode;
}
}
1 change: 1 addition & 0 deletions StringMath/Parser/TokenType.cs
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ internal enum TokenType

/// <summary>[aA-zZ_]+[aA-zZ0-9_]</summary>
Identifier,

/// <summary>1 or .1 or 1.1</summary>
Number,

4 changes: 2 additions & 2 deletions StringMath/StringMath.csproj
Original file line number Diff line number Diff line change
@@ -12,8 +12,8 @@ Supports variables and user defined operators.</Description>
<PackageProjectUrl>https://github.com/miroiu/string-math</PackageProjectUrl>
<RepositoryUrl>https://github.com/miroiu/string-math</RepositoryUrl>
<PackageTags>expression-evaluator calculator string-math math string-calculator user-defined-operators operators custom-operators</PackageTags>
<Version>4.1.2</Version>
<PackageReleaseNotes>Fixed compiled expressions not using variables that are not passed as arguments</PackageReleaseNotes>
<Version>5.0.0</Version>
<PackageReleaseNotes>Added user defined functions</PackageReleaseNotes>
<PackageLicenseFile></PackageLicenseFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Nullable>enable</Nullable>
11 changes: 11 additions & 0 deletions StringMath/Visitors/CompileExpression.cs
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@ public Expression Visit(SM.IExpression expression)
SM.ConstantExpression constantExpr => VisitConstantExpr(constantExpr),
SM.UnaryExpression unaryExpr => VisitUnaryExpr(unaryExpr),
SM.VariableExpression variableExpr => VisitVariableExpr(variableExpr),
SM.InvocationExpression invocationExpr => VisitInvocationExpr(invocationExpr),
_ => throw new NotImplementedException($"'{expression?.GetType().Name}' Convertor is not implemented.")
};

@@ -75,5 +76,15 @@ private Expression VisitUnaryExpr(SM.UnaryExpression unaryExpr) =>
nameof(IMathContext.EvaluateUnary),
null,
Expression.Constant(unaryExpr.OperatorName), Visit(unaryExpr.Operand));

private Expression VisitInvocationExpr(SM.InvocationExpression invocationExpr)
{
var args = invocationExpr.Arguments.Select(Visit).ToArray();
return Expression.Call(_contextParam,
nameof(IMathContext.InvokeFunction),
null,
Expression.Constant(invocationExpr.Name),
Expression.NewArrayInit(typeof(double), args));
}
}
}
Loading