├── .gitignore ├── SimpleExpressionEngine ├── Node.cs ├── SyntaxException.cs ├── Token.cs ├── IContext.cs ├── NodeNumber.cs ├── NodeVariable.cs ├── NodeUnary.cs ├── NodeFunctionCall.cs ├── NodeBinary.cs ├── ReflectionContext.cs ├── Properties │ └── AssemblyInfo.cs ├── SimpleExpressionEngine.csproj ├── Tokenizer.cs └── Parser.cs ├── UnitTests ├── Properties │ └── AssemblyInfo.cs ├── UnitTests.csproj └── UnitTests.cs ├── SimpleExpressionEngine.sln └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | build 4 | Debug 5 | Release 6 | TestResults 7 | .vs 8 | *.sdf 9 | *.opendb 10 | *.opensdf 11 | *.user 12 | *.aps 13 | ipch 14 | *.VC.db 15 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/Node.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleExpressionEngine 2 | { 3 | // Node - abstract class representing one node in the expression 4 | public abstract class Node 5 | { 6 | public abstract double Eval(IContext ctx); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/SyntaxException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleExpressionEngine 4 | { 5 | // Exception for syntax errors 6 | public class SyntaxException : Exception 7 | { 8 | public SyntaxException(string message) 9 | : base(message) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/Token.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleExpressionEngine 2 | { 3 | public enum Token 4 | { 5 | EOF, 6 | Add, 7 | Subtract, 8 | Multiply, 9 | Divide, 10 | OpenParens, 11 | CloseParens, 12 | Comma, 13 | Identifier, 14 | Number, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/IContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SimpleExpressionEngine 8 | { 9 | public interface IContext 10 | { 11 | double ResolveVariable(string name); 12 | double CallFunction(string name, double[] arguments); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/NodeNumber.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleExpressionEngine 2 | { 3 | // NodeNumber represents a literal number in the expression 4 | class NodeNumber : Node 5 | { 6 | public NodeNumber(double number) 7 | { 8 | _number = number; 9 | } 10 | 11 | double _number; // The number 12 | 13 | public override double Eval(IContext ctx) 14 | { 15 | // Just return it. Too easy. 16 | return _number; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/NodeVariable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SimpleExpressionEngine 8 | { 9 | // Represents a variable (or a constant) in an expression. eg: "2 * pi" 10 | public class NodeVariable : Node 11 | { 12 | public NodeVariable(string variableName) 13 | { 14 | _variableName = variableName; 15 | } 16 | 17 | string _variableName; 18 | 19 | public override double Eval(IContext ctx) 20 | { 21 | return ctx.ResolveVariable(_variableName); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/NodeUnary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleExpressionEngine 4 | { 5 | // NodeUnary for unary operations such as Negate 6 | class NodeUnary : Node 7 | { 8 | // Constructor accepts the two nodes to be operated on and function 9 | // that performs the actual operation 10 | public NodeUnary(Node rhs, Func op) 11 | { 12 | _rhs = rhs; 13 | _op = op; 14 | } 15 | 16 | Node _rhs; // Right hand side of the operation 17 | Func _op; // The callback operator 18 | 19 | public override double Eval(IContext ctx) 20 | { 21 | // Evaluate RHS 22 | var rhsVal = _rhs.Eval(ctx); 23 | 24 | // Evaluate and return 25 | var result = _op(rhsVal); 26 | return result; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/NodeFunctionCall.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SimpleExpressionEngine 8 | { 9 | public class NodeFunctionCall : Node 10 | { 11 | public NodeFunctionCall(string functionName, Node[] arguments) 12 | { 13 | _functionName = functionName; 14 | _arguments = arguments; 15 | } 16 | 17 | string _functionName; 18 | Node[] _arguments; 19 | 20 | public override double Eval(IContext ctx) 21 | { 22 | // Evaluate all arguments 23 | var argVals = new double[_arguments.Length]; 24 | for (int i=0; i<_arguments.Length; i++) 25 | { 26 | argVals[i] = _arguments[i].Eval(ctx); 27 | } 28 | 29 | // Call the function 30 | return ctx.CallFunction(_functionName, argVals); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/NodeBinary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleExpressionEngine 4 | { 5 | // NodeBinary for binary operations such as Add, Subtract etc... 6 | class NodeBinary : Node 7 | { 8 | // Constructor accepts the two nodes to be operated on and function 9 | // that performs the actual operation 10 | public NodeBinary(Node lhs, Node rhs, Func op) 11 | { 12 | _lhs = lhs; 13 | _rhs = rhs; 14 | _op = op; 15 | } 16 | 17 | Node _lhs; // Left hand side of the operation 18 | Node _rhs; // Right hand side of the operation 19 | Func _op; // The callback operator 20 | 21 | public override double Eval(IContext ctx) 22 | { 23 | // Evaluate both sides 24 | var lhsVal = _lhs.Eval(ctx); 25 | var rhsVal = _rhs.Eval(ctx); 26 | 27 | // Evaluate and return 28 | var result = _op(lhsVal, rhsVal); 29 | return result; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/ReflectionContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SimpleExpressionEngine 9 | { 10 | public class ReflectionContext : IContext 11 | { 12 | public ReflectionContext(object targetObject) 13 | { 14 | _targetObject = targetObject; 15 | } 16 | 17 | object _targetObject; 18 | 19 | public double ResolveVariable(string name) 20 | { 21 | // Find property 22 | var pi = _targetObject.GetType().GetProperty(name); 23 | if (pi == null) 24 | throw new InvalidDataException($"Unknown variable: '{name}'"); 25 | 26 | // Call the property 27 | return (double)pi.GetValue(_targetObject); 28 | } 29 | 30 | public double CallFunction(string name, double[] arguments) 31 | { 32 | // Find method 33 | var mi = _targetObject.GetType().GetMethod(name); 34 | if (mi == null) 35 | throw new InvalidDataException($"Unknown function: '{name}'"); 36 | 37 | // Convert double array to object array 38 | var argObjs = arguments.Select(x => (object)x).ToArray(); 39 | 40 | // Call the method 41 | return (double)mi.Invoke(_targetObject, argObjs); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /UnitTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("UnitTests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("UnitTests")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("36cd08b3-457b-47c9-942c-7e515383c270")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /SimpleExpressionEngine.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleExpressionEngine", "SimpleExpressionEngine\SimpleExpressionEngine.csproj", "{E0E8BAAD-A574-46FB-89B5-618955D1FA1F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{36CD08B3-457B-47C9-942C-7E515383C270}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {E0E8BAAD-A574-46FB-89B5-618955D1FA1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {E0E8BAAD-A574-46FB-89B5-618955D1FA1F}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {E0E8BAAD-A574-46FB-89B5-618955D1FA1F}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {E0E8BAAD-A574-46FB-89B5-618955D1FA1F}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {36CD08B3-457B-47C9-942C-7E515383C270}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {36CD08B3-457B-47C9-942C-7E515383C270}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {36CD08B3-457B-47C9-942C-7E515383C270}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {36CD08B3-457B-47C9-942C-7E515383C270}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("SimpleExpressionEngine")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("SimpleExpressionEngine")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("e0e8baad-a574-46fb-89b5-618955d1fa1f")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/SimpleExpressionEngine.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {E0E8BAAD-A574-46FB-89B5-618955D1FA1F} 8 | Library 9 | Properties 10 | SimpleExpressionEngine 11 | SimpleExpressionEngine 12 | v4.5.2 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 65 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/Tokenizer.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace SimpleExpressionEngine 6 | { 7 | public class Tokenizer 8 | { 9 | public Tokenizer(TextReader reader) 10 | { 11 | _reader = reader; 12 | NextChar(); 13 | NextToken(); 14 | } 15 | 16 | TextReader _reader; 17 | char _currentChar; 18 | Token _currentToken; 19 | double _number; 20 | string _identifier; 21 | 22 | public Token Token 23 | { 24 | get { return _currentToken; } 25 | } 26 | 27 | public double Number 28 | { 29 | get { return _number; } 30 | } 31 | 32 | public string Identifier 33 | { 34 | get { return _identifier; } 35 | } 36 | 37 | // Read the next character from the input strem 38 | // and store it in _currentChar, or load '\0' if EOF 39 | void NextChar() 40 | { 41 | int ch = _reader.Read(); 42 | _currentChar = ch < 0 ? '\0' : (char)ch; 43 | } 44 | 45 | // Read the next token from the input stream 46 | public void NextToken() 47 | { 48 | // Skip whitespace 49 | while (char.IsWhiteSpace(_currentChar)) 50 | { 51 | NextChar(); 52 | } 53 | 54 | // Special characters 55 | switch (_currentChar) 56 | { 57 | case '\0': 58 | _currentToken = Token.EOF; 59 | return; 60 | 61 | case '+': 62 | NextChar(); 63 | _currentToken = Token.Add; 64 | return; 65 | 66 | case '-': 67 | NextChar(); 68 | _currentToken = Token.Subtract; 69 | return; 70 | 71 | case '*': 72 | NextChar(); 73 | _currentToken = Token.Multiply; 74 | return; 75 | 76 | case '/': 77 | NextChar(); 78 | _currentToken = Token.Divide; 79 | return; 80 | 81 | case '(': 82 | NextChar(); 83 | _currentToken = Token.OpenParens; 84 | return; 85 | 86 | case ')': 87 | NextChar(); 88 | _currentToken = Token.CloseParens; 89 | return; 90 | 91 | case ',': 92 | NextChar(); 93 | _currentToken = Token.Comma; 94 | return; 95 | } 96 | 97 | // Number? 98 | if (char.IsDigit(_currentChar) || _currentChar =='.') 99 | { 100 | // Capture digits/decimal point 101 | var sb = new StringBuilder(); 102 | bool haveDecimalPoint = false; 103 | while (char.IsDigit(_currentChar) || (!haveDecimalPoint && _currentChar == '.')) 104 | { 105 | sb.Append(_currentChar); 106 | haveDecimalPoint = _currentChar == '.'; 107 | NextChar(); 108 | } 109 | 110 | // Parse it 111 | _number = double.Parse(sb.ToString(), CultureInfo.InvariantCulture); 112 | _currentToken = Token.Number; 113 | return; 114 | } 115 | 116 | // Identifier - starts with letter or underscore 117 | if (char.IsLetter(_currentChar) || _currentChar == '_') 118 | { 119 | var sb = new StringBuilder(); 120 | 121 | // Accept letter, digit or underscore 122 | while (char.IsLetterOrDigit(_currentChar) || _currentChar == '_') 123 | { 124 | sb.Append(_currentChar); 125 | NextChar(); 126 | } 127 | 128 | // Setup token 129 | _identifier = sb.ToString(); 130 | _currentToken = Token.Identifier; 131 | return; 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {36CD08B3-457B-47C9-942C-7E515383C270} 7 | Library 8 | Properties 9 | UnitTests 10 | UnitTests 11 | v4.5.2 12 | 512 13 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 14 | 10.0 15 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 16 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 17 | False 18 | UnitTest 19 | 20 | 21 | true 22 | full 23 | false 24 | bin\Debug\ 25 | DEBUG;TRACE 26 | prompt 27 | 4 28 | 29 | 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {e0e8baad-a574-46fb-89b5-618955d1fa1f} 59 | SimpleExpressionEngine 60 | 61 | 62 | 63 | 64 | 65 | 66 | False 67 | 68 | 69 | False 70 | 71 | 72 | False 73 | 74 | 75 | False 76 | 77 | 78 | 79 | 80 | 81 | 82 | 89 | -------------------------------------------------------------------------------- /UnitTests/UnitTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using SimpleExpressionEngine; 5 | 6 | namespace UnitTests 7 | { 8 | [TestClass] 9 | public class UnitTests 10 | { 11 | [TestMethod] 12 | public void TokenizerTest() 13 | { 14 | var testString = "10 + 20 - 30.123"; 15 | var t = new Tokenizer(new StringReader(testString)); 16 | 17 | // "10" 18 | Assert.AreEqual(t.Token, Token.Number); 19 | Assert.AreEqual(t.Number, 10); 20 | t.NextToken(); 21 | 22 | // "+" 23 | Assert.AreEqual(t.Token, Token.Add); 24 | t.NextToken(); 25 | 26 | // "20" 27 | Assert.AreEqual(t.Token, Token.Number); 28 | Assert.AreEqual(t.Number, 20); 29 | t.NextToken(); 30 | 31 | // "-" 32 | Assert.AreEqual(t.Token, Token.Subtract); 33 | t.NextToken(); 34 | 35 | // "30.123" 36 | Assert.AreEqual(t.Token, Token.Number); 37 | Assert.AreEqual(t.Number, 30.123); 38 | t.NextToken(); 39 | } 40 | 41 | [TestMethod] 42 | public void AddSubtractTest() 43 | { 44 | // Add 45 | Assert.AreEqual(Parser.Parse("10 + 20").Eval(null), 30); 46 | 47 | // Subtract 48 | Assert.AreEqual(Parser.Parse("10 - 20").Eval(null), -10); 49 | 50 | // Sequence 51 | Assert.AreEqual(Parser.Parse("10 + 20 - 40 + 100").Eval(null), 90); 52 | } 53 | 54 | [TestMethod] 55 | public void UnaryTest() 56 | { 57 | // Negative 58 | Assert.AreEqual(Parser.Parse("-10").Eval(null), -10); 59 | 60 | // Positive 61 | Assert.AreEqual(Parser.Parse("+10").Eval(null), 10); 62 | 63 | // Negative of a negative 64 | Assert.AreEqual(Parser.Parse("--10").Eval(null), 10); 65 | 66 | // Woah! 67 | Assert.AreEqual(Parser.Parse("--++-+-10").Eval(null), 10); 68 | 69 | // All together now 70 | Assert.AreEqual(Parser.Parse("10 + -20 - +30").Eval(null), -40); 71 | } 72 | 73 | [TestMethod] 74 | public void MultiplyDivideTest() 75 | { 76 | // Add 77 | Assert.AreEqual(Parser.Parse("10 * 20").Eval(null), 200); 78 | 79 | // Subtract 80 | Assert.AreEqual(Parser.Parse("10 / 20").Eval(null), 0.5); 81 | 82 | // Sequence 83 | Assert.AreEqual(Parser.Parse("10 * 20 / 50").Eval(null), 4); 84 | } 85 | 86 | [TestMethod] 87 | public void OrderOfOperation() 88 | { 89 | // No parens 90 | Assert.AreEqual(Parser.Parse("10 + 20 * 30").Eval(null), 610); 91 | 92 | // Parens 93 | Assert.AreEqual(Parser.Parse("(10 + 20) * 30").Eval(null), 900); 94 | 95 | // Parens and negative 96 | Assert.AreEqual(Parser.Parse("-(10 + 20) * 30").Eval(null), -900); 97 | 98 | // Nested 99 | Assert.AreEqual(Parser.Parse("-((10 + 20) * 5) * 30").Eval(null), -4500); 100 | } 101 | 102 | class MyContext : IContext 103 | { 104 | public MyContext(double r) 105 | { 106 | _r = r; 107 | } 108 | 109 | double _r; 110 | 111 | public double ResolveVariable(string name) 112 | { 113 | switch (name) 114 | { 115 | case "pi": return Math.PI; 116 | case "r": return _r; 117 | } 118 | 119 | throw new InvalidDataException($"Unknown variable: '{name}'"); 120 | } 121 | 122 | public double CallFunction(string name, double[] arguments) 123 | { 124 | throw new NotImplementedException(); 125 | } 126 | } 127 | 128 | [TestMethod] 129 | public void Variables() 130 | { 131 | var ctx = new MyContext(10); 132 | 133 | Assert.AreEqual(Parser.Parse("2 * pi * r").Eval(ctx), 2 * Math.PI * 10); 134 | } 135 | 136 | class MyFunctionContext : IContext 137 | { 138 | public MyFunctionContext() 139 | { 140 | } 141 | 142 | public double ResolveVariable(string name) 143 | { 144 | throw new InvalidDataException($"Unknown variable: '{name}'"); 145 | } 146 | 147 | public double CallFunction(string name, double[] arguments) 148 | { 149 | if (name == "rectArea") 150 | { 151 | return arguments[0] * arguments[1]; 152 | } 153 | 154 | if (name == "rectPerimeter") 155 | { 156 | return (arguments[0] + arguments[1]) * 2; 157 | } 158 | 159 | throw new InvalidDataException($"Unknown function: '{name}'"); 160 | } 161 | } 162 | 163 | [TestMethod] 164 | public void Functions() 165 | { 166 | var ctx = new MyFunctionContext(); 167 | Assert.AreEqual(Parser.Parse("rectArea(10,20)").Eval(ctx), 200); 168 | Assert.AreEqual(Parser.Parse("rectPerimeter(10,20)").Eval(ctx), 60); 169 | } 170 | 171 | class MyLibrary 172 | { 173 | public MyLibrary() 174 | { 175 | pi = Math.PI; 176 | } 177 | 178 | public double pi { get; private set; } 179 | public double r { get; set; } 180 | 181 | public double rectArea(double width, double height) 182 | { 183 | return width * height; 184 | } 185 | 186 | public double rectPerimeter(double width, double height) 187 | { 188 | return (width + height) * 2; 189 | } 190 | } 191 | 192 | [TestMethod] 193 | public void Reflection() 194 | { 195 | // Create a library of helper function 196 | var lib = new MyLibrary(); 197 | lib.r = 10; 198 | 199 | // Create a context that uses the library 200 | var ctx = new ReflectionContext(lib); 201 | 202 | // Test 203 | Assert.AreEqual(Parser.Parse("rectArea(10,20)").Eval(ctx), 200); 204 | Assert.AreEqual(Parser.Parse("rectPerimeter(10,20)").Eval(ctx), 60); 205 | Assert.AreEqual(Parser.Parse("2 * pi * r").Eval(ctx), 2 * Math.PI * 10); 206 | } 207 | 208 | } 209 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /SimpleExpressionEngine/Parser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace SimpleExpressionEngine 6 | { 7 | public class Parser 8 | { 9 | // Constructor - just store the tokenizer 10 | public Parser(Tokenizer tokenizer) 11 | { 12 | _tokenizer = tokenizer; 13 | } 14 | 15 | Tokenizer _tokenizer; 16 | 17 | // Parse an entire expression and check EOF was reached 18 | public Node ParseExpression() 19 | { 20 | // For the moment, all we understand is add and subtract 21 | var expr = ParseAddSubtract(); 22 | 23 | // Check everything was consumed 24 | if (_tokenizer.Token != Token.EOF) 25 | throw new SyntaxException("Unexpected characters at end of expression"); 26 | 27 | return expr; 28 | } 29 | 30 | // Parse an sequence of add/subtract operators 31 | Node ParseAddSubtract() 32 | { 33 | // Parse the left hand side 34 | var lhs = ParseMultiplyDivide(); 35 | 36 | while (true) 37 | { 38 | // Work out the operator 39 | Func op = null; 40 | if (_tokenizer.Token == Token.Add) 41 | { 42 | op = (a, b) => a + b; 43 | } 44 | else if (_tokenizer.Token == Token.Subtract) 45 | { 46 | op = (a, b) => a - b; 47 | } 48 | 49 | // Binary operator found? 50 | if (op == null) 51 | return lhs; // no 52 | 53 | // Skip the operator 54 | _tokenizer.NextToken(); 55 | 56 | // Parse the right hand side of the expression 57 | var rhs = ParseMultiplyDivide(); 58 | 59 | // Create a binary node and use it as the left-hand side from now on 60 | lhs = new NodeBinary(lhs, rhs, op); 61 | } 62 | } 63 | 64 | // Parse an sequence of add/subtract operators 65 | Node ParseMultiplyDivide() 66 | { 67 | // Parse the left hand side 68 | var lhs = ParseUnary(); 69 | 70 | while (true) 71 | { 72 | // Work out the operator 73 | Func op = null; 74 | if (_tokenizer.Token == Token.Multiply) 75 | { 76 | op = (a, b) => a * b; 77 | } 78 | else if (_tokenizer.Token == Token.Divide) 79 | { 80 | op = (a, b) => a / b; 81 | } 82 | 83 | // Binary operator found? 84 | if (op == null) 85 | return lhs; // no 86 | 87 | // Skip the operator 88 | _tokenizer.NextToken(); 89 | 90 | // Parse the right hand side of the expression 91 | var rhs = ParseUnary(); 92 | 93 | // Create a binary node and use it as the left-hand side from now on 94 | lhs = new NodeBinary(lhs, rhs, op); 95 | } 96 | } 97 | 98 | 99 | // Parse a unary operator (eg: negative/positive) 100 | Node ParseUnary() 101 | { 102 | while (true) 103 | { 104 | // Positive operator is a no-op so just skip it 105 | if (_tokenizer.Token == Token.Add) 106 | { 107 | // Skip 108 | _tokenizer.NextToken(); 109 | continue; 110 | } 111 | 112 | // Negative operator 113 | if (_tokenizer.Token == Token.Subtract) 114 | { 115 | // Skip 116 | _tokenizer.NextToken(); 117 | 118 | // Parse RHS 119 | // Note this recurses to self to support negative of a negative 120 | var rhs = ParseUnary(); 121 | 122 | // Create unary node 123 | return new NodeUnary(rhs, (a) => -a); 124 | } 125 | 126 | // No positive/negative operator so parse a leaf node 127 | return ParseLeaf(); 128 | } 129 | } 130 | 131 | // Parse a leaf node 132 | // (For the moment this is just a number) 133 | Node ParseLeaf() 134 | { 135 | // Is it a number? 136 | if (_tokenizer.Token == Token.Number) 137 | { 138 | var node = new NodeNumber(_tokenizer.Number); 139 | _tokenizer.NextToken(); 140 | return node; 141 | } 142 | 143 | // Parenthesis? 144 | if (_tokenizer.Token == Token.OpenParens) 145 | { 146 | // Skip '(' 147 | _tokenizer.NextToken(); 148 | 149 | // Parse a top-level expression 150 | var node = ParseAddSubtract(); 151 | 152 | // Check and skip ')' 153 | if (_tokenizer.Token != Token.CloseParens) 154 | throw new SyntaxException("Missing close parenthesis"); 155 | _tokenizer.NextToken(); 156 | 157 | // Return 158 | return node; 159 | } 160 | 161 | // Variable 162 | if (_tokenizer.Token == Token.Identifier) 163 | { 164 | // Capture the name and skip it 165 | var name = _tokenizer.Identifier; 166 | _tokenizer.NextToken(); 167 | 168 | // Parens indicate a function call, otherwise just a variable 169 | if (_tokenizer.Token != Token.OpenParens) 170 | { 171 | // Variable 172 | return new NodeVariable(name); 173 | } 174 | else 175 | { 176 | // Function call 177 | 178 | // Skip parens 179 | _tokenizer.NextToken(); 180 | 181 | // Parse arguments 182 | var arguments = new List(); 183 | while (true) 184 | { 185 | // Parse argument and add to list 186 | arguments.Add(ParseAddSubtract()); 187 | 188 | // Is there another argument? 189 | if (_tokenizer.Token == Token.Comma) 190 | { 191 | _tokenizer.NextToken(); 192 | continue; 193 | } 194 | 195 | // Get out 196 | break; 197 | } 198 | 199 | // Check and skip ')' 200 | if (_tokenizer.Token != Token.CloseParens) 201 | throw new SyntaxException("Missing close parenthesis"); 202 | _tokenizer.NextToken(); 203 | 204 | // Create the function call node 205 | return new NodeFunctionCall(name, arguments.ToArray()); 206 | } 207 | } 208 | 209 | // Don't Understand 210 | throw new SyntaxException($"Unexpect token: {_tokenizer.Token}"); 211 | } 212 | 213 | 214 | #region Convenience Helpers 215 | 216 | // Static helper to parse a string 217 | public static Node Parse(string str) 218 | { 219 | return Parse(new Tokenizer(new StringReader(str))); 220 | } 221 | 222 | // Static helper to parse from a tokenizer 223 | public static Node Parse(Tokenizer tokenizer) 224 | { 225 | var parser = new Parser(tokenizer); 226 | return parser.ParseExpression(); 227 | } 228 | 229 | #endregion 230 | } 231 | } 232 | --------------------------------------------------------------------------------