diff --git a/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs b/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs index a6ab9e45d6..500c38a78c 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Converters/MathExpressionConverterTests.cs @@ -258,4 +258,30 @@ 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)); } + + [Theory] + [InlineData("en-US", "x0 ? -123.4 : 123.4", new object?[] { true }, -123.4)] + [InlineData("en-US", "x0 ? -123.4 : 123.4", new object?[] { false }, 123.4)] + [InlineData("en-US", "str(x0)", new object?[] { 3.1415 }, "3.1415")] + [InlineData("en-US", "len(str(x0))", new object?[] { 3.1415 }, 6)] + [InlineData("en-US", "x0 * x1", new object?[] { "3.1415", 2 }, 6.283)] + [InlineData("en-US", "int(double(x0))", new object?[] { "3.1415" }, 3)] + [InlineData("en-US", "int(double(x0)) / x1", new object?[] { "3.1415", 2 }, 1.5)] + [InlineData("ar-AR", "x0 ? -123.4 : 123.4", new object?[] { true }, -123.4)] + [InlineData("ar-AR", "x0 ? -123.4 : 123.4", new object?[] { false }, 123.4)] + [InlineData("ar-AR", "str(x0)", new object?[] { 3.1415 }, "3.1415")] + [InlineData("ar-AR", "len(str(x0))", new object?[] { 3.1415 }, 6)] + [InlineData("ar-AR", "x0 * x1", new object?[] { "3.1415", 2 }, 6.283)] + [InlineData("ar-AR", "int(double(x0))", new object?[] { "3.1415" }, 3)] + [InlineData("ar-AR", "int(double(x0)) / x1", new object?[] { "3.1415", 2 }, 1.5)] + public void MathExpressionConverter_WithAlternateCulture_ReturnsCorrectNumericResult(string cultureName, string expression, object[] variables, object? expectedResult) + { + CultureInfo.CurrentCulture = new CultureInfo(cultureName); + var mathExpressionConverter = new MultiMathExpressionConverter(); + + object? result = mathExpressionConverter.Convert(variables, mathExpressionTargetType, expression); + + Assert.True(result is not null); + Assert.Equal(expectedResult, result); + } } \ 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 28167ff982..851d468d81 100644 --- a/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/MathExpressionConverter/MathExpression.shared.cs @@ -1,6 +1,5 @@ using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Text.RegularExpressions; using CommunityToolkit.Maui.Core; @@ -27,11 +26,11 @@ internal MathExpression(in string expression, in IReadOnlyList argument 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 ("+", 2, x => ConvertToDouble(x[0]) + ConvertToDouble(x[1])), + new ("-", 2, x => ConvertToDouble(x[0]) - ConvertToDouble(x[1])), + new ("*", 2, x => ConvertToDouble(x[0]) * ConvertToDouble(x[1])), + new ("/", 2, x => ConvertToDouble(x[0]) / ConvertToDouble(x[1])), + new ("%", 2, x => ConvertToDouble(x[0]) % ConvertToDouble(x[1])), new ("and", 2, x => ConvertToBoolean(x[0]) ? x[1] : x[0]), new ("or", 2, x => ConvertToBoolean(x[0]) ? x[0] : x[1]), @@ -39,44 +38,44 @@ internal MathExpression(in string expression, in IReadOnlyList argument 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 ("ge", 2, x => ConvertToDouble(x[0]) >= ConvertToDouble(x[1])), + new ("gt", 2, x => ConvertToDouble(x[0]) > ConvertToDouble(x[1])), + new ("le", 2, x => ConvertToDouble(x[0]) <= ConvertToDouble(x[1])), + new ("lt", 2, x => ConvertToDouble(x[0]) < ConvertToDouble(x[1])), + new ("neg", 1, x => -ConvertToDouble(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 ("abs", 1, x => Math.Abs(ConvertToDouble(x[0]))), + new ("acos", 1, x => Math.Acos(ConvertToDouble(x[0]))), + new ("asin", 1, x => Math.Asin(ConvertToDouble(x[0]))), + new ("atan", 1, x => Math.Atan(ConvertToDouble(x[0]))), + new ("atan2", 2, x => Math.Atan2(ConvertToDouble(x[0]), ConvertToDouble(x[1]))), + new ("ceiling", 1, x => Math.Ceiling(ConvertToDouble(x[0]))), + new ("cos", 1, x => Math.Cos(ConvertToDouble(x[0]))), + new ("cosh", 1, x => Math.Cosh(ConvertToDouble(x[0]))), + new ("exp", 1, x => Math.Exp(ConvertToDouble(x[0]))), + new ("floor", 1, x => Math.Floor(ConvertToDouble(x[0]))), + new ("ieeeremainder", 2, x => Math.IEEERemainder(ConvertToDouble(x[0]), ConvertToDouble(x[1]))), + new ("log", 2, x => Math.Log(ConvertToDouble(x[0]), ConvertToDouble(x[1]))), + new ("log10", 1, x => Math.Log10(ConvertToDouble(x[0]))), + new ("max", 2, x => Math.Max(ConvertToDouble(x[0]), ConvertToDouble(x[1]))), + new ("min", 2, x => Math.Min(ConvertToDouble(x[0]), ConvertToDouble(x[1]))), + new ("pow", 2, x => Math.Pow(ConvertToDouble(x[0]), ConvertToDouble(x[1]))), + new ("round", 2, x => Math.Round(ConvertToDouble(x[0]), ConvertToInt32(x[1]))), + new ("sign", 1, x => Math.Sign(ConvertToDouble(x[0]))), + new ("sin", 1, x => Math.Sin(ConvertToDouble(x[0]))), + new ("sinh", 1, x => Math.Sinh(ConvertToDouble(x[0]))), + new ("sqrt", 1, x => Math.Sqrt(ConvertToDouble(x[0]))), + new ("tan", 1, x => Math.Tan(ConvertToDouble(x[0]))), + new ("tanh", 1, x => Math.Tanh(ConvertToDouble(x[0]))), + new ("truncate", 1, x => Math.Truncate(ConvertToDouble(x[0]))), + new ("int", 1, x => ConvertToInt32(x[0])), + new ("double", 1, x => ConvertToDouble(x[0])), + new ("bool", 1, x => ConvertToBoolean(x[0])), + new ("str", 1, x => ConvertToString(x[0])), + new ("len", 1, x => ConvertToString(x[0])?.Length), + new ("^", 2, x => Math.Pow(ConvertToDouble(x[0]), ConvertToDouble(x[1]))), new ("pi", 0, _ => Math.PI), new ("e", 0, _ => Math.E), new ("true", 0, _ => true), @@ -248,9 +247,15 @@ internal MathExpression(in string expression, in IReadOnlyList argument null => false, double doubleValue => doubleValue != 0 && !double.IsNaN(doubleValue), string stringValue => !string.IsNullOrEmpty(stringValue), - _ => Convert.ToBoolean(b) + _ => Convert.ToBoolean(b, CultureInfo.InvariantCulture) }; + static double ConvertToDouble(object? x) => Convert.ToDouble(x, CultureInfo.InvariantCulture); + + static int ConvertToInt32(object? x) => Convert.ToInt32(x, CultureInfo.InvariantCulture); + + static string? ConvertToString(object? x) => Convert.ToString(x, CultureInfo.InvariantCulture); + bool ParsePattern(Regex regex) { var whitespaceMatch = EvaluateWhitespace().Match(Expression[ExpressionIndex..]); @@ -362,7 +367,7 @@ bool ParsePrimary() if (ParsePattern(EvaluateNumberPattern())) { string _number = PatternMatch.Groups[1].Value; - RPN.Add(new MathToken(MathTokenType.Value, _number, double.Parse(_number))); + RPN.Add(new MathToken(MathTokenType.Value, _number, double.Parse(_number, CultureInfo.InvariantCulture))); return true; }