diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Converters/MultiMathExpressionConverterPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Converters/MultiMathExpressionConverterPage.xaml index 3d5b64b484..63a6353420 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Converters/MultiMathExpressionConverterPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Converters/MultiMathExpressionConverterPage.xaml @@ -15,63 +15,86 @@ - - + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Primitives/MathOperator.shared.cs b/src/CommunityToolkit.Maui.Core/Primitives/MathOperator.shared.cs index 22d0f1de97..f2989fe5d7 100644 --- a/src/CommunityToolkit.Maui.Core/Primitives/MathOperator.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Primitives/MathOperator.shared.cs @@ -14,7 +14,7 @@ public enum MathOperatorPrecedence /// High High, /// Constant - Constant, + Constant } /// @@ -25,31 +25,25 @@ public enum MathOperatorPrecedence /// /// Name /// Number of Numerals -/// Math Operator Preference /// Calculation Function public sealed class MathOperator( - string name, - int numericCount, - MathOperatorPrecedence precedence, - Func calculateFunc) + string name, + int numericCount, + Func calculateFunc) { - /// - /// Name - /// - public string Name { get; } = name; - /// - /// Number of Numerals - /// - public int NumericCount { get; } = numericCount; + /// + /// Name + /// + public string Name { get; } = name; - /// - /// Math Operator Precedence - /// - public MathOperatorPrecedence Precedence { get; } = precedence; + /// + /// Number of Numerals + /// + public int NumericCount { get; } = numericCount; - /// - /// Calculation Function - /// - public Func CalculateFunc { get; } = calculateFunc; + /// + /// Calculation Function + /// + public Func CalculateFunc { get; } = calculateFunc; } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs b/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs index 771c9231ff..e46273ddb5 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs @@ -29,8 +29,33 @@ public void MathExpressionConverter_ReturnsCorrectResult(string expression, doub var convertResult = ((ICommunityToolkitValueConverter)mathExpressionConverter).Convert(x, mathExpressionTargetType, expression, cultureInfo) ?? throw new NullReferenceException(); var convertFromResult = mathExpressionConverter.ConvertFrom(x, expression); + Assert.True(convertFromResult is not null); Assert.True(Math.Abs((double)convertResult - expectedResult) < tolerance); - Assert.True(Math.Abs(convertFromResult - expectedResult) < tolerance); + Assert.True(Math.Abs((double)convertFromResult - expectedResult) < tolerance); + } + + [Theory] + [InlineData("3 < x", 2d, false)] + [InlineData("x > 3", 2d, false)] + [InlineData("3 < x == x > 3", 2d, true)] + [InlineData("3 <= x != 3 >= x", 2d, true)] + [InlineData("x >= 1", 2d, true)] + [InlineData("x <= 3", 2d, true)] + [InlineData("x >= 1 && (x <= 3 || x >= 0)", 2d, true)] + [InlineData("true", 2d, true)] + [InlineData("false", 2d, false)] + [InlineData("-x > 2", 3d, false)] + [InlineData("!!! (---x > 2)", 3d, true)] + public void MathExpressionConverter_WithComparisonOperator_ReturnsCorrectBooleanResult(string expression, double x, bool expectedResult) + { + var mathExpressionConverter = new MathExpressionConverter(); + + var convertResult = ((ICommunityToolkitValueConverter)mathExpressionConverter).Convert(x, mathExpressionTargetType, expression, cultureInfo) ?? throw new NullReferenceException(); + var convertFromResult = mathExpressionConverter.ConvertFrom(x, expression); + + Assert.True(convertFromResult is not null); + Assert.True((bool)convertResult == expectedResult); + Assert.True((bool)convertFromResult == expectedResult); } [Theory] @@ -38,31 +63,166 @@ public void MathExpressionConverter_ReturnsCorrectResult(string expression, doub [InlineData("(x1 + x) * x1", new object[] { 2d, 3d }, 15d)] [InlineData("3 + x * x1 / (1 - 5)^x1", new object[] { 4d, 2d }, 3.5d)] [InlineData("3 + 4 * 2 + cos(100 + x) / (x1 - 5)^2 + pow(x0, 2)", new object[] { 20d, 1d }, 411.05088631065792d)] - public void MathExpressionConverter_WithMultiplyVariable_ReturnsCorrectResult(string expression, object[] variables, double expectedResult) + public void MathExpressionConverter_WithMultipleVariable_ReturnsCorrectResult(string expression, object[] values, double expectedResult) { var mathExpressionConverter = new MultiMathExpressionConverter(); - var result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + var result = mathExpressionConverter.Convert(values, mathExpressionTargetType, expression); + Assert.NotNull(result); Assert.True(Math.Abs((double)result - expectedResult) < tolerance); } [Theory] - [InlineData("1 + 3 + 5 + (3 - 2))")] - [InlineData("1 + 2) + (9")] - [InlineData("100 + pow(2)")] - public void MathExpressionConverterThrowsArgumentException(string expression) + [InlineData("x && x1", new object?[] { true, true }, true)] + [InlineData("x && x1", new object?[] { true, false }, false)] + [InlineData("x && x1", new object?[] { false, true }, false)] + [InlineData("x && 3 == 4", new object?[] { false }, false)] + [InlineData("x && x1", new object?[] { "Cat", "Dog" }, "Dog")] + [InlineData("x && x1", new object?[] { false, "Cat" }, false)] + [InlineData("x && x1", new object?[] { "Cat", false }, false)] + [InlineData("x && x1", new object?[] { "", false }, "")] + [InlineData("x && x1", new object?[] { false, "" }, false)] + [InlineData("x && x1", new object?[] { null, "Cat" }, null)] + [InlineData("x && x1", new object?[] { "Cat", null }, null)] + [InlineData("x && x1", new object?[] { "", null }, "")] + [InlineData("x && x1", new object?[] { null, "" }, null)] + [InlineData("x || x1", new object?[] { true, true }, true)] + [InlineData("x || x1", new object?[] { false, true }, true)] + [InlineData("x || x1", new object?[] { true, false }, true)] + [InlineData("x || 3 == 4", new object?[] { false }, false)] + [InlineData("x || x1", new object?[] { "Cat", "Dog" }, "Cat")] + [InlineData("x || x1", new object?[] { false, "Cat" }, "Cat")] + [InlineData("x || x1", new object?[] { "Cat", false }, "Cat")] + [InlineData("x || x1", new object?[] { "", false }, false)] + [InlineData("x || x1", new object?[] { false, "" }, "")] + [InlineData("x || x1", new object?[] { null, "Cat" }, "Cat")] + [InlineData("x || x1", new object?[] { "Cat", null }, "Cat")] + [InlineData("x || x1", new object?[] { "", null }, null)] + [InlineData("x || x1", new object?[] { null, "" }, "")] + [InlineData("x || x1", new object?[] { false, new int[] { 1, 2, 3 } }, new int[] { 1, 2, 3 })] + public void MultiMathExpressionConverter_WithMultipleVariable_ReturnsCorrectLogicalResult(string expression, object?[] variables, object? expectedResult) { - var mathExpressionConverter = new MathExpressionConverter(); + var mathExpressionConverter = new MultiMathExpressionConverter(); + var result = mathExpressionConverter.Convert(variables, typeof(object), expression); + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData(true, true, true, true)] + [InlineData(true, false, false, true)] + [InlineData(false, true, false, true)] + [InlineData(false, false, false, false)] + [InlineData("Cat", "Dog", "Dog", "Cat")] + [InlineData(false, "Cat", false, "Cat")] + [InlineData("Cat", false, false, "Cat")] + [InlineData("", false, "", false)] + [InlineData(false, "", false, "")] + [InlineData(null, "Cat", null, "Cat")] + [InlineData("Cat", null, null, "Cat")] + [InlineData("", null, "", null)] + [InlineData(null, "", null, "")] + public void MultiMathExpressionConverter_WithAlternateLogicalOperators_ReturnsSameEvaluation(object? x, object? x1, object? expectedAndResult, object? expectedOrResult) + { + var variables = new object?[] { x, x1 }; + var mathExpressionConverter = new MultiMathExpressionConverter(); + var andResult = mathExpressionConverter.Convert(variables, typeof(object), "x && x1"); + var alternateAndResult = mathExpressionConverter.Convert(variables, typeof(object), "x and x1"); + Assert.Equal(andResult, expectedAndResult); + Assert.Equal(alternateAndResult, expectedAndResult); + var orResult = mathExpressionConverter.Convert(variables, typeof(object), "x || x1"); + var alternateOrResult = mathExpressionConverter.Convert(variables, typeof(object), "x or x1"); + Assert.Equal(orResult, expectedOrResult); + Assert.Equal(alternateOrResult, expectedOrResult); + } + + [Theory] + [InlineData("x >= x1", "x ge x1")] + [InlineData("x > x1", "x gt x1")] + [InlineData("x <= x1", "x le x1")] + [InlineData("x < x1", "x lt x1")] + public void MultiMathExpressionConverter_WithAlternateCompareOperators_ReturnsSameEvaluation(string expression, string alternateExpression) + { + var mathExpressionConverter = new MultiMathExpressionConverter(); + for (var i = 0; i <= 2; i++) + { + for (var j = 0; j <= 2; j++) + { + var variables = new object?[] { i, j }; + var result = mathExpressionConverter.Convert(variables, typeof(object), expression); + var alternateResult = mathExpressionConverter.Convert(variables, typeof(object), alternateExpression); + Assert.NotNull(result); + Assert.NotNull(alternateResult); + Assert.Equal(result, alternateResult); + } + } + } + + [Theory] + [InlineData("x == 3 && x1", new object?[] { 3d, 4d }, 4d)] + [InlineData("x != 3 || x1", new object?[] { 3d, 4d }, 4d)] + [InlineData("x + x1 || true", new object?[] { 3d, 4d }, 7d)] + [InlineData("x + x1 && false", new object?[] { 2d, -2d }, 0d)] + public void MathExpressionConverter_WithBooleanOperator_ReturnsCorrectNumberResult(string expression, object[] variables, double expectedResult) + { + var mathExpressionConverter = new MultiMathExpressionConverter(); + + object? result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + + Assert.True(result is not null); + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("x != 3 && x1", new object?[] { 3d, 4d }, false)] + [InlineData("x == 3 || x1", new object?[] { 3d, 4d }, true)] + public void MathExpressionConverter_WithBooleanOperator_ReturnsCorrectBooleanResult(string expression, object[] variables, bool expectedResult) + { + var mathExpressionConverter = new MultiMathExpressionConverter(); - Assert.Throws(() => ((ICommunityToolkitValueConverter)mathExpressionConverter).Convert(0d, mathExpressionTargetType, expression, cultureInfo)); - Assert.Throws(() => mathExpressionConverter.ConvertFrom(0d, expression)); + object? result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + + Assert.True(result is not null); + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData("x == 3 && x1", new object?[] { 3d, null})] + [InlineData("x != 3 || x1", new object?[] { 3d, null })] + [InlineData("x == 3 ? x1 : x2", new object?[] { 3d, null, 5d })] + [InlineData("x != 3 ? x1 : x2", new object?[] { 3d, 4d, null})] + public void MathExpressionConverter_ReturnsCorrectNullResult(string expression, object[] variables) + { + var mathExpressionConverter = new MultiMathExpressionConverter(); + + object? result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + + Assert.True(result is null); + } + + [Theory] + [InlineData("x == x1", new object?[] { 2d, 2d }, true)] + [InlineData("x == x1", new object?[] { 2d, null }, false)] + [InlineData("x == x1", new object?[] { null, 2d}, false)] + [InlineData("x == x1", new object?[] { null, null }, true)] + [InlineData("(x ? x1 : x2) == null", new object?[] { true, null, 2d }, true)] + public void MathExpressionConverter_WithEqualityOperator_ReturnsCorrectBooleanResult(string expression, object[] variables, bool expectedResult) + { + var mathExpressionConverter = new MultiMathExpressionConverter(); + + object? result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + + Assert.True(result is not null); + Assert.Equal(expectedResult, result); } [Theory] [InlineData(2.5)] [InlineData('c')] [InlineData(true)] + [InlineData("1 + 3 + 5 + (3 - 2))")] + [InlineData("1 + 2) + (9")] + [InlineData("100 + pow(2)")] public void MultiMathExpressionConverterInvalidParameterThrowsArgumentException(object parameter) { var mathExpressionConverter = new MultiMathExpressionConverter(); @@ -74,7 +234,7 @@ public void MultiMathExpressionConverterInvalidParameterThrowsArgumentException( public void MultiMathExpressionConverterInvalidValuesReturnsNull() { var mathExpressionConverter = new MultiMathExpressionConverter(); - var result = mathExpressionConverter.Convert([0d, null], mathExpressionTargetType, "x", cultureInfo); + var result = mathExpressionConverter.Convert([0d, null], mathExpressionTargetType, "x + x1", cultureInfo); result.Should().BeNull(); } @@ -85,9 +245,9 @@ public void MathExpressionConverterNullInputTest() Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).Convert(0.0, null, "x", null)); Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).ConvertBack(0.0, null, null, null)); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).Convert(null, typeof(bool), "x", null)); + Assert.True(((ICommunityToolkitValueConverter)new MathExpressionConverter()).Convert(null, typeof(bool), "x", null) is null); Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).Convert(null, typeof(bool), null, null)); - Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).ConvertBack(null, typeof(bool), null, null)); + Assert.Throws(() => ((ICommunityToolkitValueConverter)new MathExpressionConverter()).ConvertBack(null, typeof(bool), null, null)); } [Fact] @@ -98,4 +258,4 @@ public void MultiMathExpressionConverterNullInputTest() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. Assert.Throws(() => new MultiMathExpressionConverter().Convert([0.0, 7], typeof(bool), null, null)); } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs index 77223c0580..5c972e5f59 100644 --- a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs @@ -1,95 +1,148 @@ -using System.Globalization; +using System.Collections.ObjectModel; using System.Text.RegularExpressions; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using CommunityToolkit.Maui.Core; namespace CommunityToolkit.Maui.Converters; -sealed partial class MathExpression +enum MathTokenType { - const NumberStyles numberStyle = NumberStyles.Float | NumberStyles.AllowThousands; + Value, + Operator, +} - static readonly IFormatProvider formatProvider = new CultureInfo("en-US"); +sealed record MathToken(MathTokenType Type, string Text, object? Value); +sealed partial class MathExpression +{ readonly IReadOnlyList operators; - internal MathExpression(string expression, IEnumerable? arguments = null) + internal MathExpression(in string expression, in IReadOnlyList arguments) { ArgumentException.ThrowIfNullOrEmpty(expression, "Expression can't be null or empty."); - - var argumentList = arguments?.ToList() ?? []; + ArgumentNullException.ThrowIfNull(arguments, "Arguments cannot be null."); Expression = expression.ToLower(); - var operators = new List - { - new ("+", 2, MathOperatorPrecedence.Low, x => x[0] + x[1]), - new ("-", 2, MathOperatorPrecedence.Low, x => x[0] - x[1]), - new ("*", 2, MathOperatorPrecedence.Medium, x => x[0] * x[1]), - new ("/", 2, MathOperatorPrecedence.Medium, x => x[0] / x[1]), - new ("%", 2, MathOperatorPrecedence.Medium, x => x[0] % x[1]), - new ("abs", 1, MathOperatorPrecedence.Medium, x => Math.Abs(x[0])), - new ("acos", 1, MathOperatorPrecedence.Medium, x => Math.Acos(x[0])), - new ("asin", 1, MathOperatorPrecedence.Medium, x => Math.Asin(x[0])), - new ("atan", 1, MathOperatorPrecedence.Medium, x => Math.Atan(x[0])), - new ("atan2", 2, MathOperatorPrecedence.Medium, x => Math.Atan2(x[0], x[1])), - new ("ceiling", 1, MathOperatorPrecedence.Medium, x => Math.Ceiling(x[0])), - new ("cos", 1, MathOperatorPrecedence.Medium, x => Math.Cos(x[0])), - new ("cosh", 1, MathOperatorPrecedence.Medium, x => Math.Cosh(x[0])), - new ("exp", 1, MathOperatorPrecedence.Medium, x => Math.Exp(x[0])), - new ("floor", 1, MathOperatorPrecedence.Medium, x => Math.Floor(x[0])), - new ("ieeeremainder", 2, MathOperatorPrecedence.Medium, x => Math.IEEERemainder(x[0], x[1])), - new ("log", 2, MathOperatorPrecedence.Medium, x => Math.Log(x[0], x[1])), - new ("log10", 1, MathOperatorPrecedence.Medium, x => Math.Log10(x[0])), - new ("max", 2, MathOperatorPrecedence.Medium, x => Math.Max(x[0], x[1])), - new ("min", 2, MathOperatorPrecedence.Medium, x => Math.Min(x[0], x[1])), - new ("pow", 2, MathOperatorPrecedence.Medium, x => Math.Pow(x[0], x[1])), - new ("round", 2, MathOperatorPrecedence.Medium, x => Math.Round(x[0], Convert.ToInt32(x[1]))), - new ("sign", 1, MathOperatorPrecedence.Medium, x => Math.Sign(x[0])), - new ("sin", 1, MathOperatorPrecedence.Medium, x => Math.Sin(x[0])), - new ("sinh", 1, MathOperatorPrecedence.Medium, x => Math.Sinh(x[0])), - new ("sqrt", 1, MathOperatorPrecedence.Medium, x => Math.Sqrt(x[0])), - new ("tan", 1, MathOperatorPrecedence.Medium, x => Math.Tan(x[0])), - new ("tanh", 1, MathOperatorPrecedence.Medium, x => Math.Tanh(x[0])), - new ("truncate", 1, MathOperatorPrecedence.Medium, x => Math.Truncate(x[0])), - new ("^", 2, MathOperatorPrecedence.High, x => Math.Pow(x[0], x[1])), - new ("pi", 0, MathOperatorPrecedence.Constant, _ => Math.PI), - new ("e", 0, MathOperatorPrecedence.Constant, _ => Math.E), - }; - - if (argumentList.Count > 0) + List operators = + [ + new ("+", 2, x => Convert.ToDouble(x[0]) + Convert.ToDouble(x[1])), + new ("-", 2, x => Convert.ToDouble(x[0]) - Convert.ToDouble(x[1])), + new ("*", 2, x => Convert.ToDouble(x[0]) * Convert.ToDouble(x[1])), + new ("/", 2, x => Convert.ToDouble(x[0]) / Convert.ToDouble(x[1])), + new ("%", 2, x => Convert.ToDouble(x[0]) % Convert.ToDouble(x[1])), + + new ("and", 2, x => ConvertToBoolean(x[0]) ? x[1] : x[0]), + new ("or", 2, x => ConvertToBoolean(x[0]) ? x[0] : x[1]), + + new ("==", 2, x => object.Equals(x[0], x[1])), + new ("!=", 2, x => !object.Equals(x[0], x[1])), + + new ("ge", 2, x => Convert.ToDouble(x[0]) >= Convert.ToDouble(x[1])), + new ("gt", 2, x => Convert.ToDouble(x[0]) > Convert.ToDouble(x[1])), + new ("le", 2, x => Convert.ToDouble(x[0]) <= Convert.ToDouble(x[1])), + new ("lt", 2, x => Convert.ToDouble(x[0]) < Convert.ToDouble(x[1])), + new ("neg", 1, x => -Convert.ToDouble(x[0])), + new ("not", 1, x => !ConvertToBoolean(x[0])), + new ("if", 3, x => ConvertToBoolean(x[0]) ? x[1] : x[2]), + + new ("abs", 1, x => Math.Abs(Convert.ToDouble(x[0]))), + new ("acos", 1, x => Math.Acos(Convert.ToDouble(x[0]))), + new ("asin", 1, x => Math.Asin(Convert.ToDouble(x[0]))), + new ("atan", 1, x => Math.Atan(Convert.ToDouble(x[0]))), + new ("atan2", 2, x => Math.Atan2(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("ceiling", 1, x => Math.Ceiling(Convert.ToDouble(x[0]))), + new ("cos", 1, x => Math.Cos(Convert.ToDouble(x[0]))), + new ("cosh", 1, x => Math.Cosh(Convert.ToDouble(x[0]))), + new ("exp", 1, x => Math.Exp(Convert.ToDouble(x[0]))), + new ("floor", 1, x => Math.Floor(Convert.ToDouble(x[0]))), + new ("ieeeremainder", 2, x => Math.IEEERemainder(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("log", 2, x => Math.Log(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("log10", 1, x => Math.Log10(Convert.ToDouble(x[0]))), + new ("max", 2, x => Math.Max(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("min", 2, x => Math.Min(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("pow", 2, x => Math.Pow(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("round", 2, x => Math.Round(Convert.ToDouble(x[0]), Convert.ToInt32(x[1]))), + new ("sign", 1, x => Math.Sign(Convert.ToDouble(x[0]))), + new ("sin", 1, x => Math.Sin(Convert.ToDouble(x[0]))), + new ("sinh", 1, x => Math.Sinh(Convert.ToDouble(x[0]))), + new ("sqrt", 1, x => Math.Sqrt(Convert.ToDouble(x[0]))), + new ("tan", 1, x => Math.Tan(Convert.ToDouble(x[0]))), + new ("tanh", 1, x => Math.Tanh(Convert.ToDouble(x[0]))), + new ("truncate", 1, x => Math.Truncate(Convert.ToDouble(x[0]))), + new ("int", 1, x => Convert.ToInt32(x[0])), + new ("double", 1, x => Convert.ToDouble(x[0])), + new ("bool", 1, x => Convert.ToBoolean(x[0])), + new ("str", 1, x => x[0]?.ToString()), + new ("len", 1, x => x[0]?.ToString()?.Length), + new ("^", 2, x => Math.Pow(Convert.ToDouble(x[0]), Convert.ToDouble(x[1]))), + new ("pi", 0, _ => Math.PI), + new ("e", 0, _ => Math.E), + new ("true", 0, _ => true), + new ("false", 0, _ => false), + new ("null", 0, _ => null), + ]; + + if (arguments.Count > 0) { - operators.Add(new MathOperator("x", 0, MathOperatorPrecedence.Constant, _ => argumentList[0])); + var firstArgument = arguments[0]; + operators.Add(new MathOperator("x", 0, _ => firstArgument)); } - for (var i = 0; i < argumentList.Count; i++) + for (var i = 0; i < arguments.Count; i++) { - var index = i; - operators.Add(new MathOperator($"x{i}", 0, MathOperatorPrecedence.Constant, _ => argumentList[index])); + var currentArgument = arguments[i]; + operators.Add(new MathOperator($"x{i}", 0, _ => currentArgument)); } this.operators = operators; } + + static ReadOnlyDictionary BinaryMappingDictionary { get; } = new Dictionary + { + { "<", "lt" }, + { "<=", "le" }, + { ">", "gt" }, + { ">=", "ge" }, + { "&&", "and" }, + { "||", "or" } + }.AsReadOnly(); + + static ReadOnlyDictionary UnaryMappingDictionary { get; } = new Dictionary + { + { "-", "neg" }, + { "!", "not" } + }.AsReadOnly(); + + string Expression { get; } + + List RPN { get; } = []; + + int ExpressionIndex { get; set; } - internal string Expression { get; } + Match PatternMatch { get; set; } = Match.Empty; - public double Calculate() + public object? CalculateResult() { - var rpn = GetReversePolishNotation(Expression); + if (!ParseExpression()) + { + throw new ArgumentException("Math Expression Invalid. Failed to parse math expression."); + } - var stack = new Stack(); + var stack = new Stack(); - foreach (var value in rpn) + foreach (var token in RPN) { - if (double.TryParse(value, numberStyle, formatProvider, out var numeric)) + if (token.Type is MathTokenType.Value) { - stack.Push(numeric); + stack.Push(token.Value); continue; } - var mathOperator = operators.FirstOrDefault(x => x.Name == value) ?? - throw new ArgumentException($"Invalid math expression. Can't find operator or value with name \"{value}\"."); - - if (mathOperator.Precedence is MathOperatorPrecedence.Constant) + var mathOperator = operators.FirstOrDefault(x => x.Name == token.Text) ?? throw new ArgumentException($"Math Expression Invalid. Can't find operator or value with name \"{token.Text}\"."); + + if (mathOperator.NumericCount is 0) { stack.Push(mathOperator.CalculateFunc([])); continue; @@ -99,146 +152,310 @@ public double Calculate() if (stack.Count < operatorNumericCount) { - throw new ArgumentException("Invalid math expression."); + throw new ArgumentException($"Math Expression Invalid. Insufficient parameters to operator \"{mathOperator.Name}\"."); } - var args = new List(); + bool containsNullArgument = false; + List args = []; + for (var j = 0; j < operatorNumericCount; j++) { - args.Add(stack.Pop()); + object? val = stack.Pop(); + args.Add(val); + containsNullArgument = containsNullArgument || val is null; } args.Reverse(); - stack.Push(mathOperator.CalculateFunc([.. args])); + containsNullArgument = mathOperator.Name switch + { + "if" => args[0] is null, + "and" or "or" or "==" or "!=" => false, + _ => containsNullArgument + }; + + stack.Push(!containsNullArgument ? mathOperator.CalculateFunc([.. args]) : null); } - if (stack.Count != 1) + return stack.Count switch { - throw new ArgumentException("Invalid math expression."); + 0 => throw new InvalidOperationException($"Math Expression Invalid. Stack is unexpectedly empty."), + > 1 => throw new InvalidOperationException($"Math Expression Invalid. Stack unexpectedly contains multiple items ({stack.Count}) items when it should contain only the final result."), + _ => stack.Pop() + }; + } + + [GeneratedRegex("""^(\w+)\(""")] + private static partial Regex EvaluateFunctionStart(); + + [GeneratedRegex("""^(\,)""")] + private static partial Regex EvaluateComma(); + + [GeneratedRegex("""^(\))""")] + private static partial Regex EvaluateFunctionEnd(); + + [GeneratedRegex("""^(\?)""")] + private static partial Regex EvaluateConditionalStart(); + + [GeneratedRegex("""^(\:)""")] + private static partial Regex EvaluateConditionalElse(); + + [GeneratedRegex("""^(\|\||or)""")] + private static partial Regex EvaluateLogicalOROperator(); + + [GeneratedRegex("""^(\&\&|and)""")] + private static partial Regex EvaluateLogicalAndOperator(); + + [GeneratedRegex("""^(==|!=|eq|ne)""")] + private static partial Regex EvaluateEqualityOperators(); + + [GeneratedRegex("""^(\<\=|\>\=|\<|\>|le|ge|lt|gt)""")] + private static partial Regex EvaluateCompareOperators(); + + [GeneratedRegex("""^(\+|\-)""")] + private static partial Regex EvaluateSumOperators(); + + [GeneratedRegex("""^(\*|\/|\%)""")] + private static partial Regex EvaluateProductOperators(); + + [GeneratedRegex("""^(\^)""")] + private static partial Regex EvaluatePowerOperator(); + + [GeneratedRegex("""^(\-|\!)""")] + private static partial Regex EvaluateUnaryOperators(); + + [GeneratedRegex("""^(\-?\d+\.\d+|\-?\d+)""")] + private static partial Regex EvaluateNumberPattern(); + + [GeneratedRegex("""^["]([^"]*)["]""")] + private static partial Regex EvaluateStringPattern(); + + [GeneratedRegex("""^(\w+)""")] + private static partial Regex EvaluateConstants(); + + [GeneratedRegex("""^(\()""")] + private static partial Regex EvaluateParenStart(); + + [GeneratedRegex("""^(\))""")] + private static partial Regex EvaluateParenEnd(); + + [GeneratedRegex("""^\s*""")] + private static partial Regex EvaluateWhitespace(); + + static bool ConvertToBoolean(object? b) => b switch + { + bool x => x, + null => false, + double doubleValue => doubleValue != 0 && !double.IsNaN(doubleValue), + string stringValue => !string.IsNullOrEmpty(stringValue), + _ => Convert.ToBoolean(b) + }; + + bool ParsePattern(Regex regex) + { + var whitespaceMatch = EvaluateWhitespace().Match(Expression[ExpressionIndex..]); + if (whitespaceMatch.Success) + { + ExpressionIndex += whitespaceMatch.Length; } - return stack.Pop(); + PatternMatch = regex.Match(Expression[ExpressionIndex..]); + if (!PatternMatch.Success) + { + return false; + } + ExpressionIndex += PatternMatch.Length; + + whitespaceMatch = EvaluateWhitespace().Match(Expression[ExpressionIndex..]); + if (whitespaceMatch.Success) + { + ExpressionIndex += whitespaceMatch.Length; + } + + return true; } - [GeneratedRegex(@"(? GetReversePolishNotation(string expression) + bool ParseExpr() { - var matches = MathExpressionRegexPattern().Matches(expression) ?? throw new ArgumentException("Invalid math expression."); + return ParseConditional(); + } - var output = new List(); - var stack = new Stack<(string Name, MathOperatorPrecedence Precedence)>(); + bool ParseConditional() + { + if (!ParseLogicalOR()) + { + return false; + } - foreach (var match in matches.Cast()) + if (!ParsePattern(EvaluateConditionalStart())) { - if (string.IsNullOrEmpty(match?.Value)) - { - continue; - } + return true; + } - var value = match.Value; + if (!ParseLogicalOR()) + { + return false; + } - if (double.TryParse(value, numberStyle, formatProvider, out var numeric)) + if (!ParsePattern(EvaluateConditionalElse())) + { + return false; + } + + if (!ParseLogicalOR()) + { + return false; + } + + RPN.Add(new MathToken(MathTokenType.Operator, "if", null)); + return true; + } + + bool ParseLogicalOR() => ParseBinaryOperators(EvaluateLogicalOROperator(), ParseLogicalAnd); + + bool ParseLogicalAnd() => ParseBinaryOperators(EvaluateLogicalAndOperator(), ParseEquality); + + bool ParseEquality() => ParseBinaryOperators(EvaluateEqualityOperators(), ParseCompare); + + bool ParseCompare() => ParseBinaryOperators(EvaluateCompareOperators(), ParseSum); + + bool ParseSum() => ParseBinaryOperators(EvaluateSumOperators(), ParseProduct); + + bool ParseProduct() => ParseBinaryOperators(EvaluateProductOperators(), ParsePower); + + bool ParsePower() => ParseBinaryOperators(EvaluatePowerOperator(), ParsePrimary); + + bool ParseBinaryOperators(Regex BinaryOperators, Func ParseNext) + { + if (!ParseNext()) + { + return false; + } + int index = ExpressionIndex; + while (ParsePattern(BinaryOperators)) + { + string _operator = PatternMatch.Groups[1].Value; + if (BinaryMappingDictionary.TryGetValue(_operator, out var value)) { - if (numeric < 0) - { - var isNegative = output.Count == 0 || stack.Count != 0; - - if (!isNegative) - { - stack.Push(("-", MathOperatorPrecedence.Low)); - output.Add(Math.Abs(numeric).ToString(formatProvider)); - continue; - } - } - - output.Add(value); - continue; + _operator = value; + } + if (!ParseNext()) + { + ExpressionIndex = index; + return false; } + RPN.Add(new MathToken(MathTokenType.Operator, _operator, null)); + index = ExpressionIndex; + } + return true; + } + + bool ParsePrimary() + { + if (ParsePattern(EvaluateNumberPattern())) + { + string _number = PatternMatch.Groups[1].Value; + RPN.Add(new MathToken(MathTokenType.Value, _number, double.Parse(_number))); + return true; + } - var mathOperator = operators.FirstOrDefault(x => x.Name == value); - if (mathOperator is not null) + if (ParsePattern(EvaluateStringPattern())) + { + string _string = PatternMatch.Groups[1].Value; + RPN.Add(new MathToken(MathTokenType.Value, _string, _string)); + return true; + } + + if (ParseFunction()) + { + return true; + } + + if (ParsePattern(EvaluateConstants())) + { + string _constant = PatternMatch.Groups[1].Value; + RPN.Add(new MathToken(MathTokenType.Operator, _constant, null)); + return true; + } + + int index = ExpressionIndex; + if (ParsePattern(EvaluateParenStart())) + { + if (!ParseExpr()) { - if (mathOperator.Precedence is MathOperatorPrecedence.Constant) - { - output.Add(value); - continue; - } - - while (stack.Count > 0) - { - var (_, precedence) = stack.Peek(); - if (precedence >= mathOperator.Precedence) - { - output.Add(stack.Pop().Name); - } - else - { - break; - } - } - - stack.Push((value, mathOperator.Precedence)); + ExpressionIndex = index; + return false; } - else if (value is "(") + if (!ParsePattern(EvaluateParenEnd())) { - stack.Push((value, MathOperatorPrecedence.Lowest)); + ExpressionIndex = index; + return false; } - else if (value is ")") + return true; + } + + index = ExpressionIndex; + if (ParsePattern(EvaluateUnaryOperators())) + { + string _operator = PatternMatch.Groups[1].Value; + if (UnaryMappingDictionary.TryGetValue(_operator, out var value)) { - var isFound = false; - for (var i = stack.Count - 1; i >= 0; i--) - { - if (stack.Count == 0) - { - throw new ArgumentException("Invalid math expression."); - } - - var stackValue = stack.Pop().Name; - if (stackValue is "(") - { - isFound = true; - break; - } - - output.Add(stackValue); - } - - if (!isFound) - { - throw new ArgumentException("Invalid math expression."); - } + _operator = value; } - else if (value is ",") + if (!ParsePrimary()) { - while (stack.Count > 0) - { - var (_, precedence) = stack.Peek(); - if (precedence >= MathOperatorPrecedence.Low) - { - output.Add(stack.Pop().Name); - } - else - { - break; - } - } + ExpressionIndex = index; + return false; } + RPN.Add(new MathToken(MathTokenType.Operator, _operator, null)); + return true; } - for (var i = stack.Count - 1; i >= 0; i--) + return false; + } + + bool ParseFunction() + { + int index = ExpressionIndex; + if (!ParsePattern(EvaluateFunctionStart())) + { + return false; + } + + string text = PatternMatch.Groups[0].Value; + string functionName = PatternMatch.Groups[1].Value; + + if (!ParseExpr()) { - var (name, _) = stack.Pop(); - if (name is "(") + ExpressionIndex = index; + return false; + } + + while (ParsePattern(EvaluateComma())) + { + if (!ParseExpr()) { - throw new ArgumentException("Invalid math expression."); + ExpressionIndex = index; + return false; } + index = ExpressionIndex; + } - output.Add(name); + if (!ParsePattern(EvaluateFunctionEnd())) + { + ExpressionIndex = index; + return false; } - return output; + RPN.Add(new MathToken(MathTokenType.Operator, functionName, null)); + + return true; } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpressionConverter.shared.cs b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpressionConverter.shared.cs index 8d29adcadf..4f72e1d4f3 100644 --- a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpressionConverter.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpressionConverter.shared.cs @@ -6,23 +6,22 @@ namespace CommunityToolkit.Maui.Converters; /// Converters for Math expressions /// [AcceptEmptyServiceProvider] -public partial class MathExpressionConverter : BaseConverterOneWay +public partial class MathExpressionConverter : BaseConverterOneWay { /// - public override double DefaultConvertReturnValue { get; set; } = 0.0d; + public override object? DefaultConvertReturnValue { get; set; } = 0.0d; /// /// Calculate the incoming expression string with one variable. /// - /// The variable X for an expression + /// The variable X for an expression /// The expression to calculate. /// The culture to use in the converter. This is not implemented. /// A The result of calculating an expression. - public override double ConvertFrom(double value, string parameter, CultureInfo? culture = null) + public override object? ConvertFrom(object? inputValue, string parameter, CultureInfo? culture = null) { ArgumentNullException.ThrowIfNull(parameter); - var mathExpression = new MathExpression(parameter, [value]); - return mathExpression.Calculate(); + return new MathExpression(parameter, [inputValue]).CalculateResult(); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs index ca0325f8af..812b7b73e1 100644 --- a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs @@ -18,7 +18,6 @@ public class MultiMathExpressionConverter : MultiValueConverterExtension, ICommu /// The expression to calculate. /// The culture to use in the converter. This is not implemented. /// A The result of calculating an expression. - [return: NotNullIfNotNull(nameof(values))] public object? Convert(object?[]? values, Type targetType, [NotNull] object? parameter, CultureInfo? culture = null) { ArgumentNullException.ThrowIfNull(targetType); @@ -29,22 +28,9 @@ public class MultiMathExpressionConverter : MultiValueConverterExtension, ICommu throw new ArgumentException("The parameter should be of type String."); } - if (values is null || values.Any(x => !double.TryParse(x?.ToString(), out _))) - { - return null; - } - - var args = new List(); - foreach (var value in values) - { - var valueString = value?.ToString() ?? throw new ArgumentException("Values cannot be null."); - - var xValue = double.Parse(valueString); - args.Add(xValue); - } - - var math = new MathExpression(expression, args); - return math.Calculate(); + return values is null + ? null + : new MathExpression(expression, values).CalculateResult(); } ///