├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── codeql-analysis.yml │ ├── dotnet.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── StringMath.Benchmarks ├── Benchmarks.cs ├── Program.cs └── StringMath.Benchmarks.csproj ├── StringMath.Expressions.Tests ├── ExpressionsTests.cs ├── StringMath.Expressions.Tests.csproj └── Usings.cs ├── StringMath.Expressions ├── Expand.cs ├── Expr.cs ├── ExprConversion.cs ├── Extensions.cs ├── SMath.cs ├── Simplify.cs ├── StringMath.Expressions.csproj └── Usings.cs ├── StringMath.Tests ├── BooleanExprTests.cs ├── Extensions.cs ├── MathExprTests.cs ├── ParserTests.cs ├── StringMath.Tests.csproj └── TokenizerTests.cs ├── StringMath.sln ├── StringMath ├── AssemblyInfo.cs ├── Expressions │ ├── BinaryExpression.cs │ ├── ConstantExpression.cs │ ├── IExpression.cs │ ├── UnaryExpression.cs │ └── VariableExpression.cs ├── Extensions.cs ├── IMathContext.cs ├── MathContext.cs ├── MathException.cs ├── MathExpr.cs ├── Parser │ ├── Parser.cs │ ├── SourceText.cs │ ├── Token.cs │ ├── TokenType.cs │ └── Tokenizer.cs ├── Precedence.cs ├── StringMath.csproj ├── VariablesCollection.cs └── Visitors │ ├── CompileExpression.cs │ ├── EvaluateExpression.cs │ ├── ExpressionVisitor.cs │ └── ExtractVariables.cs └── build ├── string-math.public.snk └── string-math.snk /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # IDE0058: Expression value is never used 4 | dotnet_diagnostic.IDE0058.severity = none 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: miroiu 4 | custom: ["https://www.buymeacoffee.com/miroiu", "https://paypal.me/miroiuemanuel"] 5 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '31 18 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'csharp' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: | 20 | 3.1.x 21 | 5.0.x 22 | 6.0.x 23 | 8.0.x 24 | - name: Restore dependencies 25 | run: dotnet restore 26 | - name: Build 27 | run: dotnet build --no-restore 28 | - name: Test 29 | run: dotnet test --no-build --verbosity normal 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v*.*.*' 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup .NET Core 16 | uses: actions/setup-dotnet@v3.0.1 17 | with: 18 | dotnet-version: '8.0.x' 19 | - name: Install dependencies 20 | run: dotnet restore 21 | - name: Build 22 | run: dotnet build --configuration Release --no-restore 23 | - name: Publish the package 24 | run: dotnet nuget push "*/bin/Release/*.nupkg" -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json 25 | -------------------------------------------------------------------------------- /.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 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Miroiu Emanuel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > V3 README can be found here: 2 | 3 | > **NEW!** Boolean math example: https://github.com/miroiu/string-math/pull/6/files 4 | 5 | # String Math [![NuGet](https://img.shields.io/nuget/v/StringMath?style=flat-square&logo=nuget)](https://www.nuget.org/packages/StringMath/) [![Downloads](https://img.shields.io/nuget/dt/StringMath?label=downloads&style=flat-square&logo=nuget)](https://www.nuget.org/packages/StringMath) ![.NET](https://img.shields.io/static/v1?label=%20&message=Framework%204.6.1%20to%20NET%208&color=5C2D91&style=flat-square&logo=.net) ![](https://img.shields.io/static/v1?label=%20&message=documentation&color=yellow&style=flat-square) 6 | 7 | Calculates the value of a math expression from a string returning a double. 8 | Supports variables, user defined operators and expression compilation. 9 | 10 | ```csharp 11 | double result = "1 * (2 - 3) ^ 2".Eval(); // 1 12 | ``` 13 | 14 | ## Variables 15 | 16 | ```csharp 17 | double result = "{a} + 2 * {b}".Substitute("a", 2).Substitute("b", 3).Result; // 8 18 | ``` 19 | 20 | ### Global variables 21 | 22 | These variables are inherited and cannot be substituted. 23 | 24 | ```csharp 25 | MathExpr.AddVariable("PI", 3.1415926535897931); 26 | double result = "1 + {PI}".Eval(); // 4.1415926535897931 27 | ``` 28 | 29 | ## Custom operators 30 | 31 | ### Global operators 32 | 33 | These operators are inherited and can be overidden. 34 | 35 | ```csharp 36 | MathExpr.AddOperator("abs", a => a > 0 ? a : -a); 37 | double result = "abs -5".Eval(); // 5 38 | 39 | // Operator precedence (you can specify an int for precedence) 40 | MathExpr.AddOperator("max", (a, b) => a > b ? a : b, Precedence.Power); 41 | double result = new MathExpr("2 * 3 max 4").Result; // 8 42 | ``` 43 | 44 | ### Local operators 45 | 46 | These are applied only to the target expression. 47 | 48 | ```csharp 49 | MathExpr expr = "{PI} + 1"; 50 | expr.SetOperator("+", (a, b) => Math.Pow(a, b)); 51 | double result = expr; // 3.1415926535897931 52 | 53 | double result2 = "{PI} + 1".Eval(); // 4.1415926535897931 54 | ``` 55 | 56 | ## Advanced 57 | 58 | ### Extract variables 59 | 60 | ```csharp 61 | var expr = "{a} + {b} + {PI}".ToMathExpr(); 62 | var variables = expr.Variables; // { "a", "b", "PI" } 63 | var localVariables = expr.LocalVariables; // { "a", "b" } 64 | ``` 65 | 66 | ### Compilation 67 | 68 | ```csharp 69 | Func fn = "{a} + 2".ToMathExpr().Compile("a"); 70 | double result = fn(5); // 7 71 | ``` 72 | 73 | ### Conditional substitution 74 | 75 | ```csharp 76 | MathExpr expr = "1 / {a}".Substitute("a", 1); 77 | 78 | double temp = expr.Result; // 1 79 | 80 | if (someCondition) // true 81 | expr.Substitute("a", 2); 82 | 83 | double final = expr.Result; // 0.5 84 | ``` 85 | 86 | ### Sharing math context 87 | 88 | ```csharp 89 | MathExpr expr = "{PI} + 1"; 90 | expr.SetOperator("+", (a, b) => Math.Pow(a, b)); 91 | 92 | MathExpr expr2 = "3 + 2".ToMathExpr(expr.Context); 93 | 94 | double result = "1 + 2 + 3".Eval(expr.Context); 95 | ``` 96 | 97 | ### Custom math context 98 | 99 | ```csharp 100 | var context = new MathContext(); // new MathContext(MathContext.Default); // to inherit from global 101 | context.RegisterBinary("+", (a, b) => Math.Pow(a, b)); 102 | 103 | MathExpr expr = new MathExpr("{PI} + 1", context); 104 | MathExpr expr2 = "3 + 2".ToMathExpr(context); 105 | double result = "1 + 2 + 3".Eval(context); 106 | ``` 107 | 108 | ## Default operators 109 | 110 | ### Binary 111 | 112 | ```csharp 113 | + (addition) 114 | - (subtraction) 115 | * (multiplication) 116 | / (division) 117 | % (remainder) 118 | ^ (power) 119 | log (logarithm) 120 | max (maximum) 121 | min (minimum) 122 | ``` 123 | 124 | ### Unary 125 | 126 | ```csharp 127 | - (negation) 128 | ! (factorial) 129 | sqrt (square root) 130 | sin (sine) 131 | asin (arcsine) 132 | cos (cosine) 133 | acos (arccosine) 134 | tan (tangent) 135 | atan (arctangent) 136 | rad (convert degrees to radians) 137 | deg (convert radians to degrees) 138 | ceil (ceiling) 139 | floor (floor) 140 | round (rounding) 141 | exp (e raised to power) 142 | abs (absolute) 143 | ``` 144 | -------------------------------------------------------------------------------- /StringMath.Benchmarks/Benchmarks.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Jobs; 3 | 4 | namespace StringMath.Benchmarks 5 | { 6 | [MemoryDiagnoser(true)] 7 | [SimpleJob(RuntimeMoniker.Net80, warmupCount: 0, iterationCount: 1, launchCount: 1)] 8 | public class Benchmarks 9 | { 10 | [Benchmark] 11 | public void Tokenize() 12 | { 13 | var tokenizer = new Tokenizer("1.23235456576878798 - ((3 + {b}) max .1) ^ sqrt(-999 / 2 * 3 max 5) + !5 - 0.00000000002 / {ahghghh}"); 14 | 15 | Token token; 16 | 17 | do 18 | { 19 | token = tokenizer.ReadToken(); 20 | } 21 | while (token.Type != TokenType.EndOfCode); 22 | } 23 | 24 | [Benchmark] 25 | public void Parse() 26 | { 27 | var tokenizer = new Tokenizer("1.23235456576878798 - ((3 + {b}) max .1) ^ sqrt(-999 / 2 * 3 max 5) + !5 - 0.00000000002 / {ahghghh}"); 28 | var parser = new Parser(tokenizer, MathContext.Default); 29 | _ = parser.Parse(); 30 | } 31 | 32 | [Benchmark] 33 | public double Evaluate() 34 | { 35 | return "1.23235456576878798 - ((3 + {b}) max .1) ^ sqrt(-999 / 2 * 3 max 5) + !5 - 0.00000000002 / {ahghghh}".ToMathExpr().Substitute("b", 12989d).Substitute("ahghghh", 12345d).Result; 36 | } 37 | 38 | [Benchmark] 39 | public double Compile() 40 | { 41 | var fn = "1.23235456576878798 - ((3 + {b}) max .1) ^ sqrt(-999 / 2 * 3 max 5) + !5 - 0.00000000002 / {ahghghh}".ToMathExpr().Substitute("b", 12989d).Substitute("ahghghh", 12345d).Compile("ahghghh"); 42 | return fn(12345d); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /StringMath.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using StringMath.Benchmarks; 3 | 4 | BenchmarkRunner.Run(); 5 | -------------------------------------------------------------------------------- /StringMath.Benchmarks/StringMath.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8 6 | enable 7 | enable 8 | true 9 | true 10 | 11 | ../build/string-math.snk 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /StringMath.Expressions.Tests/ExpressionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath.Expressions.Tests 2 | { 3 | public class ExpressionsTests 4 | { 5 | [Theory] 6 | [InlineData("5 * (3 + 2)", "5 * 3 + 5 * 2")] 7 | [InlineData("5 * (3 - 2)", "5 * 3 - 5 * 2")] 8 | [InlineData("5 * (-3 - 2)", "5 * -3 - 5 * 2")] 9 | [InlineData("5 * -(-3 - 2)", "5 * 3 + 5 * 2")] 10 | [InlineData("{a} * (3 + {b})", "{a} * 3 + {a} * {b}")] 11 | [InlineData("{a} * (3 - {b})", "{a} * 3 - {a} * {b}")] 12 | public void Expand(string input, string expected) 13 | { 14 | var actual = input.Expand(); 15 | 16 | Assert.Equal(expected, actual.Text); 17 | } 18 | 19 | [Theory] 20 | [InlineData("5^2", "25")] 21 | [InlineData("(5 - 2) * {a}", "3 * {a}")] 22 | [InlineData("(-5 + 2) * {a}", "-3 * {a}")] 23 | [InlineData("-(-5 + 2) * {a}", "3 * {a}")] 24 | [InlineData("-(-5 + 1)", "4")] 25 | [InlineData("-(-5 + -1)", "6")] 26 | [InlineData("-{a} + 2", "-{a} + 2")] 27 | public void Simplify(string input, string expected) 28 | { 29 | var actual = input.Simplify(); 30 | 31 | Assert.Equal(expected, actual.Text); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /StringMath.Expressions.Tests/StringMath.Expressions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /StringMath.Expressions.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /StringMath.Expressions/Expand.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath.Expressions; 2 | 3 | public static partial class Extensions 4 | { 5 | public static Expr Expand(this Expr expr) 6 | { 7 | static Expr E(Expr e) => e switch 8 | { 9 | // 5 * (a + b) => 5 * a + 5 * b 10 | Mul(Number num, Sum sum) => num * E(sum.Left) + num * E(sum.Right), 11 | 12 | // (a + b) * 5 => 5 * a + 5 * b 13 | Mul(Sum sum, Number num) => num * E(sum.Left) + num * E(sum.Right), 14 | 15 | // (a - b) * 5 => 5 * a - 5 * b 16 | Mul(Number num, Diff diff) => num * E(diff.Left) - num * E(diff.Right), 17 | 18 | // 5 * (a - b) => 5 * a - 5 * b 19 | Mul(Diff diff, Number num) => num * E(diff.Left) - num * E(diff.Right), 20 | 21 | // c * (a + b) => c * a + c * b 22 | Mul(Variable num, Sum sum) => num * E(sum.Left) + num * E(sum.Right), 23 | 24 | // (a + b) * c => c * a + c * b 25 | Mul(Sum sum, Variable num) => num * E(sum.Left) + num * E(sum.Right), 26 | 27 | // c * (a - b) => c * a - c * b 28 | Mul(Variable num, Diff diff) => num * E(diff.Left) - num * E(diff.Right), 29 | 30 | // (a - b) * c => c * a - c * b 31 | Mul(Diff diff, Variable num) => num * E(diff.Left) - num * E(diff.Right), 32 | 33 | // -a * -b => a * b 34 | Mul(Neg(Expr a), Neg(Expr b)) => E(a) * E(b), 35 | 36 | // -(a - b) => -a + b 37 | Neg(Neg(Expr a)) => E(a), 38 | 39 | // -(a - b) => -a + b 40 | Neg(Diff(Expr a, Expr b)) => -E(a) + E(b), 41 | 42 | // -(a + b) => -a - b 43 | Neg(Sum(Expr a, Expr b)) => -E(a) - E(b), 44 | 45 | // -a / -b => a / b 46 | Div(Neg(Expr a), Neg(Expr b)) => E(a) * E(b), 47 | 48 | // x^(m + n) => x^m * x^n 49 | Pow(Expr x, Sum(Expr m, Expr n)) => (E(x) ^ E(m)) * (E(x) ^ E(n)), 50 | 51 | // x^(m - n) => x^m / x^n 52 | Pow(Expr x, Diff(Expr m, Expr n)) => (E(x) ^ E(m)) / (E(x) ^ E(n)), 53 | 54 | // (x^m)^n => x^(m*n) 55 | Pow(Pow(Expr x, Expr m), Expr n) => E(x) ^ (E(m) * E(n)), 56 | 57 | // (x/y)^-m => (y/x)^m 58 | Pow(Div(Expr x, Expr y), Neg(Expr m)) => (E(y) / E(x)) ^ E(m), 59 | 60 | // (x*y)^m => x^m & y^m 61 | Pow(Mul(Expr x, Expr y), Expr m) => (E(x) ^ E(m)) * (E(y) ^ E(m)), 62 | 63 | // (x/y)^m => x^m / y^m 64 | Pow(Div(Expr x, Expr y), Expr m) => (E(x) ^ E(m)) / (E(y) ^ E(m)), 65 | 66 | // x^-m => 1 / x^m 67 | Pow(Expr x, Neg(Expr m)) => 1 / (E(x) ^ E(m)), 68 | 69 | // x^2 => x * x 70 | Pow(Expr x, Number(2)) => E(x) * E(x), 71 | 72 | // generic 73 | Binary(Expr a, string op, Expr b) => SMath.Binary(E(a), op, E(b)), 74 | Unary(Expr a, string op) => SMath.Unary(E(a), op), 75 | 76 | _ => e 77 | }; 78 | 79 | return E(expr); 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /StringMath.Expressions/Expr.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath.Expressions; 2 | 3 | public record Expr 4 | { 5 | public static Mul operator *(Expr left, Expr right) => new(left, right); 6 | public static Div operator /(Expr left, Expr right) => new(left, right); 7 | public static Sum operator +(Expr left, Expr right) => new(left, right); 8 | public static Diff operator -(Expr left, Expr right) => new(left, right); 9 | public static Pow operator ^(Expr left, Expr right) => new(left, right); 10 | public static Mod operator %(Expr left, Expr right) => new(left, right); 11 | public static Neg operator -(Expr operand) => new(operand); 12 | 13 | public static implicit operator Expr(double value) => new Number(value); 14 | public static implicit operator Expr(string value) => value.ToExpr(); 15 | } 16 | 17 | public record Variable(string Name) : Expr 18 | { 19 | public static implicit operator string(Variable var) => var.Name; 20 | public static implicit operator Variable(string name) => new(name); 21 | } 22 | 23 | public record Number(double Value) : Expr 24 | { 25 | public static implicit operator Number(double val) => new(val); 26 | public static implicit operator double(Number num) => num.Value; 27 | } 28 | public record Binary(Expr Left, string Op, Expr Right) : Expr; 29 | public record Unary(Expr Value, string Op) : Expr; 30 | 31 | public record Neg(Expr Value) : Unary(Value, "-"); 32 | public record Factorial(Expr Value) : Unary(Value, "!"); 33 | public record Sin(Expr Value) : Unary(Value, "sin"); 34 | public record Cos(Expr Value) : Unary(Value, "cos"); 35 | public record Abs(Expr Value) : Unary(Value, "abs"); 36 | public record Sqrt(Expr Value) : Unary(Value, "sqrt"); 37 | public record Tan(Expr Value) : Unary(Value, "tan"); 38 | public record Atan(Expr Value) : Unary(Value, "atan"); 39 | public record Exp(Expr Value) : Unary(Value, "exp"); 40 | 41 | public record Sum(Expr Left, Expr Right) : Binary(Left, "+", Right); 42 | public record Diff(Expr Left, Expr Right) : Binary(Left, "-", Right); 43 | public record Mul(Expr Left, Expr Right) : Binary(Left, "*", Right); 44 | public record Div(Expr Left, Expr Right) : Binary(Left, "/", Right); 45 | public record Pow(Expr Left, Expr Right) : Binary(Left, "^", Right); 46 | public record Mod(Expr Left, Expr Right) : Binary(Left, "%", Right); 47 | public record Log(Expr Left, Expr Right) : Binary(Left, "log", Right); 48 | -------------------------------------------------------------------------------- /StringMath.Expressions/ExprConversion.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath.Expressions; 2 | 3 | public static class Factory 4 | { 5 | public delegate Binary BinaryExprFactory(Expr left, string op, Expr right); 6 | public delegate Unary UnaryExprFactory(string op, Expr operand); 7 | 8 | public static BinaryExprFactory BinaryExpr = (left, op, right) => op switch 9 | { 10 | "+" => new Sum(left, right), 11 | "-" => new Diff(left, right), 12 | "/" => new Div(left, right), 13 | "*" => new Mul(left, right), 14 | "^" => new Pow(left, right), 15 | "%" => new Mod(left, right), 16 | "log" => new Log(left, right), 17 | _ => new Binary(left, op, right), 18 | }; 19 | 20 | public static UnaryExprFactory UnaryExpr = (op, operand) => op switch 21 | { 22 | "-" => new Neg(operand), 23 | "!" => new Factorial(operand), 24 | "sin" => new Sin(operand), 25 | "cos" => new Cos(operand), 26 | "abs" => new Abs(operand), 27 | "tan" => new Tan(operand), 28 | "atan" => new Atan(operand), 29 | "sqrt" => new Sqrt(operand), 30 | "exp" => new Exp(operand), 31 | _ => new Unary(operand, op) 32 | }; 33 | } 34 | 35 | internal static partial class ExprConversion 36 | { 37 | public static Expr Convert(IExpression expression) 38 | { 39 | Expr result = expression switch 40 | { 41 | BinaryExpression binaryExpr => ConvertBinaryExpr(binaryExpr), 42 | ConstantExpression constantExpr => ConvertConstantExpr(constantExpr), 43 | UnaryExpression unaryExpr => ConvertUnaryExpr(unaryExpr), 44 | VariableExpression variableExpr => ConvertVariableExpr(variableExpr), 45 | _ => throw new NotImplementedException($"'{expression.Type}' Convertor is not implemented.") 46 | }; 47 | 48 | return result; 49 | } 50 | 51 | public static Expr ConvertVariableExpr(VariableExpression variableExpr) => new Variable(variableExpr.Name); 52 | 53 | public static Expr ConvertConstantExpr(ConstantExpression constantExpr) => new Number(constantExpr.Value); 54 | 55 | public static Expr ConvertBinaryExpr(BinaryExpression binaryExpr) 56 | { 57 | Expr left = Convert(binaryExpr.Left); 58 | Expr right = Convert(binaryExpr.Right); 59 | 60 | return Factory.BinaryExpr(left, binaryExpr.OperatorName, right); 61 | } 62 | 63 | public static Expr ConvertUnaryExpr(UnaryExpression unaryExpr) 64 | { 65 | Expr operand = Convert(unaryExpr.Operand); 66 | return Factory.UnaryExpr(unaryExpr.OperatorName, operand); 67 | } 68 | } 69 | 70 | internal partial class ExprConversion 71 | { 72 | public static IExpression Convert(Expr expression) 73 | { 74 | IExpression result = expression switch 75 | { 76 | Binary binaryExpr => ConvertBinaryExpr(binaryExpr), 77 | Number constantExpr => ConvertConstantExpr(constantExpr), 78 | Unary unaryExpr => ConvertUnaryExpr(unaryExpr), 79 | Variable variableExpr => ConvertVariableExpr(variableExpr), 80 | _ => throw new NotImplementedException($"'{expression?.GetType().Name}' Convertor is not implemented.") 81 | }; 82 | 83 | return result; 84 | } 85 | 86 | public static IExpression ConvertVariableExpr(Variable variableExpr) => new VariableExpression(variableExpr.Name); 87 | 88 | public static IExpression ConvertConstantExpr(Number constantExpr) => new ConstantExpression(constantExpr.Value); 89 | 90 | public static IExpression ConvertBinaryExpr(Binary binaryExpr) => new BinaryExpression(Convert(binaryExpr.Left), binaryExpr.Op, Convert(binaryExpr.Right)); 91 | 92 | public static IExpression ConvertUnaryExpr(Unary unaryExpr) => new UnaryExpression(unaryExpr.Op, Convert(unaryExpr.Value)); 93 | } -------------------------------------------------------------------------------- /StringMath.Expressions/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath.Expressions; 2 | 3 | public static partial class Extensions 4 | { 5 | public static Expr ToExpr(this MathExpr expr) 6 | => ExprConversion.Convert(expr.Expression); 7 | 8 | public static Expr ToExpr(this string expr) 9 | => ExprConversion.Convert(((MathExpr)expr).Expression); 10 | 11 | public static MathExpr ToMathExpr(this Expr expr, IMathContext? context = default) 12 | { 13 | var expression = ExprConversion.Convert(expr); 14 | return new MathExpr(expression, context ?? MathContext.Default); 15 | } 16 | 17 | public static MathExpr Expand(this string expr) 18 | => ((MathExpr)expr).Expand(); 19 | 20 | /// Attempts to simplify the given expression. 21 | /// The expression. 22 | /// A new simplified expression. 23 | public static MathExpr Simplify(this string expr) 24 | => ((MathExpr)expr).Simplify(); 25 | 26 | public static MathExpr Expand(this MathExpr expr) 27 | { 28 | Expr expanded = expr.ToExpr(); 29 | Expr temp; 30 | 31 | do 32 | { 33 | temp = expanded; 34 | expanded = expanded.Expand(); 35 | } 36 | while (expanded != temp); 37 | 38 | return expanded.ToMathExpr(expr.Context); 39 | } 40 | 41 | /// Attempts to simplify the given expression. 42 | /// The expression. 43 | /// A new simplified expression. 44 | public static MathExpr Simplify(this MathExpr expr) 45 | { 46 | Expr simplified = expr.ToExpr(); 47 | Expr temp; 48 | 49 | do 50 | { 51 | temp = simplified; 52 | simplified = simplified.Simplify(expr.Context); 53 | } 54 | while (simplified != temp); 55 | 56 | return simplified.ToMathExpr(expr.Context); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /StringMath.Expressions/SMath.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath.Expressions; 2 | 3 | public static class SMath 4 | { 5 | public static Variable Var(string name) => new(name); 6 | public static Binary Binary(Expr left, string op, Expr right) => Factory.BinaryExpr(left, op, right); 7 | public static Unary Unary(Expr left, string op) => Factory.UnaryExpr(op, left); 8 | 9 | public static Neg Neg(Expr Value) => new(Value); 10 | public static Factorial Factorial(Expr Value) => new(Value); 11 | public static Sin Sin(Expr Value) => new(Value); 12 | public static Cos Cos(Expr Value) => new(Value); 13 | public static Abs Abs(Expr Value) => new(Value); 14 | public static Sqrt Sqrt(Expr Value) => new(Value); 15 | public static Tan Tan(Expr Value) => new(Value); 16 | public static Atan Atan(Expr Value) => new(Value); 17 | public static Exp Exp(Expr Value) => new(Value); 18 | 19 | public static Sum Sum(Expr Left, Expr Right) => new(Left, Right); 20 | public static Diff Diff(Expr Left, Expr Right) => new(Left, Right); 21 | public static Mul Mul(Expr Left, Expr Right) => new(Left, Right); 22 | public static Div Div(Expr Left, Expr Right) => new(Left, Right); 23 | public static Pow Pow(Expr Left, Expr Right) => new(Left, Right); 24 | public static Mod Mod(Expr Left, Expr Right) => new(Left, Right); 25 | public static Log Log(Expr Left, Expr Right) => new(Left, Right); 26 | } -------------------------------------------------------------------------------- /StringMath.Expressions/Simplify.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath.Expressions; 2 | 3 | public static partial class Extensions 4 | { 5 | /// Attempts to simplify the given expression. 6 | /// The expression. 7 | /// A new simplified expression. 8 | public static Expr Simplify(this Expr expr, IMathContext context) 9 | { 10 | Expr S(Expr e) => e switch 11 | { 12 | // -5 => -5 13 | Unary(Number a, string op) => context.EvaluateUnary(op, a.Value), 14 | 15 | // 3 * 2 => 6 16 | Binary(Number a, string op, Number b) => context.EvaluateBinary(op, a.Value, b.Value), 17 | 18 | // a - 0 => a 19 | Diff(Expr expr, Number(0)) => S(expr), 20 | 21 | // 0 - a => -a 22 | Diff(Number(0), Expr expr) => -S(expr), 23 | 24 | // 0 + a => a 25 | Sum(Number(0), Expr expr) => S(expr), 26 | 27 | // a + 0 => a 28 | Sum(Expr expr, Number(0)) => S(expr), 29 | 30 | // 0 * a => 0 31 | Mul(Number(0), Expr) => 0, 32 | 33 | // a * 0 => 0 34 | Mul(Expr, Number(0)) => 0, 35 | 36 | // 1 * a => a 37 | Mul(Number(1), Expr expr) => S(expr), 38 | 39 | // a * 1 => a 40 | Mul(Expr expr, Number(1)) => S(expr), 41 | 42 | // x^1 => x 43 | Pow(Expr x, Number(1)) => S(x), 44 | 45 | // x^0 => 1 46 | Pow(Expr, Number(0)) => 1, 47 | 48 | // generic 49 | Binary(Expr a, string op, Expr b) => SMath.Binary(S(a), op, S(b)), 50 | Unary(Expr a, string op) => SMath.Unary(S(a), op), 51 | 52 | _ => e 53 | }; 54 | 55 | return S(expr); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /StringMath.Expressions/StringMath.Expressions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6 5 | enable 6 | enable 7 | 8 | 9 | 10 | true 11 | ../build/string-math.snk 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /StringMath.Expressions/Usings.cs: -------------------------------------------------------------------------------- 1 | global using System; -------------------------------------------------------------------------------- /StringMath.Tests/BooleanExprTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | 4 | namespace StringMath.Tests 5 | { 6 | [TestFixture] 7 | internal class BooleanExprTests 8 | { 9 | private MathContext _context; 10 | 11 | [SetUp] 12 | public void Setup() 13 | { 14 | MathExpr.AddVariable("true", 1); 15 | MathExpr.AddVariable("false", 0); 16 | 17 | _context = new MathContext(); 18 | _context.RegisterLogical("and", (a, b) => a && b, Precedence.Multiplication); 19 | _context.RegisterLogical("or", (a, b) => a || b, Precedence.Addition); 20 | _context.RegisterLogical(">", (a, b) => a > b, Precedence.Power); 21 | _context.RegisterLogical("<", (a, b) => a < b, Precedence.Power); 22 | _context.RegisterLogical("!", (a) => !a); 23 | } 24 | 25 | [Test] 26 | public void Evaluate_Variable_Substitution() 27 | { 28 | MathExpr expr = new MathExpr("{a} and 1", _context); 29 | Assert.IsFalse(expr.Substitute("a", false).EvalBoolean()); 30 | Assert.IsTrue(expr.Substitute("a", true).EvalBoolean()); 31 | } 32 | 33 | [Test] 34 | public void Evaluate_Boolean_Numbers() 35 | { 36 | bool expr = "1 and 1".EvalBoolean(_context); 37 | Assert.IsTrue(expr); 38 | 39 | bool result = "1 and 0 or !0 and 3 > 2".EvalBoolean(_context); 40 | Assert.IsTrue(result); 41 | } 42 | 43 | [Test] 44 | public void Evaluate_Globals_Variables() 45 | { 46 | Assert.IsTrue("{true} or {false} and {true}".EvalBoolean(_context)); 47 | Assert.IsTrue("{true} or {false}".EvalBoolean(_context)); 48 | Assert.IsFalse("{false} or {false}".EvalBoolean(_context)); 49 | Assert.IsFalse("{true} and {false}".EvalBoolean(_context)); 50 | Assert.IsTrue("{true} and {true}".EvalBoolean(_context)); 51 | } 52 | 53 | [Test] 54 | public void Evaluate_Binary_Operation() 55 | { 56 | _context.RegisterBinary("+", (a, b) => a + b); 57 | Assert.IsTrue("(3 + 5) > 7".EvalBoolean(_context)); 58 | } 59 | } 60 | 61 | static class BooleanMathExtensions 62 | { 63 | public static bool EvalBoolean(this string value) => value.ToMathExpr().EvalBoolean(); 64 | 65 | public static bool EvalBoolean(this string value, IMathContext context) => value.ToMathExpr(context).EvalBoolean(); 66 | 67 | public static bool EvalBoolean(this MathExpr value) => value.Result != 0; 68 | 69 | public static MathExpr Substitute(this MathExpr expr, string name, bool value) 70 | => expr.Substitute(name, value is true ? 1 : 0); 71 | 72 | public static void RegisterLogical(this IMathContext context, string operatorName, Func operation, Precedence? precedence) 73 | => context.RegisterBinary(operatorName, (a, b) => operation(a != 0, b != 0) ? 1 : 0, precedence); 74 | 75 | public static void RegisterLogical(this IMathContext context, string operatorName, Func operation, Precedence? precedence) 76 | => context.RegisterBinary(operatorName, (a, b) => operation(a, b) ? 1 : 0, precedence); 77 | 78 | public static void RegisterLogical(this IMathContext context, string operatorName, Func operation) 79 | => context.RegisterUnary(operatorName, (a) => operation(a != 0) ? 1 : 0); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /StringMath.Tests/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace StringMath.Tests 4 | { 5 | static class Extensions 6 | { 7 | public static List ReadAllTokens(this string input) 8 | { 9 | Tokenizer tokenizer = new Tokenizer(input); 10 | List tokens = new List(); 11 | 12 | Token t; 13 | do 14 | { 15 | t = tokenizer.ReadToken(); 16 | tokens.Add(t); 17 | } 18 | while (t.Type != TokenType.EndOfCode); 19 | 20 | return tokens; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /StringMath.Tests/MathExprTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | 4 | namespace StringMath.Tests 5 | { 6 | [TestFixture] 7 | internal class MathExprTests 8 | { 9 | [SetUp] 10 | public void Setup() 11 | { 12 | MathExpr.AddOperator("abs", a => a > 0 ? a : -a); 13 | MathExpr.AddOperator("x", (a, b) => a * b); 14 | MathExpr.AddOperator("<<", (a, b) => a * Math.Pow(2, (int)b)); 15 | MathExpr.AddOperator("<>", (a, b) => double.Parse($"{a}{b}"), Precedence.Prefix); 16 | MathExpr.AddOperator("e", (a, b) => double.Parse($"{a}e{b}"), Precedence.Power); 17 | MathExpr.AddOperator("sind", a => Math.Sin(a * (Math.PI / 180))); 18 | } 19 | 20 | [Test] 21 | public void Substitute_Should_Not_Overwrite_Global_Variable() 22 | { 23 | MathExpr.AddVariable("PI", Math.PI); 24 | MathExpr expr = "3 + {PI}"; 25 | 26 | MathException ex = Assert.Throws(() => expr.Substitute("PI", 1)); 27 | Assert.AreEqual(ex.Code, MathException.ErrorCode.READONLY_VARIABLE); 28 | } 29 | 30 | [Test] 31 | public void Substitute_Should_Not_Set_Missing_Variable() 32 | { 33 | MathExpr expr = "3 + 2"; 34 | 35 | MathException ex = Assert.Throws(() => expr.Substitute("a", 1)); 36 | Assert.AreEqual(ex.Code, MathException.ErrorCode.UNEXISTING_VARIABLE); 37 | } 38 | 39 | [Test] 40 | public void Substitute_Should_Set_Local_Variable() 41 | { 42 | MathExpr expr = "3 + {a}"; 43 | expr.Substitute("a", 1); 44 | 45 | Assert.AreEqual(4, expr.Result); 46 | } 47 | 48 | [Test] 49 | public void Substitute_Should_Clear_Cache() 50 | { 51 | MathExpr expr = "{a} + 3"; 52 | expr.Substitute("a", 3); 53 | 54 | Assert.AreEqual(6, expr.Result); 55 | 56 | expr.Substitute("a", 2); 57 | Assert.AreEqual(5, expr.Result); 58 | } 59 | 60 | [Test] 61 | public void Indexer_Should_Set_Local_Variable() 62 | { 63 | MathExpr expr = "3 + {a} + {b}"; 64 | expr["a"] = 1; 65 | expr["b"] = 2; 66 | 67 | Assert.AreEqual(6, expr.Result); 68 | } 69 | 70 | [Test] 71 | public void Double_Conversion_Should_Evaluate_Expression() 72 | { 73 | double result = (MathExpr)"3 + 5"; 74 | 75 | Assert.AreEqual(8, result); 76 | } 77 | 78 | [Test] 79 | [TestCase("{a}", new[] { "a" })] 80 | [TestCase("2 * {a} - {PI}", new[] { "a" })] 81 | [TestCase("({a} - 5) * 4 + {E}", new[] { "a" })] 82 | public void LocalVariables_Should_Exclude_Global_Variables(string input, string[] expected) 83 | { 84 | MathExpr expr = input; 85 | 86 | Assert.AreEqual(expected, expr.LocalVariables); 87 | } 88 | 89 | [Test] 90 | [TestCase("{a}", new[] { "a" })] 91 | [TestCase("2 * {a} - {PI}", new[] { "a", "PI" })] 92 | [TestCase("({a} - 5) * 4 + {E}", new[] { "a", "E" })] 93 | public void Variables_Should_Include_Global_Variables(string input, string[] expected) 94 | { 95 | MathExpr expr = input; 96 | 97 | Assert.AreEqual(expected, expr.Variables); 98 | } 99 | 100 | [Test] 101 | public void SetOperator_Binary_Should_Overwrite_Specified_Operator() 102 | { 103 | MathExpr.AddOperator("*", (x, y) => x * y); 104 | MathExpr expr = "5 * 3"; 105 | expr.SetOperator("*", (x, y) => x); 106 | 107 | Assert.AreEqual(5, expr.Result); 108 | } 109 | 110 | [Test] 111 | public void SetOperator_Binary_Should_Not_Overwrite_Global_Operator() 112 | { 113 | MathExpr.AddOperator("*", (x, y) => x * y); 114 | MathExpr expr = "5 * 3"; 115 | expr.SetOperator("*", (x, y) => x); 116 | 117 | MathExpr expr2 = "4 * 3"; 118 | 119 | Assert.AreEqual(5, expr.Result); 120 | Assert.AreEqual(12, expr2.Result); 121 | } 122 | 123 | [Test] 124 | public void SetOperator_Unary_Should_Overwrite_Specified_Operator() 125 | { 126 | MathExpr.AddOperator(">>", (x) => x * x); 127 | MathExpr expr = ">> 5"; 128 | expr.SetOperator(">>", (x) => x); 129 | 130 | Assert.AreEqual(5, expr.Result); 131 | } 132 | 133 | [Test] 134 | public void SetOperator_Unary_Should_Not_Overwrite_Global_Operator() 135 | { 136 | MathExpr.AddOperator("-", (x) => -x); 137 | MathExpr expr = "-5"; 138 | expr.SetOperator("-", (x) => x); 139 | 140 | MathExpr expr2 = "-5"; 141 | 142 | Assert.AreEqual(5, expr.Result); 143 | Assert.AreEqual(-5, expr2.Result); 144 | } 145 | 146 | [Test] 147 | [TestCase("1 +\t 2", 3)] 148 | [TestCase("-1.5 + 3", 1.5)] 149 | [TestCase("4!", 24)] 150 | [TestCase("(4 + 1)!", 120)] 151 | [TestCase("(3! + 1) * 2", 14)] 152 | [TestCase("2 ^ 3", 8)] 153 | [TestCase("1 + 16 log 2", 5)] 154 | [TestCase("1 + sqrt 4", 3)] 155 | [TestCase("sind(90) + sind 30", 1.5)] 156 | [TestCase("((1 + 1) + ((1 + 1) + (((1) + 1)) + 1))", 7)] 157 | public void Evaluate(string input, double expected) 158 | { 159 | double result = input.Eval(); 160 | Assert.AreEqual(expected, result); 161 | } 162 | 163 | [TestCase("{b}+3*{a}", 3, 2, 11)] 164 | public void Evaluate(string input, double a, double b, double expected) 165 | { 166 | MathExpr expr = input; 167 | expr["a"] = a; 168 | expr["b"] = b; 169 | 170 | Assert.AreEqual(expected, expr.Result); 171 | } 172 | 173 | public void Evaluate_Using_GlobalVariables() 174 | { 175 | MathExpr.AddVariable("PI", Math.PI); 176 | MathExpr expr = "{PI}"; 177 | 178 | Assert.AreEqual(Math.PI, expr.Result); 179 | } 180 | 181 | [Test] 182 | [TestCase("{a}+2", 1, 3)] 183 | [TestCase("2*{a}+2", 3, 8)] 184 | [TestCase("2*{a}+2*{a}", 3, 12)] 185 | [TestCase("({a})", 3, 3)] 186 | public void Evaluate(string input, double variable, double expected) 187 | { 188 | MathExpr expr = input.Substitute("a", variable); 189 | 190 | Assert.AreEqual(expected, expr.Result); 191 | } 192 | 193 | [Test] 194 | [TestCase("abs -5", 5)] 195 | [TestCase("abs(-1)", 1)] 196 | [TestCase("3 max 2", 3)] 197 | [TestCase("2 x\r\n 5", 10)] 198 | [TestCase("3 << 2", 12)] 199 | [TestCase("-3 <> 2", -32)] 200 | public void Evaluate_CustomOperators(string input, double expected) 201 | { 202 | MathExpr expr = input; 203 | Assert.AreEqual(expected, expr.Result); 204 | } 205 | 206 | [Test] 207 | [TestCase("abs -5 + {a}", 1, 6)] 208 | [TestCase("{a} + 2 * abs(-1) / {a}", 1, 3)] 209 | [TestCase("3 max {a}", 2, 3)] 210 | [TestCase("2 x\r\n {a}", 5, 10)] 211 | [TestCase("{a} << {a}", 3, 24)] 212 | [TestCase("-3 <> {a}", 2, -32)] 213 | [TestCase("{a}+2", 1, 3)] 214 | [TestCase("2*{a}+2", 3, 8)] 215 | [TestCase("2*{a}+2*{a}", 3, 12)] 216 | [TestCase("({a})", 3, 3)] 217 | [TestCase("2 * ({a} + 3 + 5)", 1, 18)] 218 | public void Evaluate_With_Variables(string input, double variable, double expected) 219 | { 220 | MathExpr expr = input; 221 | expr.Substitute("a", variable); 222 | 223 | Assert.AreEqual(expr.Result, expected); 224 | } 225 | 226 | [Test] 227 | [TestCase("abs -5", 5)] 228 | [TestCase("abs(-1)", 1)] 229 | [TestCase("3 max 2", 3)] 230 | [TestCase("2 x\r\n 5", 10)] 231 | [TestCase("3 << 2", 12)] 232 | [TestCase("-3 <> 2", -32)] 233 | public void Evaluate_CachedOperation_Without_Variables(string input, double expected) 234 | { 235 | MathExpr expr = input; 236 | 237 | Assert.AreEqual(expected, expr.Result); 238 | Assert.AreEqual(expected, expr.Result); 239 | } 240 | 241 | [Test] 242 | [TestCase("{a}+2")] 243 | public void Evaluate_Unassigned_Variable_Exception(string input) 244 | { 245 | MathExpr expr = input; 246 | 247 | MathException exception = Assert.Throws(() => expr.Result.ToString()); 248 | Assert.AreEqual(MathException.ErrorCode.UNASSIGNED_VARIABLE, exception.Code); 249 | } 250 | 251 | [Test] 252 | public void Evaluate_Sharing_Context() 253 | { 254 | MathExpr expr = "{a} + 1".Substitute("a", 2); 255 | expr.SetOperator("+", (a, b) => Math.Pow(a, b)); 256 | 257 | Assert.AreEqual(2, expr.Result); 258 | 259 | MathExpr expr2 = "3 + 2".ToMathExpr(expr.Context); 260 | Assert.AreEqual(9, expr2.Result); 261 | 262 | double result = "1 + 2 + 3".Eval(expr.Context); 263 | Assert.AreEqual(1, result); 264 | } 265 | 266 | [Test] 267 | public void Evaluate_Custom_Context() 268 | { 269 | var context = new MathContext(); 270 | context.RegisterBinary("+", (a, b) => Math.Pow(a, b)); 271 | 272 | MathExpr expr = new MathExpr("{a} + 1", context).Substitute("a", 2); 273 | Assert.AreEqual(2, expr.Result); 274 | 275 | MathExpr expr2 = "3 + 2".ToMathExpr(context); 276 | Assert.AreEqual(9, expr2.Result); 277 | 278 | double result = "1 + 2 + 3".Eval(context); 279 | Assert.AreEqual(1, result); 280 | } 281 | 282 | [Test] 283 | [TestCase("1 + 5", 6)] 284 | [TestCase("1 + -5", -4)] 285 | [TestCase("2 * (abs(-5) + 1)", 12)] 286 | public void Compile(string input, double expected) 287 | { 288 | Func fn = input.ToMathExpr().Compile(); 289 | double result = fn(); 290 | 291 | Assert.AreEqual(expected, result); 292 | } 293 | 294 | [Test] 295 | [TestCase("1 + {a}", new[] { "a" }, new[] { 1d }, 2)] 296 | public void Compile_1Variable(string input, string[] paramsOrder, double[] paramsValues, double expected) 297 | { 298 | var fn = input.ToMathExpr().Compile(paramsOrder[0]); 299 | double result = fn(paramsValues[0]); 300 | 301 | Assert.AreEqual(expected, result); 302 | } 303 | 304 | [Test] 305 | [TestCase("(1 + {a}) * {b}", new[] { "b", "a" }, new[] { 2d, 3d }, 8)] 306 | [TestCase("(1 + {b}) * {a}", new[] { "b", "a" }, new[] { 2d, 3d }, 9)] 307 | public void Compile_2Variables(string input, string[] paramsOrder, double[] paramsValues, double expected) 308 | { 309 | var fn = input.ToMathExpr().Compile(paramsOrder[0], paramsOrder[1]); 310 | double result = fn(paramsValues[0], paramsValues[1]); 311 | 312 | Assert.AreEqual(expected, result); 313 | } 314 | 315 | [Test] 316 | [TestCase("({c} - 1 + {a}) * {b}", new[] { "b", "a", "c" }, new[] { 2d, 3d, 2d }, 8)] 317 | [TestCase("({c} - 1 + {b}) * {a}", new[] { "b", "a", "c" }, new[] { 2d, 3d, 2d }, 9)] 318 | public void Compile_3Variables(string input, string[] paramsOrder, double[] paramsValues, double expected) 319 | { 320 | var fn = input.ToMathExpr().Compile(paramsOrder[0], paramsOrder[1], paramsOrder[2]); 321 | double result = fn(paramsValues[0], paramsValues[1], paramsValues[2]); 322 | 323 | Assert.AreEqual(expected, result); 324 | } 325 | 326 | [Test] 327 | public void Compile_Throws_Missing_Variable() 328 | { 329 | MathException ex = Assert.Throws(() => "1 + {a}".ToMathExpr().Compile("b")); 330 | 331 | Assert.AreEqual(MathException.ErrorCode.UNEXISTING_VARIABLE, ex.Code); 332 | } 333 | 334 | [Test] 335 | public void Compile_Throws_Missing_Variable_When_No_Parameter_Provided() 336 | { 337 | MathException ex = Assert.Throws(() => "1 + {a}".ToMathExpr().Compile()); 338 | 339 | Assert.AreEqual(MathException.ErrorCode.UNEXISTING_VARIABLE, ex.Code); 340 | } 341 | 342 | [Test] 343 | public void Compile_Resolves_Remaining_Variables() 344 | { 345 | var expr = "1 + {a}".ToMathExpr().Substitute("a", 3); 346 | var fn = expr.Compile(); 347 | double result = fn(); 348 | 349 | Assert.AreEqual(4, result); 350 | } 351 | 352 | [Test] 353 | public void Compile_Resolves_Remaining_Variables2() 354 | { 355 | var expr = "1 + {a} * {b}".ToMathExpr().Substitute("a", 3); 356 | var fn = expr.Compile("b"); 357 | double result = fn(2); 358 | 359 | Assert.AreEqual(7, result); 360 | } 361 | 362 | [Test] 363 | public void Compile_Resolves_Global_Variables() 364 | { 365 | var expr = "1 + {PI}".ToMathExpr(); 366 | var fn = expr.Compile(); 367 | double result = fn(); 368 | 369 | Assert.AreEqual(1 + Math.PI, result); 370 | } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /StringMath.Tests/ParserTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using StringMath.Expressions; 3 | 4 | namespace StringMath.Tests 5 | { 6 | [TestFixture] 7 | internal class ParserTests 8 | { 9 | private IMathContext _context; 10 | 11 | [OneTimeSetUp] 12 | public void Setup() 13 | { 14 | _context = MathContext.Default; 15 | } 16 | 17 | [Test] 18 | [TestCase("1 * 2", "1 * 2")] 19 | [TestCase("1 ^ (6 % 2)", "1 ^ (6 % 2)")] 20 | [TestCase("1 - ((3 + 8) max 1)", "1 - (3 + 8) max 1")] 21 | [TestCase("5! + ({a} / 3)", "5! + {a} / 3")] 22 | [TestCase("-9.53", "-9.53")] 23 | [TestCase("1.15215345346", "1.15215345346")] 24 | [TestCase("0", "0")] 25 | [TestCase("!2", "2!")] 26 | public void ParseMathExpression(string input, string expected) 27 | { 28 | Tokenizer tokenizer = new Tokenizer(input); 29 | Parser parser = new Parser(tokenizer, _context); 30 | 31 | IExpression result = parser.Parse(); 32 | string actual = result.ToString(); 33 | 34 | Assert.AreEqual(expected, actual); 35 | } 36 | 37 | [Test] 38 | [TestCase("1 2 * 2")] 39 | [TestCase("1 * 2 {a}")] 40 | [TestCase("1 * {}")] 41 | [TestCase("{}")] 42 | [TestCase("()")] 43 | [TestCase(")")] 44 | [TestCase("{a}{b}")] 45 | [TestCase("")] 46 | [TestCase(" ")] 47 | [TestCase("\0")] 48 | [TestCase("\01+2")] 49 | [TestCase("\n")] 50 | [TestCase("\t")] 51 | [TestCase("{")] 52 | [TestCase("}")] 53 | [TestCase("(1+2")] 54 | [TestCase("1+(2")] 55 | [TestCase("1+2)")] 56 | [TestCase("1+")] 57 | [TestCase("1.")] 58 | [TestCase("1..1")] 59 | [TestCase("--1")] 60 | [TestCase("-+1")] 61 | [TestCase("{")] 62 | [TestCase("}")] 63 | [TestCase("asd")] 64 | [TestCase("1 + asd")] 65 | [TestCase("{-a}")] 66 | [TestCase("*{a}")] 67 | [TestCase("1 + 2 1")] 68 | public void ParseBadExpression_Exception(string input) 69 | { 70 | Tokenizer tokenizer = new Tokenizer(input); 71 | Parser parser = new Parser(tokenizer, _context); 72 | 73 | MathException exception = Assert.Throws(() => parser.Parse()); 74 | Assert.AreEqual(MathException.ErrorCode.UNEXPECTED_TOKEN, exception.Code); 75 | } 76 | 77 | [Test] 78 | [TestCase("{a} pow 3", "{a} pow 3")] 79 | [TestCase("rand (3 + 5)", "rand(3 + 5)")] 80 | [TestCase("rand (3 pow 5)", "rand(3 pow 5)")] 81 | public void ParseExpression_CustomOperators(string input, string expected) 82 | { 83 | MathContext context = new MathContext(_context); 84 | context.RegisterBinary("pow", (a, b) => a); 85 | context.RegisterUnary("rand", (a) => a); 86 | 87 | Tokenizer tokenizer = new Tokenizer(input); 88 | Parser parser = new Parser(tokenizer, context); 89 | 90 | IExpression result = parser.Parse(); 91 | string actual = result.ToString(); 92 | 93 | Assert.AreEqual(expected, actual); 94 | } 95 | 96 | [Test] 97 | [TestCase("{a} pow 3")] 98 | [TestCase("rand 3")] 99 | public void ParseExpression_CustomOperators_Exception(string expected) 100 | { 101 | MathContext context = new MathContext(); 102 | 103 | Tokenizer tokenizer = new Tokenizer(expected); 104 | Parser parser = new Parser(tokenizer, context); 105 | 106 | MathException exception = Assert.Throws(() => parser.Parse()); 107 | Assert.AreEqual(MathException.ErrorCode.UNEXPECTED_TOKEN, exception.Code); 108 | } 109 | 110 | [Test] 111 | [TestCase("{a}", "a")] 112 | [TestCase("{var}", "var")] 113 | [TestCase("{__var}", "__var")] 114 | [TestCase("{_}", "_")] 115 | [TestCase("{_12}", "_12")] 116 | [TestCase("{a_}", "a_")] 117 | [TestCase("{a_a}", "a_a")] 118 | [TestCase("{a123_}", "a123_")] 119 | [TestCase("{a13}", "a13")] 120 | public void ParseVariableExpression(string expected, string name) 121 | { 122 | Tokenizer tokenizer = new Tokenizer(expected); 123 | Parser parser = new Parser(tokenizer, _context); 124 | 125 | IExpression result = parser.Parse(); 126 | string actual = result.ToString(); 127 | 128 | Assert.IsInstanceOf(result); 129 | Assert.AreEqual(name, ((VariableExpression)result).Name); 130 | Assert.AreEqual(expected, actual); 131 | } 132 | 133 | [Test] 134 | [TestCase("{}")] 135 | [TestCase("{1a}")] 136 | [TestCase("{123}")] 137 | [TestCase("{ }")] 138 | [TestCase("{ a }")] 139 | [TestCase("{-a}")] 140 | public void ParseVariableExpression_Exception(string expected) 141 | { 142 | Tokenizer tokenizer = new Tokenizer(expected); 143 | Parser parser = new Parser(tokenizer, _context); 144 | 145 | MathException exception = Assert.Throws(() => parser.Parse()); 146 | Assert.AreEqual(MathException.ErrorCode.UNEXPECTED_TOKEN, exception.Code); 147 | } 148 | 149 | [Test] 150 | [TestCase("10 + 3", "10 + 3")] 151 | [TestCase("cos(90.0) - 5", "cos(90) - 5")] 152 | [TestCase("round(1) * (2 + 3)", "round(1) * (2 + 3)")] 153 | [TestCase("!1.5 / 3", "1.5! / 3")] 154 | [TestCase("1.5! ^ sqrt 3", "1.5! ^ sqrt(3)")] 155 | [TestCase("{a} - abs (5 % 3)", "{a} - abs(5 % 3)")] 156 | [TestCase("{a} - 3 + {b}", "{a} - 3 + {b}")] 157 | [TestCase("{a} / 3 * {b}", "{a} / 3 * {b}")] 158 | [TestCase("1 + 2 + 3", "1 + 2 + 3")] 159 | [TestCase("1 / 2 / 3", "1 / 2 / 3")] 160 | public void ParseBinaryExpression(string input, string expected) 161 | { 162 | Tokenizer tokenizer = new Tokenizer(input); 163 | Parser parser = new Parser(tokenizer, _context); 164 | 165 | IExpression result = parser.Parse(); 166 | string actual = result.ToString(_context); 167 | 168 | Assert.IsInstanceOf(result); 169 | Assert.AreEqual(expected, actual); 170 | } 171 | 172 | [Test] 173 | [TestCase("sin(90.0 + 2)", "sin(90 + 2)")] 174 | [TestCase("cos(90.0)", "cos(90)")] 175 | [TestCase("round(1)", "round(1)")] 176 | [TestCase("!1.5", "1.5!")] 177 | [TestCase("1.5!", "1.5!")] 178 | [TestCase("abs.5", "abs(0.5)")] 179 | [TestCase("-999", "-999")] 180 | [TestCase("sqrt(-999 / 2 * 3 max 5)", "sqrt(-999 / 2 * 3 max 5)")] 181 | [TestCase("-(sqrt5)", "-(sqrt(5))")] 182 | [TestCase("- sqrt5", "-(sqrt(5))")] 183 | [TestCase("sqrt{a}", "sqrt({a})")] 184 | public void ParseUnaryExpression(string input, string expected) 185 | { 186 | Tokenizer tokenizer = new Tokenizer(input); 187 | Parser parser = new Parser(tokenizer, _context); 188 | 189 | IExpression result = parser.Parse(); 190 | string actual = result.ToString(_context); 191 | 192 | Assert.IsInstanceOf(result); 193 | Assert.AreEqual(expected, actual); 194 | } 195 | 196 | [Test] 197 | [TestCase("fail 5")] 198 | [TestCase("-+5")] 199 | [TestCase("+5")] 200 | public void ParseUnaryExpression_Exception(string input) 201 | { 202 | Tokenizer tokenizer = new Tokenizer(input); 203 | Parser parser = new Parser(tokenizer, _context); 204 | 205 | MathException exception = Assert.Throws(() => parser.Parse()); 206 | Assert.AreEqual(MathException.ErrorCode.UNEXPECTED_TOKEN, exception.Code); 207 | } 208 | 209 | [Test] 210 | [TestCase("1", "1")] 211 | [TestCase("1.5", "1.5")] 212 | [TestCase(".5", "0.5")] 213 | [TestCase("9999999", "9999999")] 214 | public void ParseConstantExpression(string input, string expected) 215 | { 216 | Tokenizer tokenizer = new Tokenizer(input); 217 | Parser parser = new Parser(tokenizer, _context); 218 | 219 | IExpression result = parser.Parse(); 220 | string actual = result.ToString(_context); 221 | 222 | Assert.IsInstanceOf(result); 223 | Assert.AreEqual(expected, actual); 224 | } 225 | 226 | [Test] 227 | [TestCase("1a")] 228 | [TestCase("1.a")] 229 | [TestCase(".5a")] 230 | [TestCase(".a")] 231 | [TestCase("9.01+")] 232 | public void ParseConstantExpression_Exception(string expected) 233 | { 234 | Tokenizer tokenizer = new Tokenizer(expected); 235 | Parser parser = new Parser(tokenizer, _context); 236 | 237 | MathException exception = Assert.Throws(() => parser.Parse()); 238 | Assert.AreEqual(MathException.ErrorCode.UNEXPECTED_TOKEN, exception.Code); 239 | } 240 | 241 | [Test] 242 | [TestCase("(1)", "1")] 243 | [TestCase("((1))", "1")] 244 | [TestCase("(1 + 2)", "1 + 2")] 245 | [TestCase("((1 + 2) % 3)", "(1 + 2) % 3")] 246 | [TestCase("(5! * (1 + 2))", "5! * (1 + 2)")] 247 | [TestCase("(5 + (1 + 2))", "5 + 1 + 2")] 248 | [TestCase("((5 - {a}) + (1 + 2))", "5 - {a} + 1 + 2")] 249 | [TestCase("((5 - 2) + (1 + 2! * 3))", "5 - 2 + 1 + 2! * 3")] 250 | [TestCase("((5 - 2) + ((-1 + 2) * 3))", "5 - 2 + (-1 + 2) * 3")] 251 | public void ParseGroupingExpression(string input, string expected) 252 | { 253 | Tokenizer tokenizer = new Tokenizer(input); 254 | Parser parser = new Parser(tokenizer, _context); 255 | 256 | IExpression result = parser.Parse(); 257 | string actual = result.ToString(_context); 258 | 259 | Assert.AreEqual(expected, actual); 260 | } 261 | 262 | [Test] 263 | [TestCase("()")] 264 | [TestCase("(")] 265 | [TestCase("(1")] 266 | [TestCase("(1")] 267 | [TestCase("((1)")] 268 | [TestCase("1 + 2(")] 269 | [TestCase("(1 + 2 * 3")] 270 | [TestCase("5 - 2( + (1 + 2)")] 271 | [TestCase("({a} + (1 + 2)")] 272 | public void ParseGroupingExpression_Fail(string expected) 273 | { 274 | Tokenizer tokenizer = new Tokenizer(expected); 275 | Parser parser = new Parser(tokenizer, _context); 276 | 277 | MathException exception = Assert.Throws(() => parser.Parse()); 278 | Assert.AreEqual(MathException.ErrorCode.UNEXPECTED_TOKEN, exception.Code); 279 | } 280 | } 281 | } -------------------------------------------------------------------------------- /StringMath.Tests/StringMath.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8;net6;net5;netcoreapp3.1;net48;net472;net461 5 | 6 | false 7 | 8 | true 9 | 10 | ../build/string-math.snk 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 8.0 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /StringMath.Tests/TokenizerTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace StringMath.Tests 6 | { 7 | [TestFixture] 8 | internal class TokenizerTests 9 | { 10 | [Test] 11 | [TestCase("-1 * 3.5", new[] { TokenType.Operator, TokenType.Number, TokenType.Operator, TokenType.Number })] 12 | [TestCase("2 pow 3", new[] { TokenType.Number, TokenType.Operator, TokenType.Number })] 13 | [TestCase("{a} + 2", new[] { TokenType.Identifier, TokenType.Operator, TokenType.Number })] 14 | [TestCase("(-1) + 2", new[] { TokenType.OpenParen, TokenType.Operator, TokenType.Number, TokenType.CloseParen, TokenType.Operator, TokenType.Number })] 15 | [TestCase("5!", new[] { TokenType.Number, TokenType.Exclamation })] 16 | public void ReadToken(string input, TokenType[] expected) 17 | { 18 | IEnumerable actualTokens = input.ReadAllTokens() 19 | .Where(token => token.Type != TokenType.EndOfCode) 20 | .Select(t => t.Type); 21 | Assert.That(actualTokens, Is.EquivalentTo(expected)); 22 | } 23 | 24 | [Test] 25 | [TestCase(" 123 1")] 26 | [TestCase("123\n2")] 27 | [TestCase("\t123\n 3")] 28 | [TestCase("\t123\r\n 5")] 29 | public void ReadToken_IgnoresWhitespace(string input) 30 | { 31 | // Arrange 32 | Tokenizer tokenizer = new Tokenizer(input); 33 | 34 | // Act 35 | Token token1 = tokenizer.ReadToken(); 36 | Token token2 = tokenizer.ReadToken(); 37 | Token token3 = tokenizer.ReadToken(); 38 | 39 | // Assert 40 | Assert.AreEqual(TokenType.Number, token1.Type); 41 | Assert.AreEqual(TokenType.Number, token2.Type); 42 | Assert.AreEqual(TokenType.EndOfCode, token3.Type); 43 | } 44 | 45 | [Test] 46 | [TestCase("{a}")] 47 | [TestCase("{asdas}")] 48 | [TestCase("{x1}")] 49 | [TestCase("{x_1}")] 50 | [TestCase("{x_}")] 51 | [TestCase("{_x}")] 52 | [TestCase("{_}")] 53 | public void ReadIdentifier(string input) 54 | { 55 | // Arrange 56 | Tokenizer tokenizer = new Tokenizer(input); 57 | 58 | // Act 59 | Token token = tokenizer.ReadToken(); 60 | 61 | // Assert 62 | Assert.AreEqual(TokenType.Identifier, token.Type); 63 | Assert.AreEqual(input, $"{{{token.Text}}}"); 64 | } 65 | 66 | [Test] 67 | [TestCase("{}")] 68 | [TestCase("{ }")] 69 | [TestCase("{ }")] 70 | [TestCase("{1}")] 71 | [TestCase("{a.}")] 72 | [TestCase("{1a}")] 73 | [TestCase("{{a}")] 74 | [TestCase("{a a}")] 75 | public void ReadIdentifier_Exception(string input) 76 | { 77 | // Arrange 78 | Tokenizer tokenizer = new Tokenizer(input); 79 | 80 | // Act & Assert 81 | MathException exception = Assert.Throws(() => tokenizer.ReadToken()); 82 | Assert.AreEqual(MathException.ErrorCode.UNEXPECTED_TOKEN, exception.Code); 83 | } 84 | 85 | [Test] 86 | [TestCase("pow")] 87 | [TestCase("<")] 88 | [TestCase("**")] 89 | [TestCase("")] 90 | [TestCase("o&")] 91 | [TestCase("a@a")] 92 | public void ReadOperator(string input) 93 | { 94 | // Arrange 95 | Tokenizer tokenizer = new Tokenizer(input); 96 | 97 | // Act 98 | Token token = tokenizer.ReadToken(); 99 | 100 | // Assert 101 | Assert.AreEqual(TokenType.Operator, token.Type); 102 | Assert.AreEqual(input, token.Text); 103 | } 104 | 105 | [Test] 106 | [TestCase(".1")] 107 | [TestCase("0.1")] 108 | [TestCase("0.00000000002")] 109 | [TestCase("0")] 110 | [TestCase("9999")] 111 | [TestCase("9999.0")] 112 | [TestCase("99.01")] 113 | public void ReadNumber(string input) 114 | { 115 | // Arrange 116 | Tokenizer tokenizer = new Tokenizer(input); 117 | 118 | // Act 119 | Token token = tokenizer.ReadToken(); 120 | 121 | // Assert 122 | Assert.AreEqual(TokenType.Number, token.Type); 123 | Assert.AreEqual(input, token.Text); 124 | } 125 | 126 | [Test] 127 | [TestCase("1.")] 128 | [TestCase("..1")] 129 | [TestCase("1..")] 130 | [TestCase("1.0.")] 131 | [TestCase("1.0.1")] 132 | [TestCase(".a")] 133 | public void ReadNumber_Exception(string input) 134 | { 135 | // Arrange 136 | Tokenizer tokenizer = new Tokenizer(input); 137 | 138 | // Act & Assert 139 | MathException exception = Assert.Throws(() => tokenizer.ReadToken()); 140 | Assert.AreEqual(MathException.ErrorCode.UNEXPECTED_TOKEN, exception.Code); 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /StringMath.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32825.248 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StringMath", "StringMath\StringMath.csproj", "{CBBA016F-1219-4752-A855-B50B201F0DB8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StringMath.Tests", "StringMath.Tests\StringMath.Tests.csproj", "{93A22489-0CD0-43BC-9D9A-880992836FB0}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F756F957-7BCE-4946-9E8D-BBAA772184D8}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StringMath.Expressions", "StringMath.Expressions\StringMath.Expressions.csproj", "{BFB11FF7-416B-4F3A-8727-F8BDDA537056}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StringMath.Expressions.Tests", "StringMath.Expressions.Tests\StringMath.Expressions.Tests.csproj", "{00793D59-9C2F-4F38-95AF-1B3E6FBCFFEB}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StringMath.Benchmarks", "StringMath.Benchmarks\StringMath.Benchmarks.csproj", "{FF1B3456-A328-460F-84C0-45A483C1628D}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {CBBA016F-1219-4752-A855-B50B201F0DB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {CBBA016F-1219-4752-A855-B50B201F0DB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {CBBA016F-1219-4752-A855-B50B201F0DB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {CBBA016F-1219-4752-A855-B50B201F0DB8}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {93A22489-0CD0-43BC-9D9A-880992836FB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {93A22489-0CD0-43BC-9D9A-880992836FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {93A22489-0CD0-43BC-9D9A-880992836FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {93A22489-0CD0-43BC-9D9A-880992836FB0}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {BFB11FF7-416B-4F3A-8727-F8BDDA537056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {BFB11FF7-416B-4F3A-8727-F8BDDA537056}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {BFB11FF7-416B-4F3A-8727-F8BDDA537056}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {BFB11FF7-416B-4F3A-8727-F8BDDA537056}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {00793D59-9C2F-4F38-95AF-1B3E6FBCFFEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {00793D59-9C2F-4F38-95AF-1B3E6FBCFFEB}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {00793D59-9C2F-4F38-95AF-1B3E6FBCFFEB}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {00793D59-9C2F-4F38-95AF-1B3E6FBCFFEB}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {FF1B3456-A328-460F-84C0-45A483C1628D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {FF1B3456-A328-460F-84C0-45A483C1628D}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {FF1B3456-A328-460F-84C0-45A483C1628D}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {FF1B3456-A328-460F-84C0-45A483C1628D}.Release|Any CPU.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(SolutionProperties) = preSolution 49 | HideSolutionNode = FALSE 50 | EndGlobalSection 51 | GlobalSection(ExtensibilityGlobals) = postSolution 52 | SolutionGuid = {FB51B94D-8381-4522-A3A2-AD17D3146B7D} 53 | EndGlobalSection 54 | EndGlobal 55 | -------------------------------------------------------------------------------- /StringMath/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("StringMath.Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010095705e27153197c1680f057db0c1140798ecab901198302b1127588fea996d9cb703f94e0c9ba3f424be80a59a1c6cef9e8ea22012603bb5c323e134c349454ee6a8842c811163539130a6673eb135448714c4bb1f49d33fd3f815c4202182beb946cc0f9ae029bdf7e2e1de937603073778cd5f53c98794ffccf0719fa048ee")] 4 | [assembly: InternalsVisibleTo("StringMath.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010095705e27153197c1680f057db0c1140798ecab901198302b1127588fea996d9cb703f94e0c9ba3f424be80a59a1c6cef9e8ea22012603bb5c323e134c349454ee6a8842c811163539130a6673eb135448714c4bb1f49d33fd3f815c4202182beb946cc0f9ae029bdf7e2e1de937603073778cd5f53c98794ffccf0719fa048ee")] 5 | [assembly: InternalsVisibleTo("StringMath.Expressions, PublicKey=002400000480000094000000060200000024000052534131000400000100010095705e27153197c1680f057db0c1140798ecab901198302b1127588fea996d9cb703f94e0c9ba3f424be80a59a1c6cef9e8ea22012603bb5c323e134c349454ee6a8842c811163539130a6673eb135448714c4bb1f49d33fd3f815c4202182beb946cc0f9ae029bdf7e2e1de937603073778cd5f53c98794ffccf0719fa048ee")] 6 | -------------------------------------------------------------------------------- /StringMath/Expressions/BinaryExpression.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath.Expressions 2 | { 3 | /// A binary expression. 4 | internal sealed class BinaryExpression : IExpression 5 | { 6 | /// Initializez a new instance of a binary expression. 7 | /// The left expression tree. 8 | /// The binary operator's name. 9 | /// The right expression tree. 10 | public BinaryExpression(IExpression left, string operatorName, IExpression right) 11 | { 12 | Left = left; 13 | OperatorName = operatorName; 14 | Right = right; 15 | } 16 | 17 | /// The left expression tree. 18 | public IExpression Left { get; } 19 | 20 | /// The binary operator's name. 21 | public string OperatorName { get; } 22 | 23 | /// The right expression tree. 24 | public IExpression Right { get; } 25 | 26 | /// 27 | public ExpressionType Type => ExpressionType.BinaryExpression; 28 | 29 | /// 30 | public override string ToString() 31 | => ToString(MathContext.Default); 32 | 33 | public string ToString(IMathContext context) 34 | { 35 | bool addLeft = Left is BinaryExpression left && context.GetBinaryPrecedence(OperatorName) > context.GetBinaryPrecedence(left.OperatorName); 36 | bool addRight = Right is BinaryExpression right && context.GetBinaryPrecedence(OperatorName) > context.GetBinaryPrecedence(right.OperatorName); 37 | 38 | string? leftStr = addLeft ? $"({Left.ToString(context)})" : Left.ToString(context); 39 | string? rightStr = addRight ? $"({Right.ToString(context)})" : Right.ToString(context); 40 | 41 | return $"{leftStr} {OperatorName} {rightStr}"; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /StringMath/Expressions/ConstantExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace StringMath.Expressions 4 | { 5 | /// A constant expression. 6 | internal sealed class ConstantExpression : IExpression 7 | { 8 | /// Initializes a new instance of a constant expression. 9 | /// The value of the constant. 10 | public ConstantExpression(string value) 11 | { 12 | Value = double.Parse(value, CultureInfo.InvariantCulture.NumberFormat); 13 | } 14 | 15 | /// Initializes a new instance of a constant expression. 16 | /// The value of the constant. 17 | public ConstantExpression(double value) 18 | { 19 | Value = value; 20 | } 21 | 22 | /// The constant value. 23 | public double Value { get; } 24 | 25 | /// 26 | public ExpressionType Type => ExpressionType.ConstantExpression; 27 | 28 | /// 29 | public override string ToString() 30 | => ToString(MathContext.Default); 31 | 32 | public string ToString(IMathContext context) 33 | => Value.ToString(); 34 | } 35 | } -------------------------------------------------------------------------------- /StringMath/Expressions/IExpression.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath.Expressions 2 | { 3 | /// Available expression types. 4 | internal enum ExpressionType 5 | { 6 | /// . 7 | UnaryExpression, 8 | /// . 9 | BinaryExpression, 10 | /// . 11 | VariableExpression, 12 | /// . 13 | ConstantExpression, 14 | } 15 | 16 | /// Contract for expressions. 17 | internal interface IExpression 18 | { 19 | /// The type of the expression. 20 | ExpressionType Type { get; } 21 | 22 | /// Creates a string representation of the expression in the provided context. 23 | /// The context in which this expression is printed. 24 | /// A string representation of the expression. 25 | string ToString(IMathContext context); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /StringMath/Expressions/UnaryExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StringMath.Expressions 4 | { 5 | /// An unary expression. 6 | internal sealed class UnaryExpression : IExpression 7 | { 8 | /// Initializes a new instance of an unary expression. 9 | /// The unary operator's name. 10 | /// The operand expression. 11 | public UnaryExpression(string operatorName, IExpression operand) 12 | { 13 | OperatorName = operatorName; 14 | Operand = operand; 15 | } 16 | 17 | /// The unary operator's name. 18 | public string OperatorName { get; } 19 | 20 | /// The operand expression. 21 | public IExpression Operand { get; } 22 | 23 | /// 24 | public ExpressionType Type => ExpressionType.UnaryExpression; 25 | 26 | /// 27 | public override string ToString() 28 | => ToString(MathContext.Default); 29 | 30 | public string ToString(IMathContext context) 31 | => OperatorName.Length > 2 || Operand is BinaryExpression || Operand is UnaryExpression 32 | ? $"{OperatorName}({Operand.ToString(context)})" 33 | : string.Equals(OperatorName, "!", StringComparison.Ordinal) 34 | ? $"{Operand.ToString(context)}{OperatorName}" 35 | : $"{OperatorName}{Operand.ToString(context)}"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /StringMath/Expressions/VariableExpression.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath.Expressions 2 | { 3 | /// A variable expression. 4 | internal sealed class VariableExpression : IExpression 5 | { 6 | /// Initializes a new instance of a variable expression. 7 | /// The variable name. 8 | public VariableExpression(string name) 9 | => Name = name; 10 | 11 | /// The variable name. 12 | public string Name { get; } 13 | 14 | /// 15 | public ExpressionType Type => ExpressionType.VariableExpression; 16 | 17 | /// 18 | public override string ToString() 19 | => ToString(MathContext.Default); 20 | 21 | public string ToString(IMathContext context) 22 | => $"{{{Name}}}"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /StringMath/Extensions.cs: -------------------------------------------------------------------------------- 1 | using StringMath.Expressions; 2 | using System; 3 | 4 | namespace StringMath 5 | { 6 | /// Helpful extension methods. 7 | internal static class Extensions 8 | { 9 | /// Throws an if is null. 10 | /// The parameter's type. 11 | /// The parameter's value. 12 | /// The parameter's name. 13 | public static void EnsureNotNull(this T value, string name) where T : class 14 | { 15 | if (value == default) 16 | { 17 | throw new ArgumentNullException(name); 18 | } 19 | } 20 | 21 | /// Converts a token type to a readable string. 22 | /// The token type. 23 | /// A readable string. 24 | public static string ToReadableString(this TokenType tokenType) 25 | { 26 | return tokenType switch 27 | { 28 | TokenType.Identifier => "identifier", 29 | TokenType.Number => "number", 30 | TokenType.Operator => "operator", 31 | TokenType.EndOfCode => "[EOC]", 32 | TokenType.OpenParen => "(", 33 | TokenType.CloseParen => ")", 34 | TokenType.Exclamation => "!", 35 | _ => tokenType.ToString(), 36 | }; 37 | } 38 | 39 | public static IExpression Parse(this string text, IMathContext context) 40 | { 41 | text.EnsureNotNull(nameof(text)); 42 | 43 | Tokenizer tokenizer = new Tokenizer(text); 44 | Parser parser = new Parser(tokenizer, context); 45 | return parser.Parse(); 46 | } 47 | } 48 | 49 | /// Extensions for . 50 | public static class MathExprExtensions 51 | { 52 | /// Converts a string expression to a . 53 | /// The string to convert. 54 | /// A . 55 | public static MathExpr ToMathExpr(this string expr) => (MathExpr)expr; 56 | 57 | /// Converts a string expression to a . 58 | /// The string to convert. 59 | /// The context to use for the resulting expression. 60 | /// A . 61 | public static MathExpr ToMathExpr(this string expr, IMathContext context) => new MathExpr(expr, context); 62 | 63 | /// Evaluates a math expression from a string. 64 | /// The math expression. 65 | /// The result as a double. 66 | public static double Eval(this string value) => value.ToMathExpr().Result; 67 | 68 | /// Evaluates a math expression from a string in a given context. 69 | /// The math expression. 70 | /// The context used to evaluate the expression. 71 | /// The result as a double. 72 | public static double Eval(this string value, IMathContext context) => value.ToMathExpr(context).Result; 73 | 74 | /// Converts a string to a and substitutes the given variable. 75 | /// The math expression. 76 | /// The variable's name. 77 | /// The variable's value. 78 | /// A . 79 | public static MathExpr Substitute(this string expr, string var, double val) => expr.ToMathExpr().Substitute(var, val); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /StringMath/IMathContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StringMath 4 | { 5 | /// Contract for math context. 6 | public interface IMathContext 7 | { 8 | /// The parent math context to inherit operators from. 9 | IMathContext? Parent { get; } 10 | 11 | /// Registers a binary operator implementation. 12 | /// The name of the operator. 13 | /// The implementation of the operator. 14 | /// The precedence of the operator. 15 | void RegisterBinary(string operatorName, Func operation, Precedence? precedence = default); 16 | 17 | /// Registers an unary operator implementation. Precedence is . 18 | /// The name of the operator. 19 | /// The implementation of the operator. 20 | void RegisterUnary(string operatorName, Func operation); 21 | 22 | /// Evaluates a binary operation. 23 | /// The operator. 24 | /// Left value. 25 | /// Right value. 26 | /// The result. 27 | double EvaluateBinary(string op, double a, double b); 28 | 29 | /// Evaluates an unary operation. 30 | /// The operator. 31 | /// The value. 32 | /// The result. 33 | double EvaluateUnary(string op, double a); 34 | 35 | /// Returns the precedence of a binary operator. Unary operators have precedence. 36 | /// The operator. 37 | /// A value. 38 | Precedence GetBinaryPrecedence(string operatorName); 39 | 40 | /// Tells whether an operator is binary. 41 | /// The operator. 42 | /// True if the operator is binary, false if it does not exist or it is unary. 43 | bool IsBinary(string operatorName); 44 | 45 | /// Tells whether an operator is unary. 46 | /// The operator. 47 | /// True if the operator is unary, false if it does not exist or it is binary. 48 | bool IsUnary(string operatorName); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /StringMath/MathContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace StringMath 5 | { 6 | /// 7 | /// Inherits operators from . 8 | public sealed class MathContext : IMathContext 9 | { 10 | private readonly Dictionary> _binaryEvaluators = new Dictionary>(StringComparer.Ordinal); 11 | private readonly Dictionary> _unaryEvaluators = new Dictionary>(StringComparer.Ordinal); 12 | private readonly Dictionary _binaryPrecedence = new Dictionary(StringComparer.Ordinal); 13 | 14 | /// The global instance used by methods. 15 | public static readonly IMathContext Default = new MathContext(); 16 | 17 | /// The parent context to inherit from. 18 | public IMathContext? Parent { get; } 19 | 20 | static MathContext() 21 | { 22 | var rad = Math.PI / 180; 23 | var deg = 180 / Math.PI; 24 | 25 | Default.RegisterBinary("+", (a, b) => a + b, Precedence.Addition); 26 | Default.RegisterBinary("-", (a, b) => a - b, Precedence.Addition); 27 | Default.RegisterBinary("*", (a, b) => a * b, Precedence.Multiplication); 28 | Default.RegisterBinary("/", (a, b) => a / b, Precedence.Multiplication); 29 | Default.RegisterBinary("%", (a, b) => a % b, Precedence.Multiplication); 30 | Default.RegisterBinary("^", Math.Pow, Precedence.Power); 31 | Default.RegisterBinary("log", Math.Log, Precedence.Logarithmic); 32 | Default.RegisterBinary("max", Math.Max, Precedence.UserDefined); 33 | Default.RegisterBinary("min", Math.Min, Precedence.UserDefined); 34 | 35 | Default.RegisterUnary("-", a => -a); 36 | Default.RegisterUnary("!", ComputeFactorial); 37 | Default.RegisterUnary("sqrt", Math.Sqrt); 38 | Default.RegisterUnary("sin", Math.Sin); 39 | Default.RegisterUnary("asin", Math.Asin); 40 | Default.RegisterUnary("cos", Math.Cos); 41 | Default.RegisterUnary("acos", Math.Acos); 42 | Default.RegisterUnary("tan", Math.Tan); 43 | Default.RegisterUnary("atan", Math.Atan); 44 | Default.RegisterUnary("ceil", Math.Ceiling); 45 | Default.RegisterUnary("floor", Math.Floor); 46 | Default.RegisterUnary("round", Math.Round); 47 | Default.RegisterUnary("exp", Math.Exp); 48 | Default.RegisterUnary("abs", Math.Abs); 49 | Default.RegisterUnary("rad", a => rad * a); 50 | Default.RegisterUnary("deg", a => deg * a); 51 | } 52 | 53 | /// Creates a new instance of a MathContext. 54 | /// The parent context to inherit operators from. 55 | public MathContext(IMathContext parent) 56 | => Parent = parent; 57 | 58 | /// Creates a new instance of a MathContext. 59 | public MathContext() { } 60 | 61 | /// 62 | public bool IsUnary(string operatorName) 63 | => _unaryEvaluators.ContainsKey(operatorName) || (Parent?.IsUnary(operatorName) ?? false); 64 | 65 | /// 66 | public bool IsBinary(string operatorName) 67 | => _binaryEvaluators.ContainsKey(operatorName) || (Parent?.IsBinary(operatorName) ?? false); 68 | 69 | /// 70 | public Precedence GetBinaryPrecedence(string operatorName) 71 | { 72 | return _binaryPrecedence.TryGetValue(operatorName, out var value) 73 | ? value 74 | : Parent?.GetBinaryPrecedence(operatorName) 75 | ?? throw MathException.MissingBinaryOperator(operatorName); 76 | } 77 | 78 | /// 79 | public void RegisterBinary(string operatorName, Func operation, Precedence? precedence = default) 80 | { 81 | operatorName.EnsureNotNull(nameof(operatorName)); 82 | operation.EnsureNotNull(nameof(operation)); 83 | 84 | _binaryEvaluators[operatorName] = operation; 85 | _binaryPrecedence[operatorName] = precedence ?? Precedence.UserDefined; 86 | } 87 | 88 | /// 89 | public void RegisterUnary(string operatorName, Func operation) 90 | { 91 | operatorName.EnsureNotNull(nameof(operatorName)); 92 | operation.EnsureNotNull(nameof(operation)); 93 | 94 | _unaryEvaluators[operatorName] = operation; 95 | } 96 | 97 | /// 98 | public double EvaluateBinary(string op, double a, double b) 99 | { 100 | double result = _binaryEvaluators.TryGetValue(op, out var value) 101 | ? value(a, b) 102 | : Parent?.EvaluateBinary(op, a, b) 103 | ?? throw MathException.MissingBinaryOperator(op); 104 | 105 | return result; 106 | } 107 | 108 | /// 109 | public double EvaluateUnary(string op, double a) 110 | { 111 | double result = _unaryEvaluators.TryGetValue(op, out var value) 112 | ? value(a) 113 | : Parent?.EvaluateUnary(op, a) 114 | ?? throw MathException.MissingUnaryOperator(op); 115 | 116 | return result; 117 | } 118 | 119 | #region Factorial 120 | 121 | private static readonly Dictionary _factorials = new Dictionary 122 | { 123 | [0] = 1d, 124 | [1] = 1d, 125 | [2] = 2d, 126 | [3] = 6d, 127 | [4] = 24d, 128 | [5] = 120d, 129 | [6] = 720d, 130 | [7] = 5040d, 131 | [8] = 40320d, 132 | [9] = 362880d, 133 | [10] = 3628800d, 134 | [11] = 39916800d, 135 | [12] = 479001600d, 136 | [13] = 6227020800d, 137 | [14] = 87178291200d, 138 | [15] = 1307674368000d, 139 | [16] = 20922789888000d, 140 | [17] = 355687428096000d, 141 | [18] = 6402373705728000d, 142 | [19] = 1.21645100408832E+17d, 143 | [20] = 2.43290200817664E+18d, 144 | [21] = 5.109094217170944E+19d, 145 | [22] = 1.1240007277776077E+21d, 146 | [23] = 2.585201673888498E+22d, 147 | [24] = 6.204484017332394E+23d, 148 | [25] = 1.5511210043330986E+25d, 149 | [26] = 4.0329146112660565E+26d, 150 | [27] = 1.0888869450418352E+28d, 151 | [28] = 3.0488834461171384E+29d, 152 | [29] = 8.841761993739701E+30d, 153 | [30] = 2.6525285981219103E+32d, 154 | [31] = 8.222838654177922E+33d, 155 | [32] = 2.631308369336935E+35d, 156 | [33] = 8.683317618811886E+36d, 157 | [34] = 2.9523279903960412E+38d, 158 | [35] = 1.0333147966386144E+40d, 159 | [36] = 3.719933267899012E+41d, 160 | [37] = 1.3763753091226343E+43d, 161 | [38] = 5.23022617466601E+44d, 162 | [39] = 2.0397882081197442E+46d, 163 | [40] = 8.159152832478977E+47d, 164 | [41] = 3.3452526613163803E+49d, 165 | [42] = 1.4050061177528798E+51d, 166 | [43] = 6.041526306337383E+52d, 167 | [44] = 2.6582715747884485E+54d, 168 | [45] = 1.1962222086548019E+56d, 169 | [46] = 5.5026221598120885E+57d, 170 | [47] = 2.5862324151116818E+59d, 171 | [48] = 1.2413915592536073E+61d, 172 | [49] = 6.082818640342675E+62d, 173 | [50] = 3.0414093201713376E+64d, 174 | [51] = 1.5511187532873822E+66d, 175 | [52] = 8.065817517094388E+67d, 176 | [53] = 4.2748832840600255E+69d, 177 | [54] = 2.308436973392414E+71d, 178 | [55] = 1.2696403353658276E+73d, 179 | [56] = 7.109985878048635E+74d, 180 | [57] = 4.052691950487722E+76d, 181 | [58] = 2.350561331282879E+78d, 182 | [59] = 1.3868311854568986E+80d, 183 | [60] = 8.320987112741392E+81d, 184 | [61] = 5.075802138772248E+83d, 185 | [62] = 3.146997326038794E+85d, 186 | [63] = 1.98260831540444E+87d, 187 | [64] = 1.2688693218588417E+89d, 188 | [65] = 8.247650592082472E+90d, 189 | [66] = 5.443449390774431E+92d, 190 | [67] = 3.647111091818868E+94d, 191 | [68] = 2.4800355424368305E+96d, 192 | [69] = 1.711224524281413E+98d, 193 | [70] = 1.197857166996989E+100d, 194 | [71] = 8.504785885678622E+101d, 195 | [72] = 6.123445837688608E+103d, 196 | [73] = 4.4701154615126834E+105d, 197 | [74] = 3.3078854415193856E+107d, 198 | [75] = 2.480914081139539E+109d, 199 | [76] = 1.8854947016660498E+111d, 200 | [77] = 1.4518309202828584E+113d, 201 | [78] = 1.1324281178206295E+115d, 202 | [79] = 8.946182130782973E+116d, 203 | [80] = 7.156945704626378E+118d, 204 | [81] = 5.797126020747366E+120d, 205 | [82] = 4.75364333701284E+122d, 206 | [83] = 3.945523969720657E+124d, 207 | [84] = 3.314240134565352E+126d, 208 | [85] = 2.8171041143805494E+128d, 209 | [86] = 2.4227095383672724E+130d, 210 | [87] = 2.107757298379527E+132d, 211 | [88] = 1.8548264225739836E+134d, 212 | [89] = 1.6507955160908452E+136d, 213 | [90] = 1.4857159644817607E+138d, 214 | [91] = 1.3520015276784023E+140d, 215 | [92] = 1.24384140546413E+142d, 216 | [93] = 1.1567725070816409E+144d, 217 | [94] = 1.0873661566567424E+146d, 218 | [95] = 1.0329978488239052E+148d, 219 | [96] = 9.916779348709491E+149d, 220 | [97] = 9.619275968248206E+151d, 221 | [98] = 9.426890448883242E+153d, 222 | [99] = 9.33262154439441E+155d, 223 | [100] = 9.33262154439441E+157d, 224 | [101] = 9.425947759838354E+159d, 225 | [102] = 9.614466715035121E+161d, 226 | [103] = 9.902900716486175E+163d, 227 | [104] = 1.0299016745145622E+166d, 228 | [105] = 1.0813967582402903E+168d, 229 | [106] = 1.1462805637347078E+170d, 230 | [107] = 1.2265202031961373E+172d, 231 | [108] = 1.3246418194518284E+174d, 232 | [109] = 1.4438595832024928E+176d, 233 | [110] = 1.5882455415227421E+178d, 234 | [111] = 1.7629525510902437E+180d, 235 | [112] = 1.9745068572210728E+182d, 236 | [113] = 2.2311927486598123E+184d, 237 | [114] = 2.543559733472186E+186d, 238 | [115] = 2.925093693493014E+188d, 239 | [116] = 3.3931086844518965E+190d, 240 | [117] = 3.969937160808719E+192d, 241 | [118] = 4.6845258497542883E+194d, 242 | [119] = 5.574585761207603E+196d, 243 | [120] = 6.689502913449124E+198d, 244 | [121] = 8.09429852527344E+200d, 245 | [122] = 9.875044200833598E+202d, 246 | [123] = 1.2146304367025325E+205d, 247 | [124] = 1.5061417415111404E+207d, 248 | [125] = 1.8826771768889254E+209d, 249 | [126] = 2.372173242880046E+211d, 250 | [127] = 3.012660018457658E+213d, 251 | [128] = 3.8562048236258025E+215d, 252 | [129] = 4.9745042224772855E+217d, 253 | [130] = 6.466855489220472E+219d, 254 | [131] = 8.471580690878817E+221d, 255 | [132] = 1.118248651196004E+224d, 256 | [133] = 1.4872707060906852E+226d, 257 | [134] = 1.992942746161518E+228d, 258 | [135] = 2.6904727073180495E+230d, 259 | [136] = 3.659042881952547E+232d, 260 | [137] = 5.01288874827499E+234d, 261 | [138] = 6.917786472619486E+236d, 262 | [139] = 9.615723196941086E+238d, 263 | [140] = 1.346201247571752E+241d, 264 | [141] = 1.89814375907617E+243d, 265 | [142] = 2.6953641378881614E+245d, 266 | [143] = 3.8543707171800706E+247d, 267 | [144] = 5.550293832739301E+249d, 268 | [145] = 8.047926057471987E+251d, 269 | [146] = 1.17499720439091E+254d, 270 | [147] = 1.7272458904546376E+256d, 271 | [148] = 2.5563239178728637E+258d, 272 | [149] = 3.808922637630567E+260d, 273 | [150] = 5.7133839564458505E+262d, 274 | [151] = 8.627209774233235E+264d, 275 | [152] = 1.3113358856834518E+267d, 276 | [153] = 2.006343905095681E+269d, 277 | [154] = 3.089769613847349E+271d, 278 | [155] = 4.789142901463391E+273d, 279 | [156] = 7.47106292628289E+275d, 280 | [157] = 1.1729568794264138E+278d, 281 | [158] = 1.8532718694937338E+280d, 282 | [159] = 2.946702272495037E+282d, 283 | [160] = 4.714723635992059E+284d, 284 | [161] = 7.590705053947215E+286d, 285 | [162] = 1.2296942187394488E+289d, 286 | [163] = 2.0044015765453015E+291d, 287 | [164] = 3.2872185855342945E+293d, 288 | [165] = 5.423910666131586E+295d, 289 | [166] = 9.003691705778433E+297d, 290 | [167] = 1.5036165148649983E+300d, 291 | [168] = 2.526075744973197E+302d, 292 | [169] = 4.2690680090047027E+304d, 293 | [170] = 7.257415615307994E+306d, 294 | }; 295 | 296 | private static double ComputeFactorial(double value) 297 | { 298 | if (value > 170) 299 | { 300 | throw new InvalidOperationException("Result is too big."); 301 | } 302 | 303 | if (value < 0) 304 | { 305 | throw new ArgumentException("Value cannot be negative.", nameof(value)); 306 | } 307 | 308 | return _factorials[value]; 309 | } 310 | 311 | #endregion 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /StringMath/MathException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using StringMath.Expressions; 3 | 4 | namespace StringMath 5 | { 6 | /// Base library exception. 7 | public sealed class MathException : Exception 8 | { 9 | /// Available error codes. 10 | public enum ErrorCode 11 | { 12 | /// Unexpected token. 13 | UNEXPECTED_TOKEN = 0, 14 | /// Unassigned variable. 15 | UNASSIGNED_VARIABLE = 1, 16 | /// Missing operator. 17 | UNDEFINED_OPERATOR = 2, 18 | /// Missing variable. 19 | UNEXISTING_VARIABLE = 4, 20 | /// Readonly variable. 21 | READONLY_VARIABLE = 8, 22 | } 23 | 24 | /// Initializes a new instance of a . 25 | /// The error code. 26 | /// The message to describe the error. 27 | private MathException(ErrorCode errorCode, string message) : base(message) 28 | { 29 | Code = errorCode; 30 | } 31 | 32 | /// The error code of the exception. 33 | public ErrorCode Code { get; } 34 | 35 | /// The position of the token where the exception was raised. 36 | public int Position { get; private set; } 37 | 38 | internal static MathException UnexpectedToken(Token token, string? expected = default) 39 | { 40 | string expectedMessage = expected != default ? $" Expected {expected}" : string.Empty; 41 | return new MathException(ErrorCode.UNEXPECTED_TOKEN, $"Unexpected token `{token.Text}` at position {token.Position}.{expectedMessage}") 42 | { 43 | Position = token.Position 44 | }; 45 | } 46 | 47 | internal static MathException UnexpectedToken(Token token, char expected) 48 | => UnexpectedToken(token, expected.ToString()); 49 | 50 | internal static MathException UnexpectedToken(Token token, TokenType tokenType) 51 | => UnexpectedToken(token, tokenType.ToReadableString()); 52 | 53 | internal static Exception UnassignedVariable(VariableExpression variableExpr) 54 | => new MathException(ErrorCode.UNASSIGNED_VARIABLE, $"Use of unassigned variable '{variableExpr.Name}'."); 55 | 56 | internal static Exception MissingBinaryOperator(string op) 57 | => new MathException(ErrorCode.UNDEFINED_OPERATOR, $"Undefined binary operator '{op}'."); 58 | 59 | internal static Exception MissingUnaryOperator(string op) 60 | => new MathException(ErrorCode.UNDEFINED_OPERATOR, $"Undefined unary operator '{op}'."); 61 | 62 | internal static Exception MissingVariable(string variable) 63 | => new MathException(ErrorCode.UNEXISTING_VARIABLE, $"Variable '{variable}' does not exist."); 64 | 65 | internal static Exception ReadonlyVariable(string name) 66 | => new MathException(ErrorCode.READONLY_VARIABLE, $"Variable '{name}' is read-only."); 67 | } 68 | } -------------------------------------------------------------------------------- /StringMath/MathExpr.cs: -------------------------------------------------------------------------------- 1 | using StringMath.Expressions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace StringMath 7 | { 8 | /// A mathematical expression. 9 | public class MathExpr 10 | { 11 | private readonly EvaluateExpression _evaluator; 12 | private readonly VariablesCollection _variables = new VariablesCollection(); 13 | private IReadOnlyCollection? _localVariables; 14 | private IReadOnlyCollection? _allVariables; 15 | private double? _cachedResult; 16 | 17 | internal IExpression Expression { get; } 18 | 19 | /// The in which this expression is evaluated. 20 | public IMathContext Context { get; } 21 | 22 | /// A collection of variable names excluding globals from . 23 | public IReadOnlyCollection LocalVariables => _localVariables ??= Variables.Where(x => !VariablesCollection.Default.Contains(x)).ToList(); 24 | 25 | /// A collection of variable names including globals extracted from the . 26 | public IReadOnlyCollection Variables 27 | { 28 | get 29 | { 30 | if (_allVariables == null) 31 | { 32 | var extractor = new ExtractVariables(); 33 | extractor.Visit(Expression); 34 | _allVariables = extractor.Variables; 35 | } 36 | 37 | return _allVariables; 38 | } 39 | } 40 | 41 | /// Constructs a from a string. 42 | /// The math expression. 43 | public MathExpr(string text) : this(text, new MathContext(MathContext.Default)) 44 | { 45 | } 46 | 47 | /// Constructs a from a string. 48 | /// The math expression. 49 | /// The in which this expression is evaluated. 50 | public MathExpr(string text, IMathContext context) : this(text.Parse(context), context) 51 | { 52 | } 53 | 54 | /// Constructs a from an expression tree. 55 | /// The expression tree. 56 | /// The in which this expression is evaluated. 57 | internal MathExpr(IExpression expression, IMathContext context) 58 | { 59 | expression.EnsureNotNull(nameof(expression)); 60 | context.EnsureNotNull(nameof(context)); 61 | 62 | Context = context; 63 | 64 | Expression = expression; 65 | _evaluator = new EvaluateExpression(Context, _variables); 66 | } 67 | 68 | /// The result of the expression. 69 | /// The variables used in the expression must be set before getting the result. 70 | public double Result 71 | { 72 | get 73 | { 74 | if (!_cachedResult.HasValue) 75 | { 76 | var result = (ConstantExpression)_evaluator.Visit(Expression); 77 | _cachedResult = result.Value; 78 | } 79 | 80 | return _cachedResult.Value; 81 | } 82 | } 83 | 84 | /// Creates a string representation of the current expression. 85 | public string Text => Expression.ToString(Context); 86 | 87 | /// Substitutes the variable with the given value. 88 | /// The name of the variable. 89 | /// The new value. 90 | public MathExpr Substitute(string name, double value) 91 | { 92 | if (VariablesCollection.Default.TryGetValue(name, out _)) 93 | { 94 | throw MathException.ReadonlyVariable(name); 95 | } 96 | 97 | if (LocalVariables.Contains(name)) 98 | { 99 | _cachedResult = null; 100 | _variables[name] = value; 101 | } 102 | else 103 | { 104 | throw MathException.MissingVariable(name); 105 | } 106 | 107 | return this; 108 | } 109 | 110 | /// Compiles a into a delegate. 111 | /// A type safe delegate. 112 | public Func Compile() 113 | { 114 | var exp = new CompileExpression(_variables).Compile>(Expression); 115 | return () => exp(Context); 116 | } 117 | 118 | /// Compiles a into a delegate. 119 | /// A type safe delegate. 120 | public Func Compile(string var) 121 | { 122 | var exp = new CompileExpression(_variables).Compile>(Expression, var); 123 | return (double x) => exp(Context, x); 124 | } 125 | 126 | /// Compiles a into a delegate. 127 | /// A type safe delegate. 128 | public Func Compile(string var1, string var2) 129 | { 130 | var exp = new CompileExpression(_variables).Compile>(Expression, var1, var2); 131 | return (x, y) => exp(Context, x, y); 132 | } 133 | 134 | /// Compiles a into a delegate. 135 | /// A type safe delegate. 136 | public Func Compile(string var1, string var2, string var3) 137 | { 138 | var exp = new CompileExpression(_variables).Compile>(Expression, var1, var2, var3); 139 | return (x, y, z) => exp(Context, x, y, z); 140 | } 141 | 142 | /// Compiles a into a delegate. 143 | /// A type safe delegate. 144 | public Func Compile(string var1, string var2, string var3, string var4) 145 | { 146 | var exp = new CompileExpression(_variables).Compile>(Expression, var1, var2, var3, var4); 147 | return (x, y, z, w) => exp(Context, x, y, z, w); 148 | } 149 | 150 | /// Compiles a into a delegate. 151 | /// A type safe delegate. 152 | public Func Compile(string var1, string var2, string var3, string var4, string var5) 153 | { 154 | var exp = new CompileExpression(_variables).Compile>(Expression, var1, var2, var3, var4, var5); 155 | return (x, y, z, w, q) => exp(Context, x, y, z, w, q); 156 | } 157 | 158 | /// Converts a string to a . 159 | /// The value to convert. 160 | public static implicit operator MathExpr(string value) => new MathExpr(value); 161 | 162 | /// Evaluates a . 163 | /// 164 | public static implicit operator double(MathExpr expression) => expression.Result; 165 | 166 | /// 167 | public double this[string name] 168 | { 169 | set => Substitute(name, value); 170 | } 171 | 172 | /// Add a new binary operator or overwrite an existing operator implementation. 173 | /// The operator's string representation. 174 | /// The operation to execute for this operator. 175 | /// precedence by default. 176 | /// The current math expression. 177 | /// Operators are inherited from . 178 | public MathExpr SetOperator(string name, Func operation, Precedence? precedence = default) 179 | { 180 | _cachedResult = null; 181 | Context.RegisterBinary(name, operation, precedence); 182 | return this; 183 | } 184 | 185 | /// Add a new unary operator or overwrite an existing operator implementation. is always . 186 | /// The operator's string representation. 187 | /// The operation to execute for this operator. 188 | /// The current math expression. 189 | /// Operators are inherited from . 190 | public MathExpr SetOperator(string name, Func operation) 191 | { 192 | _cachedResult = null; 193 | Context.RegisterUnary(name, operation); 194 | return this; 195 | } 196 | 197 | /// Add a new binary operator or overwrite an existing operator implementation. 198 | /// The operator's string representation. 199 | /// The operation to execute for this operator. 200 | /// precedence by default. 201 | /// Operators will be available in all expressions. 202 | public static void AddOperator(string name, Func operation, Precedence? precedence = default) 203 | => MathContext.Default.RegisterBinary(name, operation, precedence); 204 | 205 | /// Add a new unary operator or overwrite an existing operator implementation. is always . 206 | /// The operator's string representation. 207 | /// The operation to execute for this operator. 208 | /// The current math expression. 209 | /// Operators will be available in all expressions. Operators are inherited from . 210 | public static void AddOperator(string name, Func operation) 211 | => MathContext.Default.RegisterUnary(name, operation); 212 | 213 | /// 214 | /// Variables will be available in all expressions. 215 | public static void AddVariable(string name, double value) 216 | => VariablesCollection.Default[name] = value; 217 | 218 | /// 219 | public override string ToString() => Text; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /StringMath/Parser/Parser.cs: -------------------------------------------------------------------------------- 1 | using StringMath.Expressions; 2 | 3 | namespace StringMath 4 | { 5 | /// A simple parser. 6 | internal sealed class Parser 7 | { 8 | private readonly Tokenizer _tokenzier; 9 | private readonly IMathContext _mathContext; 10 | private Token _currentToken; 11 | 12 | /// Initializes a new instance of a parser. 13 | /// The tokenizer. 14 | /// The math context. 15 | public Parser(Tokenizer tokenizer, IMathContext mathContext) 16 | { 17 | _tokenzier = tokenizer; 18 | _mathContext = mathContext; 19 | } 20 | 21 | /// 22 | public IExpression Parse() 23 | { 24 | _currentToken = _tokenzier.ReadToken(); 25 | IExpression result = ParseBinaryExpression(); 26 | Match(TokenType.EndOfCode); 27 | return result; 28 | } 29 | 30 | private IExpression ParseBinaryExpression(IExpression? left = default, Precedence? parentPrecedence = default) 31 | { 32 | if (left == default) 33 | { 34 | if (_mathContext.IsUnary(_currentToken.Text)) 35 | { 36 | Token operatorToken = Take(); 37 | left = new UnaryExpression(operatorToken.Text, ParseBinaryExpression(left, Precedence.Prefix)); 38 | } 39 | else 40 | { 41 | left = ParsePrimaryExpression(); 42 | 43 | if (_currentToken.Type == TokenType.Exclamation) 44 | { 45 | Token operatorToken = Take(); 46 | left = new UnaryExpression(operatorToken.Text, left); 47 | } 48 | } 49 | } 50 | 51 | while (!IsEndOfStatement()) 52 | { 53 | if (_mathContext.IsBinary(_currentToken.Text)) 54 | { 55 | Precedence precedence = _mathContext.GetBinaryPrecedence(_currentToken.Text); 56 | if (parentPrecedence >= precedence) 57 | { 58 | return left; 59 | } 60 | 61 | Token operatorToken = Take(); 62 | left = new BinaryExpression(left, operatorToken.Text, ParseBinaryExpression(parentPrecedence: precedence)); 63 | } 64 | else 65 | { 66 | return left; 67 | } 68 | } 69 | 70 | return left; 71 | } 72 | 73 | private IExpression ParsePrimaryExpression() 74 | { 75 | switch (_currentToken.Type) 76 | { 77 | case TokenType.Number: 78 | return new ConstantExpression(Take().Text); 79 | 80 | case TokenType.Identifier: 81 | return new VariableExpression(Take().Text); 82 | 83 | case TokenType.OpenParen: 84 | return ParseGroupingExpression(); 85 | 86 | default: 87 | throw MathException.UnexpectedToken(_currentToken); 88 | } 89 | } 90 | 91 | private IExpression ParseGroupingExpression() 92 | { 93 | Take(); 94 | 95 | IExpression expr = ParseBinaryExpression(); 96 | Match(TokenType.CloseParen); 97 | 98 | return expr; 99 | } 100 | 101 | private Token Match(TokenType tokenType) 102 | { 103 | return _currentToken.Type == tokenType 104 | ? Take() 105 | : throw MathException.UnexpectedToken(_currentToken, tokenType); 106 | } 107 | 108 | private Token Take() 109 | { 110 | Token previous = _currentToken; 111 | _currentToken = _tokenzier.ReadToken(); 112 | return previous; 113 | } 114 | 115 | private bool IsEndOfStatement() 116 | { 117 | return _currentToken.Type == TokenType.EndOfCode; 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /StringMath/Parser/SourceText.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace StringMath 5 | { 6 | internal sealed class SourceText : IEnumerator 7 | { 8 | public string Text { get; } 9 | public int Position { get; private set; } 10 | public char Current => Text[Position]; 11 | object IEnumerator.Current => Current; 12 | 13 | // The string terminator is used by the tokenizer to produce EndOfCode tokens 14 | public SourceText(string source) 15 | => Text = $"{source}\0"; 16 | 17 | public bool MoveNext() 18 | { 19 | if (Position + 1 < Text.Length) 20 | { 21 | Position++; 22 | return true; 23 | } 24 | 25 | return false; 26 | } 27 | 28 | public char Peek(int count = 1) 29 | { 30 | int location = Position + count; 31 | 32 | char result = location < Text.Length && location >= 0 ? Text[location] : '\0'; 33 | return result; 34 | } 35 | 36 | public void Reset() 37 | { 38 | Position = 0; 39 | } 40 | 41 | public void Dispose() 42 | { 43 | Reset(); 44 | } 45 | 46 | public override string ToString() 47 | { 48 | return $"{Current} :{Position}"; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /StringMath/Parser/Token.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath 2 | { 3 | /// A token containing basic information about some text. 4 | internal readonly struct Token 5 | { 6 | /// Initializes a new instance of a token. 7 | /// The token type. 8 | /// The token value. 9 | /// The token's position in the input string. 10 | public Token(TokenType type, string text, int position) 11 | { 12 | Type = type; 13 | Text = text; 14 | Position = position; 15 | } 16 | 17 | /// The token's position in the input string. 18 | public readonly int Position; 19 | 20 | /// The token value. 21 | public readonly string Text; 22 | 23 | /// The token type. 24 | public readonly TokenType Type; 25 | 26 | /// 27 | public override string ToString() 28 | { 29 | return $"{Text} ({Type}):{Position}"; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /StringMath/Parser/TokenType.cs: -------------------------------------------------------------------------------- 1 | namespace StringMath 2 | { 3 | /// Available token types. 4 | internal enum TokenType 5 | { 6 | /// Unknown token. 7 | Unknown, 8 | 9 | /// \0 10 | EndOfCode, 11 | 12 | /// [aA-zZ_]+[aA-zZ0-9_] 13 | Identifier, 14 | /// 1 or .1 or 1.1 15 | Number, 16 | 17 | /// ( 18 | OpenParen, 19 | /// ) 20 | CloseParen, 21 | 22 | /// Everything excluding ( ) { } ! . 0 1 2 3 4 5 6 7 8 9 \0 23 | Operator, 24 | 25 | /// ! 26 | Exclamation 27 | } 28 | } -------------------------------------------------------------------------------- /StringMath/Parser/Tokenizer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | 4 | namespace StringMath 5 | { 6 | /// 7 | internal sealed partial class Tokenizer 8 | { 9 | private readonly SourceText _text; 10 | 11 | // Excluded characters for custom operators 12 | private static readonly HashSet _invalidOperatorCharacters = new HashSet 13 | { 14 | '(', ')', '{', '}', '!', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '\0' 15 | }; 16 | 17 | /// Creates a new instance of the tokenizer. 18 | /// The text to tokenize. 19 | public Tokenizer(SourceText text) 20 | { 21 | _text = text; 22 | } 23 | 24 | /// Creates a new instance of the tokenizer. 25 | /// The text to tokenize. 26 | public Tokenizer(string text) : this(new SourceText(text)) 27 | { 28 | } 29 | 30 | /// 31 | public Token ReadToken() 32 | { 33 | switch (_text.Current) 34 | { 35 | case '.': 36 | case '0': 37 | case '1': 38 | case '2': 39 | case '3': 40 | case '4': 41 | case '5': 42 | case '6': 43 | case '7': 44 | case '8': 45 | case '9': 46 | return new Token(TokenType.Number, ReadNumber(_text), _text.Position); 47 | 48 | case '(': 49 | _text.MoveNext(); 50 | return new Token(TokenType.OpenParen, "(", _text.Position); 51 | 52 | case ')': 53 | _text.MoveNext(); 54 | return new Token(TokenType.CloseParen, ")", _text.Position); 55 | 56 | case '{': 57 | return new Token(TokenType.Identifier, ReadIdentifier(_text), _text.Position); 58 | 59 | case '!': 60 | _text.MoveNext(); 61 | return new Token(TokenType.Exclamation, "!", _text.Position); 62 | 63 | case ' ': 64 | case '\t': 65 | ReadWhiteSpace(_text); 66 | return ReadToken(); 67 | 68 | case '\r': 69 | case '\n': 70 | _text.MoveNext(); 71 | return ReadToken(); 72 | 73 | case '\0': 74 | return new Token(TokenType.EndOfCode, "\0", _text.Position); 75 | 76 | default: 77 | string op = ReadOperator(_text); 78 | return new Token(TokenType.Operator, op, _text.Position); 79 | } 80 | } 81 | 82 | /// 83 | public override string? ToString() 84 | { 85 | return _text.ToString(); 86 | } 87 | 88 | private string ReadIdentifier(SourceText stream) 89 | { 90 | const char identifierTerminator = '}'; 91 | 92 | StringBuilder builder = new StringBuilder(12); 93 | stream.MoveNext(); 94 | 95 | if (char.IsLetter(stream.Current) || stream.Current == '_') 96 | { 97 | builder.Append(stream.Current); 98 | stream.MoveNext(); 99 | } 100 | else 101 | { 102 | Token token = new Token(TokenType.Unknown, stream.Current.ToString(), stream.Position); 103 | throw MathException.UnexpectedToken(token, TokenType.Identifier); 104 | } 105 | 106 | while (stream.Current != identifierTerminator) 107 | { 108 | if (char.IsLetterOrDigit(stream.Current) || stream.Current == '_') 109 | { 110 | builder.Append(stream.Current); 111 | stream.MoveNext(); 112 | } 113 | else 114 | { 115 | Token token = new Token(TokenType.Unknown, stream.Current.ToString(), stream.Position); 116 | throw MathException.UnexpectedToken(token, identifierTerminator); 117 | } 118 | } 119 | 120 | stream.MoveNext(); 121 | string text = builder.ToString(); 122 | 123 | if (text.Length == 0) 124 | { 125 | Token token = new Token(TokenType.Unknown, identifierTerminator.ToString(), stream.Position - 1); 126 | throw MathException.UnexpectedToken(token, identifierTerminator); 127 | } 128 | 129 | return text; 130 | } 131 | 132 | private string ReadOperator(SourceText stream) 133 | { 134 | StringBuilder builder = new StringBuilder(3); 135 | 136 | while (!char.IsWhiteSpace(stream.Current) && !_invalidOperatorCharacters.Contains(stream.Current)) 137 | { 138 | builder.Append(stream.Current); 139 | stream.MoveNext(); 140 | } 141 | 142 | return builder.ToString(); 143 | } 144 | 145 | private string ReadNumber(SourceText stream) 146 | { 147 | StringBuilder builder = new StringBuilder(8); 148 | bool hasDot = false; 149 | 150 | while (true) 151 | { 152 | if (stream.Current == '.') 153 | { 154 | if (!hasDot) 155 | { 156 | hasDot = true; 157 | 158 | builder.Append(stream.Current); 159 | stream.MoveNext(); 160 | } 161 | else 162 | { 163 | Token token = new Token(TokenType.Unknown, stream.Current.ToString(), stream.Position); 164 | throw MathException.UnexpectedToken(token, TokenType.Number); 165 | } 166 | } 167 | else if (char.IsDigit(stream.Current)) 168 | { 169 | builder.Append(stream.Current); 170 | stream.MoveNext(); 171 | } 172 | else 173 | { 174 | break; 175 | } 176 | } 177 | 178 | char peeked = stream.Peek(-1); 179 | 180 | if (peeked == '.') 181 | { 182 | Token token = new Token(TokenType.Unknown, peeked.ToString(), stream.Position); 183 | throw MathException.UnexpectedToken(token, TokenType.Number); 184 | } 185 | 186 | return builder.ToString(); 187 | } 188 | 189 | private void ReadWhiteSpace(SourceText stream) 190 | { 191 | while (char.IsWhiteSpace(stream.Current) && stream.MoveNext()) { } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /StringMath/Precedence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StringMath 4 | { 5 | /// The operator precedence. 6 | public readonly struct Precedence : IEquatable 7 | { 8 | /// The lowest precedence value. 9 | public static readonly Precedence None = new Precedence(int.MinValue); 10 | 11 | /// Addition precedence (0). 12 | public static readonly Precedence Addition = new Precedence(0); 13 | 14 | /// Multiplication precedence (4). 15 | public static readonly Precedence Multiplication = new Precedence(4); 16 | 17 | /// Power precedence (8). 18 | public static readonly Precedence Power = new Precedence(8); 19 | 20 | /// Logarithmic precedence (16). 21 | public static readonly Precedence Logarithmic = new Precedence(16); 22 | 23 | /// User defined precedence (32). 24 | public static readonly Precedence UserDefined = new Precedence(32); 25 | 26 | /// The highest precedence value. 27 | public static readonly Precedence Prefix = new Precedence(int.MaxValue); 28 | 29 | private readonly int _value; 30 | 31 | private Precedence(int value) 32 | => _value = value; 33 | 34 | /// Gets the value of precedence. 35 | /// The precedence. 36 | public static implicit operator int(Precedence? precedence) 37 | => precedence?._value ?? None; 38 | 39 | /// Gets the precedence from a value. 40 | /// The value. 41 | public static implicit operator Precedence(int precedence) 42 | { 43 | return precedence switch 44 | { 45 | int.MinValue => None, 46 | 0 => Addition, 47 | 1 => Multiplication, 48 | 2 => Power, 49 | 3 => Logarithmic, 50 | 4 => UserDefined, 51 | 5 => Prefix, 52 | _ => new Precedence(precedence), 53 | }; 54 | } 55 | 56 | /// 57 | public bool Equals(Precedence other) 58 | => other._value == _value; 59 | 60 | /// 61 | public override bool Equals(object? obj) 62 | => obj is Precedence && Equals(obj); 63 | 64 | /// 65 | public override int GetHashCode() 66 | => _value.GetHashCode(); 67 | 68 | /// 69 | public static bool operator ==(Precedence left, Precedence right) 70 | => left.Equals(right); 71 | 72 | /// 73 | public static bool operator !=(Precedence left, Precedence right) 74 | => !(left == right); 75 | } 76 | } -------------------------------------------------------------------------------- /StringMath/StringMath.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8;net6;net5;netstandard2.1;netcoreapp3.1;net48;net472;net461 5 | true 6 | miroiu 7 | 8 | Calculates the value of a math expression from a string returning a double. 9 | Supports variables and user defined operators. 10 | Miroiu Emanuel 11 | MIT 12 | https://github.com/miroiu/string-math 13 | https://github.com/miroiu/string-math 14 | expression-evaluator calculator string-math math string-calculator user-defined-operators operators custom-operators 15 | 4.1.2 16 | Fixed compiled expressions not using variables that are not passed as arguments 17 | 18 | true 19 | enable 20 | true 21 | ../build/string-math.snk 22 | README.md 23 | git 24 | 25 | 26 | 27 | 8.0 28 | 29 | 30 | 31 | preview 32 | 33 | 34 | 35 | 36 | 37 | True 38 | \ 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /StringMath/VariablesCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace StringMath 6 | { 7 | /// A collection of variables. 8 | internal interface IVariablesCollection : IEnumerable 9 | { 10 | /// Overwrites the value of a variable. 11 | /// The variable's name. 12 | /// The new value. 13 | void SetValue(string name, double value); 14 | 15 | /// Gets the value of the variable. 16 | /// The variable's name. 17 | /// The value of the variable. 18 | /// true if the variable exists, false otherwise. 19 | bool TryGetValue(string name, out double value); 20 | 21 | /// Tells whether the variable is defined or not. 22 | /// The name of the variable. 23 | /// True if variable was previously defined. False otherwise. 24 | bool Contains(string name); 25 | 26 | /// 27 | double this[string name] { set; } 28 | } 29 | 30 | /// 31 | internal class VariablesCollection : IVariablesCollection 32 | { 33 | public static readonly VariablesCollection Default = new VariablesCollection(); 34 | 35 | private readonly Dictionary _values = new Dictionary(); 36 | 37 | static VariablesCollection() 38 | { 39 | Default["PI"] = Math.PI; 40 | Default["E"] = Math.E; 41 | } 42 | 43 | /// 44 | public void CopyTo(IVariablesCollection other) 45 | { 46 | other.EnsureNotNull(nameof(other)); 47 | 48 | foreach (var kvp in _values) 49 | { 50 | other.SetValue(kvp.Key, kvp.Value); 51 | } 52 | } 53 | 54 | /// 55 | public IEnumerator GetEnumerator() => _values.GetEnumerator(); 56 | 57 | /// 58 | public double this[string name] 59 | { 60 | set => SetValue(name, value); 61 | } 62 | 63 | /// 64 | public void SetValue(string name, double value) 65 | { 66 | name.EnsureNotNull(nameof(name)); 67 | _values[name] = value; 68 | } 69 | 70 | /// 71 | public bool TryGetValue(string name, out double value) 72 | => _values.TryGetValue(name, out value) || Default._values.TryGetValue(name, out value); 73 | 74 | IEnumerator IEnumerable.GetEnumerator() => _values.Keys.GetEnumerator(); 75 | 76 | /// 77 | public bool Contains(string name) => _values.ContainsKey(name); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /StringMath/Visitors/CompileExpression.cs: -------------------------------------------------------------------------------- 1 | using SM = StringMath.Expressions; 2 | using System; 3 | using System.Linq.Expressions; 4 | using System.Linq; 5 | using System.Collections.Generic; 6 | 7 | namespace StringMath 8 | { 9 | internal class CompileExpression 10 | { 11 | private static readonly ParameterExpression _contextParam = Expression.Parameter(typeof(IMathContext), "__internal_ctx"); 12 | private List _parameters = new List(); 13 | private readonly IVariablesCollection _variables; 14 | 15 | public CompileExpression(IVariablesCollection variables) 16 | { 17 | _variables = variables; 18 | } 19 | 20 | public T Compile(SM.IExpression expression, params string[] parameters) 21 | => VisitWithParameters(expression, parameters).Compile(); 22 | 23 | private Expression VisitWithParameters(SM.IExpression expression, params string[] parameters) 24 | { 25 | _parameters = new List(1 + parameters.Length) 26 | { 27 | _contextParam 28 | }; 29 | 30 | foreach (var parameter in parameters) 31 | { 32 | _parameters.Add(Expression.Parameter(typeof(double), parameter)); 33 | } 34 | 35 | var result = Visit(expression); 36 | return Expression.Lambda(result, _parameters); 37 | } 38 | 39 | public Expression Visit(SM.IExpression expression) 40 | { 41 | Expression result = expression switch 42 | { 43 | SM.BinaryExpression binaryExpr => VisitBinaryExpr(binaryExpr), 44 | SM.ConstantExpression constantExpr => VisitConstantExpr(constantExpr), 45 | SM.UnaryExpression unaryExpr => VisitUnaryExpr(unaryExpr), 46 | SM.VariableExpression variableExpr => VisitVariableExpr(variableExpr), 47 | _ => throw new NotImplementedException($"'{expression?.GetType().Name}' Convertor is not implemented.") 48 | }; 49 | 50 | return result; 51 | } 52 | 53 | private Expression VisitVariableExpr(SM.VariableExpression variableExpr) 54 | { 55 | var parameter = _parameters.FirstOrDefault(x => x.Name == variableExpr.Name); 56 | if (parameter != null) 57 | return parameter; 58 | 59 | if(_variables.TryGetValue(variableExpr.Name, out var variable)) 60 | return Expression.Constant(variable); 61 | 62 | throw MathException.MissingVariable(variableExpr.Name); 63 | } 64 | 65 | private Expression VisitConstantExpr(SM.ConstantExpression constantExpr) => Expression.Constant(constantExpr.Value); 66 | 67 | private Expression VisitBinaryExpr(SM.BinaryExpression binaryExpr) => 68 | Expression.Call(_contextParam, 69 | nameof(IMathContext.EvaluateBinary), 70 | null, 71 | Expression.Constant(binaryExpr.OperatorName), Visit(binaryExpr.Left), Visit(binaryExpr.Right)); 72 | 73 | private Expression VisitUnaryExpr(SM.UnaryExpression unaryExpr) => 74 | Expression.Call(_contextParam, 75 | nameof(IMathContext.EvaluateUnary), 76 | null, 77 | Expression.Constant(unaryExpr.OperatorName), Visit(unaryExpr.Operand)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /StringMath/Visitors/EvaluateExpression.cs: -------------------------------------------------------------------------------- 1 | using StringMath.Expressions; 2 | 3 | namespace StringMath 4 | { 5 | /// 6 | internal sealed class EvaluateExpression : ExpressionVisitor 7 | { 8 | private readonly IMathContext _context; 9 | private readonly IVariablesCollection _variables; 10 | 11 | /// Initializez a new instance of an expression evaluator. 12 | /// The math context. 13 | /// The variables collection. 14 | public EvaluateExpression(IMathContext context, IVariablesCollection variables) 15 | { 16 | context.EnsureNotNull(nameof(context)); 17 | variables.EnsureNotNull(nameof(variables)); 18 | 19 | _variables = variables; 20 | _context = context; 21 | } 22 | 23 | protected override IExpression VisitBinaryExpr(BinaryExpression binaryExpr) 24 | { 25 | ConstantExpression leftExpr = (ConstantExpression)Visit(binaryExpr.Left); 26 | ConstantExpression rightExpr = (ConstantExpression)Visit(binaryExpr.Right); 27 | 28 | double result = _context.EvaluateBinary(binaryExpr.OperatorName, leftExpr.Value, rightExpr.Value); 29 | return new ConstantExpression(result); 30 | } 31 | 32 | protected override IExpression VisitUnaryExpr(UnaryExpression unaryExpr) 33 | { 34 | ConstantExpression valueExpr = (ConstantExpression)Visit(unaryExpr.Operand); 35 | 36 | double result = _context.EvaluateUnary(unaryExpr.OperatorName, valueExpr.Value); 37 | return new ConstantExpression(result); 38 | } 39 | 40 | protected override IExpression VisitVariableExpr(VariableExpression variableExpr) 41 | { 42 | return _variables.TryGetValue(variableExpr.Name, out double value) 43 | ? new ConstantExpression(value) 44 | : throw MathException.UnassignedVariable(variableExpr); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /StringMath/Visitors/ExpressionVisitor.cs: -------------------------------------------------------------------------------- 1 | using StringMath.Expressions; 2 | 3 | namespace StringMath 4 | { 5 | /// Contract for expression visitors. 6 | internal abstract class ExpressionVisitor 7 | { 8 | /// Visits an expression tree and transforms it into another expression tree. 9 | /// The expression to transform. 10 | /// A new expression tree. 11 | public IExpression Visit(IExpression expression) 12 | { 13 | IExpression result = expression switch 14 | { 15 | BinaryExpression binaryExpr => VisitBinaryExpr(binaryExpr), 16 | ConstantExpression constantExpr => VisitConstantExpr(constantExpr), 17 | UnaryExpression unaryExpr => VisitUnaryExpr(unaryExpr), 18 | VariableExpression variableExpr => VisitVariableExpr(variableExpr), 19 | _ => expression 20 | }; 21 | 22 | return result; 23 | } 24 | 25 | protected virtual IExpression VisitVariableExpr(VariableExpression variableExpr) => variableExpr; 26 | 27 | protected virtual IExpression VisitConstantExpr(ConstantExpression constantExpr) => constantExpr; 28 | 29 | protected virtual IExpression VisitBinaryExpr(BinaryExpression binaryExpr) => binaryExpr; 30 | 31 | protected virtual IExpression VisitUnaryExpr(UnaryExpression unaryExpr) => unaryExpr; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /StringMath/Visitors/ExtractVariables.cs: -------------------------------------------------------------------------------- 1 | using StringMath.Expressions; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace StringMath 6 | { 7 | internal class ExtractVariables : ExpressionVisitor 8 | { 9 | private readonly HashSet _variables = new HashSet(StringComparer.Ordinal); 10 | 11 | public IReadOnlyCollection Variables => _variables; 12 | 13 | protected override IExpression VisitBinaryExpr(BinaryExpression binaryExpr) 14 | { 15 | Visit(binaryExpr.Left); 16 | Visit(binaryExpr.Right); 17 | 18 | return binaryExpr; 19 | } 20 | 21 | protected override IExpression VisitUnaryExpr(UnaryExpression unaryExpr) 22 | { 23 | Visit(unaryExpr.Operand); 24 | 25 | return unaryExpr; 26 | } 27 | 28 | protected override IExpression VisitVariableExpr(VariableExpression variableExpr) 29 | { 30 | _variables.Add(variableExpr.Name); 31 | 32 | return variableExpr; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /build/string-math.public.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miroiu/string-math/b93a28d3f2eefd56bf0dc48500982154fd1b372c/build/string-math.public.snk -------------------------------------------------------------------------------- /build/string-math.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miroiu/string-math/b93a28d3f2eefd56bf0dc48500982154fd1b372c/build/string-math.snk --------------------------------------------------------------------------------