Skip to content
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

Initial implementation of user defined functions #12

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
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
{
Expand All @@ -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]
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Up @@ -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>
Expand Down
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
Expand Up @@ -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>
Expand All @@ -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>
Expand All @@ -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
Expand Up @@ -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);
Expand Down Expand Up @@ -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)
{
Expand All @@ -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)
{
Expand Down Expand Up @@ -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>
Expand Down
5 changes: 5 additions & 0 deletions StringMath/MathException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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
Expand Up @@ -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>
Expand All @@ -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)
Expand Down
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
{
Expand Down Expand Up @@ -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()
Expand All @@ -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()
{
Expand All @@ -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
Expand Up @@ -11,6 +11,7 @@ internal enum TokenType

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

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

Expand Down
4 changes: 2 additions & 2 deletions StringMath/StringMath.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
11 changes: 11 additions & 0 deletions StringMath/Visitors/CompileExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
};

Expand Down Expand Up @@ -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
Loading