diff --git a/Math expression eval/UnitTest/TestAll.cs b/Math expression eval/UnitTest/TestAll.cs index 48fef10..0133f72 100644 --- a/Math expression eval/UnitTest/TestAll.cs +++ b/Math expression eval/UnitTest/TestAll.cs @@ -23,9 +23,14 @@ THE SOFTWARE. */ using Microsoft.VisualStudio.TestTools.UnitTesting; using org.matheval; +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 { @@ -1188,5 +1193,150 @@ public void Or_Operator_Test() .Bind("b", 1); Assert.AreEqual(true, expr4.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()); + + ParserUnregisterFunctions(); + } + + 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; + } + } + + [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/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/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 d673c6a..7075342 100644 --- a/Math expression eval/org.matheval/Parser/Parser.cs +++ b/Math expression eval/org.matheval/Parser/Parser.cs @@ -30,8 +30,12 @@ THE SOFTWARE. using System; 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 @@ -46,6 +50,16 @@ public class Parser /// private ExpressionContext Dc; + /// + /// Create Dictionary Functions have key is string and value a list of FunctionExecutor + /// + private static Dictionary> Functions = new Dictionary>(); + + /// + /// + /// + private static bool InternalFunctionsRegistered = false; + /// /// Create Dictionary Operators have key is string and value is interface IOperator /// @@ -61,13 +75,59 @@ public class Parser /// private Dictionary Constants = null; + public static void RegisterFunction(Type type) + { + if(InternalFunctionsRegistered) + { + Functions = new Dictionary>(); + InternalFunctionsRegistered = false; + } + RegisterFunctionInternal(type); + } + + public static void RegisterFunction(IFunction function) + { + if (InternalFunctionsRegistered) + { + Functions = new Dictionary>(); + InternalFunctionsRegistered = false; + } + RegisterFunctionInternal(function); + } + + private static void RegisterFunctionInternal(Type type) + { + RegisterFunctionInternal((IFunction)Activator.CreateInstance(type)); + } + + private static void RegisterFunctionInternal(IFunction function) + { + 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 +137,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 +148,7 @@ public Parser(string formular) /// public Parser(ExpressionContext dc) { - this.InitOperators(); - this.InitConstants(); + this.Init(); this.Dc = dc; } @@ -100,8 +158,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 +243,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 (!InternalFunctionsRegistered) + { + var iFunctionType = typeof(IFunction); + var types = iFunctionType.Assembly.GetTypes().Where(p => iFunctionType.IsAssignableFrom(p) && p != iFunctionType); + + foreach (var type in types) + { + RegisterFunctionInternal(type); + } + InternalFunctionsRegistered = true; + } + } + /// /// Init Operators /// @@ -341,26 +423,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 +454,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 +808,44 @@ 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 + { + 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(); + } + + public override bool Equals(object obj) + { + return obj is FunctionExecutor other + && _toString == other._toString; + } + + public override string ToString() + { + return _toString; + } } }