Files
PowerToys/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest/ExtendedCalculatorParserTests.cs

433 lines
19 KiB
C#
Raw Normal View History

// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
2021-08-16 15:25:06 +02:00
using System.Collections.Generic;
using System.Globalization;
2021-08-16 15:25:06 +02:00
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.PowerToys.Run.Plugin.Calculator.UnitTests
{
2021-08-16 15:25:06 +02:00
[TestClass]
public class ExtendedCalculatorParserTests
{
2021-08-16 15:25:06 +02:00
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void InputValid_ThrowError_WhenCalledNullOrEmpty(string input)
{
// Act
2021-08-16 15:25:06 +02:00
Assert.ThrowsException<ArgumentNullException>(() => CalculateHelper.InputValid(input));
}
2021-08-16 15:25:06 +02:00
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void Interpret_ThrowError_WhenCalledNullOrEmpty(string input)
{
// Arrange
var engine = new CalculateEngine();
// Act
Assert.ThrowsException<ArgumentNullException>(() => engine.Interpret(input, CultureInfo.CurrentCulture, out _));
}
2021-08-16 15:25:06 +02:00
[DataTestMethod]
[DataRow("test")]
[DataRow("[10,10]")] // '[10,10]' is interpreted as array by mages engine
public void Interpret_NoResult_WhenCalled(string input)
{
// Arrange
var engine = new CalculateEngine();
// Act
var result = engine.Interpret(input, CultureInfo.CurrentCulture, out _);
// Assert
Assert.AreEqual(default(CalculateResult), result);
}
2021-08-16 15:25:06 +02:00
private static IEnumerable<object[]> Interpret_NoErrors_WhenCalledWithRounding_Data =>
new[]
{
new object[] { "2 * 2", 4M },
new object[] { "-2 ^ 2", 4M },
new object[] { "-(2 ^ 2)", -4M },
new object[] { "2 * pi", 6.28318530717959M },
new object[] { "round(2 * pi)", 6M },
new object[] { "1 == 2", default(decimal) },
new object[] { "pi * ( sin ( cos ( 2)))", -1.26995475603563M },
new object[] { "5.6/2", 2.8M },
new object[] { "123 * 4.56", 560.88M },
new object[] { "1 - 9.0 / 10", 0.1M },
new object[] { "0.5 * ((2*-395.2)+198.2)", -296.1M },
new object[] { "2+2.11", 4.11M },
new object[] { "8.43 + 4.43 - 12.86", 0M },
new object[] { "8.43 + 4.43 - 12.8", 0.06M },
new object[] { "exp(5)", 148.413159102577M },
new object[] { "e^5", 148.413159102577M },
new object[] { "e*2", 5.43656365691809M },
new object[] { "ln(3)", 1.09861228866810M },
new object[] { "log(3)", 0.47712125471966M },
new object[] { "log2(3)", 1.58496250072116M },
new object[] { "log10(3)", 0.47712125471966M },
new object[] { "ln(e)", 1M },
2021-08-16 15:25:06 +02:00
new object[] { "cosh(0)", 1M },
};
[DataTestMethod]
[DynamicData(nameof(Interpret_NoErrors_WhenCalledWithRounding_Data))]
public void Interpret_NoErrors_WhenCalledWithRounding(string input, decimal expectedResult)
{
// Arrange
var engine = new CalculateEngine();
// Act
// Using InvariantCulture since this is internal
var result = engine.Interpret(input, CultureInfo.InvariantCulture, out _);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(CalculateEngine.Round(expectedResult), result.RoundedResult);
}
2021-08-16 15:25:06 +02:00
private static IEnumerable<object[]> Interpret_QuirkOutput_WhenCalled_Data =>
new[]
{
new object[] { "123 456", 56088M }, // BUG: Framework accepts ' ' as multiplication
};
[DynamicData(nameof(Interpret_QuirkOutput_WhenCalled_Data))]
[DataTestMethod]
public void Interpret_QuirkOutput_WhenCalled(string input, decimal expectedResult)
{
// Arrange
var engine = new CalculateEngine();
// Act
// Using InvariantCulture since this is internal
var result = engine.Interpret(input, CultureInfo.InvariantCulture, out _);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(expectedResult, result.Result);
}
private static IEnumerable<object[]> Interpret_GreaterPrecision_WhenCalled_Data =>
new[]
{
new object[] { "0.100000000000000000000", 0.1M },
new object[] { "0.200000000000000000000000", 0.2M },
};
[DynamicData(nameof(Interpret_GreaterPrecision_WhenCalled_Data))]
[DataTestMethod]
public void Interpret_GreaterPrecision_WhenCalled(string input, decimal expectedResult)
{
// Arrange
var engine = new CalculateEngine();
// Act
// Using InvariantCulture since this is internal
var result = engine.Interpret(input, CultureInfo.InvariantCulture, out _);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(expectedResult, result.Result);
}
2021-08-16 15:25:06 +02:00
private static IEnumerable<object[]> Interpret_DifferentCulture_WhenCalled_Data =>
new[]
{
new object[] { "4.5/3", 1.5M, "nl-NL" },
new object[] { "4.5/3", 1.5M, "en-EN" },
new object[] { "4.5/3", 1.5M, "de-DE" },
};
[DataTestMethod]
[DynamicData(nameof(Interpret_DifferentCulture_WhenCalled_Data))]
public void Interpret_DifferentCulture_WhenCalled(string input, decimal expectedResult, string cultureName)
{
// Arrange
var cultureInfo = CultureInfo.GetCultureInfo(cultureName);
var engine = new CalculateEngine();
// Act
var result = engine.Interpret(input, cultureInfo, out _);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(CalculateEngine.Round(expectedResult), result.RoundedResult);
}
2021-08-16 15:25:06 +02:00
[DataTestMethod]
[DataRow("log(3)", true)]
[DataRow("ln(3)", true)]
[DataRow("log2(3)", true)]
[DataRow("log10(3)", true)]
[DataRow("log2", false)]
[DataRow("log10", false)]
[DataRow("log", false)]
[DataRow("ln", false)]
2021-08-16 15:25:06 +02:00
[DataRow("ceil(2 * (pi ^ 2))", true)]
[DataRow("((1 * 2)", false)]
[DataRow("(1 * 2)))", false)]
[DataRow("abcde", false)]
[DataRow("1 + 2 +", false)]
[DataRow("1+2*", false)]
[DataRow("1+2/", false)]
[DataRow("1+2%", false)]
2021-08-16 15:25:06 +02:00
[DataRow("1 && 3 &&", false)]
[DataRow("sqrt( 36)", true)]
[DataRow("max 4", false)]
[DataRow("sin(0)", true)]
[DataRow("sinh(1)", true)]
[DataRow("tanh(0)", true)]
[DataRow("artanh(pi/2)", true)]
[DataRow("cosh", false)]
[DataRow("cos", false)]
[DataRow("abs", false)]
[DataRow("1+1.1e3", true)]
[DataRow("randi(8)", true)]
[DataRow("randi()", false)]
[DataRow("randi(0.5)", true)]
[DataRow("rand()", true)]
[DataRow("rand(0.5)", false)]
[DataRow("0X78AD+0o123", true)]
[DataRow("0o9", false)]
public void InputValid_TestValid_WhenCalled(string input, bool valid)
{
// Act
var result = CalculateHelper.InputValid(input);
// Assert
Assert.AreEqual(valid, result);
}
2021-08-16 15:25:06 +02:00
[DataTestMethod]
[DataRow("1-1")]
[DataRow("sin(0)")]
[DataRow("sinh(0)")]
public void Interpret_MustReturnResult_WhenResultIsZero(string input)
{
// Arrange
var engine = new CalculateEngine();
// Act
// Using InvariantCulture since this is internal
var result = engine.Interpret(input, CultureInfo.InvariantCulture, out _);
// Assert
Assert.IsNotNull(result);
2021-08-16 15:25:06 +02:00
Assert.AreEqual(0.0M, result.Result);
}
2021-08-16 15:25:06 +02:00
private static IEnumerable<object[]> Interpret_MustReturnExpectedResult_WhenCalled_Data =>
new[]
{
new object[] { "factorial(5)", 120M },
new object[] { "sign(-2)", -1M },
new object[] { "sign(2)", +1M },
new object[] { "abs(-2)", 2M },
new object[] { "abs(2)", 2M },
new object[] { "0+(1*2)/(0+1)", 2M }, // Validate that division by "(0+1)" is not interpret as division by zero.
new object[] { "0+(1*2)/0.5", 4M }, // Validate that division by number with decimal digits is not interpret as division by zero.
new object[] { "0+(1*2)/0o004", 0.5M }, // Validate that division by an octal number with zeroes is not treated as division by zero.
2021-08-16 15:25:06 +02:00
};
[DataTestMethod]
[DynamicData(nameof(Interpret_MustReturnExpectedResult_WhenCalled_Data))]
public void Interpret_MustReturnExpectedResult_WhenCalled(string input, decimal expectedResult)
{
// Arrange
var engine = new CalculateEngine();
// Act
// Using en-us culture to have a fixed number style
var result = engine.Interpret(input, new CultureInfo("en-us", false), out _);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(expectedResult, result.Result);
}
[Run] Fix Scientific Notation Errors in Calculator Plugin (#28884) * Parse scientific notation properly After adding logic to replace `e` as a mathematical constant, there are bugs when trying to use expressions like `5e3`. This change parses the `<number>e<number>` format into expanded form to prevent replacement with the constant. Regex explanation: `(\d+\.*\d*)[eE](-*\d+) `(\d+\.*\d*)`: Match any number of digits, followed by an optional `.` and more optional digits. The expression is used to capture things like: `5.0`, `1.`, `1` before the `e` `[eE]`: Match either upper or lowercase `e` `(-*\d+)`: Capture an optional `-` sign as well as a number of digits * Update regex to be more tolerant of weird entries The new regex captures a wider variety of numbers. See this post for details on the regex used: https://stackoverflow.com/a/23872060 * Fix regular expression failing unit tests Using `[` didn't capture the expression properly. Had to use `(` instead. * Allow only for uppercase E * Only allow one decimal in second grouping * Support various decimal separator types Previous regular expression did not allow decimal separators that were not ".". The new expression uses the culture info decimal separator instead, which should work better. * Only allow integers after `E` * Remove single use variable * Update regex to only accept integers after `E` Missed this expression in my last update. Whoops * Update src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateHelper.cs * Update NumberTranslator to parse hex as a whole * Remove `hexRegex` as object member The hex regex stuff is only used once, there's no need to keep track of it in the object's state. Just create it during the translation process. * Add unit tests for sci notation in other cultures --------- Co-authored-by: Jaime Bernardo <jaime@janeasystems.com> Co-authored-by: Stefan Markovic <57057282+stefansjfw@users.noreply.github.com>
2024-01-03 06:22:09 -05:00
private static IEnumerable<object[]> Interpret_TestScientificNotation_WhenCalled_Data =>
new[]
{
new object[] { "0.2E1", "en-US", 2M },
new object[] { "0,2E1", "pt-PT", 2M },
};
[DataTestMethod]
[DynamicData(nameof(Interpret_TestScientificNotation_WhenCalled_Data))]
public void Interpret_TestScientificNotation_WhenCalled(string input, string sourceCultureName, decimal expectedResult)
{
// Arrange
var translator = NumberTranslator.Create(new CultureInfo(sourceCultureName, false), new CultureInfo("en-US", false));
var engine = new CalculateEngine();
// Act
// Using en-us culture to have a fixed number style
var translatedInput = translator.Translate(input);
var result = engine.Interpret(translatedInput, new CultureInfo("en-US", false), out _);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(expectedResult, result.Result);
}
[DataTestMethod]
[DataRow("sin(90)", "sin((pi / 180) * (90))")]
[DataRow("arcsin(0.5)", "(180 / pi) * (arcsin(0.5))")]
[DataRow("sin(sin(30))", "sin((pi / 180) * (sin((pi / 180) * (30))))")]
[DataRow("cos(tan(45))", "cos((pi / 180) * (tan((pi / 180) * (45))))")]
[DataRow("arctan(sin(30))", "(180 / pi) * (arctan(sin((pi / 180) * (30))))")]
[DataRow("sin(cos(tan(30)))", "sin((pi / 180) * (cos((pi / 180) * (tan((pi / 180) * (30))))))")]
[DataRow("sin(arcsin(0.5))", "sin((pi / 180) * ((180 / pi) * (arcsin(0.5))))")]
[DataRow("sin(30) + cos(60)", "sin((pi / 180) * (30)) + cos((pi / 180) * (60))")]
[DataRow("sin(30 + 15)", "sin((pi / 180) * (30 + 15))")]
[DataRow("sin(45) * cos(45) - tan(30)", "sin((pi / 180) * (45)) * cos((pi / 180) * (45)) - tan((pi / 180) * (30))")]
[DataRow("arcsin(arccos(0.5))", "(180 / pi) * (arcsin((180 / pi) * (arccos(0.5))))")]
[DataRow("sin(sin(sin(30)))", "sin((pi / 180) * (sin((pi / 180) * (sin((pi / 180) * (30))))))")]
[DataRow("log(10)", "log(10)")]
[DataRow("sin(30) + pi", "sin((pi / 180) * (30)) + pi")]
[DataRow("sin(-30)", "sin((pi / 180) * (-30))")]
[DataRow("sin((30))", "sin((pi / 180) * ((30)))")]
[DataRow("arcsin(1) * 2", "(180 / pi) * (arcsin(1)) * 2")]
[DataRow("cos(1/2)", "cos((pi / 180) * (1/2))")]
[DataRow("sin ( 90 )", "sin ((pi / 180) * ( 90 ))")]
[DataRow("cos(arcsin(sin(45)))", "cos((pi / 180) * ((180 / pi) * (arcsin(sin((pi / 180) * (45))))))")]
public void UpdateTrigFunctions_Degrees(string input, string expectedResult)
{
// Call UpdateTrigFunctions in degrees mode
string result = CalculateHelper.UpdateTrigFunctions(input, CalculateEngine.TrigMode.Degrees);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(expectedResult, result);
}
[DataTestMethod]
[DataRow("sin(90)", "sin((pi / 200) * (90))")]
[DataRow("arcsin(0.5)", "(200 / pi) * (arcsin(0.5))")]
[DataRow("sin(sin(30))", "sin((pi / 200) * (sin((pi / 200) * (30))))")]
[DataRow("cos(tan(45))", "cos((pi / 200) * (tan((pi / 200) * (45))))")]
[DataRow("arctan(sin(30))", "(200 / pi) * (arctan(sin((pi / 200) * (30))))")]
[DataRow("sin(cos(tan(30)))", "sin((pi / 200) * (cos((pi / 200) * (tan((pi / 200) * (30))))))")]
[DataRow("sin(arcsin(0.5))", "sin((pi / 200) * ((200 / pi) * (arcsin(0.5))))")]
[DataRow("sin(30) + cos(60)", "sin((pi / 200) * (30)) + cos((pi / 200) * (60))")]
[DataRow("sin(30 + 15)", "sin((pi / 200) * (30 + 15))")]
[DataRow("sin(45) * cos(45) - tan(30)", "sin((pi / 200) * (45)) * cos((pi / 200) * (45)) - tan((pi / 200) * (30))")]
[DataRow("arcsin(arccos(0.5))", "(200 / pi) * (arcsin((200 / pi) * (arccos(0.5))))")]
[DataRow("sin(sin(sin(30)))", "sin((pi / 200) * (sin((pi / 200) * (sin((pi / 200) * (30))))))")]
[DataRow("log(10)", "log(10)")]
[DataRow("sin(30) + pi", "sin((pi / 200) * (30)) + pi")]
[DataRow("sin(-30)", "sin((pi / 200) * (-30))")]
[DataRow("sin((30))", "sin((pi / 200) * ((30)))")]
[DataRow("arcsin(1) * 2", "(200 / pi) * (arcsin(1)) * 2")]
[DataRow("cos(1/2)", "cos((pi / 200) * (1/2))")]
[DataRow("sin ( 90 )", "sin ((pi / 200) * ( 90 ))")]
[DataRow("cos(arcsin(sin(45)))", "cos((pi / 200) * ((200 / pi) * (arcsin(sin((pi / 200) * (45))))))")]
public void UpdateTrigFunctions_Gradians(string input, string expectedResult)
{
// Call UpdateTrigFunctions in gradians mode
string result = CalculateHelper.UpdateTrigFunctions(input, CalculateEngine.TrigMode.Gradians);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(expectedResult, result);
}
[DataTestMethod]
[DataRow("rad(30)", "(180 / pi) * (30)")]
[DataRow("rad( 30 )", "(180 / pi) * ( 30 )")]
[DataRow("deg(30)", "(30)")]
[DataRow("grad(30)", "(9 / 10) * (30)")]
[DataRow("rad( 30)", "(180 / pi) * ( 30)")]
[DataRow("rad(30 )", "(180 / pi) * (30 )")]
[DataRow("rad( 30 )", "(180 / pi) * ( 30 )")]
[DataRow("rad(deg(30))", "(180 / pi) * ((30))")]
[DataRow("deg(rad(30))", "((180 / pi) * (30))")]
[DataRow("grad(rad(30))", "(9 / 10) * ((180 / pi) * (30))")]
[DataRow("rad(grad(30))", "(180 / pi) * ((9 / 10) * (30))")]
[DataRow("rad(30) + deg(45)", "(180 / pi) * (30) + (45)")]
[DataRow("sin(rad(30))", "sin((180 / pi) * (30))")]
[DataRow("cos( rad( 45 ) )", "cos( (180 / pi) * ( 45 ) )")]
[DataRow("tan(rad(grad(90)))", "tan((180 / pi) * ((9 / 10) * (90)))")]
[DataRow("rad(30) + rad(45)", "(180 / pi) * (30) + (180 / pi) * (45)")]
[DataRow("rad(30) * grad(90)", "(180 / pi) * (30) * (9 / 10) * (90)")]
[DataRow("rad(30)/rad(45)", "(180 / pi) * (30)/(180 / pi) * (45)")]
public void ExpandTrigConversions_Degrees(string input, string expectedResult)
{
// Call ExpandTrigConversions in degrees mode
string result = CalculateHelper.ExpandTrigConversions(input, CalculateEngine.TrigMode.Degrees);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(expectedResult, result);
}
[DataTestMethod]
[DataRow("rad(30)", "(30)")]
[DataRow("rad( 30 )", "( 30 )")]
[DataRow("deg(30)", "(pi / 180) * (30)")]
[DataRow("grad(30)", "(pi / 200) * (30)")]
[DataRow("rad( 30)", "( 30)")]
[DataRow("rad(30 )", "(30 )")]
[DataRow("rad( 30 )", "( 30 )")]
[DataRow("rad(deg(30))", "((pi / 180) * (30))")]
[DataRow("deg(rad(30))", "(pi / 180) * ((30))")]
[DataRow("grad(rad(30))", "(pi / 200) * ((30))")]
[DataRow("rad(grad(30))", "((pi / 200) * (30))")]
[DataRow("rad(30) + deg(45)", "(30) + (pi / 180) * (45)")]
[DataRow("sin(rad(30))", "sin((30))")]
[DataRow("cos( rad( 45 ) )", "cos( ( 45 ) )")]
[DataRow("tan(rad(grad(90)))", "tan(((pi / 200) * (90)))")]
[DataRow("rad(30) + rad(45)", "(30) + (45)")]
[DataRow("rad(30) * grad(90)", "(30) * (pi / 200) * (90)")]
[DataRow("rad(30)/rad(45)", "(30)/(45)")]
public void ExpandTrigConversions_Radians(string input, string expectedResult)
{
// Call ExpandTrigConversions in radians mode
string result = CalculateHelper.ExpandTrigConversions(input, CalculateEngine.TrigMode.Radians);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(expectedResult, result);
}
[DataTestMethod]
[DataRow("rad(30)", "(200 / pi) * (30)")]
[DataRow("rad( 30 )", "(200 / pi) * ( 30 )")]
[DataRow("deg(30)", "(10 / 9) * (30)")]
[DataRow("grad(30)", "(30)")]
[DataRow("rad( 30)", "(200 / pi) * ( 30)")]
[DataRow("rad(30 )", "(200 / pi) * (30 )")]
[DataRow("rad( 30 )", "(200 / pi) * ( 30 )")]
[DataRow("rad(deg(30))", "(200 / pi) * ((10 / 9) * (30))")]
[DataRow("deg(rad(30))", "(10 / 9) * ((200 / pi) * (30))")]
[DataRow("grad(rad(30))", "((200 / pi) * (30))")]
[DataRow("rad(grad(30))", "(200 / pi) * ((30))")]
[DataRow("rad(30) + deg(45)", "(200 / pi) * (30) + (10 / 9) * (45)")]
[DataRow("sin(rad(30))", "sin((200 / pi) * (30))")]
[DataRow("cos( rad( 45 ) )", "cos( (200 / pi) * ( 45 ) )")]
[DataRow("tan(rad(grad(90)))", "tan((200 / pi) * ((90)))")]
[DataRow("rad(30) + rad(45)", "(200 / pi) * (30) + (200 / pi) * (45)")]
[DataRow("rad(30) * grad(90)", "(200 / pi) * (30) * (90)")]
[DataRow("rad(30)/rad(45)", "(200 / pi) * (30)/(200 / pi) * (45)")]
public void ExpandTrigConversions_Gradians(string input, string expectedResult)
{
// Call ExpandTrigConversions in gradians mode
string result = CalculateHelper.ExpandTrigConversions(input, CalculateEngine.TrigMode.Gradians);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(expectedResult, result);
}
}
}