├── .gitignore ├── CHANGES.md ├── LICENSE ├── MathParser.sln ├── MathParser ├── BooleanParser.cs ├── MathParser.cs ├── MathParser.csproj ├── MathParserException.cs └── Scripting │ ├── IScriptParserLog.cs │ ├── MultilineScriptParserLog.cs │ ├── NullScriptParserLog.cs │ ├── ScriptParser.cs │ └── ScriptParserException.cs ├── MathParserTest ├── BooleanParserTest.cs ├── MathParserTest.csproj ├── Properties │ └── AssemblyInfo.cs ├── ScriptParserTest.cs ├── Test.cs └── packages.config └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # Benchmark Results 46 | BenchmarkDotNet.Artifacts/ 47 | 48 | # .NET Core 49 | project.lock.json 50 | project.fragment.lock.json 51 | artifacts/ 52 | **/Properties/launchSettings.json 53 | 54 | *_i.c 55 | *_p.c 56 | *_i.h 57 | *.ilk 58 | *.meta 59 | *.obj 60 | *.pch 61 | *.pdb 62 | *.pgc 63 | *.pgd 64 | *.rsp 65 | *.sbr 66 | *.tlb 67 | *.tli 68 | *.tlh 69 | *.tmp 70 | *.tmp_proj 71 | *.log 72 | *.vspscc 73 | *.vssscc 74 | .builds 75 | *.pidb 76 | *.svclog 77 | *.scc 78 | 79 | # Chutzpah Test files 80 | _Chutzpah* 81 | 82 | # Visual C++ cache files 83 | ipch/ 84 | *.aps 85 | *.ncb 86 | *.opendb 87 | *.opensdf 88 | *.sdf 89 | *.cachefile 90 | *.VC.db 91 | *.VC.VC.opendb 92 | 93 | # Visual Studio profiler 94 | *.psess 95 | *.vsp 96 | *.vspx 97 | *.sap 98 | 99 | # Visual Studio Trace Files 100 | *.e2e 101 | 102 | # TFS 2012 Local Workspace 103 | $tf/ 104 | 105 | # Guidance Automation Toolkit 106 | *.gpState 107 | 108 | # ReSharper is a .NET coding add-in 109 | _ReSharper*/ 110 | *.[Rr]e[Ss]harper 111 | *.DotSettings.user 112 | 113 | # JustCode is a .NET coding add-in 114 | .JustCode 115 | 116 | # TeamCity is a build add-in 117 | _TeamCity* 118 | 119 | # DotCover is a Code Coverage Tool 120 | *.dotCover 121 | 122 | # AxoCover is a Code Coverage Tool 123 | .axoCover/* 124 | !.axoCover/settings.json 125 | 126 | # Visual Studio code coverage results 127 | *.coverage 128 | *.coveragexml 129 | 130 | # NCrunch 131 | _NCrunch_* 132 | .*crunch*.local.xml 133 | nCrunchTemp_* 134 | 135 | # MightyMoose 136 | *.mm.* 137 | AutoTest.Net/ 138 | 139 | # Web workbench (sass) 140 | .sass-cache/ 141 | 142 | # Installshield output folder 143 | [Ee]xpress/ 144 | 145 | # DocProject is a documentation generator add-in 146 | DocProject/buildhelp/ 147 | DocProject/Help/*.HxT 148 | DocProject/Help/*.HxC 149 | DocProject/Help/*.hhc 150 | DocProject/Help/*.hhk 151 | DocProject/Help/*.hhp 152 | DocProject/Help/Html2 153 | DocProject/Help/html 154 | 155 | # Click-Once directory 156 | publish/ 157 | 158 | # Publish Web Output 159 | *.[Pp]ublish.xml 160 | *.azurePubxml 161 | # Note: Comment the next line if you want to checkin your web deploy settings, 162 | # but database connection strings (with potential passwords) will be unencrypted 163 | *.pubxml 164 | *.publishproj 165 | 166 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 167 | # checkin your Azure Web App publish settings, but sensitive information contained 168 | # in these scripts will be unencrypted 169 | PublishScripts/ 170 | 171 | # NuGet Packages 172 | *.nupkg 173 | # The packages folder can be ignored because of Package Restore 174 | **/[Pp]ackages/* 175 | # except build/, which is used as an MSBuild target. 176 | !**/[Pp]ackages/build/ 177 | # Uncomment if necessary however generally it will be regenerated when needed 178 | #!**/[Pp]ackages/repositories.config 179 | # NuGet v3's project.json files produces more ignorable files 180 | *.nuget.props 181 | *.nuget.targets 182 | 183 | # Microsoft Azure Build Output 184 | csx/ 185 | *.build.csdef 186 | 187 | # Microsoft Azure Emulator 188 | ecf/ 189 | rcf/ 190 | 191 | # Windows Store app package directories and files 192 | AppPackages/ 193 | BundleArtifacts/ 194 | Package.StoreAssociation.xml 195 | _pkginfo.txt 196 | *.appx 197 | 198 | # Visual Studio cache files 199 | # files ending in .cache can be ignored 200 | *.[Cc]ache 201 | # but keep track of directories ending in .cache 202 | !*.[Cc]ache/ 203 | 204 | # Others 205 | ClientBin/ 206 | ~$* 207 | *~ 208 | *.dbmdl 209 | *.dbproj.schemaview 210 | *.jfm 211 | *.pfx 212 | *.publishsettings 213 | orleans.codegen.cs 214 | 215 | # Since there are multiple workflows, uncomment next line to ignore bower_components 216 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 217 | #bower_components/ 218 | 219 | # RIA/Silverlight projects 220 | Generated_Code/ 221 | 222 | # Backup & report files from converting an old project file 223 | # to a newer Visual Studio version. Backup files are not needed, 224 | # because we have git ;-) 225 | _UpgradeReport_Files/ 226 | Backup*/ 227 | UpgradeLog*.XML 228 | UpgradeLog*.htm 229 | 230 | # SQL Server files 231 | *.mdf 232 | *.ldf 233 | *.ndf 234 | 235 | # Business Intelligence projects 236 | *.rdl.data 237 | *.bim.layout 238 | *.bim_*.settings 239 | 240 | # Microsoft Fakes 241 | FakesAssemblies/ 242 | 243 | # GhostDoc plugin setting file 244 | *.GhostDoc.xml 245 | 246 | # Node.js Tools for Visual Studio 247 | .ntvs_analysis.dat 248 | node_modules/ 249 | 250 | # Typescript v1 declaration files 251 | typings/ 252 | 253 | # Visual Studio 6 build log 254 | *.plg 255 | 256 | # Visual Studio 6 workspace options file 257 | *.opt 258 | 259 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 260 | *.vbw 261 | 262 | # Visual Studio LightSwitch build output 263 | **/*.HTMLClient/GeneratedArtifacts 264 | **/*.DesktopClient/GeneratedArtifacts 265 | **/*.DesktopClient/ModelManifest.xml 266 | **/*.Server/GeneratedArtifacts 267 | **/*.Server/ModelManifest.xml 268 | _Pvt_Extensions 269 | 270 | # Paket dependency manager 271 | .paket/paket.exe 272 | paket-files/ 273 | 274 | # FAKE - F# Make 275 | .fake/ 276 | 277 | # JetBrains Rider 278 | .idea/ 279 | *.sln.iml 280 | 281 | # CodeRush 282 | .cr/ 283 | 284 | # Python Tools for Visual Studio (PTVS) 285 | __pycache__/ 286 | *.pyc 287 | 288 | # Cake - Uncomment if you are using it 289 | # tools/** 290 | # !tools/packages.config 291 | 292 | # Tabs Studio 293 | *.tss 294 | 295 | # Telerik's JustMock configuration file 296 | *.jmconfig 297 | 298 | # BizTalk build output 299 | *.btp.cs 300 | *.btm.cs 301 | *.odx.cs 302 | *.xsd.cs 303 | 304 | # OpenCover UI analysis results 305 | OpenCover/ 306 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Version 3.0.0 (???) 2 | * [Fixed some issues with leading minus signs.][cfb4fd1c4cb5d9d622e98d3374d6b49fe3ebd908] (See [#18][18] and [#21][21]) 3 | * [Changed the rounding behavior of `round` from `MidpointRounding.ToEven` to `MidpointRounding.AwayFromZero`. This means 0.5 goes to 1 instead of 0.][0924543515062b1df0ecbe8450beb4f26edbbc16] 4 | * [Added the boolean operators ">=", "<=", "!=", and "==".][e568d2d52f62c337846cc098868eb86bde9474a2] (See [#20][20]) 5 | * [Fixed a divide-by-zero error with the "/" and ":" operators. They now return zero in such case.][fe03fc9d9182e96fef16b5d692b7a78b9c9110f4] 6 | * [Only target the .NET Standard (1.0 - 2.0).][9187f397f6be341979f1e54b34fac42f05e89c35] 7 | * [Added `MathParserException` for parser-related exceptions.][66c256a7a887d58a17a48abebee918d705a07f1c] 8 | * [Added `random` to the list of predefined functions for getting random numbers within a range:][01ccd20c12ac59a20307f4fff9090a780df62adc] 9 | 10 | ``` 11 | - 0 arguments: 0 inclusive - 1 exclusive 12 | - 1 argument : 0 inclusive - arg1 exclusive 13 | - 2 arguments: arg1 inclusive - arg2 exclusive 14 | ``` 15 | 16 | * [The variable declarator ("let") may now be changed.][01c297bdaf870db5690e71832d966f6924c6cc37] 17 | * [Fixed an issue where numbers would lose their sign.][4fef360f03a6c230fcbdf71e2a5e32fe0d53f420] 18 | * [Added `BooleanParser` for parsing and evaluating boolean expressions.][eceb5378b500c6e4abf495b299c8dbe6dcd41991] 19 | * [Added `ScriptParser` and other related items for parsing and evaluating "script"-like expressions.][55c610ebf4f8b55a3296933ed61a6f071501028f] 20 | 21 | `ScriptParser` can handle things like if statements: 22 | ``` 23 | let x = 5 24 | 25 | 0 26 | if (x >= 4) 27 | 1 28 | 29 | end if 30 | ``` 31 | 32 | * Added more unit tests. 33 | * Documentation improvements. 34 | * Simplified some internal logic. 35 | * Other small changes and improvements. 36 | 37 | ### Special Thanks 38 | dennisvg111 39 | Ctznkane525 40 | 41 | [18]: https://github.com/MathosProject/Mathos-Parser/issues/18 42 | [21]: https://github.com/MathosProject/Mathos-Parser/issues/21 43 | [20]: https://github.com/MathosProject/Mathos-Parser/issues/20 44 | [cfb4fd1c4cb5d9d622e98d3374d6b49fe3ebd908]: https://github.com/MathosProject/Mathos-Parser/commit/cfb4fd1c4cb5d9d622e98d3374d6b49fe3ebd908 45 | [0924543515062b1df0ecbe8450beb4f26edbbc16]: https://github.com/MathosProject/Mathos-Parser/commit/0924543515062b1df0ecbe8450beb4f26edbbc16 46 | [e568d2d52f62c337846cc098868eb86bde9474a2]: https://github.com/MathosProject/Mathos-Parser/commit/e568d2d52f62c337846cc098868eb86bde9474a2 47 | [fe03fc9d9182e96fef16b5d692b7a78b9c9110f4]: https://github.com/MathosProject/Mathos-Parser/commit/fe03fc9d9182e96fef16b5d692b7a78b9c9110f4 48 | [9187f397f6be341979f1e54b34fac42f05e89c35]: https://github.com/MathosProject/Mathos-Parser/commit/9187f397f6be341979f1e54b34fac42f05e89c35 49 | [66c256a7a887d58a17a48abebee918d705a07f1c]: https://github.com/MathosProject/Mathos-Parser/commit/66c256a7a887d58a17a48abebee918d705a07f1c 50 | [01ccd20c12ac59a20307f4fff9090a780df62adc]: https://github.com/MathosProject/Mathos-Parser/commit/01ccd20c12ac59a20307f4fff9090a780df62adc 51 | [01c297bdaf870db5690e71832d966f6924c6cc37]: https://github.com/MathosProject/Mathos-Parser/commit/01c297bdaf870db5690e71832d966f6924c6cc37 52 | [4fef360f03a6c230fcbdf71e2a5e32fe0d53f420]: https://github.com/MathosProject/Mathos-Parser/commit/4fef360f03a6c230fcbdf71e2a5e32fe0d53f420 53 | [eceb5378b500c6e4abf495b299c8dbe6dcd41991]: https://github.com/MathosProject/Mathos-Parser/commit/eceb5378b500c6e4abf495b299c8dbe6dcd41991 54 | [55c610ebf4f8b55a3296933ed61a6f071501028f]: https://github.com/MathosProject/Mathos-Parser/commit/55c610ebf4f8b55a3296933ed61a6f071501028f 55 | 56 | # Version 2.0.0 (2018-6-24) 57 | * Target the .Net Standard. 58 | * Dropped support of for `decimal` in favor of `double`. 59 | * `OperatorList` has been removed and `OperatorAction` is now `Operators`. 60 | * Added a `CultureInfo` parameter to the constructor for better localization. 61 | * Added ln, acos, asin, atan, and ceil to the list of predefined functions. 62 | * Leading zeros may now be omitted before decimal points. 63 | 64 | ``` 65 | 0.5 # Before 66 | .5 # After 67 | ``` 68 | 69 | * Minor performance improvements. 70 | * Updates to the documentation 71 | * Other small changes and improvements. 72 | 73 | # Version 1.0.10.1 (2015-1-3) 74 | * Fixed a problem with functions and unary operators. (#1) 75 | 76 | The following would throw an exception: 77 | ``` 78 | -sin(5) 79 | ``` 80 | 81 | * Fixed some internal logic. 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2012-2019, Mathos Project 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MathParser.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.15 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MathParser", "MathParser\MathParser.csproj", "{030EF4D8-7AC6-4E42-A568-C0E88CDD0802}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MathParserTest", "MathParserTest\MathParserTest.csproj", "{B7997EA5-F765-4C5B-B320-B613A3066199}" 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 | {030EF4D8-7AC6-4E42-A568-C0E88CDD0802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {030EF4D8-7AC6-4E42-A568-C0E88CDD0802}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {030EF4D8-7AC6-4E42-A568-C0E88CDD0802}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {030EF4D8-7AC6-4E42-A568-C0E88CDD0802}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {B7997EA5-F765-4C5B-B320-B613A3066199}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {B7997EA5-F765-4C5B-B320-B613A3066199}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {B7997EA5-F765-4C5B-B320-B613A3066199}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {B7997EA5-F765-4C5B-B320-B613A3066199}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {AB574459-9BF8-44B6-A078-04EF54DF67B3} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /MathParser/BooleanParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Mathos.Parser 7 | { 8 | public class BooleanParser 9 | { 10 | public CultureInfo CultureInfo { get { return mathParser.CultureInfo; } } 11 | private MathParser mathParser; 12 | 13 | public BooleanParser(MathParser parser) 14 | { 15 | this.mathParser = parser; 16 | } 17 | 18 | /// 19 | /// Converts the given double to a boolean based on truthy logic, so non-zero values return 1 and zero returns false 20 | /// 21 | /// 22 | /// 23 | public bool ToBoolean(double value) 24 | { 25 | return !Equals(value, 0); 26 | } 27 | 28 | /// 29 | /// Converts the given boolean to a truthy double, zo true gives 1 and false gives 0 30 | /// 31 | /// 32 | /// 33 | public double ToDouble(bool value) 34 | { 35 | return value ? 1 : 0; 36 | } 37 | 38 | /// 39 | /// Converts the give double to a truthy value, so any non-zero value returns 1, and zeroes return zero 40 | /// 41 | /// 42 | /// 43 | public double ToTruthy(double value) 44 | { 45 | return Equals(value, 0) ? 0 : 1; 46 | } 47 | 48 | private bool Equals(double a, double b) 49 | { 50 | return Math.Abs(a - b) < 0.00000001; 51 | } 52 | 53 | public double ProgrammaticallyParse(string condition) 54 | { 55 | return ToTruthy(ProgrammaticallyParseToNumber(condition)); 56 | } 57 | 58 | private double ProgrammaticallyParseToNumber(string condition) 59 | { 60 | condition = condition.Replace("&&", "&"); 61 | condition = condition.Replace("||", "|"); 62 | condition = Regex.Replace(condition, @"\btrue\b", "1", RegexOptions.IgnoreCase); 63 | condition = Regex.Replace(condition, @"\bfalse\b", "0", RegexOptions.IgnoreCase); 64 | condition = condition.Replace("||", "|"); 65 | condition = Regex.Replace(condition, @"\band\b", "&", RegexOptions.IgnoreCase); 66 | condition = Regex.Replace(condition, @"\bor\b", "|", RegexOptions.IgnoreCase); 67 | condition = condition.Trim(); 68 | 69 | if (condition.Contains("(")) 70 | { 71 | int open = condition.IndexOf("("); 72 | int close = open + 1; 73 | 74 | char[] myArray = condition.ToCharArray(); 75 | Stack myStack = new Stack(); 76 | for (int i = open; i < myArray.Length; i++) 77 | { 78 | if (myArray[i] == '(') 79 | { 80 | myStack.Push(myArray[i]); 81 | } 82 | if (myArray[i] == ')') 83 | { 84 | myStack.Pop(); 85 | } 86 | if (myStack.Count == 0) 87 | { 88 | close = i; 89 | break; 90 | } 91 | if (i == myArray.Length - 1) 92 | { 93 | throw new Exception("Brackets don't match"); 94 | } 95 | } 96 | 97 | string innerCondition = condition.Substring(open + 1, (close) - (open + 1)); 98 | // check to see if the brackets aren't part of a operator call, like abs() 99 | if (open == 0 || (!char.IsLetterOrDigit(condition[open - 1]))) 100 | { 101 | condition = condition.Replace("(" + innerCondition + ")", ProgrammaticallyParseToNumber(innerCondition) + ""); 102 | } 103 | } 104 | string[] conditions = condition.Split('&', '|'); 105 | foreach (string c in conditions) 106 | { 107 | condition = condition.Replace(c, mathParser.ProgrammaticallyParse(c).ToString(CultureInfo)); 108 | } 109 | 110 | var tokens = Lexer(condition); 111 | return BasicBooleanExpression(tokens); 112 | } 113 | 114 | private List Lexer(string expr) 115 | { 116 | var token = ""; 117 | var tokens = new List(); 118 | for (var i = 0; i < expr.Length; i++) 119 | { 120 | var ch = expr[i]; 121 | 122 | if (char.IsWhiteSpace(ch)) 123 | { 124 | continue; 125 | } 126 | if (char.IsDigit(ch)) 127 | { 128 | token += ch; 129 | 130 | while (i + 1 < expr.Length && (char.IsDigit(expr[i + 1]) || expr[i + 1] == '.')) 131 | { 132 | token += expr[++i]; 133 | } 134 | tokens.Add(token); 135 | token = ""; 136 | 137 | continue; 138 | } 139 | if (ch == '.') 140 | { 141 | token += ch; 142 | 143 | while (i + 1 < expr.Length && char.IsDigit(expr[i + 1])) 144 | { 145 | token += expr[++i]; 146 | } 147 | tokens.Add(token); 148 | token = ""; 149 | 150 | continue; 151 | } 152 | if (ch == '-') 153 | { 154 | token += ch; 155 | 156 | while (i + 1 < expr.Length && (char.IsDigit(expr[i + 1]) || expr[i + 1] == '.')) 157 | { 158 | token += expr[++i]; 159 | } 160 | tokens.Add(token); 161 | token = ""; 162 | 163 | continue; 164 | } 165 | tokens.Add(ch.ToString()); 166 | } 167 | 168 | return tokens; 169 | } 170 | 171 | /// 172 | /// Executes a basic boolean expression. Note that AND goes before OR in the order of operations, so true AND false OR true equals true 173 | /// 174 | /// 175 | /// 176 | private double BasicBooleanExpression(List tokens) 177 | { 178 | // PERFORMING A BASIC BOOLEAN EXPRESSION CALCULATION 179 | // THIS METHOD CAN ONLY OPERATE WITH NUMBERS AND LOGIC OPERATORS 180 | // AND WILL NOT UNDERSTAND ANYTHING BEYOND THAT. 181 | 182 | switch (tokens.Count) 183 | { 184 | case 1: 185 | return double.Parse(tokens[0], CultureInfo); 186 | case 2: 187 | var op = tokens[0]; 188 | 189 | if (op == "-" || op == "+") 190 | { 191 | var first = op == "+" ? "" : (tokens[1].Substring(0, 1) == "-" ? "" : "-"); 192 | 193 | return double.Parse(first + tokens[1], CultureInfo); 194 | } 195 | throw new Exception("Can't parse tokens as boolean expression: " + tokens[0] + " " + tokens[1]); 196 | case 0: 197 | return 0; 198 | } 199 | 200 | int andIndex = tokens.IndexOf("&"); 201 | while (andIndex > 0) 202 | { 203 | var left = double.Parse(tokens[andIndex - 1], CultureInfo); 204 | var right = double.Parse(tokens[andIndex + 1], CultureInfo); 205 | var result = ToBoolean(left) && ToBoolean(right); 206 | tokens[andIndex - 1] = result ? "1" : "0"; 207 | tokens.RemoveAt(andIndex); 208 | tokens.RemoveAt(andIndex); 209 | andIndex = tokens.IndexOf("&"); 210 | } 211 | 212 | int orIndex = tokens.IndexOf("|"); 213 | while (orIndex > 0) 214 | { 215 | var left = double.Parse(tokens[orIndex - 1], CultureInfo); 216 | var right = double.Parse(tokens[orIndex + 1], CultureInfo); 217 | var result = ToBoolean(left) || ToBoolean(right); 218 | tokens[orIndex - 1] = result ? "1" : "0"; 219 | tokens.RemoveAt(orIndex); 220 | tokens.RemoveAt(orIndex); 221 | orIndex = tokens.IndexOf("|"); 222 | } 223 | 224 | return double.Parse(tokens[0], CultureInfo); 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /MathParser/MathParser.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2019, Mathos Project. 3 | * All rights reserved. 4 | * 5 | * Please see the license file in the project folder 6 | * or go to https://github.com/MathosProject/Mathos-Parser/blob/master/LICENSE.md. 7 | * 8 | * Please feel free to ask me directly at my email! 9 | * artem@artemlos.net 10 | */ 11 | 12 | using System; 13 | using System.Linq; 14 | using System.Globalization; 15 | using System.Collections.Generic; 16 | 17 | namespace Mathos.Parser 18 | { 19 | /// 20 | /// A mathematical expression parser and evaluator. 21 | /// 22 | /// 23 | /// This is considered the default parser for mathematical expressions and provides baseline functionality. 24 | /// For more specialized parsers, see and . 25 | /// 26 | public class MathParser 27 | { 28 | private const char GeqSign = (char) 8805; 29 | private const char LeqSign = (char) 8804; 30 | private const char NeqSign = (char) 8800; 31 | 32 | #region Properties 33 | 34 | /// 35 | /// This contains all of the binary operators defined for the parser. 36 | /// 37 | public Dictionary> Operators { get; set; } 38 | 39 | /// 40 | /// This contains all of the functions defined for the parser. 41 | /// 42 | public Dictionary> LocalFunctions { get; set; } 43 | 44 | /// 45 | /// This contains all of the variables defined for the parser. 46 | /// 47 | public Dictionary LocalVariables { get; set; } 48 | 49 | /// 50 | /// The culture information to use when parsing expressions. 51 | /// 52 | [Obsolete] 53 | public CultureInfo CultureInfo { get; set; } 54 | 55 | /// 56 | /// A random number generator that may be used by functions and operators. 57 | /// 58 | public Random Random { get; set; } = new Random(); 59 | 60 | /// 61 | /// The keyword to use for variable declarations when parsing. The default value is "let". 62 | /// 63 | public string VariableDeclarator { get; set; } = "let"; 64 | 65 | #endregion 66 | 67 | /// 68 | /// Constructs a new with optional functions, operators, and variables. 69 | /// 70 | /// If true, the parser will be initialized with the functions abs, sqrt, pow, root, rem, sign, exp, floor, ceil, round, truncate, log, ln, random, and trigonometric functions. 71 | /// If true, the parser will be initialized with the operators ^, %, :, /, *, -, +, >, <, ≥, ≤, ≠, and =. 72 | /// If true, the parser will be initialized with the variables pi, tao, e, phi, major, minor, pitograd, and piofgrad. 73 | /// The culture information to use when parsing expressions. If null, the parser will use the invariant culture. 74 | public MathParser( 75 | bool loadPreDefinedFunctions = true, 76 | bool loadPreDefinedOperators = true, 77 | bool loadPreDefinedVariables = true, 78 | CultureInfo cultureInfo = null) 79 | { 80 | if (loadPreDefinedOperators) 81 | { 82 | Operators = new Dictionary> 83 | { 84 | ["^"] = Math.Pow, 85 | ["%"] = (a, b) => a % b, 86 | [":"] = (a, b) => 87 | { 88 | if (b != 0) 89 | return a / b; 90 | else if (a > 0) 91 | return double.PositiveInfinity; 92 | else if (a < 0) 93 | return double.NegativeInfinity; 94 | else 95 | return double.NaN; 96 | }, 97 | ["/"] = (a, b) => 98 | { 99 | if (b != 0) 100 | return a / b; 101 | else if (a > 0) 102 | return double.PositiveInfinity; 103 | else if (a < 0) 104 | return double.NegativeInfinity; 105 | else 106 | return double.NaN; 107 | }, 108 | ["*"] = (a, b) => a * b, 109 | ["-"] = (a, b) => a - b, 110 | ["+"] = (a, b) => a + b, 111 | 112 | [">"] = (a, b) => a > b ? 1 : 0, 113 | ["<"] = (a, b) => a < b ? 1 : 0, 114 | ["" + GeqSign] = (a, b) => a > b || Math.Abs(a - b) < 0.00000001 ? 1 : 0, 115 | ["" + LeqSign] = (a, b) => a < b || Math.Abs(a - b) < 0.00000001 ? 1 : 0, 116 | ["" + NeqSign] = (a, b) => Math.Abs(a - b) < 0.00000001 ? 0 : 1, 117 | ["="] = (a, b) => Math.Abs(a - b) < 0.00000001 ? 1 : 0 118 | }; 119 | } 120 | else 121 | { 122 | Operators = new Dictionary>(); 123 | } 124 | 125 | if (loadPreDefinedFunctions) 126 | { 127 | LocalFunctions = new Dictionary> 128 | { 129 | ["abs"] = inputs => Math.Abs(inputs[0]), 130 | 131 | ["cos"] = inputs => Math.Cos(inputs[0]), 132 | ["cosh"] = inputs => Math.Cosh(inputs[0]), 133 | ["acos"] = inputs => Math.Acos(inputs[0]), 134 | ["arccos"] = inputs => Math.Acos(inputs[0]), 135 | 136 | ["sin"] = inputs => Math.Sin(inputs[0]), 137 | ["sinh"] = inputs => Math.Sinh(inputs[0]), 138 | ["asin"] = inputs => Math.Asin(inputs[0]), 139 | ["arcsin"] = inputs => Math.Asin(inputs[0]), 140 | 141 | ["tan"] = inputs => Math.Tan(inputs[0]), 142 | ["tanh"] = inputs => Math.Tanh(inputs[0]), 143 | ["atan"] = inputs => Math.Atan(inputs[0]), 144 | ["arctan"] = inputs => Math.Atan(inputs[0]), 145 | 146 | ["sqrt"] = inputs => Math.Sqrt(inputs[0]), 147 | ["pow"] = inputs => Math.Pow(inputs[0], inputs[1]), 148 | ["root"] = inputs => Math.Pow(inputs[0], 1 / inputs[1]), 149 | ["rem"] = inputs => Math.IEEERemainder(inputs[0], inputs[1]), 150 | 151 | ["sign"] = inputs => Math.Sign(inputs[0]), 152 | ["exp"] = inputs => Math.Exp(inputs[0]), 153 | 154 | ["floor"] = inputs => Math.Floor(inputs[0]), 155 | ["ceil"] = inputs => Math.Ceiling(inputs[0]), 156 | ["ceiling"] = inputs => Math.Ceiling(inputs[0]), 157 | ["round"] = inputs => Math.Round(inputs[0], MidpointRounding.AwayFromZero), 158 | ["truncate"] = inputs => inputs[0] < 0 ? -Math.Floor(-inputs[0]) : Math.Floor(inputs[0]), 159 | 160 | ["log"] = inputs => 161 | { 162 | switch (inputs.Length) 163 | { 164 | case 1: 165 | return Math.Log10(inputs[0]); 166 | case 2: 167 | return Math.Log(inputs[0], inputs[1]); 168 | default: 169 | return 0; 170 | } 171 | }, 172 | 173 | ["random"] = inputs => 174 | { 175 | if (inputs.Length == 0 || (inputs.Length == 1 && inputs[0] == 0)) 176 | { 177 | inputs = new double[1]; 178 | inputs[0] = 1; 179 | } 180 | if (inputs.Length == 2) 181 | { 182 | return Random.NextDouble() * (inputs[1] - inputs[0]) + inputs[0]; 183 | } 184 | return Random.NextDouble() * inputs[0]; 185 | }, 186 | 187 | ["ln"] = inputs => Math.Log(inputs[0]) 188 | }; 189 | } 190 | else 191 | { 192 | LocalFunctions = new Dictionary>(); 193 | } 194 | 195 | if (loadPreDefinedVariables) 196 | { 197 | LocalVariables = new Dictionary 198 | { 199 | ["pi"] = 3.14159265358979, 200 | ["tao"] = 6.28318530717959, 201 | 202 | ["e"] = 2.71828182845905, 203 | ["phi"] = 1.61803398874989, 204 | ["major"] = 0.61803398874989, 205 | ["minor"] = 0.38196601125011, 206 | 207 | ["pitograd"] = 57.2957795130823, 208 | ["piofgrad"] = 0.01745329251994 209 | }; 210 | } 211 | else 212 | { 213 | LocalVariables = new Dictionary(); 214 | } 215 | 216 | CultureInfo = cultureInfo ?? CultureInfo.InvariantCulture; 217 | } 218 | 219 | /// 220 | /// Parse and evaluate a mathematical expression. 221 | /// 222 | /// 223 | /// This method does not evaluate variable declarations. 224 | /// For a method that does, please use . 225 | /// 226 | /// 227 | /// 228 | /// using System.Diagnostics; 229 | /// 230 | /// var parser = new MathParser(false, true, false); 231 | /// Debug.Assert(parser.Parse("2 + 2") == 4); 232 | /// 233 | /// 234 | /// The math expression to parse and evaluate. 235 | /// Returns the result of executing the given math expression. 236 | public double Parse(string mathExpression) 237 | { 238 | return MathParserLogic(Lexer(mathExpression)); 239 | } 240 | 241 | /// 242 | /// Evaluate a mathematical expression in the form of tokens. 243 | /// 244 | /// 245 | /// This method does not evaluate variable declarations. 246 | /// For a method that does, please use . 247 | /// 248 | /// 249 | /// 250 | /// using System.Diagnostics; 251 | /// 252 | /// var parser = new MathParser(false, true, false); 253 | /// var tokens = parser.GetTokens("2 + 2"); 254 | /// Debug.Assert(parser.Parse(tokens) == 4); 255 | /// 256 | /// 257 | /// The math expression in tokens to parse and evaluate. 258 | /// Returns the result of executing the given math expression. 259 | public double Parse(IReadOnlyCollection tokens) 260 | { 261 | return MathParserLogic(new List(tokens)); 262 | } 263 | 264 | /// 265 | /// Parse and evaluate a mathematical expression with comments and variable declarations taken into account. 266 | /// 267 | /// 268 | /// The syntax for declaring/editing a variable is either "let a = 0", "let a be 0", or "let a := 0" where 269 | /// "let" is the keyword specified by . 270 | /// 271 | /// This method evaluates comments and variable declarations. 272 | /// For a method that doesn't, please use either or . 273 | /// 274 | /// 275 | /// 276 | /// using System.Diagnostics; 277 | /// 278 | /// var parser = new MathParser(false, true, false); 279 | /// parser.ProgrammaticallyParse("let my_var = 7"); 280 | /// 281 | /// Debug.Assert(parser.Parse("my_var - 3") == 4); 282 | /// 283 | /// 284 | /// The math expression to parse and evaluate. 285 | /// If true, attempt to correct any typos found in the expression. 286 | /// If true, treat "#" as a single-line comment and treat "#{" and "}#" as multi-line comments. 287 | /// Returns the result of executing the given math expression. 288 | public double ProgrammaticallyParse(string mathExpression, bool correctExpression = true, bool identifyComments = true) 289 | { 290 | if (identifyComments) 291 | { 292 | // Delete Comments #{Comment}# 293 | mathExpression = System.Text.RegularExpressions.Regex.Replace(mathExpression, "#\\{.*?\\}#", ""); 294 | 295 | // Delete Comments #Comment 296 | mathExpression = System.Text.RegularExpressions.Regex.Replace(mathExpression, "#.*$", ""); 297 | } 298 | 299 | if (correctExpression) 300 | { 301 | // this refers to the Correction function which will correct stuff like artn to arctan, etc. 302 | mathExpression = Correction(mathExpression); 303 | } 304 | 305 | string varName; 306 | double varValue; 307 | 308 | if (mathExpression.Contains(VariableDeclarator)) 309 | { 310 | if (mathExpression.Contains("be")) 311 | { 312 | varName = mathExpression.Substring(mathExpression.IndexOf(VariableDeclarator, StringComparison.Ordinal) + 3, 313 | mathExpression.IndexOf("be", StringComparison.Ordinal) - 314 | mathExpression.IndexOf(VariableDeclarator, StringComparison.Ordinal) - 3); 315 | mathExpression = mathExpression.Replace(varName + "be", ""); 316 | } 317 | else 318 | { 319 | varName = mathExpression.Substring(mathExpression.IndexOf(VariableDeclarator, StringComparison.Ordinal) + 3, 320 | mathExpression.IndexOf("=", StringComparison.Ordinal) - 321 | mathExpression.IndexOf(VariableDeclarator, StringComparison.Ordinal) - 3); 322 | mathExpression = mathExpression.Replace(varName + "=", ""); 323 | } 324 | 325 | varName = varName.Replace(" ", ""); 326 | mathExpression = mathExpression.Replace(VariableDeclarator, ""); 327 | 328 | varValue = Parse(mathExpression); 329 | 330 | if (LocalVariables.ContainsKey(varName)) 331 | { 332 | LocalVariables[varName] = varValue; 333 | } 334 | else 335 | { 336 | LocalVariables.Add(varName, varValue); 337 | } 338 | 339 | return varValue; 340 | } 341 | 342 | if (!mathExpression.Contains(":=")) 343 | { 344 | return Parse(mathExpression); 345 | } 346 | 347 | //mathExpression = mathExpression.Replace(" ", ""); // remove white space 348 | varName = mathExpression.Substring(0, mathExpression.IndexOf(":=", StringComparison.Ordinal)); 349 | mathExpression = mathExpression.Replace(varName + ":=", ""); 350 | 351 | varValue = Parse(mathExpression); 352 | varName = varName.Replace(" ", ""); 353 | 354 | if (LocalVariables.ContainsKey(varName)) 355 | { 356 | LocalVariables[varName] = varValue; 357 | } 358 | else 359 | { 360 | LocalVariables.Add(varName, varValue); 361 | } 362 | 363 | return varValue; 364 | } 365 | 366 | /// 367 | /// Tokenize a mathematical expression. 368 | /// 369 | /// 370 | /// This method does not evaluate the expression. 371 | /// For a method that does, please use one of the Parse methods. 372 | /// 373 | /// 374 | /// 375 | /// using System.Diagnostics; 376 | /// 377 | /// var parser = new MathParser(false, true, false); 378 | /// parser.GetTokens("2 + 2"); 379 | /// 380 | /// 381 | /// The math expression to tokenize. 382 | /// Returns the tokens of the given math expression. 383 | public IReadOnlyCollection GetTokens(string mathExpression) 384 | { 385 | return Lexer(mathExpression); 386 | } 387 | 388 | #region Core 389 | 390 | // This will correct sqrt() and arctan() typos. 391 | private string Correction(string input) 392 | { 393 | // Word corrections 394 | 395 | input = System.Text.RegularExpressions.Regex.Replace(input, "\\b(sqr|sqrt)\\b", "sqrt", System.Text.RegularExpressions.RegexOptions.IgnoreCase); 396 | input = System.Text.RegularExpressions.Regex.Replace(input, "\\b(atan2|arctan2)\\b", "arctan2", System.Text.RegularExpressions.RegexOptions.IgnoreCase); 397 | //... and more 398 | 399 | return input; 400 | } 401 | 402 | private List Lexer(string expr) 403 | { 404 | var token = ""; 405 | var tokens = new List(); 406 | 407 | expr = expr.Replace("+-", "-"); 408 | expr = expr.Replace("-+", "-"); 409 | expr = expr.Replace("--", "+"); 410 | expr = expr.Replace("==", "="); 411 | expr = expr.Replace(">=", "" + GeqSign); 412 | expr = expr.Replace("<=", "" + LeqSign); 413 | expr = expr.Replace("!=", "" + NeqSign); 414 | 415 | for (var i = 0; i < expr.Length; i++) 416 | { 417 | var ch = expr[i]; 418 | 419 | if (char.IsWhiteSpace(ch)) 420 | { 421 | continue; 422 | } 423 | 424 | if (char.IsLetter(ch)) 425 | { 426 | if (i != 0 && (char.IsDigit(expr[i - 1]) || expr[i - 1] == ')')) 427 | { 428 | tokens.Add("*"); 429 | } 430 | 431 | token += ch; 432 | 433 | while (i + 1 < expr.Length && char.IsLetterOrDigit(expr[i + 1])) 434 | { 435 | token += expr[++i]; 436 | } 437 | 438 | tokens.Add(token); 439 | token = ""; 440 | 441 | continue; 442 | } 443 | 444 | if (char.IsDigit(ch)) 445 | { 446 | token += ch; 447 | 448 | while (i + 1 < expr.Length && (char.IsDigit(expr[i + 1]) || expr[i + 1] == '.')) 449 | { 450 | token += expr[++i]; 451 | } 452 | 453 | tokens.Add(token); 454 | token = ""; 455 | 456 | continue; 457 | } 458 | 459 | if (ch == '.') 460 | { 461 | token += ch; 462 | 463 | while (i + 1 < expr.Length && char.IsDigit(expr[i + 1])) 464 | { 465 | token += expr[++i]; 466 | } 467 | 468 | tokens.Add(token); 469 | token = ""; 470 | 471 | continue; 472 | } 473 | 474 | if (i + 1 < expr.Length && 475 | (ch == '-' || ch == '+') && 476 | char.IsDigit(expr[i + 1]) && 477 | (i == 0 || (tokens.Count > 0 && Operators.ContainsKey(tokens.Last())) || i - 1 > 0 && expr[i - 1] == '(')) 478 | { 479 | // if the above is true, then the token for that negative number will be "-1", not "-","1". 480 | // to sum up, the above will be true if the minus sign is in front of the number, but 481 | // at the beginning, for example, -1+2, or, when it is inside the brakets (-1), or when it comes after another operator. 482 | // NOTE: this works for + as well! 483 | 484 | token += ch; 485 | 486 | while (i + 1 < expr.Length && (char.IsDigit(expr[i + 1]) || expr[i + 1] == '.')) 487 | { 488 | token += expr[++i]; 489 | } 490 | 491 | tokens.Add(token); 492 | token = ""; 493 | 494 | continue; 495 | } 496 | 497 | if (ch == '(') 498 | { 499 | if (i != 0 && (char.IsDigit(expr[i - 1]) || char.IsDigit(expr[i - 1]) || expr[i - 1] == ')')) 500 | { 501 | tokens.Add("*"); 502 | tokens.Add("("); 503 | } 504 | else 505 | { 506 | tokens.Add("("); 507 | } 508 | } 509 | else 510 | { 511 | tokens.Add(ch.ToString()); 512 | } 513 | } 514 | 515 | return tokens; 516 | } 517 | 518 | private double MathParserLogic(List tokens) 519 | { 520 | // Variables replacement 521 | for (var i = 0; i < tokens.Count; i++) 522 | { 523 | if (LocalVariables.Keys.Contains(tokens[i])) 524 | { 525 | tokens[i] = LocalVariables[tokens[i]].ToString(CultureInfo); 526 | } 527 | } 528 | 529 | while (tokens.IndexOf("(") != -1) 530 | { 531 | // getting data between "(" and ")" 532 | var open = tokens.LastIndexOf("("); 533 | var close = tokens.IndexOf(")", open); // in case open is -1, i.e. no "(" // , open == 0 ? 0 : open - 1 534 | 535 | if (open >= close) 536 | { 537 | throw new ArithmeticException("No closing bracket/parenthesis. Token: " + open.ToString(CultureInfo)); 538 | } 539 | 540 | var roughExpr = new List(); 541 | 542 | for (var i = open + 1; i < close; i++) 543 | { 544 | roughExpr.Add(tokens[i]); 545 | } 546 | 547 | double tmpResult; 548 | 549 | var args = new List(); 550 | var functionName = tokens[open == 0 ? 0 : open - 1]; 551 | 552 | if (LocalFunctions.Keys.Contains(functionName)) 553 | { 554 | if (roughExpr.Contains(",")) 555 | { 556 | // converting all arguments into a double array 557 | for (var i = 0; i < roughExpr.Count; i++) 558 | { 559 | var defaultExpr = new List(); 560 | var firstCommaOrEndOfExpression = 561 | roughExpr.IndexOf(",", i) != -1 562 | ? roughExpr.IndexOf(",", i) 563 | : roughExpr.Count; 564 | 565 | while (i < firstCommaOrEndOfExpression) 566 | { 567 | defaultExpr.Add(roughExpr[i++]); 568 | } 569 | 570 | args.Add(defaultExpr.Count == 0 ? 0 : BasicArithmeticalExpression(defaultExpr)); 571 | } 572 | 573 | // finally, passing the arguments to the given function 574 | tmpResult = double.Parse(LocalFunctions[functionName](args.ToArray()).ToString(CultureInfo), CultureInfo); 575 | } 576 | else 577 | { 578 | if (roughExpr.Count == 0) 579 | tmpResult = LocalFunctions[functionName](new double[0]); 580 | else 581 | { 582 | tmpResult = double.Parse(LocalFunctions[functionName](new[] 583 | { 584 | BasicArithmeticalExpression(roughExpr) 585 | }).ToString(CultureInfo), CultureInfo); 586 | } 587 | } 588 | } 589 | else 590 | { 591 | // if no function is need to execute following expression, pass it 592 | // to the "BasicArithmeticalExpression" method. 593 | tmpResult = BasicArithmeticalExpression(roughExpr); 594 | } 595 | 596 | // when all the calculations have been done 597 | // we replace the "opening bracket with the result" 598 | // and removing the rest. 599 | tokens[open] = tmpResult.ToString(CultureInfo); 600 | tokens.RemoveRange(open + 1, close - open); 601 | 602 | if (LocalFunctions.Keys.Contains(functionName)) 603 | { 604 | // if we also executed a function, removing 605 | // the function name as well. 606 | tokens.RemoveAt(open - 1); 607 | } 608 | } 609 | 610 | // at this point, we should have replaced all brackets 611 | // with the appropriate values, so we can simply 612 | // calculate the expression. it's not so complex 613 | // any more! 614 | return BasicArithmeticalExpression(tokens); 615 | } 616 | 617 | private double BasicArithmeticalExpression(List tokens) 618 | { 619 | // PERFORMING A BASIC ARITHMETICAL EXPRESSION CALCULATION 620 | // THIS METHOD CAN ONLY OPERATE WITH NUMBERS AND OPERATORS 621 | // AND WILL NOT UNDERSTAND ANYTHING BEYOND THAT. 622 | 623 | double token0; 624 | double token1; 625 | 626 | switch (tokens.Count) 627 | { 628 | case 1: 629 | if (!double.TryParse(tokens[0], NumberStyles.Number, CultureInfo, out token0)) 630 | { 631 | throw new MathParserException("local variable " + tokens[0] + " is undefined"); 632 | } 633 | 634 | return token0; 635 | case 2: 636 | var op = tokens[0]; 637 | 638 | if (op == "-" || op == "+") 639 | { 640 | var first = op == "+" ? "" : (tokens[1].Substring(0, 1) == "-" ? "" : "-"); 641 | 642 | if (!double.TryParse(first + tokens[1], NumberStyles.Number, CultureInfo, out token1)) 643 | { 644 | throw new MathParserException("local variable " + first + tokens[1] + " is undefined"); 645 | } 646 | 647 | return token1; 648 | } 649 | 650 | if (!Operators.ContainsKey(op)) 651 | { 652 | throw new MathParserException("operator " + op + " is not defined"); 653 | } 654 | 655 | if (!double.TryParse(tokens[1], NumberStyles.Number, CultureInfo, out token1)) 656 | { 657 | throw new MathParserException("local variable " + tokens[1] + " is undefined"); 658 | } 659 | 660 | return Operators[op](0, token1); 661 | case 0: 662 | return 0; 663 | } 664 | 665 | foreach (var op in Operators) 666 | { 667 | int opPlace; 668 | 669 | while ((opPlace = tokens.IndexOf(op.Key)) != -1) 670 | { 671 | double rhs; 672 | 673 | if (!double.TryParse(tokens[opPlace + 1], NumberStyles.Number, CultureInfo, out rhs)) 674 | { 675 | throw new MathParserException("local variable " + tokens[opPlace + 1] + " is undefined"); 676 | } 677 | 678 | if (op.Key == "-" && opPlace == 0) 679 | { 680 | var result = op.Value(0.0, rhs); 681 | tokens[0] = result.ToString(CultureInfo); 682 | tokens.RemoveRange(opPlace + 1, 1); 683 | } 684 | else 685 | { 686 | double lhs; 687 | 688 | if (!double.TryParse(tokens[opPlace - 1], NumberStyles.Number, CultureInfo, out lhs)) 689 | { 690 | throw new MathParserException("local variable " + tokens[opPlace - 1] + " is undefined"); 691 | } 692 | 693 | var result = op.Value(lhs, rhs); 694 | tokens[opPlace - 1] = result.ToString(CultureInfo); 695 | tokens.RemoveRange(opPlace, 2); 696 | } 697 | } 698 | } 699 | 700 | if (!double.TryParse(tokens[0], NumberStyles.Number, CultureInfo, out token0)) 701 | { 702 | throw new MathParserException("local variable " + tokens[0] + " is undefined"); 703 | } 704 | 705 | return token0; 706 | } 707 | 708 | #endregion 709 | } 710 | } -------------------------------------------------------------------------------- /MathParser/MathParser.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard1.0;netstandard1.1;netstandard1.2;netstandard1.3;netstandard1.4;netstandard1.5;netstandard1.6;netstandard2.0 4 | Mathos.Parser 5 | 3.0.0 6 | Mathos Parser is a mathematical expression parser targeting the .NET Framework and .NET Standard that parses all kinds of mathematical expressions with the ability to use custom functions, operators, and variables. 7 | Mathos Project 8 | MathosParser 9 | https://github.com/MathosProject/Mathos-Parser/blob/master/LICENSE.md 10 | https://github.com/MathosProject/Mathos-Parser 11 | https://github.com/MathosProject/Mathos-Parser 12 | Copyright © 2012-2019, Mathos Project. 13 | Mathos Project 14 | true 15 | true 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /MathParser/MathParserException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mathos.Parser 4 | { 5 | public sealed class MathParserException : Exception 6 | { 7 | 8 | public MathParserException() 9 | { 10 | } 11 | 12 | public MathParserException(string message) : base(message) 13 | { 14 | } 15 | 16 | public MathParserException(string message, Exception innerException) : base(message, innerException) 17 | { 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /MathParser/Scripting/IScriptParserLog.cs: -------------------------------------------------------------------------------- 1 | namespace Mathos.Parser.Scripting 2 | { 3 | public interface IScriptParserLog 4 | { 5 | void Log(string log); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /MathParser/Scripting/MultilineScriptParserLog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace Mathos.Parser.Scripting 5 | { 6 | /// 7 | /// An implementation of that appends logs to a multiline string 8 | /// 9 | public class MultilineScriptParserLog : IScriptParserLog 10 | { 11 | private StringBuilder sb; 12 | public MultilineScriptParserLog() 13 | { 14 | sb = new StringBuilder(); 15 | } 16 | public string Output { get { return sb.ToString(); } } 17 | public void Log(string log) 18 | { 19 | if (Output.Length > 0) 20 | { 21 | sb.Append(Environment.NewLine); 22 | } 23 | sb.Append(log); 24 | } 25 | 26 | public void Clear() 27 | { 28 | sb.Clear(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MathParser/Scripting/NullScriptParserLog.cs: -------------------------------------------------------------------------------- 1 | namespace Mathos.Parser.Scripting 2 | { 3 | /// 4 | /// An implementation of that doesn't do anything with logs 5 | /// 6 | public sealed class NullScriptParserLog : IScriptParserLog 7 | { 8 | public void Log(string log) 9 | { 10 | //don't do anything with logs in this class 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MathParser/Scripting/ScriptParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | 6 | namespace Mathos.Parser.Scripting 7 | { 8 | public class ScriptParser 9 | { 10 | public Dictionary LocalVariables { get { return mathParser.LocalVariables; } } 11 | public Dictionary> Operators { get { return mathParser.Operators; } } 12 | public Dictionary> LocalFunctions { get { return mathParser.LocalFunctions; } } 13 | public IScriptParserLog Logger { get; set; } 14 | public string LogFunctionName { get; set; } = "print"; 15 | 16 | private MathParser mathParser; 17 | private BooleanParser booleanParser; 18 | public ScriptParser(IScriptParserLog logger = null, bool loadPreDefinedFunctions = true, bool loadPreDefinedOperators = true, bool loadPreDefinedVariables = true, CultureInfo cultureInfo = null) 19 | { 20 | mathParser = new MathParser(loadPreDefinedFunctions, loadPreDefinedOperators, loadPreDefinedVariables, cultureInfo); 21 | booleanParser = new BooleanParser(mathParser); 22 | this.Logger = logger; 23 | if (logger == null) 24 | { 25 | this.Logger = new NullScriptParserLog(); 26 | } 27 | } 28 | 29 | /// 30 | /// Executes a string containg multiple lines as a script, and returns the last calculated number 31 | /// 32 | /// 33 | /// 34 | public double ExecuteMultiline(string script) 35 | { 36 | string[] lines = script.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); 37 | return ExecuteLines(lines); 38 | } 39 | 40 | /// 41 | /// Executes a list of strings as a script, line by line, and returns the last calculated number 42 | /// 43 | /// 44 | /// 45 | public double ExecuteLines(IEnumerable linesEnumerable) 46 | { 47 | var lines = linesEnumerable.ToArray(); 48 | 49 | double lastOutput = 0; 50 | Stack chainStates = new Stack(); 51 | int lineNumber = 0; 52 | try 53 | { 54 | while (lineNumber < lines.Length) 55 | { 56 | var line = lines[lineNumber].Trim().ToLowerInvariant(); 57 | if (string.IsNullOrWhiteSpace(line)) 58 | { 59 | lineNumber++; 60 | continue; 61 | } 62 | IfChainState currentState = IfChainState.Executing; 63 | if (chainStates.Count > 0) 64 | { 65 | currentState = chainStates.Peek(); 66 | } 67 | if (line.StartsWith("if")) 68 | { 69 | if (currentState == IfChainState.Executing) 70 | { 71 | string condition = line.Substring(line.IndexOf("if") + 2).Trim(); 72 | var result = booleanParser.ProgrammaticallyParse(condition); 73 | bool executeIf = booleanParser.ToBoolean(result); 74 | chainStates.Push(executeIf ? IfChainState.Executing : IfChainState.NotExecuted); 75 | } 76 | else 77 | { 78 | // The if statement this if statement is in is not executing, so we push executed on the stack to make sure the contents of this don't get executed, and any following if else/else statements don't get executed either 79 | chainStates.Push(IfChainState.Executed); 80 | } 81 | } 82 | else if (line.StartsWith("else if") || line.StartsWith("elif")) 83 | { 84 | var oldState = chainStates.Pop(); 85 | if (oldState == IfChainState.NotExecuted) 86 | { 87 | string condition = line.Substring(line.IndexOf("if") + 2).Trim(); 88 | var result = booleanParser.ProgrammaticallyParse(condition); 89 | bool executeIf = booleanParser.ToBoolean(result); 90 | chainStates.Push(executeIf ? IfChainState.Executing : IfChainState.NotExecuted); 91 | } 92 | else 93 | { 94 | chainStates.Push(IfChainState.Executed); 95 | } 96 | } 97 | else if (line.StartsWith("else")) 98 | { 99 | var oldState = chainStates.Pop(); 100 | chainStates.Push(oldState == IfChainState.NotExecuted ? IfChainState.Executing : IfChainState.Executed); 101 | } 102 | else if (line == "end if" || line == "endif") 103 | { 104 | chainStates.Pop(); 105 | } 106 | else 107 | { 108 | if (currentState == IfChainState.Executing) 109 | { 110 | if (line.StartsWith(LogFunctionName + " ") || line.StartsWith(LogFunctionName + "(") || line.StartsWith(LogFunctionName + "\"")) 111 | { 112 | string logExpression = line.Substring(LogFunctionName.Length).Trim(); 113 | LogString(logExpression); 114 | } 115 | else 116 | { 117 | lastOutput = mathParser.ProgrammaticallyParse(line); 118 | } 119 | } 120 | } 121 | lineNumber++; 122 | } 123 | } 124 | catch (Exception e) 125 | { 126 | throw new ScriptParserException(lineNumber + 1, e); 127 | } 128 | return lastOutput; 129 | } 130 | 131 | private void LogString(string expr) 132 | { 133 | if (expr[0] == '(') 134 | { 135 | int closingBracket = expr.LastIndexOf(')'); 136 | if (closingBracket < 0) 137 | { 138 | throw new ScriptParserException("No matching closing bracket/parenthesis in print expression found"); 139 | } 140 | expr = expr.Substring(1, closingBracket - 1); 141 | } 142 | string joinedString = ""; 143 | bool isString = false; 144 | bool escapeNext = false; 145 | string token = ""; 146 | for (var i = 0; i < expr.Length; i++) 147 | { 148 | var ch = expr[i]; 149 | 150 | if (ch == '"' && !escapeNext) 151 | { 152 | if (!string.IsNullOrWhiteSpace(token)) 153 | { 154 | joinedString += isString ? token : mathParser.ProgrammaticallyParse(token.Trim()).ToString(mathParser.CultureInfo); 155 | } 156 | token = ""; 157 | isString = !isString; 158 | } 159 | else if (ch == '\\' && !escapeNext) 160 | { 161 | escapeNext = true; 162 | } 163 | else 164 | { 165 | token += ch; 166 | escapeNext = false; 167 | } 168 | } 169 | if (!string.IsNullOrWhiteSpace(token)) 170 | { 171 | joinedString += isString ? token : mathParser.ProgrammaticallyParse(token.Trim()).ToString(mathParser.CultureInfo); 172 | } 173 | Logger.Log(joinedString); 174 | } 175 | 176 | private enum IfChainState 177 | { 178 | // No if/else if in this chain has been executed yet, so the following else/else if can be executed 179 | NotExecuted = 0, 180 | // The current if/else if/else in this chain is executing, so we execute any code found, and any following else/else if cannot be executed 181 | Executing = 1, 182 | // The current if/else if/else is not executing, but a previous one did, so following else/else if cannot be executed 183 | Executed = 2 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /MathParser/Scripting/ScriptParserException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mathos.Parser.Scripting 4 | { 5 | public sealed class ScriptParserException : Exception 6 | { 7 | public ScriptParserException() : base() 8 | { 9 | } 10 | 11 | public ScriptParserException(string message) : base(message) 12 | { 13 | } 14 | 15 | public ScriptParserException(string message, Exception innerException) : base(message, innerException) 16 | { 17 | } 18 | 19 | public ScriptParserException(int line, Exception innerException) : base(innerException.GetType().Name + " on line " + line + ", " + Environment.NewLine + innerException.Message, innerException) 20 | { 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /MathParserTest/BooleanParserTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace Mathos.Parser.Test 5 | { 6 | [TestClass] 7 | public class BooleanParserTest 8 | { 9 | private BooleanParser parser; 10 | private MathParser mathParser; 11 | 12 | [TestInitialize] 13 | public void Initialize() 14 | { 15 | mathParser = new MathParser(); 16 | parser = new BooleanParser(mathParser); 17 | } 18 | 19 | [TestMethod] 20 | public void TruthyConvert() 21 | { 22 | Assert.AreEqual(parser.ProgrammaticallyParse("0"), 0); 23 | Assert.AreEqual(parser.ProgrammaticallyParse("1"), 1); 24 | Assert.AreEqual(parser.ProgrammaticallyParse("2346"), 1); 25 | Assert.AreEqual(parser.ProgrammaticallyParse("-1"), 1); 26 | Assert.AreEqual(parser.ProgrammaticallyParse("1-1"), 0); 27 | Assert.AreEqual(parser.ProgrammaticallyParse("0.5"), 1); 28 | } 29 | 30 | [TestMethod] 31 | public void BasicBoolean() 32 | { 33 | Assert.AreEqual(parser.ProgrammaticallyParse("0 || 0"), 0); 34 | Assert.AreEqual(parser.ProgrammaticallyParse("0 || 1"), 1); 35 | Assert.AreEqual(parser.ProgrammaticallyParse("1 || 0"), 1); 36 | Assert.AreEqual(parser.ProgrammaticallyParse("1 || 1"), 1); 37 | Assert.AreEqual(parser.ProgrammaticallyParse("0 && 0"), 0); 38 | Assert.AreEqual(parser.ProgrammaticallyParse("0 && 1"), 0); 39 | Assert.AreEqual(parser.ProgrammaticallyParse("1 && 0"), 0); 40 | Assert.AreEqual(parser.ProgrammaticallyParse("1 && 1"), 1); 41 | Assert.AreEqual(parser.ProgrammaticallyParse("1 && 0 || 1"), 1); 42 | } 43 | 44 | [TestMethod] 45 | public void ArithmicBoolean() 46 | { 47 | Assert.AreEqual(parser.ProgrammaticallyParse("0 <= 0.5"), 1); 48 | // 1 == 1 49 | Assert.AreEqual(parser.ProgrammaticallyParse("2 ^0 == (0.5 * 12) / 6 || 0"), 1, "Math operations in brackets not executed succesfully, possibly from converting doubles to truthy too early."); 50 | // 0 && 0 51 | Assert.AreEqual(parser.ProgrammaticallyParse("(2 ^ 0) - 1 && 3 - 5 + 2"), 0); 52 | 53 | for (int i = 0; i < 10; i++) 54 | { 55 | if (i % 2 == 0) 56 | { 57 | Assert.AreEqual(parser.ProgrammaticallyParse(i + " % 2 == 0"), 1); 58 | } 59 | if (i % 2 == 1) 60 | { 61 | Assert.AreEqual(parser.ProgrammaticallyParse(i + " % 2 == 0"), 0); 62 | } 63 | } 64 | } 65 | 66 | [TestMethod] 67 | public void BooleanOrderOfOperations() 68 | { 69 | Assert.AreEqual(parser.ProgrammaticallyParse("1 || 1 && 0"), 1); 70 | Assert.AreEqual(parser.ProgrammaticallyParse("0 && 1 || 1"), 1); 71 | Assert.AreEqual(parser.ProgrammaticallyParse("0 && (1 || 1)"), 0); 72 | Assert.AreEqual(parser.ProgrammaticallyParse("(1 || 1) && 0"), 0); 73 | Assert.AreEqual(parser.ProgrammaticallyParse("1 && 0 || 1"), 1); 74 | Assert.AreEqual(parser.ProgrammaticallyParse("1 && 1 || 0"), 1); 75 | } 76 | 77 | [TestMethod] 78 | public void BooleanVariables() 79 | { 80 | // x = 12 81 | mathParser.ProgrammaticallyParse("let x = (5^2 - 1) / 2"); 82 | 83 | Assert.AreEqual(parser.ProgrammaticallyParse("let y = 0"), 0); 84 | 85 | Assert.AreEqual(parser.ProgrammaticallyParse("x == 12"), 1); 86 | Assert.AreEqual(parser.ProgrammaticallyParse("x % 2 == 0 && x % 3 == 0 && x % 4 == 0"), 1); 87 | Assert.AreEqual(parser.ProgrammaticallyParse("x && y"), 0); 88 | Assert.AreEqual(parser.ProgrammaticallyParse("x || y"), 1); 89 | } 90 | 91 | [TestMethod] 92 | public void BooleanOperations() 93 | { 94 | // x = 12 95 | mathParser.ProgrammaticallyParse("let x = (5^2 - 1) / 2"); 96 | 97 | Assert.AreEqual(parser.ProgrammaticallyParse("x == abs(-12)"), 1); 98 | Assert.AreEqual(parser.ProgrammaticallyParse("x / 4 >= floor(pi)"), 1); 99 | Assert.AreEqual(parser.ProgrammaticallyParse("x / 3 < ceil(pi)"), 0); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /MathParserTest/MathParserTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {B7997EA5-F765-4C5B-B320-B613A3066199} 8 | Library 9 | Properties 10 | Mathos.Parser.Test 11 | MathParserTest 12 | v4.6 13 | 512 14 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 15.0 16 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 17 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 18 | False 19 | UnitTest 20 | 21 | 22 | 23 | 24 | 25 | true 26 | full 27 | false 28 | bin\Debug\ 29 | DEBUG;TRACE 30 | prompt 31 | 4 32 | false 33 | 34 | 35 | pdbonly 36 | true 37 | bin\Release\ 38 | TRACE 39 | prompt 40 | 4 41 | false 42 | 43 | 44 | bin\NUnitBuild\ 45 | TRACE;NUNIT 46 | true 47 | pdbonly 48 | AnyCPU 49 | prompt 50 | MinimumRecommendedRules.ruleset 51 | false 52 | 53 | 54 | 55 | ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll 56 | 57 | 58 | ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | {030ef4d8-7ac6-4e42-a568-c0e88cdd0802} 72 | MathParser 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /MathParserTest/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("MathParserTest")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("Mathos Project")] 8 | [assembly: AssemblyProduct("MathParserTest")] 9 | [assembly: AssemblyCopyright("Copyright © 2012-2018, Mathos Project.")] 10 | [assembly: AssemblyTrademark("")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("b7997ea5-f765-4c5b-b320-b613a3066199")] 16 | 17 | // [assembly: AssemblyVersion("1.0.*")] 18 | [assembly: AssemblyVersion("1.0.0.0")] 19 | [assembly: AssemblyFileVersion("1.0.0.0")] 20 | -------------------------------------------------------------------------------- /MathParserTest/ScriptParserTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | using Mathos.Parser.Scripting; 5 | 6 | namespace Mathos.Parser.Test 7 | { 8 | [TestClass] 9 | public class ScriptParserTest 10 | { 11 | private ScriptParser parser; 12 | private MultilineScriptParserLog log; 13 | 14 | [TestInitialize] 15 | public void Initialize() 16 | { 17 | log = new MultilineScriptParserLog(); 18 | parser = new ScriptParser(log); 19 | } 20 | 21 | [TestMethod] 22 | public void ListScriptOutput() 23 | { 24 | string[] lines = new string[] 25 | { 26 | "let y = 5 * pi", 27 | "let y = floor(y)", 28 | "y" 29 | }; 30 | 31 | var result15 = parser.ExecuteLines(lines); 32 | 33 | Assert.AreEqual(15, result15); 34 | } 35 | 36 | [TestMethod] 37 | public void MultilineScriptOutput() 38 | { 39 | string script3 = 40 | "2 * 5" + Environment.NewLine + 41 | "8 + 2" + Environment.NewLine + 42 | "56/4" + Environment.NewLine + 43 | "9 / 3"; 44 | 45 | string script5 = 46 | "let x = 5" + Environment.NewLine + 47 | "x"; 48 | 49 | var result3 = parser.ExecuteMultiline(script3); 50 | var result5 = parser.ExecuteMultiline(script5); 51 | 52 | Assert.AreEqual(3, result3); 53 | Assert.AreEqual(5, result5); 54 | } 55 | 56 | [TestMethod] 57 | public void NoEmptyLineOutput() 58 | { 59 | string script = 60 | "let x = 5" + Environment.NewLine + 61 | "x" + Environment.NewLine + 62 | " \t" + Environment.NewLine; // <- space + tab 63 | 64 | var result = parser.ExecuteMultiline(script); 65 | 66 | Assert.AreEqual(5, result); 67 | } 68 | 69 | [TestMethod] 70 | public void IfOutput() 71 | { 72 | string script1 = 73 | "let x = 5" + Environment.NewLine + 74 | "0" + Environment.NewLine + 75 | "if (x >= 4)" + Environment.NewLine + 76 | "1" + Environment.NewLine + 77 | "end if"; 78 | 79 | string script0 = 80 | "0" + Environment.NewLine + 81 | "if (x < 4)" + Environment.NewLine + 82 | "1" + Environment.NewLine + 83 | "end if"; 84 | 85 | var result1 = parser.ExecuteMultiline(script1); 86 | var result0 = parser.ExecuteMultiline(script0); 87 | 88 | Assert.AreEqual(1, result1); 89 | Assert.AreEqual(0, result0); 90 | } 91 | 92 | [TestMethod] 93 | public void ElseIfOutput() 94 | { 95 | string script2 = 96 | "let x = 5" + Environment.NewLine + 97 | "0" + Environment.NewLine + 98 | "if (x >= 6)" + Environment.NewLine + 99 | "1" + Environment.NewLine + 100 | "else if (x >= 3)" + Environment.NewLine + 101 | "2" + Environment.NewLine + 102 | "end if"; 103 | 104 | string script1 = 105 | "0" + Environment.NewLine + 106 | "if (x >= 4)" + Environment.NewLine + 107 | "1" + Environment.NewLine + 108 | "else if (x >= 3)" + Environment.NewLine + 109 | "2" + Environment.NewLine + 110 | "else if (x >= 2)" + Environment.NewLine + 111 | "3" + Environment.NewLine + 112 | "end if"; 113 | 114 | var result2 = parser.ExecuteMultiline(script2); 115 | var result1 = parser.ExecuteMultiline(script1); 116 | 117 | Assert.AreEqual(2, result2); 118 | Assert.AreEqual(1, result1); 119 | } 120 | 121 | [TestMethod] 122 | public void ElseOutput() 123 | { 124 | string script2 = 125 | "let x = 5" + Environment.NewLine + 126 | "0" + Environment.NewLine + 127 | "if (x >= 6)" + Environment.NewLine + 128 | "1" + Environment.NewLine + 129 | "else" + Environment.NewLine + 130 | "2" + Environment.NewLine + 131 | "end if"; 132 | 133 | string script1 = 134 | "0" + Environment.NewLine + 135 | "if (x >= 4)" + Environment.NewLine + 136 | "1" + Environment.NewLine + 137 | "else if (x >= 3)" + Environment.NewLine + 138 | "2" + Environment.NewLine + 139 | "else" + Environment.NewLine + 140 | "3" + Environment.NewLine + 141 | "end if"; 142 | 143 | var result2 = parser.ExecuteMultiline(script2); 144 | var result1 = parser.ExecuteMultiline(script1); 145 | 146 | Assert.AreEqual(2, result2); 147 | Assert.AreEqual(1, result1); 148 | } 149 | 150 | [TestMethod] 151 | public void NestedIfs() 152 | { 153 | string[] lines0 = new string[] 154 | { 155 | "let abc = 123", 156 | "0", 157 | "if abc < 100", 158 | "1", 159 | "if abc > 100", 160 | "2", 161 | "end if", 162 | "end if" 163 | }; 164 | 165 | string[] lines4 = new string[] 166 | { 167 | "let abc = 123", 168 | "4", 169 | "if abc < 100", 170 | "1", 171 | "if abc > 100", 172 | "2", 173 | "else", 174 | "3", 175 | "end if", 176 | "end if" 177 | }; 178 | 179 | string[] lines2 = new string[] 180 | { 181 | "let abc = 123", 182 | "5", 183 | "if abc < 100", 184 | "1", 185 | "else", 186 | "if abc > 100", 187 | "2", 188 | "end if", 189 | "end if" 190 | }; 191 | 192 | var result0 = parser.ExecuteLines(lines0); 193 | var result4 = parser.ExecuteLines(lines4); 194 | var result2 = parser.ExecuteLines(lines2); 195 | 196 | Assert.AreEqual(0, result0); 197 | Assert.AreEqual(4, result4); 198 | Assert.AreEqual(2, result2); 199 | } 200 | 201 | [TestMethod] 202 | [ExpectedException(typeof(ScriptParserException))] 203 | public void ExceptionLinenumbers() 204 | { 205 | string[] lines = new string[] 206 | { 207 | "let abc = 123", 208 | "5", 209 | "if foo < 100", //line 3 contains undefined variable 210 | "1", 211 | "else", 212 | "if abc > 100", 213 | "2", 214 | "end if", 215 | "end if" 216 | }; 217 | try 218 | { 219 | parser.ExecuteLines(lines); 220 | } 221 | catch (ScriptParserException e) 222 | { 223 | Assert.IsTrue(e.Message.ToLowerInvariant().Contains("foo") && e.Message.ToLowerInvariant().Contains("line 3")); 224 | throw e; 225 | } 226 | } 227 | 228 | [TestMethod] 229 | public void Logs() 230 | { 231 | string[] lines1 = new string[] 232 | { 233 | "let abc = 123", 234 | "print(\"abc: \" abc)" 235 | }; 236 | string[] lines2 = new string[] 237 | { 238 | "let x = 42", 239 | "print \"answer (\" x \") = 6 * 9" 240 | }; 241 | string[] lines3 = new string[] 242 | { 243 | "let x = 5 * 5", 244 | "if x == 5 ^ 2", 245 | "print(\"math works, see \" 5 ^ 2)", 246 | "else", 247 | "print(\"huh?\")", 248 | "end if" 249 | }; 250 | 251 | parser.ExecuteLines(lines1); 252 | Assert.AreEqual("abc: 123", log.Output); 253 | 254 | log.Clear(); 255 | parser.ExecuteLines(lines2); 256 | Assert.AreEqual("answer (42) = 6 * 9", log.Output); 257 | 258 | log.Clear(); 259 | parser.ExecuteLines(lines3); 260 | Assert.AreEqual("math works, see 25", log.Output); 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /MathParserTest/Test.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace Mathos.Parser.Test 7 | { 8 | [TestClass] 9 | public class Test 10 | { 11 | [TestMethod] 12 | public void BasicArithmetic() 13 | { 14 | var parser = new MathParser(); 15 | 16 | Assert.AreEqual(7, parser.Parse("5 + 2")); 17 | Assert.AreEqual(11, parser.Parse("5 + 2 * 3")); 18 | Assert.AreEqual(17, parser.Parse("27 - 3 * 3 + 1 - 4 / 2")); 19 | Assert.AreEqual(282429536481, parser.Parse("(27 ^ 2) ^ 4")); 20 | } 21 | 22 | [TestMethod] 23 | public void AdvancedArithmetic() 24 | { 25 | var parser = new MathParser(); 26 | 27 | Assert.AreEqual(30, parser.Parse("3(7+3)")); 28 | Assert.AreEqual(20, parser.Parse("(2+3)(3+1)")); 29 | } 30 | 31 | [TestMethod] 32 | public void DivideByZero() 33 | { 34 | var parser = new MathParser(); 35 | 36 | Assert.AreEqual(double.PositiveInfinity, parser.Parse("5 / 0")); 37 | Assert.AreEqual(double.NegativeInfinity, parser.Parse("(-30) / 0")); 38 | Assert.AreEqual(double.NaN, parser.Parse("0 / 0")); 39 | 40 | Assert.AreEqual(double.PositiveInfinity, parser.Parse("5 : 0")); 41 | Assert.AreEqual(double.NegativeInfinity, parser.Parse("(-30) : 0")); 42 | Assert.AreEqual(double.NaN, parser.Parse("0 : 0")); 43 | } 44 | 45 | [TestMethod] 46 | public void ConditionalStatements() 47 | { 48 | var parser = new MathParser(); 49 | 50 | Assert.AreEqual(1, parser.Parse("2 + 3 = 1 + 4")); 51 | Assert.AreEqual(1, parser.Parse("3 + 2 > 2 - 1")); 52 | Assert.AreEqual(1, parser.Parse("(2+3)(3+1) < 50 - 20")); 53 | 54 | Assert.AreEqual(0, parser.Parse("2 + 2 = 22")); 55 | Assert.AreEqual(0, parser.Parse("(2+3)(3+1) > 50 - 20")); 56 | Assert.AreEqual(0, parser.Parse("100 < 10")); 57 | 58 | Assert.AreEqual(1, parser.Parse("2.5 <= 3")); 59 | Assert.AreEqual(1, parser.Parse("(2+3)(3+1) <= 50 - 20")); 60 | 61 | Assert.AreEqual(0, parser.Parse("100 <= 10")); 62 | Assert.AreEqual(0, parser.Parse("(2+3)(3+1) >= 50 - 20")); 63 | } 64 | 65 | [TestMethod] 66 | public void ProgramicallyAddVariables() 67 | { 68 | var parser = new MathParser(); 69 | 70 | parser.ProgrammaticallyParse("let a = 2pi"); 71 | Assert.AreEqual(parser.LocalVariables["pi"] * 2, parser.Parse("a"), 0.00000000000001); 72 | 73 | parser.ProgrammaticallyParse("b := 20"); 74 | Assert.AreEqual(20, parser.Parse("b")); 75 | 76 | parser.ProgrammaticallyParse("let c be 25 + 2(2+3)"); 77 | Assert.AreEqual(35, parser.Parse("c")); 78 | 79 | parser.VariableDeclarator = "dim"; 80 | parser.ProgrammaticallyParse("dim d = 5 ^3"); 81 | Assert.AreEqual(125, parser.Parse("d")); 82 | } 83 | 84 | [TestMethod] 85 | public void CustomFunctions() 86 | { 87 | var parser = new MathParser(); 88 | 89 | parser.LocalFunctions.Add("timesTwo", inputs => inputs[0] * 2); 90 | Assert.AreEqual(6, parser.Parse("timesTwo(3)")); 91 | Assert.AreEqual(42, parser.Parse("timesTwo((2+3)(3+1) + 1)")); 92 | 93 | parser.LocalFunctions.Add("square", inputs => inputs[0] * inputs[0]); 94 | Assert.AreEqual(16, parser.Parse("square(4)")); 95 | 96 | parser.LocalFunctions.Add("cube", inputs => inputs[0] * inputs[0] * inputs[0]); 97 | Assert.AreEqual(8, parser.Parse("cube(2)")); 98 | 99 | parser.LocalFunctions.Add("constF", inputs => 12); 100 | Assert.AreEqual(12, parser.Parse("constF()")); 101 | Assert.AreEqual(144, parser.Parse("constF() * constF()")); 102 | 103 | parser.LocalFunctions.Add("argCount", inputs => inputs.Length); 104 | Assert.AreEqual(0, parser.Parse("argCount()")); 105 | Assert.AreEqual(1, parser.Parse("argCount(1)")); 106 | Assert.AreEqual(2, parser.Parse("argCount(argCount(1), -5)")); 107 | Assert.AreEqual(2, parser.Parse("argCount(argCount(1, 0), argCount())")); 108 | } 109 | 110 | [TestMethod] 111 | public void CustomFunctionsWithSeveralArguments() 112 | { 113 | var parser = new MathParser(false); 114 | 115 | parser.LocalFunctions.Add("log", delegate(double[] input) 116 | { 117 | switch (input.Length) 118 | { 119 | case 1: 120 | return Math.Log10(input[0]); 121 | case 2: 122 | return Math.Log(input[0], input[1]); 123 | default: 124 | return 0; 125 | } 126 | }); 127 | 128 | Assert.AreEqual(0.301029996, parser.Parse("log(2)"), 0.000000001); 129 | Assert.AreEqual(0.630929754, parser.Parse("log(2,3)"), 0.000000001); 130 | } 131 | 132 | [TestMethod] 133 | [ExpectedException(typeof(MathParserException))] 134 | public void UndefinedVariableException() 135 | { 136 | var parser = new MathParser(); 137 | 138 | try 139 | { 140 | parser.ProgrammaticallyParse("unknownvar * 5"); 141 | } 142 | catch (Exception e) 143 | { 144 | // Tests to see if the message the exception gives is clear enough 145 | Assert.IsTrue(e.Message.ToLowerInvariant().Contains("variable") && e.Message.ToLowerInvariant().Contains("unknownvar")); 146 | throw e; 147 | } 148 | } 149 | 150 | [TestMethod] 151 | [ExpectedException(typeof(MathParserException))] 152 | public void UndefinedOperatorException() 153 | { 154 | var parser = new MathParser(); 155 | 156 | try 157 | { 158 | parser.ProgrammaticallyParse("unknownoperator(5)"); 159 | } 160 | catch (Exception e) 161 | { 162 | // Tests to see if the message the exception gives is clear enough 163 | Assert.IsTrue(e.Message.ToLowerInvariant().Contains("operator") && e.Message.ToLowerInvariant().Contains("unknownoperator")); 164 | throw e; 165 | } 166 | } 167 | 168 | [TestMethod] 169 | public void NegativeNumbers() 170 | { 171 | var parser = new MathParser(); 172 | 173 | Assert.AreEqual(0, parser.Parse("-1+1")); 174 | Assert.AreEqual(1, parser.Parse("--1")); 175 | Assert.AreEqual(-2, parser.Parse("-2")); 176 | Assert.AreEqual(-2, parser.Parse("(-2)")); 177 | // Assert.AreEqual(2, parser.Parse("-(-2)")); TODO: Fix 178 | Assert.AreEqual(4, parser.Parse("(-2)(-2)")); 179 | Assert.AreEqual(-3, parser.Parse("-(3+2+1+6)/4")); 180 | 181 | parser.LocalVariables.Add("x", 50); 182 | 183 | Assert.AreEqual(-100, parser.Parse("-x - x")); 184 | Assert.AreEqual(-75, parser.Parse("-x * 1.5")); 185 | } 186 | 187 | [TestMethod] 188 | public void Trigonometry() 189 | { 190 | var parser = new MathParser(); 191 | 192 | Assert.AreEqual(Math.Cos(32) + 3, parser.Parse("cos(32) + 3")); 193 | } 194 | 195 | [TestMethod] 196 | public void CustomizeOperators() 197 | { 198 | var parser = new MathParser(); 199 | 200 | parser.Operators.Add("$", (a, b) => a * 2 + b * 3); 201 | 202 | Assert.AreEqual(3 * 2 + 3 * 2, parser.Parse("3 $ 2")); 203 | } 204 | 205 | [TestMethod] 206 | public void DoubleOperations() 207 | { 208 | var parserDefault = new MathParser(); 209 | 210 | Assert.AreEqual(double.Parse("0.055", parserDefault.CultureInfo), parserDefault.Parse("-0.245 + 0.3")); 211 | } 212 | 213 | [TestMethod] 214 | public void ExecutionTime() 215 | { 216 | var timer = new Stopwatch(); 217 | var parser = new MathParser(); 218 | 219 | parser.Parse("5+2*3*1+2((1-2)(2-3))*-1"); // Warm-up 220 | 221 | GC.Collect(); 222 | GC.WaitForPendingFinalizers(); 223 | 224 | timer.Start(); 225 | 226 | parser.Parse("5+2"); 227 | parser.Parse("5+2*3*1+2((1-2)(2-3))"); 228 | parser.Parse("5+2*3*1+2((1-2)(2-3))*-1"); 229 | 230 | timer.Stop(); 231 | 232 | Debug.WriteLine("Parse Time: " + timer.Elapsed.TotalMilliseconds + "ms"); 233 | } 234 | 235 | [TestMethod] 236 | public void BuiltInFunctions() 237 | { 238 | var parser = new MathParser(); 239 | 240 | Assert.AreEqual(21, parser.Parse("round(21.333333333333)")); 241 | Assert.AreEqual(1, parser.Parse("pow(2,0)")); 242 | } 243 | 244 | [TestMethod] 245 | [ExpectedException(typeof(ArithmeticException))] 246 | public void ExceptionCatching() 247 | { 248 | var parser = new MathParser(); 249 | 250 | parser.Parse("(-1"); 251 | parser.Parse("rem(20,1,,,,)"); 252 | } 253 | 254 | [TestMethod] 255 | public void StrangeStuff() 256 | { 257 | var parser = new MathParser(); 258 | 259 | parser.Operators.Add("times", (x, y) => x * y); 260 | parser.Operators.Add("dividedby", (x, y) => x / y); 261 | parser.Operators.Add("plus", (x, y) => x + y); 262 | parser.Operators.Add("minus", (x, y) => x - y); 263 | 264 | Debug.WriteLine(parser.Parse("5 plus 3 dividedby 2 times 3").ToString(parser.CultureInfo)); 265 | } 266 | 267 | [TestMethod] 268 | public void TestLongExpression() 269 | { 270 | var parser = new MathParser(); 271 | 272 | Assert.AreEqual(2, parser.Parse("4^2-2*3^2+4")); 273 | } 274 | 275 | [TestMethod] 276 | public void SpeedTests() 277 | { 278 | var parser = new MathParser(); 279 | 280 | parser.LocalVariables.Add("x",10); 281 | 282 | var list = parser.GetTokens("(3x+2)"); 283 | var time = BenchmarkUtil.Benchmark(() => parser.Parse("(3x+2)"), 25000); 284 | var time2 = BenchmarkUtil.Benchmark(() => parser.Parse(list), 25000); 285 | 286 | Assert.IsTrue(time >= time2); 287 | } 288 | 289 | [TestMethod] 290 | public void DetailedSpeedTestWithOptimization() 291 | { 292 | var parser = new MathParser(); 293 | 294 | parser.LocalVariables.Add("x", 5); 295 | 296 | var expr = "(3x+2)(2(2x+1))"; 297 | 298 | const int itr = 3000; 299 | var creationTimeAndTokenization = BenchmarkUtil.Benchmark( () => parser.GetTokens(expr) ,1); 300 | var tokens = parser.GetTokens(expr); 301 | 302 | var parsingTime = BenchmarkUtil.Benchmark(() => parser.Parse(tokens), itr); 303 | var totalTime = creationTimeAndTokenization + parsingTime; 304 | 305 | Console.WriteLine("Parsing Time: " + parsingTime); 306 | Console.WriteLine("Total Time: " + totalTime); 307 | 308 | var parsingTime2 = BenchmarkUtil.Benchmark(() => parser.Parse(expr), itr); 309 | 310 | Console.WriteLine("Parsing Time 2: " + parsingTime2); 311 | Console.WriteLine("Total Time: " + parsingTime2); 312 | } 313 | 314 | [TestMethod] 315 | public void DetailedSpeedTestWithoutOptimization() 316 | { 317 | var parser = new MathParser(); 318 | 319 | parser.LocalVariables.Add("x", 5); 320 | 321 | var expr = "(3x+2)(2(2x+1))"; 322 | const int itr = 50; 323 | 324 | var parsingTime = BenchmarkUtil.Benchmark(() => parser.Parse(expr), itr); 325 | 326 | Console.WriteLine("Parsing Time: " + parsingTime); 327 | Console.WriteLine("Total Time: " + parsingTime); 328 | } 329 | 330 | [TestMethod] 331 | public void CommaPiBug() 332 | { 333 | var parser = new MathParser(); 334 | var result = parser.Parse("pi"); 335 | 336 | Assert.AreEqual(result, parser.LocalVariables["pi"], 0.00000000000001); 337 | } 338 | 339 | [TestMethod] 340 | public void NumberNotations() 341 | { 342 | var parser = new MathParser(); 343 | 344 | Assert.AreEqual(0.0005, parser.Parse("5 * 10^-4")); 345 | } 346 | 347 | [TestMethod] 348 | public void NoLeadingZero() 349 | { 350 | var parser = new MathParser(); 351 | 352 | Assert.AreEqual(0.5, parser.Parse(".5")); 353 | Assert.AreEqual(0.5, parser.Parse(".25 + .25")); 354 | Assert.AreEqual(2.0, parser.Parse("1.5 + .5")); 355 | Assert.AreEqual(-0.25, parser.Parse(".25 + (-.5)")); 356 | Assert.AreEqual(0.25, parser.Parse(".5(.5)")); 357 | } 358 | 359 | public class BenchmarkUtil 360 | { 361 | public static double Benchmark(Action action, int iterations) 362 | { 363 | double time = 0; 364 | const int innerCount = 5; 365 | 366 | GC.Collect(); 367 | GC.WaitForPendingFinalizers(); 368 | GC.Collect(); 369 | 370 | for (var i = 0; i < innerCount; i++) 371 | action.Invoke(); 372 | 373 | var watch = Stopwatch.StartNew(); 374 | 375 | for (var i = 0; i < iterations; i++) 376 | { 377 | action.Invoke(); 378 | 379 | time += Convert.ToDouble(watch.ElapsedMilliseconds) / Convert.ToDouble(iterations); 380 | } 381 | 382 | return time; 383 | } 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /MathParserTest/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/github/license/MathosProject/Mathos-Parser.svg?label=LICENSE&style=for-the-badge)](https://github.com/MathosProject/Mathos-Parser/blob/master/LICENSE.md) 2 | [![NuGet](https://img.shields.io/nuget/dt/MathosParser.svg?label=NUGET%20DOWNLOADS&style=for-the-badge)](https://www.nuget.org/packages/MathosParser/) 3 | 4 | Mathos Parser 5 | ============= 6 | **Mathos Parser** is a mathematical expression parser and evaluator for the .NET Standard. It can parse various kinds of mathematical expressions out of the box and can be extended with custom functions, operators, and variables. 7 | 8 | * The CIL version (compiles expressions into IL code): https://github.com/MathosProject/Mathos-Parser-CIL (Outdated) 9 | 10 | Documentation and examples are on the [wiki](https://github.com/MathosProject/Mathos-Parser/wiki). 11 | 12 | ## Features 13 | 14 | * Parse and evaluate mathematical expressions out of the box. 15 | * Customize and override existing operators, functions, and variables. 16 | * Culture independent. 17 | * And much more! 18 | 19 | ## Introduction 20 | 21 | Mathos Parser is a part of the Mathos Project, a project that aims to provide useful mathematics APIs and utilities to make life easier. This math parser is fully independent of the [Mathos Core Library](https://github.com/MathosProject/Mathos-Project), so this library achieves powerful math parsing and evaluation without external dependencies. 22 | 23 | ## How to use 24 | 25 | Mathos Parser is very easy to use and understand. This section provides examples for the following: 26 | 27 | * Custom variables 28 | * Custom operators 29 | * Custom functions 30 | * Variables through parsing 31 | * Multi-argument functions 32 | 33 | ### Custom Variables 34 | ```csharp 35 | // Create a parser. 36 | MathParser parser = new MathParser(); 37 | 38 | // Add a variable. 39 | parser.LocalVariables.Add("a", 25); 40 | 41 | // How about another. 42 | parser.LocalVariables.Add("猫", 5); 43 | 44 | // Use the variables as you would normally. 45 | Assert.AreEqual(30, parser.Parse("a + 猫")); 46 | ``` 47 | 48 | ### Custom Operators 49 | ```csharp 50 | // Create a parser. 51 | MathParser parser = new MathParser(); 52 | 53 | // Add the custom operator. 54 | parser.Operators.Add("λ", (left, right) => Math.Pow(left, right)); 55 | 56 | // Evaluate using the new operator. 57 | Assert.AreEqual(Math.Pow(3, 2), parser.Parse("3 λ 2")); 58 | ``` 59 | 60 | ### Custom Functions 61 | ```csharp 62 | // Create a parser. 63 | MathParser parser = new MathParser(); 64 | 65 | // Add the function. 66 | parser.LocalFunctions.Add("timesTwo", inputs => inputs[0] * 2); 67 | 68 | // Use the new function. 69 | Assert.AreEqual(8, parser.Parse("timesTwo(4)")); 70 | ``` 71 | 72 | ### Variables Through Parsing 73 | ```csharp 74 | // Create a parser. 75 | MathParser parser = new MathParser(); 76 | 77 | // Define the variable. 78 | parser.ProgrammaticallyParse("let a = 25"); 79 | 80 | // Evaluation. 81 | Assert.AreEqual(30, parser.Parse("a + 5")); 82 | ``` 83 | 84 | ### Multi-Argument Functions 85 | ```csharp 86 | // Create a parser. 87 | MathParser parser = new MathParser(); 88 | 89 | // Add the function and its implementation. 90 | parser.LocalFunctions.Add("clamp", delegate (double[] inputs) 91 | { 92 | // The value. 93 | var value = inputs[0]; 94 | 95 | // The maximum value. 96 | var min = inputs[1]; 97 | 98 | // The minimum value. 99 | var max = inputs[2]; 100 | 101 | if (value > max) 102 | return max; 103 | 104 | if (value < min) 105 | return min; 106 | 107 | return value; 108 | }); 109 | 110 | // Use the new function. 111 | Assert.AreEqual(3, parser.Parse("clamp(3,-1,5)")); 112 | Assert.AreEqual(-1, parser.Parse("clamp(-5,-1,5)")); 113 | Assert.AreEqual(5, parser.Parse("clamp(8,-1,5)")); 114 | ``` 115 | --------------------------------------------------------------------------------