From 026bd4c55b2bad4182091f1015bf71d6bd96a665 Mon Sep 17 00:00:00 2001 From: Bradley Myers Date: Thu, 29 Dec 2022 21:31:52 -0500 Subject: [PATCH] Check all symbols are unique & migrate to .NET 7 The former checks ensured no duplicate symbols within each category, but did not cross check them with other categories. --- .github/actions/build/action.yml | 2 +- .../Calculator.Examples.csproj | 4 +- Calculator.Examples/ConsoleIO.cs | 7 +- Calculator.Tests/Calculator.Tests.csproj | 2 +- Calculator.Tests/LexerTests.cs | 6 +- Calculator/Calculator.cs | 124 ++++++++++++------ Calculator/Calculator.csproj | 5 +- Calculator/Lexer.cs | 2 +- Calculator/Models/Constant.cs | 16 ++- Calculator/Models/Operator.cs | 20 ++- Calculator/Standardizer.cs | 47 +++---- Calculator/UniqueList.cs | 72 ---------- README.md | 5 +- 13 files changed, 158 insertions(+), 154 deletions(-) delete mode 100644 Calculator/UniqueList.cs diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 16c7971..165f5d6 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -13,7 +13,7 @@ runs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Restore dependencies run: dotnet restore diff --git a/Calculator.Examples/Calculator.Examples.csproj b/Calculator.Examples/Calculator.Examples.csproj index a763c94..33796d2 100644 --- a/Calculator.Examples/Calculator.Examples.csproj +++ b/Calculator.Examples/Calculator.Examples.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Examples Bradley Myers @@ -10,7 +10,7 @@ Copyright © 2021 Bradley Myers. All rights reserved. https://github.com/BLM16/Tokenized-Calculator git - 2.1.0 + 2.1.1 LICENSE calculator icon.png diff --git a/Calculator.Examples/ConsoleIO.cs b/Calculator.Examples/ConsoleIO.cs index 249dd09..d31f06f 100644 --- a/Calculator.Examples/ConsoleIO.cs +++ b/Calculator.Examples/ConsoleIO.cs @@ -2,9 +2,10 @@ using BLM16.Util.Calculator; using BLM16.Util.Calculator.Models; -var modulusOperator = new Operator('%', 20, (a, b) => a % b); - -var calculator = new Calculator(new[] { modulusOperator }); +var calculator = new Calculator +{ + Operators = new[] { DefaultOperators.Modulus } +}; while (true) { diff --git a/Calculator.Tests/Calculator.Tests.csproj b/Calculator.Tests/Calculator.Tests.csproj index c031c09..6e9a3d7 100644 --- a/Calculator.Tests/Calculator.Tests.csproj +++ b/Calculator.Tests/Calculator.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 false Bradley Myers diff --git a/Calculator.Tests/LexerTests.cs b/Calculator.Tests/LexerTests.cs index 0c07bf3..025fe85 100644 --- a/Calculator.Tests/LexerTests.cs +++ b/Calculator.Tests/LexerTests.cs @@ -129,7 +129,7 @@ private static IEnumerable NegativeExpressions new Token(TokenType.RBRACK, ')'), } }; - + yield return new object[] { "2-(17-5)", @@ -177,6 +177,7 @@ private static IEnumerable NegativeExpressions new Token(TokenType.NUMBER, '0'), new Token(TokenType.OPERATOR, DefaultOperators.Subtraction), new Token(TokenType.NUMBER, '6'), + new Token(TokenType.RBRACK, ')'), new Token(TokenType.OPERATOR, DefaultOperators.Multiplication), new Token(TokenType.LBRACK, '('), new Token(TokenType.LBRACK, '('), @@ -188,8 +189,7 @@ private static IEnumerable NegativeExpressions new Token(TokenType.NUMBER, '0'), new Token(TokenType.OPERATOR, DefaultOperators.Subtraction), new Token(TokenType.NUMBER, '5'), - new Token(TokenType.RBRACK, ')'), - new Token(TokenType.RBRACK, ')'), + new Token(TokenType.RBRACK, ')'), new Token(TokenType.RBRACK, ')'), new Token(TokenType.RBRACK, ')') } diff --git a/Calculator/Calculator.cs b/Calculator/Calculator.cs index 49c4276..d2c2eb3 100644 --- a/Calculator/Calculator.cs +++ b/Calculator/Calculator.cs @@ -1,5 +1,6 @@ using BLM16.Util.Calculator.Models; using System; +using System.Collections.Generic; using System.Linq; [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Calculator.Tests")] @@ -11,55 +12,77 @@ namespace BLM16.Util.Calculator; public class Calculator { /// - /// The Standardizer for standardizing the equation + /// Contains a list of all the symbols used /// - private readonly Standardizer standardizer; - /// - /// The Lexer for tokenizing the equation - /// - private readonly Lexer lexer; - /// - /// The Parser for evaluating the equation - /// - private readonly Parser parser; + private readonly List _symbols = new(); + + private Operator[] _operators = Array.Empty(); + private Constant[] _constants = DefaultConstantList; + private Function[] _functions = DefaultFunctionList; /// - /// The operators the calculator can use + /// The operators the calculator can use. + /// Always includes . /// - private readonly Operator[] operators; + /// Thrown when duplicate operators are encountered. + public Operator[] Operators + { + get => _operators; + init + { + var symbols = value.Select(o => o.Symbol.ToString()); + + CheckDuplicateSymbolsAndThrow(symbols); + + _symbols.AddRange(symbols); + _operators = _operators.Concat(value).ToArray(); + } + } + /// /// The constants the calculator can use /// - private readonly Constant[] constants; + /// Thrown when duplicate constants are encountered. + public Constant[] Constants + { + get => _constants; + init + { + var symbols = value.SelectMany(c => c.Symbols); + + CheckDuplicateSymbolsAndThrow(symbols); + + _symbols.AddRange(symbols); + _constants = value; + } + } + /// /// The functions the calculator can use /// - private readonly Function[] functions; + /// Thrown when duplicate functions are encountered. + public Function[] Functions + { + get => _functions; + init + { + var symbols = value.SelectMany(f => f.Symbols); - /// The list of operators the calculator recognizes. Always includes . - /// The list of constants the calculator recognizes. Defaults to if no value is provided. - /// The list of functions the calculator recognizes. Defaults to if no value is provided. - public Calculator(Operator[] operators = null, Constant[] constants = null, Function[] functions = null) + CheckDuplicateSymbolsAndThrow(symbols); + + _symbols.AddRange(symbols); + _functions = value; + } + } + + public Calculator() { - // Verify operators are unique - this.operators = new UniqueList((a, b) => a.Symbol.Equals(b.Symbol)) - .With(BuiltinOperatorList) // Add default operators - .With(operators ?? Array.Empty()) // Add provided operators if there are any - .ToArray(); - - // Verify constants are unique - this.constants = new UniqueList((a, b) => a.Symbols.Intersect(b.Symbols).Any()) - .With(constants ?? DefaultConstantList) // Use the default constants if no constants are provided - .ToArray(); - - // Verify functions are unique - this.functions = new UniqueList((a, b) => a.Symbols.Intersect(b.Symbols).Any()) - .With(functions ?? DefaultFunctionList) // Use the default functions if no functions are provided - .ToArray(); - - standardizer = new Standardizer(this.operators, this.constants, this.functions); - lexer = new Lexer(this.operators); - parser = new Parser(); + // Initialize Operators here instead of the backing field so the custom + // init logic adds the operator symbols to _symbols. + // Functions and Constants are overwritten instead of preserving builtins + // therefore their symbols should not be added to _symbols and the init + // logic does not need to run. This stops builtin operator duplication. + Operators = BuiltinOperatorList; } /// @@ -72,13 +95,32 @@ public double Calculate(string equation) { equation = equation.ToLower(); - equation = standardizer.Standardize(equation); - var tokens = lexer.Parse(equation); - var result = parser.Evaluate(tokens); + equation = new Standardizer(Operators, Constants, Functions).Standardize(equation); + var tokens = new Lexer(Operators).Parse(equation); + var result = new Parser().Evaluate(tokens); return result; } + /// + /// Checks if contains any duplicate elements + /// or if symbols has any duplicates in . + /// + /// The symbols to check duplicates for + /// Thrown when duplicate elements are encountered. + private void CheckDuplicateSymbolsAndThrow(IEnumerable symbols) + { + var duplicateSymbolsWithExisting = symbols.Intersect(_symbols); + var duplicateSymbolsInGiven = symbols.GroupBy(s => s) + .Where(s => s.Count() > 1) + .Select(s => s.Key); + + var duplicates = duplicateSymbolsWithExisting.Union(duplicateSymbolsInGiven).ToArray(); + + if (duplicates.Length > 0) + throw new ArgumentException($"Duplicate symbols found: {string.Join(", ", duplicates)}"); + } + #region Default Symbols /// @@ -124,6 +166,6 @@ public double Calculate(string equation) DefaultFunctions.Deg, DefaultFunctions.Rad }; - + #endregion } diff --git a/Calculator/Calculator.csproj b/Calculator/Calculator.csproj index e0b20e3..fe9640a 100644 --- a/Calculator/Calculator.csproj +++ b/Calculator/Calculator.csproj @@ -1,14 +1,14 @@ - net6.0 + net7.0 BLM16 BLM16 Copyright © 2021 Bradley Myers. All rights reserved. https://github.com/BLM16/Tokenized-Calculator git - 4.2.0 + 4.3.0 LICENSE calculator; math; solve BLM16.Util.$(AssemblyName) @@ -19,6 +19,7 @@ BLM16.Util.$(MSBuildProjectName.Replace(" ", "_")) icon.png icon.ico + Calculator diff --git a/Calculator/Lexer.cs b/Calculator/Lexer.cs index 8243a30..c9a4419 100644 --- a/Calculator/Lexer.cs +++ b/Calculator/Lexer.cs @@ -107,7 +107,7 @@ public List Parse(string equation) if (op.Any()) { var cur_op = op.First(); - if (negCount > 0 && !(cur_op > DefaultOperators.Subtraction)) + if (negCount > 0) { tokens.Add(new Token(TokenType.RBRACK, ')')); negCount--; diff --git a/Calculator/Models/Constant.cs b/Calculator/Models/Constant.cs index 682ce1a..91d8f26 100644 --- a/Calculator/Models/Constant.cs +++ b/Calculator/Models/Constant.cs @@ -36,13 +36,13 @@ public Constant(double value, params string[] symbols) public static class DefaultConstants { /// - /// The constant PI + /// The constant PI. Uses the value of . /// public static Constant PI => new(Math.PI, "pi", "π"); /// - /// Euler's number + /// The constant E. Uses the value of . /// public static Constant E => new(Math.E, "e"); @@ -52,4 +52,16 @@ public static Constant E /// public static Constant Phi => new(1.6180339887498948, "phi", "φ"); + + /// + /// 2π. Uses the value of . + /// + public static Constant Tau + => new(Math.Tau, "tau", "τ"); + + /// + /// The speed of light in m/s + /// + public static Constant C + => new(299792458, "c"); } diff --git a/Calculator/Models/Operator.cs b/Calculator/Models/Operator.cs index 05913ed..fa82bda 100644 --- a/Calculator/Models/Operator.cs +++ b/Calculator/Models/Operator.cs @@ -96,5 +96,23 @@ public static Operator Division /// The default exponent operator /// public static Operator Exponent - => new('^', 30, (double num1, double exponent) => Math.Pow(num1, exponent)); + => new('^', 30, Math.Pow); + + /// + /// The default modulus operator. + /// + /// + /// The % operator in C# is the remainder operator and does not perform mathematical modulus. + /// This operator should be used if mod is desired as opposed to remainder. + /// + public static Operator Modulus + => new('%', 20, (double num1, double num2) => + { + if (num2 == 0) throw new DivideByZeroException($"{num1} {Modulus.Symbol} {num2}"); + if (num2 == -1) return 0; + + double m = num1 % num2; + if (m < 0) m = (num2 < 0) ? m - num2 : m + num2; + return m; + }); } diff --git a/Calculator/Standardizer.cs b/Calculator/Standardizer.cs index c91fb55..5eb71da 100644 --- a/Calculator/Standardizer.cs +++ b/Calculator/Standardizer.cs @@ -9,7 +9,7 @@ namespace BLM16.Util.Calculator; /// /// Has the logic to standardize an equation into something the calculator can parse /// -internal class Standardizer +internal partial class Standardizer { /// /// The list of operators recognized by the calculator @@ -38,25 +38,21 @@ public Standardizer(Operator[] operators, Constant[] constants, Function[] funct /// The provided equation that is completely standardized public string Standardize(string equation) => AddMultiplicationSigns(ReplaceConstants(ComputeFunctions(FixBrackets(FixRepeatingOperators(RemoveWhitespace(equation)))))); - /// - /// Removes all the whitespace characters in a string - /// - /// The equation to remove whitespace from - /// The provided equation with removed whitespace - private static string RemoveWhitespace(string equation) - { - // Matches all whitespace characters - var whitespaceChars = new Regex(@"\s+"); - return whitespaceChars.Replace(equation, ""); - } - - /// - /// Fixes the equation by ensuring there are equal number of brackets - /// - /// The equation to fix the brackets in - /// Thrown when there are more closing brackets than opening ones. - /// The provided equation with the correct number of brackets - private static string FixBrackets(string equation) + /// + /// Removes all the whitespace characters in a string + /// + /// The equation to remove whitespace from + /// The provided equation with removed whitespace + private static string RemoveWhitespace(string equation) + => MatchWhiteSpace().Replace(equation, ""); + + /// + /// Fixes the equation by ensuring there are equal number of brackets + /// + /// The equation to fix the brackets in + /// Thrown when there are more closing brackets than opening ones. + /// The provided equation with the correct number of brackets + private static string FixBrackets(string equation) { int lBrack = 0, rBrack = 0; @@ -151,8 +147,12 @@ private string ComputeFunctions(string equation) } // Create a new calculator with the same operators, constants, and functions to recursively evalutate the function's contents - // We must use Except to remove the builtin operators as they will be added by default leading to duplicated operators - var calc = new Calculator(Operators.Except(Calculator.BuiltinOperatorList).ToArray(), Constants, Functions); + var calc = new Calculator + { + Operators = Operators.Except(Calculator.BuiltinOperatorList).ToArray(), // Builtins would result in duplicates and throw + Constants = Constants, + Functions = Functions + }; // Get the value captured by the function var sub = equation[(startIndex + f.Key.Length + 1)..endIndex]; @@ -213,4 +213,7 @@ private string AddMultiplicationSigns(string equation) return string.Join("", eq); } + + [GeneratedRegex("\\s+")] + private static partial Regex MatchWhiteSpace(); } diff --git a/Calculator/UniqueList.cs b/Calculator/UniqueList.cs deleted file mode 100644 index 32c1037..0000000 --- a/Calculator/UniqueList.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace BLM16.Util.Calculator; - -/// -/// Represents a strongly typed list of unique objects according to a -/// -/// The type of unique elements in the list -public class UniqueList : IEnumerable -{ - /// - /// Contains the list of unique values - /// - private readonly List _values = new(); - - /// - /// Compares the equality between 2 values to determine if they are unique - /// - /// - /// Returns the equality of 1 and 2 - /// - private readonly Func comparator; - - /// - /// Creates a new with an equality comparator - /// - /// A delegate that returns the equality of 2 values - public UniqueList(Func comparator) - { - this.comparator = comparator; - } - - /// - /// Adds a value to the list that is unique according to the - /// - /// The unique value to be added - /// Thrown when is not unique - public void Add(T value) - { - foreach (var val in _values) - { - if (comparator(value, val)) - throw new ArgumentException($"{typeof(T)} must be unique"); - } - - _values.Add(value); - } - - /// - /// Adds a range of values to the list that are unique according to the - /// - /// The range of unique values to be added - /// - /// Thrown when contains a value that is not unique - public UniqueList With(IEnumerable enumerable) - { - foreach (var val in enumerable) - Add(val); - - return this; - } - - public IEnumerator GetEnumerator() - { - foreach (var val in _values) - yield return val; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/README.md b/README.md index 7659ad8..88ab514 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Tokenized Calculator ==================== -[![C#](https://img.shields.io/static/v1?label=C%23&message=v10&color=brightgreen&link=https://docs.microsoft.com/en-us/dotnet/)](https://docs.microsoft.com/en-us/dotnet/) [![License](https://img.shields.io/badge/license-MIT-blue.svg?label=License&link=https://mit-license.org/)](https://github.com/BLM16/Tokenized-Calculator/blob/master/LICENSE) [![Nuget](https://img.shields.io/nuget/v/BLM16.Util.Calculator?label=Nuget&logo=nuget)](https://www.nuget.org/packages/BLM16.Util.Calculator/) +[![C#](https://img.shields.io/static/v1?label=C%23&message=v11&color=brightgreen)](https://docs.microsoft.com/en-us/dotnet/) [![License](https://img.shields.io/badge/license-MIT-blue.svg?label=License)](https://github.com/BLM16/Tokenized-Calculator/blob/master/LICENSE) [![Nuget](https://img.shields.io/nuget/v/BLM16.Util.Calculator?label=Nuget&logo=nuget)](https://www.nuget.org/packages/BLM16.Util.Calculator/) This library parses and solves math equations from strings. Order of Operations Rules are followed. @@ -16,8 +16,7 @@ This library parses and solves math equations from strings. Order of Operations --- Predefined operators, constants, and functions exist for your convenience. Not all of them used by default however. -* `Phi` is defined as a constant, however it is not used by default. It exists to be used with custom constants as needed. -* Hyperbolic trigonometric functions are defined and not used by default. +In the calculator's initializer you can add things from DefaultOperators, DefaultConstants, DefaultFunctions, or your own custom values. ## License This code is licensed under the [MIT License](https://github.com/BLM16/Tokenized-Calculator/blob/master/LICENSE)