diff --git a/StringMath.Tests/BooleanExprTests.cs b/StringMath.Tests/BooleanExprTests.cs index cc13b4e..c5a3f41 100644 --- a/StringMath.Tests/BooleanExprTests.cs +++ b/StringMath.Tests/BooleanExprTests.cs @@ -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 diff --git a/StringMath.Tests/MathExprTests.cs b/StringMath.Tests/MathExprTests.cs index 670f5e1..2f43d6c 100644 --- a/StringMath.Tests/MathExprTests.cs +++ b/StringMath.Tests/MathExprTests.cs @@ -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); + } } } diff --git a/StringMath/Expressions/IExpression.cs b/StringMath/Expressions/IExpression.cs index 91b4bf2..f78d215 100644 --- a/StringMath/Expressions/IExpression.cs +++ b/StringMath/Expressions/IExpression.cs @@ -11,6 +11,8 @@ internal enum ExpressionType VariableExpression, /// . ConstantExpression, + /// . + Invocation, } /// Contract for expressions. diff --git a/StringMath/Expressions/InvocationExpression.cs b/StringMath/Expressions/InvocationExpression.cs new file mode 100644 index 0000000..50b80cf --- /dev/null +++ b/StringMath/Expressions/InvocationExpression.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StringMath.Expressions +{ + internal class InvocationExpression : IExpression + { + public InvocationExpression(string name, IReadOnlyCollection arguments) + { + Name = name; + Arguments = arguments; + } + + public ExpressionType Type => ExpressionType.Invocation; + + public string Name { get; } + public IReadOnlyCollection Arguments { get; } + + /// + public override string ToString() + => ToString(MathContext.Default); + + public string ToString(IMathContext context) + => $"{Name}({string.Join(", ", Arguments.Select(x => x.ToString()))})"; + } +} diff --git a/StringMath/IMathContext.cs b/StringMath/IMathContext.cs index 4234503..f49b6c1 100644 --- a/StringMath/IMathContext.cs +++ b/StringMath/IMathContext.cs @@ -19,6 +19,11 @@ public interface IMathContext /// The implementation of the operator. void RegisterUnary(string operatorName, Func operation); + /// Registers a function implementation. + /// The name of the function. + /// The implementation of the function. + void RegisterFunction(string functionName, Func body); + /// Evaluates a binary operation. /// The operator. /// Left value. @@ -32,6 +37,12 @@ public interface IMathContext /// The result. double EvaluateUnary(string op, double a); + /// Invokes a function by name and returns the result. + /// The function name. + /// The function arguments. + /// The result. + double InvokeFunction(string name, params double[] args); + /// Returns the precedence of a binary operator. Unary operators have precedence. /// The operator. /// A value. @@ -46,5 +57,10 @@ public interface IMathContext /// The operator. /// True if the operator is unary, false if it does not exist or it is binary. bool IsUnary(string operatorName); + + /// Tells whether an identifier is a function. + /// + /// + bool IsFunction(string functionName); } } diff --git a/StringMath/MathContext.cs b/StringMath/MathContext.cs index 4f7b6ee..f69cafc 100644 --- a/StringMath/MathContext.cs +++ b/StringMath/MathContext.cs @@ -7,6 +7,7 @@ namespace StringMath /// Inherits operators from . public sealed class MathContext : IMathContext { + private readonly Dictionary> _functions = new Dictionary>(StringComparer.Ordinal); private readonly Dictionary> _binaryEvaluators = new Dictionary>(StringComparer.Ordinal); private readonly Dictionary> _unaryEvaluators = new Dictionary>(StringComparer.Ordinal); private readonly Dictionary _binaryPrecedence = new Dictionary(StringComparer.Ordinal); @@ -66,6 +67,10 @@ public bool IsUnary(string operatorName) public bool IsBinary(string operatorName) => _binaryEvaluators.ContainsKey(operatorName) || (Parent?.IsBinary(operatorName) ?? false); + /// + public bool IsFunction(string functionName) + => _functions.ContainsKey(functionName) || (Parent?.IsFunction(functionName) ?? false); + /// public Precedence GetBinaryPrecedence(string operatorName) { @@ -85,6 +90,15 @@ public void RegisterBinary(string operatorName, Func ope _binaryPrecedence[operatorName] = precedence ?? Precedence.UserDefined; } + /// + public void RegisterFunction(string functionName, Func body) + { + functionName.EnsureNotNull(nameof(functionName)); + body.EnsureNotNull(nameof(body)); + + _functions[functionName] = body; + } + /// public void RegisterUnary(string operatorName, Func operation) { @@ -116,6 +130,17 @@ public double EvaluateUnary(string op, double a) return result; } + /// + 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 _factorials = new Dictionary diff --git a/StringMath/MathException.cs b/StringMath/MathException.cs index 4e17e0b..b203100 100644 --- a/StringMath/MathException.cs +++ b/StringMath/MathException.cs @@ -19,6 +19,8 @@ public enum ErrorCode UNEXISTING_VARIABLE = 4, /// Readonly variable. READONLY_VARIABLE = 8, + /// Missing function. + UNDEFINED_FUNCTION = 16, } /// Initializes a new instance of a . @@ -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}'."); } } \ No newline at end of file diff --git a/StringMath/MathExpr.cs b/StringMath/MathExpr.cs index 39e1256..f14b713 100644 --- a/StringMath/MathExpr.cs +++ b/StringMath/MathExpr.cs @@ -194,6 +194,17 @@ public MathExpr SetOperator(string name, Func operation) return this; } + /// Adds a new function or overwrites and existing one. + /// The name of the function. + /// The code to execute for this function. + /// The current math expression. + public MathExpr SetFunction(string name, Func body) + { + _cachedResult = null; + Context.RegisterFunction(name, body); + return this; + } + /// Add a new binary operator or overwrite an existing operator implementation. /// The operator's string representation. /// The operation to execute for this operator. @@ -210,6 +221,12 @@ public static void AddOperator(string name, Func operati public static void AddOperator(string name, Func operation) => MathContext.Default.RegisterUnary(name, operation); + /// Adds a new function or overwrites and existing one. + /// The name of the function. + /// The code to execute for this function. + public static void AddFunction(string name, Func body) + => MathContext.Default.RegisterFunction(name, body); + /// /// Variables will be available in all expressions. public static void AddVariable(string name, double value) diff --git a/StringMath/Parser/Parser.cs b/StringMath/Parser/Parser.cs index 11aaa35..30788dd 100644 --- a/StringMath/Parser/Parser.cs +++ b/StringMath/Parser/Parser.cs @@ -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(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; } } \ No newline at end of file diff --git a/StringMath/Parser/TokenType.cs b/StringMath/Parser/TokenType.cs index 21f6bf9..c1b063e 100644 --- a/StringMath/Parser/TokenType.cs +++ b/StringMath/Parser/TokenType.cs @@ -11,6 +11,7 @@ internal enum TokenType /// [aA-zZ_]+[aA-zZ0-9_] Identifier, + /// 1 or .1 or 1.1 Number, diff --git a/StringMath/StringMath.csproj b/StringMath/StringMath.csproj index f330116..78e7cbe 100644 --- a/StringMath/StringMath.csproj +++ b/StringMath/StringMath.csproj @@ -12,8 +12,8 @@ Supports variables and user defined operators. https://github.com/miroiu/string-math https://github.com/miroiu/string-math expression-evaluator calculator string-math math string-calculator user-defined-operators operators custom-operators - 4.1.2 - Fixed compiled expressions not using variables that are not passed as arguments + 5.0.0 + Added user defined functions true enable diff --git a/StringMath/Visitors/CompileExpression.cs b/StringMath/Visitors/CompileExpression.cs index c808249..0e565cd 100644 --- a/StringMath/Visitors/CompileExpression.cs +++ b/StringMath/Visitors/CompileExpression.cs @@ -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)); +} } } diff --git a/StringMath/Visitors/EvaluateExpression.cs b/StringMath/Visitors/EvaluateExpression.cs index be04eef..6e1a828 100644 --- a/StringMath/Visitors/EvaluateExpression.cs +++ b/StringMath/Visitors/EvaluateExpression.cs @@ -1,4 +1,5 @@ using StringMath.Expressions; +using System.Linq; namespace StringMath { @@ -43,5 +44,17 @@ protected override IExpression VisitVariableExpr(VariableExpression variableExpr ? new ConstantExpression(value) : throw MathException.UnassignedVariable(variableExpr); } + + protected override IExpression VisitInvocationExpr(InvocationExpression invocationExpr) + { + var args = invocationExpr.Arguments + .Select(Visit) + .Cast() + .Select(x => x.Value) + .ToArray(); + + double result = _context.InvokeFunction(invocationExpr.Name, args); + return new ConstantExpression(result); + } } } diff --git a/StringMath/Visitors/ExpressionVisitor.cs b/StringMath/Visitors/ExpressionVisitor.cs index 24a7157..1d04aa3 100644 --- a/StringMath/Visitors/ExpressionVisitor.cs +++ b/StringMath/Visitors/ExpressionVisitor.cs @@ -16,6 +16,7 @@ public IExpression Visit(IExpression expression) ConstantExpression constantExpr => VisitConstantExpr(constantExpr), UnaryExpression unaryExpr => VisitUnaryExpr(unaryExpr), VariableExpression variableExpr => VisitVariableExpr(variableExpr), + InvocationExpression invocationExpr => VisitInvocationExpr(invocationExpr), _ => expression }; @@ -29,5 +30,7 @@ public IExpression Visit(IExpression expression) protected virtual IExpression VisitBinaryExpr(BinaryExpression binaryExpr) => binaryExpr; protected virtual IExpression VisitUnaryExpr(UnaryExpression unaryExpr) => unaryExpr; + + protected virtual IExpression VisitInvocationExpr(InvocationExpression invocationExpr) => invocationExpr; } } diff --git a/StringMath/Visitors/ExtractVariables.cs b/StringMath/Visitors/ExtractVariables.cs index 7db512e..6d1c5fa 100644 --- a/StringMath/Visitors/ExtractVariables.cs +++ b/StringMath/Visitors/ExtractVariables.cs @@ -31,5 +31,15 @@ protected override IExpression VisitVariableExpr(VariableExpression variableExpr return variableExpr; } + + protected override IExpression VisitInvocationExpr(InvocationExpression invocationExpr) + { + foreach(var arg in invocationExpr.Arguments) + { + Visit(arg); + } + + return invocationExpr; + } } }