├── .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 | [](https://github.com/MathosProject/Mathos-Parser/blob/master/LICENSE.md)
2 | [](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 |
--------------------------------------------------------------------------------