From 3b969823eaac73b5789f854efcdd0b2d827b9acb Mon Sep 17 00:00:00 2001 From: bwakabats Date: Mon, 14 Aug 2023 22:41:56 +0100 Subject: [PATCH 1/5] External Functions --- Math expression eval/UnitTest/TestAll.cs | 159 ++++++++++++++++++ .../org.matheval/Common/Afe_Common.cs | 2 +- .../org.matheval/Parser/Parser.cs | 127 +++++++++++--- 3 files changed, 262 insertions(+), 26 deletions(-) diff --git a/Math expression eval/UnitTest/TestAll.cs b/Math expression eval/UnitTest/TestAll.cs index 48fef10..9c9b09a 100644 --- a/Math expression eval/UnitTest/TestAll.cs +++ b/Math expression eval/UnitTest/TestAll.cs @@ -23,9 +23,12 @@ THE SOFTWARE. */ using Microsoft.VisualStudio.TestTools.UnitTesting; using org.matheval; +using org.matheval.Common; +using org.matheval.Functions; using System; using System.Collections.Generic; using System.Globalization; +using System.Text; namespace UnitTest { @@ -1188,5 +1191,161 @@ public void Or_Operator_Test() .Bind("b", 1); Assert.AreEqual(true, expr4.Eval()); } + + [TestMethod] + public void Rand_0Parameters_Test() + { + var expr1 = new Expression("Rand()"); + // Shouldn't error + var _ = expr1.Eval(); + } + + [DataTestMethod] + [DataRow("xxx")] + [DataRow(1d)] + [DataRow(true)] + [DataRow(false)] + public void Rand_1Parameter_Test(object seed) + { + string seedString = seed is string ? $"\"{seed.ToString()}\"" : seed.ToString(); + var expr = new Expression($"Rand({seedString})"); + var random = new Random(seed.GetHashCode()); + var expected = Math.Round(random.NextDouble(), 6); + + // Seed should always return same value + Assert.AreEqual(expected, expr.Eval()); + } + + [TestMethod] + public void Rand_2Parameters_Test() + { + var expr = new Expression("Rand(10,12)"); + bool got10 = false; + bool got11 = false; + for (int count = 0; count < 1000; count++) + { + // Either 10 or 11 + var actual = expr.Eval(); + if (actual == 10) + { + got10 = true; + } + else if (actual == 11) + { + got11 = true; + } + else + { + Assert.Fail("Expected 10 or 11"); + } + if (got10 && got11) // Stop if you found a 10 and 11 + break; + } + + if (!got10 || !got11) + { + Assert.Fail("Expected 10 or 11"); + } + } + + [DataTestMethod] + [DataRow("seed")] + [DataRow(1d)] + [DataRow(true)] + [DataRow(false)] + public void Rand_3Parameters_Test(object seed) + { + string seedString = seed is string ? $"\"{seed.ToString()}\"" : seed.ToString(); + var expr = new Expression($"Rand({seedString},1000,2000)"); + var random = new Random(seed.GetHashCode()); + var expected = random.Next(1000, 2000); + + // Seed should always return same value + Assert.AreEqual(expected, expr.Eval()); + } + + [TestMethod] + [DataRow("JOIN('-', 1,'a','#')", "1-a-#")] + [DataRow("JOIN('-', 1,'a')", "1-a")] + [DataRow("JOIN('-', 1)", "1")] + [DataRow("JOIN('-')", "")] + [DataRow("MID('12345',3,1)", "3")] + [DataRow("MID('12345',3,2)", "34")] + [DataRow("MID('12345',3,3)", "345")] + [DataRow("MID('12345',3,4)", "345")] + [DataRow("MID('12345',3)", "345")] + [DataRow("MID('12345',4)", "45")] + [DataRow("MID('12345',5)", "5")] + public void Custom_Function_Test(string formula, string expected) + { + //register new custom functions + Parser.RegisterFunction(typeof(joinFunction)); + Parser.RegisterFunction(typeof(midFunction)); + + //call function + var expr = new Expression(formula); + Assert.AreEqual(expected, expr.Eval()); + } + + public class joinFunction : IFunction + { + public List GetInfo() + { + return new List { + new FunctionDef("join", new Type[] { typeof(object) }, typeof(string), -1) + }; + } + + public object Execute(Dictionary args, ExpressionContext dc) + { + if (args.Count < 2) + return string.Empty; + + string delimiter = null; + var output = new StringBuilder(); + foreach (var arg in args) + { + if (arg.Key == Afe_Common.Const_Key_One) + { + delimiter = arg.Value.ToString(); + } + else + { + if (output.Length > 0) + { + output.Append(delimiter); + } + output.Append(arg.Value); + } + } + return output.ToString(); + } + } + + public class midFunction : IFunction + { + public List GetInfo() + { + return new List { new FunctionDef(Afe_Common.Const_MID, new System.Type[] { typeof(string), typeof(decimal) }, typeof(string), 2) }; + } + + public object Execute(Dictionary args, ExpressionContext dc) + { + return this.Mid( + Afe_Common.ToString(args[Afe_Common.Const_Key_One], dc.WorkingCulture), + Decimal.ToInt32(Afe_Common.ToDecimal(args[Afe_Common.Const_Key_Two], dc.WorkingCulture)) + ); + } + + private string Mid(string stringValue, int index) + { + if (!string.IsNullOrEmpty(stringValue) && index > 0 && index <= stringValue.Length) + { + int len = stringValue.Length - index + 1; + return stringValue.Substring(index - 1, len); + } + return string.Empty; + } + } } } diff --git a/Math expression eval/org.matheval/Common/Afe_Common.cs b/Math expression eval/org.matheval/Common/Afe_Common.cs index dd4c207..28d6492 100644 --- a/Math expression eval/org.matheval/Common/Afe_Common.cs +++ b/Math expression eval/org.matheval/Common/Afe_Common.cs @@ -573,7 +573,7 @@ public static class Afe_Common /// /// Function Random /// - public const string Const_Random = "random"; + public const string Const_Random = "rand"; /// /// Function Ceil diff --git a/Math expression eval/org.matheval/Parser/Parser.cs b/Math expression eval/org.matheval/Parser/Parser.cs index d673c6a..f29c2a3 100644 --- a/Math expression eval/org.matheval/Parser/Parser.cs +++ b/Math expression eval/org.matheval/Parser/Parser.cs @@ -30,6 +30,7 @@ THE SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using static org.matheval.Common.Afe_Common; namespace org.matheval @@ -46,6 +47,11 @@ public class Parser /// private ExpressionContext Dc; + /// + /// Create Dictionary Functions have key is string and value a list of FunctionExecutor + /// + private static Dictionary> Functions = null; + /// /// Create Dictionary Operators have key is string and value is interface IOperator /// @@ -61,13 +67,40 @@ public class Parser /// private Dictionary Constants = null; + public static void RegisterFunction(Type type) + { + RegisterFunction((IFunction)Activator.CreateInstance(type)); + } + + public static void RegisterFunction(IFunction function) + { + InitFunctions(); + foreach (var functionDef in function.GetInfo()) + { + var name = functionDef.Name; + if (!Functions.TryGetValue(name, out var functionExecutors)) + { + functionExecutors = new List(); + Functions.Add(name, functionExecutors); + } + var functionExecutor = new FunctionExecutor(function, functionDef); + if (!functionExecutors.Contains(functionExecutor)) + { + functionExecutors.Add(functionExecutor); + } + else + { + Console.WriteLine("Already registered function: " + name); + } + } + } + /// /// Initializes a new instance structure with no param /// public Parser() { - this.InitOperators(); - this.InitConstants(); + this.Init(); this.Dc = new ExpressionContext(6, MidpointRounding.ToEven, "yyyy-MM-dd", "yyyy-MM-dd HH:mm", @"hh\:mm", CultureInfo.InvariantCulture); } @@ -77,8 +110,7 @@ public Parser() /// formular public Parser(string formular) { - this.InitOperators(); - this.InitConstants(); + this.Init(); this.Dc = new ExpressionContext(6, MidpointRounding.ToEven, "yyyy-MM-dd", "yyyy-MM-dd HH:mm", @"hh\:mm", CultureInfo.InvariantCulture); this.Lexer = new Lexer(formular, this); //this.Lexer.GetToken(); @@ -89,8 +121,7 @@ public Parser(string formular) /// public Parser(ExpressionContext dc) { - this.InitOperators(); - this.InitConstants(); + this.Init(); this.Dc = dc; } @@ -100,8 +131,7 @@ public Parser(ExpressionContext dc) /// formular public Parser(ExpressionContext dc, string formular) { - this.InitOperators(); - this.InitConstants(); + this.Init(); this.Dc = dc; this.Lexer = new Lexer(formular, this); } @@ -186,6 +216,31 @@ public void AddConstant(string constantName, Object value) Constants.Add(constantName.ToLowerInvariant(), value); } + private void Init() + { + InitFunctions(); + this.InitOperators(); + this.InitConstants(); + } + + /// + /// Init Function + /// + private static void InitFunctions() + { + if (Functions == null) + { + Functions = new Dictionary>(); + var iFunctionType = typeof(IFunction); + var types = iFunctionType.Assembly.GetTypes().Where(p => iFunctionType.IsAssignableFrom(p) && p != iFunctionType); + + foreach (var type in types) + { + RegisterFunction(type); + } + } + } + /// /// Init Operators /// @@ -341,26 +396,14 @@ private Implements.Node ParseIdentifier() } } this.Lexer.GetToken();// eat ) - IFunction funcExecuter; - try - { - Type t = Type.GetType("org.matheval.Functions." + identifierStr.ToLowerInvariant() + "Function", true); - Object obj = (Activator.CreateInstance(t)); - - if (obj == null) - { - throw new Exception(); - } - funcExecuter = (IFunction)obj; - } - catch (Exception e) + if (!Functions.TryGetValue(identifierStr.ToLowerInvariant(), out var funcExecuters)) { throw new Exception(string.Format(Afe_Common.MSG_METH_NOTFOUND, new string[] { identifierStr.ToUpperInvariant() })); } - - List functionInfos = funcExecuter.GetInfo(); - foreach (FunctionDef functionInfo in functionInfos) + + foreach (var funcExecuter in funcExecuters) { + var functionInfo = funcExecuter.FunctionDef; //getParamCount() = -1 when params is unlimited if ((functionInfo.ParamCount != -1 && args.Count != functionInfo.ParamCount) || (functionInfo.ParamCount == -1 && args.Count < 1)) @@ -384,7 +427,7 @@ private Implements.Node ParseIdentifier() if (paramsValid) { - CallFuncNode callFuncNode = new CallFuncNode(identifierStr, args, functionInfo.ReturnType, funcExecuter); + CallFuncNode callFuncNode = new CallFuncNode(identifierStr, args, functionInfo.ReturnType, funcExecuter.Function); return callFuncNode; } } @@ -738,6 +781,40 @@ private Implements.Node ParsePrm() throw new Exception(string.Format(Afe_Common.MSG_UNEXPECT_TOKEN_AT_POS, new String[] { this.Lexer.CurrentToken.ToString(), this.Lexer.LexerPosition.ToString() })); } + } + + internal class FunctionExecutor + { + public IFunction Function { get; set; } + public FunctionDef FunctionDef { get; set; } + + public FunctionExecutor(IFunction function, FunctionDef functionDef) + { + Function = function; + FunctionDef = functionDef; + } + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is FunctionExecutor other + && ToString() == other.ToString(); + } + + public override string ToString() + { + return Function.GetType().FullName + + ":" + + FunctionDef.Name + + "(" + + (FunctionDef.ParamCount == -1 ? "params " : "") + + (FunctionDef.Args == null ? "" : string.Join(",", FunctionDef.Args.Select(x => x.FullName).ToArray())) + + ")" + + FunctionDef.ReturnType.FullName; + } } } From 9a9f6ec8b20429fb8e112f5f761091a039db9902 Mon Sep 17 00:00:00 2001 From: bwakabats Date: Mon, 14 Aug 2023 22:44:41 +0100 Subject: [PATCH 2/5] Oops - Remove RAND test from another PR --- Math expression eval/UnitTest/TestAll.cs | 72 ------------------------ 1 file changed, 72 deletions(-) diff --git a/Math expression eval/UnitTest/TestAll.cs b/Math expression eval/UnitTest/TestAll.cs index 9c9b09a..6c0798d 100644 --- a/Math expression eval/UnitTest/TestAll.cs +++ b/Math expression eval/UnitTest/TestAll.cs @@ -1192,78 +1192,6 @@ public void Or_Operator_Test() Assert.AreEqual(true, expr4.Eval()); } - [TestMethod] - public void Rand_0Parameters_Test() - { - var expr1 = new Expression("Rand()"); - // Shouldn't error - var _ = expr1.Eval(); - } - - [DataTestMethod] - [DataRow("xxx")] - [DataRow(1d)] - [DataRow(true)] - [DataRow(false)] - public void Rand_1Parameter_Test(object seed) - { - string seedString = seed is string ? $"\"{seed.ToString()}\"" : seed.ToString(); - var expr = new Expression($"Rand({seedString})"); - var random = new Random(seed.GetHashCode()); - var expected = Math.Round(random.NextDouble(), 6); - - // Seed should always return same value - Assert.AreEqual(expected, expr.Eval()); - } - - [TestMethod] - public void Rand_2Parameters_Test() - { - var expr = new Expression("Rand(10,12)"); - bool got10 = false; - bool got11 = false; - for (int count = 0; count < 1000; count++) - { - // Either 10 or 11 - var actual = expr.Eval(); - if (actual == 10) - { - got10 = true; - } - else if (actual == 11) - { - got11 = true; - } - else - { - Assert.Fail("Expected 10 or 11"); - } - if (got10 && got11) // Stop if you found a 10 and 11 - break; - } - - if (!got10 || !got11) - { - Assert.Fail("Expected 10 or 11"); - } - } - - [DataTestMethod] - [DataRow("seed")] - [DataRow(1d)] - [DataRow(true)] - [DataRow(false)] - public void Rand_3Parameters_Test(object seed) - { - string seedString = seed is string ? $"\"{seed.ToString()}\"" : seed.ToString(); - var expr = new Expression($"Rand({seedString},1000,2000)"); - var random = new Random(seed.GetHashCode()); - var expected = random.Next(1000, 2000); - - // Seed should always return same value - Assert.AreEqual(expected, expr.Eval()); - } - [TestMethod] [DataRow("JOIN('-', 1,'a','#')", "1-a-#")] [DataRow("JOIN('-', 1,'a')", "1-a")] From 6709950fa29e4f9c2200c3c2e816d1123fae60af Mon Sep 17 00:00:00 2001 From: bwakabats Date: Tue, 15 Aug 2023 09:14:10 +0100 Subject: [PATCH 3/5] Add the ability to override internal functions --- Math expression eval/UnitTest/TestAll.cs | 63 +++++++++++++++++++ .../org.matheval/Parser/Parser.cs | 39 ++++++++++-- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/Math expression eval/UnitTest/TestAll.cs b/Math expression eval/UnitTest/TestAll.cs index 6c0798d..0133f72 100644 --- a/Math expression eval/UnitTest/TestAll.cs +++ b/Math expression eval/UnitTest/TestAll.cs @@ -26,8 +26,10 @@ THE SOFTWARE. using org.matheval.Common; using org.matheval.Functions; using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Reflection; using System.Text; namespace UnitTest @@ -1213,6 +1215,8 @@ public void Custom_Function_Test(string formula, string expected) //call function var expr = new Expression(formula); Assert.AreEqual(expected, expr.Eval()); + + ParserUnregisterFunctions(); } public class joinFunction : IFunction @@ -1275,5 +1279,64 @@ private string Mid(string stringValue, int index) return string.Empty; } } + + [TestMethod] + [DataRow("ISBLANK(null)", true, true)] + [DataRow("ISBLANK('')", true, true)] + [DataRow("ISBLANK('A')", false, false)] + [DataRow("ISBLANK(' ')", false, true)] + [DataRow("ISBLANK('\r\n')", false, true)] + [DataRow("ISBLANK('\t')", false, true)] + public void Custom_Function_Replacement_Test(string formula, bool expectedBefore, bool expectedAfter) + { + //call function + var expr = new Expression(formula); + Assert.AreEqual(expectedBefore, expr.Eval()); + + //register new custom functions + Parser.RegisterFunction(typeof(isblankFunction)); + + //call function + expr = new Expression(formula); + Assert.AreEqual(expectedAfter, expr.Eval()); + + ParserUnregisterFunctions(); + } + + /// + /// Alter the ISBLANK function to return true if the value is whitespace + /// + public class isblankFunction : IFunction + { + /// + /// Get Information + /// + /// FunctionDefs + public List GetInfo() + { + return new List{ + new FunctionDef(Afe_Common.Const_Isblank, new System.Type[]{ typeof(Object) }, typeof(Boolean), 1)}; + } + + /// + /// Execute + /// + /// args + /// dc + /// True or False + public Object Execute(Dictionary args, ExpressionContext dc) + { + return string.IsNullOrWhiteSpace(Afe_Common.ToString(args[Afe_Common.Const_Key_One], dc.WorkingCulture)); + } + } + + private static void ParserUnregisterFunctions() + { + var field = typeof(Parser).GetField("Functions", BindingFlags.Static | BindingFlags.NonPublic); + field.SetValue(null, new Dictionary>()); + + field = typeof(Parser).GetField("InternalFunctionsRegistered", BindingFlags.Static | BindingFlags.NonPublic); + field.SetValue(null, false); + } } } diff --git a/Math expression eval/org.matheval/Parser/Parser.cs b/Math expression eval/org.matheval/Parser/Parser.cs index f29c2a3..19e5cd8 100644 --- a/Math expression eval/org.matheval/Parser/Parser.cs +++ b/Math expression eval/org.matheval/Parser/Parser.cs @@ -31,8 +31,11 @@ THE SOFTWARE. using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; using static org.matheval.Common.Afe_Common; +[assembly: InternalsVisibleTo("UnitTest")] + namespace org.matheval { public class Parser @@ -50,7 +53,12 @@ public class Parser /// /// Create Dictionary Functions have key is string and value a list of FunctionExecutor /// - private static Dictionary> Functions = null; + private static Dictionary> Functions = new Dictionary>(); + + /// + /// + /// + private static bool InternalFunctionsRegistered = false; /// /// Create Dictionary Operators have key is string and value is interface IOperator @@ -69,12 +77,31 @@ public class Parser public static void RegisterFunction(Type type) { - RegisterFunction((IFunction)Activator.CreateInstance(type)); + if(InternalFunctionsRegistered) + { + Functions = new Dictionary>(); + InternalFunctionsRegistered = false; + } + RegisterFunctionInternal(type); } public static void RegisterFunction(IFunction function) { - InitFunctions(); + if (InternalFunctionsRegistered) + { + Functions = new Dictionary>(); + InternalFunctionsRegistered = false; + } + RegisterFunctionInternal(function); + } + + public static void RegisterFunctionInternal(Type type) + { + RegisterFunctionInternal((IFunction)Activator.CreateInstance(type)); + } + + public static void RegisterFunctionInternal(IFunction function) + { foreach (var functionDef in function.GetInfo()) { var name = functionDef.Name; @@ -228,16 +255,16 @@ private void Init() /// private static void InitFunctions() { - if (Functions == null) + if (!InternalFunctionsRegistered) { - Functions = new Dictionary>(); var iFunctionType = typeof(IFunction); var types = iFunctionType.Assembly.GetTypes().Where(p => iFunctionType.IsAssignableFrom(p) && p != iFunctionType); foreach (var type in types) { - RegisterFunction(type); + RegisterFunctionInternal(type); } + InternalFunctionsRegistered = true; } } From 0bdb16713f51a27c65a2eb5b29e5f149594ec866 Mon Sep 17 00:00:00 2001 From: bwakabats Date: Wed, 16 Aug 2023 00:48:03 +0100 Subject: [PATCH 4/5] Tweak FunctionExecutor ToString --- .../org.matheval/Functions/FunctionDef.cs | 9 ++++++ .../org.matheval/Parser/Parser.cs | 28 +++++++++++-------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Math expression eval/org.matheval/Functions/FunctionDef.cs b/Math expression eval/org.matheval/Functions/FunctionDef.cs index ec4f4a5..3d7fa6a 100644 --- a/Math expression eval/org.matheval/Functions/FunctionDef.cs +++ b/Math expression eval/org.matheval/Functions/FunctionDef.cs @@ -45,6 +45,15 @@ public class FunctionDef /// public int ParamCount; + /// + /// Function def constructor. paramCount=args.Length + /// + /// Function name + /// Param type + /// return datatype + public FunctionDef(string name, System.Type[] args, System.Type returnType) + : this(name, args, returnType, args.Length) { } + /// /// Function def constructor /// diff --git a/Math expression eval/org.matheval/Parser/Parser.cs b/Math expression eval/org.matheval/Parser/Parser.cs index 19e5cd8..4fc6f52 100644 --- a/Math expression eval/org.matheval/Parser/Parser.cs +++ b/Math expression eval/org.matheval/Parser/Parser.cs @@ -812,36 +812,40 @@ private Implements.Node ParsePrm() internal class FunctionExecutor { - public IFunction Function { get; set; } - public FunctionDef FunctionDef { get; set; } + private string _toString; + + public IFunction Function { get; private set; } + public FunctionDef FunctionDef { get; private set; } public FunctionExecutor(IFunction function, FunctionDef functionDef) { Function = function; FunctionDef = functionDef; + + _toString = Function.GetType().FullName + + ":" + + FunctionDef.Name + + "(" + + (FunctionDef.ParamCount == -1 ? "params " : "") + + (FunctionDef.Args == null ? "" : string.Join(",", FunctionDef.Args.Select(x => x.FullName).ToArray())) + + ")" + + FunctionDef.ReturnType.FullName; } public override int GetHashCode() { - return ToString().GetHashCode(); + return _toString.GetHashCode(); } public override bool Equals(object obj) { return obj is FunctionExecutor other - && ToString() == other.ToString(); + && _toString == other._toString; } public override string ToString() { - return Function.GetType().FullName - + ":" - + FunctionDef.Name - + "(" - + (FunctionDef.ParamCount == -1 ? "params " : "") - + (FunctionDef.Args == null ? "" : string.Join(",", FunctionDef.Args.Select(x => x.FullName).ToArray())) - + ")" - + FunctionDef.ReturnType.FullName; + return _toString; } } } From c3d32daf88285c86cbd85a39be2b8404305562e2 Mon Sep 17 00:00:00 2001 From: bwakabats Date: Sat, 16 Sep 2023 15:21:45 +0100 Subject: [PATCH 5/5] Update Parser.cs RegisterFunctionInternal should be private --- Math expression eval/org.matheval/Parser/Parser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Math expression eval/org.matheval/Parser/Parser.cs b/Math expression eval/org.matheval/Parser/Parser.cs index 4fc6f52..7075342 100644 --- a/Math expression eval/org.matheval/Parser/Parser.cs +++ b/Math expression eval/org.matheval/Parser/Parser.cs @@ -95,12 +95,12 @@ public static void RegisterFunction(IFunction function) RegisterFunctionInternal(function); } - public static void RegisterFunctionInternal(Type type) + private static void RegisterFunctionInternal(Type type) { RegisterFunctionInternal((IFunction)Activator.CreateInstance(type)); } - public static void RegisterFunctionInternal(IFunction function) + private static void RegisterFunctionInternal(IFunction function) { foreach (var functionDef in function.GetInfo()) {