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();
}
///