diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Converters/MultiMathExpressionConverterPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Converters/MultiMathExpressionConverterPage.xaml index 3d5b64b48..63a635342 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 f2b137f5b..ba7faedee 100644 --- a/src/CommunityToolkit.Maui.Core/Primitives/MathOperator.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Primitives/MathOperator.shared.cs @@ -27,17 +27,14 @@ public sealed class MathOperator /// /// Name /// Number of Numerals - /// Math Operator Preference /// Calculation Function public MathOperator( string name, int numericCount, - MathOperatorPrecedence precedence, - Func calculateFunc) + Func calculateFunc) { Name = name; CalculateFunc = calculateFunc; - Precedence = precedence; NumericCount = numericCount; } @@ -59,5 +56,5 @@ public MathOperator( /// /// Calculation Function /// - public Func CalculateFunc { get; } + public Func CalculateFunc { get; } } \ 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 771c9231f..09a5d0b9f 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,7 +63,7 @@ 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[] variables, double expectedResult) { var mathExpressionConverter = new MultiMathExpressionConverter(); @@ -47,16 +72,74 @@ public void MathExpressionConverter_WithMultiplyVariable_ReturnsCorrectResult(st Assert.True(Math.Abs((double)result - expectedResult) < tolerance); } + [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(); + + 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("1 + 3 + 5 + (3 - 2))")] [InlineData("1 + 2) + (9")] [InlineData("100 + pow(2)")] - public void MathExpressionConverterThrowsArgumentException(string expression) + public void MathExpressionConverter_WithInvalidExpressions_ReturnsNullResult(string expression) { var mathExpressionConverter = new MathExpressionConverter(); - Assert.Throws(() => ((ICommunityToolkitValueConverter)mathExpressionConverter).Convert(0d, mathExpressionTargetType, expression, cultureInfo)); - Assert.Throws(() => mathExpressionConverter.ConvertFrom(0d, expression)); + Assert.Null(((ICommunityToolkitValueConverter)mathExpressionConverter).Convert(0d, mathExpressionTargetType, expression, cultureInfo)); + Assert.Null(mathExpressionConverter.ConvertFrom(0d, expression)); } [Theory] @@ -74,7 +157,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 +168,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] diff --git a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs index 2d1840407..296624523 100644 --- a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs @@ -1,9 +1,18 @@ using System.Globalization; using System.Text.RegularExpressions; +using System.Diagnostics; using CommunityToolkit.Maui.Core; namespace CommunityToolkit.Maui.Converters; +enum MathTokenType +{ + Value, + Operator, +}; + +record MathToken(MathTokenType type, string text, object? value); + sealed partial class MathExpression { const NumberStyles numberStyle = NumberStyles.Float | NumberStyles.AllowThousands; @@ -11,9 +20,19 @@ sealed partial class MathExpression static readonly IFormatProvider formatProvider = new CultureInfo("en-US"); readonly IReadOnlyList operators; - readonly IReadOnlyList arguments; - - internal MathExpression(string expression, IEnumerable? arguments = null) + readonly IReadOnlyList arguments; + + internal static bool __bool(object? b) => + b switch + { + bool x => x, + null => false, + double doubleValue => doubleValue != 0 && doubleValue != double.NaN, + string stringValue => !string.IsNullOrEmpty(stringValue), + _ => Convert.ToBoolean(b) + }; + + internal MathExpression(string expression, IEnumerable? arguments = null) { ArgumentException.ThrowIfNullOrEmpty(expression, "Expression can't be null or empty."); @@ -23,49 +42,72 @@ internal MathExpression(string expression, IEnumerable? arguments = null 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), + 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 ("&&", 2, x => __bool(x[0]) ? x[1] : x[0]), + new ("||", 2, x => __bool(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 (">=", 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 ("neg", 1, x => -Convert.ToDouble(x[0])), + new ("not", 1, x => !__bool(x[0])), + new ("if", 3, x => __bool(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 (argumentList.Count > 0) { - operators.Add(new MathOperator("x", 0, MathOperatorPrecedence.Constant, _ => argumentList[0])); + operators.Add(new MathOperator("x", 0, _ => argumentList[0])); } for (var i = 0; i < argumentList.Count; i++) { var index = i; - operators.Add(new MathOperator($"x{i}", 0, MathOperatorPrecedence.Constant, _ => argumentList[index])); + operators.Add(new MathOperator($"x{i}", 0, _ => argumentList[index])); } this.operators = operators; @@ -74,24 +116,38 @@ internal MathExpression(string expression, IEnumerable? arguments = null internal string Expression { get; } - public double Calculate() + internal int ExpressionIndex { get; set; } = 0; + + internal Match PatternMatch { get; set; } = Match.Empty; + + internal List RPN { get; } = new(); + + public object? Calculate() { - var rpn = GetReversePolishNotation(Expression); + if (!ParseExpression()) + { + Trace.TraceWarning("Invalid math expression. Failed to parse expression."); + return null; + } - 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 == 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}\"."); + var mathOperator = operators.FirstOrDefault(x => x.Name == token.text); + if (mathOperator is null) + { + Trace.TraceWarning($"Invalid math expression. Can't find operator or value with name \"{token.text}\"."); + return null; + } - if (mathOperator.Precedence is MathOperatorPrecedence.Constant) + if (mathOperator.NumericCount == 0) { stack.Push(mathOperator.CalculateFunc([])); continue; @@ -101,146 +157,313 @@ public double Calculate() if (stack.Count < operatorNumericCount) { - throw new ArgumentException("Invalid math expression."); + Trace.TraceWarning($"Invalid math expression. Insufficient parameters to operator \"{mathOperator.Name}\"."); + return null; } - var args = new List(); + bool nullGuard = false; + var args = new List(); for (var j = 0; j < operatorNumericCount; j++) { - args.Add(stack.Pop()); + object? val = stack.Pop(); + args.Add(val); + nullGuard = nullGuard || (val is null); } args.Reverse(); - stack.Push(mathOperator.CalculateFunc([.. args])); + switch (mathOperator.Name) + { + case "if": + case "&&": + case "||": + case "==": + case "!=": + nullGuard = false; + break; + } + + stack.Push(!nullGuard ? mathOperator.CalculateFunc([.. args]) : null); } - if (stack.Count != 1) + if (stack.Count == 0) { - throw new ArgumentException("Invalid math expression."); + Trace.TraceWarning($"Invalid math expression. Stack is unexpectedly empty."); + return null; + } + + if (stack.Count > 1) + { + Trace.WriteLine($"Invalid math expression. Stack unexpectedly contains too many ({stack.Count}) items."); } return stack.Pop(); } - [GeneratedRegex(@"(? GetReversePolishNotation(string expression) + bool ParseExpr() { - var matches = MathExpressionRegexPattern().Matches(expression) ?? throw new ArgumentException("Invalid math expression."); + return ParseConditional(); + } + + [GeneratedRegex("""^(\?)""")] + private static partial Regex ConditionalStart(); - var output = new List(); - var stack = new Stack<(string Name, MathOperatorPrecedence Precedence)>(); + [GeneratedRegex("""^(\:)""")] + private static partial Regex ConditionalElse(); - foreach (var match in matches.Cast()) + bool ParseConditional() + { + if (!ParseLogicalOR()) { - if (string.IsNullOrEmpty(match?.Value)) - { - continue; - } + return false; + } - var value = match.Value; + if (!ParsePattern(ConditionalStart())) + { + return true; + } - if (double.TryParse(value, numberStyle, formatProvider, out var numeric)) + if (!ParseLogicalOR()) + { + return false; + } + + if (!ParsePattern(ConditionalElse())) + { + return false; + } + + if (!ParseLogicalOR()) + { + return false; + } + + RPN.Add(new MathToken(MathTokenType.Operator, "if", null)); + return true; + } + + [GeneratedRegex("""^(\|\||or)""")] + private static partial Regex LogicalOROperator(); + + bool ParseLogicalOR() => ParseBinaryOperators(LogicalOROperator(), ParseLogicalAnd); + + [GeneratedRegex("""^(\&\&|and)""")] + private static partial Regex LogicalAndOperator(); + + bool ParseLogicalAnd() => ParseBinaryOperators(LogicalAndOperator(), ParseEquality); + + [GeneratedRegex("""^(==|!=|eq|ne)""")] + private static partial Regex EqualityOperators(); + + bool ParseEquality() => ParseBinaryOperators(EqualityOperators(), ParseCompare); + + [GeneratedRegex("""^(\<\=|\>\=|\<|\>|le|ge|lt|gt)""")] + private static partial Regex CompareOperators(); + + bool ParseCompare() => ParseBinaryOperators(CompareOperators(), ParseSum); + + [GeneratedRegex("""^(\+|\-)""")] + private static partial Regex SumOperators(); + + bool ParseSum() => ParseBinaryOperators(SumOperators(), ParseProduct); + + [GeneratedRegex("""^(\*|\/|\%)""")] + private static partial Regex ProductOperators(); + + bool ParseProduct() => ParseBinaryOperators(ProductOperators(), ParsePower); + + [GeneratedRegex("""^(\^)""")] + private static partial Regex PowerOperator(); + + bool ParsePower() => ParseBinaryOperators(PowerOperator(), ParsePrimary); + + [GeneratedRegex("""^(\-|\!)""")] + private static partial Regex UnaryOperators(); + + static Dictionary unaryMapping { get; } = new Dictionary() + { + { "-", "neg" }, + { "!", "not" } + }; + + bool ParseBinaryOperators(Regex BinaryOperators, Func ParseNext) + { + if (!ParseNext()) + { + return false; + } + int index = ExpressionIndex; + while (ParsePattern(BinaryOperators)) + { + string _operator = PatternMatch.Groups[1].Value; + if (!ParseNext()) { - if (numeric < 0) - { - var isNegative = output.Count == 0 || stack.Count != 0; - - if (!isNegative) - { - stack.Push(("-", MathOperatorPrecedence.Low)); - output.Add(Math.Abs(numeric).ToString()); - continue; - } - } - - output.Add(value); - continue; + ExpressionIndex = index; + return false; } + RPN.Add(new MathToken(MathTokenType.Operator, _operator, null)); + index = ExpressionIndex; + } + return true; + } + + [GeneratedRegex("""^(\-?\d+\.\d+|\-?\d+)""")] + private static partial Regex NumberPattern(); + + [GeneratedRegex("""^["]([^"]*)["]""")] + private static partial Regex StringPattern(); + + [GeneratedRegex("""^(\w+)""")] + private static partial Regex Constants(); + + [GeneratedRegex("""^(\()""")] + private static partial Regex ParenStart(); + + [GeneratedRegex("""^(\))""")] + private static partial Regex ParenEnd(); + + bool ParsePrimary() + { + if (ParsePattern(NumberPattern())) + { + string _number = PatternMatch.Groups[1].Value; + RPN.Add(new MathToken(MathTokenType.Value, _number, double.Parse(_number))); + return true; + } - var @operator = operators.FirstOrDefault(x => x.Name == value); - if (@operator != null) + if (ParsePattern(StringPattern())) + { + string _string = PatternMatch.Groups[1].Value; + RPN.Add(new MathToken(MathTokenType.Value, _string, _string)); + return true; + } + + if (ParseFunction()) + { + return true; + } + + if (ParsePattern(Constants())) + { + string _constant = PatternMatch.Groups[1].Value; + RPN.Add(new MathToken(MathTokenType.Operator, _constant, null)); + return true; + } + + int index = ExpressionIndex; + if (ParsePattern(ParenStart())) + { + if (!ParseExpr()) { - if (@operator.Precedence is MathOperatorPrecedence.Constant) - { - output.Add(value); - continue; - } - - while (stack.Count > 0) - { - var (name, precedence) = stack.Peek(); - if (precedence >= @operator.Precedence) - { - output.Add(stack.Pop().Name); - } - else - { - break; - } - } - - stack.Push((value, @operator.Precedence)); + ExpressionIndex = index; + return false; } - else if (value is "(") + if (!ParsePattern(ParenEnd())) { - stack.Push((value, MathOperatorPrecedence.Lowest)); + ExpressionIndex = index; + return false; } - else if (value is ")") + return true; + } + + index = ExpressionIndex; + if (ParsePattern(UnaryOperators())) + { + string _operator = PatternMatch.Groups[1].Value; + if (unaryMapping.ContainsKey(_operator)) { - 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 = unaryMapping[_operator]; } - else if (value is ",") + if (!ParsePrimary()) { - while (stack.Count > 0) - { - var (name, 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; + } + + [GeneratedRegex("""^(\w+)\(""")] + private static partial Regex FunctionStart(); + + [GeneratedRegex("""^(\,)""")] + private static partial Regex Comma(); + + [GeneratedRegex("""^(\))""")] + private static partial Regex FunctionEnd(); + + bool ParseFunction() + { + int index = ExpressionIndex; + if (!ParsePattern(FunctionStart())) + { + return false; + } + + string text = PatternMatch.Groups[0].Value; + string functionName = PatternMatch.Groups[1].Value; + + if (!ParseExpr()) { - var (name, precedence) = stack.Pop(); - if (name is "(") + ExpressionIndex = index; + return false; + } + + while (ParsePattern(Comma())) + { + if (!ParseExpr()) { - throw new ArgumentException("Invalid math expression."); + ExpressionIndex = index; + return false; } + index = ExpressionIndex; + } + + if (!ParsePattern(FunctionEnd())) + { + ExpressionIndex = index; + return false; + } + + RPN.Add(new MathToken(MathTokenType.Operator, functionName, null)); + + return true; + } + + [GeneratedRegex("""^\s*""")] + private static partial Regex Whitespace(); + + public bool ParsePattern(Regex regex) + { + var whitespaceMatch = Whitespace().Match(Expression.Substring(ExpressionIndex)); + if (whitespaceMatch.Success) + { + ExpressionIndex += whitespaceMatch.Length; + } - output.Add(name); + PatternMatch = regex.Match(Expression.Substring(ExpressionIndex)); + if (!PatternMatch.Success) + { + return false; + } + ExpressionIndex += PatternMatch.Length; + + whitespaceMatch = Whitespace().Match(Expression.Substring(ExpressionIndex)); + if (whitespaceMatch.Success) + { + ExpressionIndex += whitespaceMatch.Length; } - return output; + 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 ceaefd5c6..21fae33ca 100644 --- a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpressionConverter.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpressionConverter.shared.cs @@ -5,10 +5,10 @@ namespace CommunityToolkit.Maui.Converters; /// /// Converters for Math expressions /// -public class MathExpressionConverter : BaseConverterOneWay +public 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. @@ -17,7 +17,7 @@ public class MathExpressionConverter : BaseConverterOneWayThe 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? value, string parameter, CultureInfo? culture = null) { ArgumentNullException.ThrowIfNull(parameter); diff --git a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs index 11317d780..eb6a2f151 100644 --- a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MultiMathExpressionConverter.shared.cs @@ -28,21 +28,7 @@ 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); + var math = new MathExpression(expression, values!); return math.Calculate(); }