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;
+ }
}
}