diff --git a/doc/devdocs/modules/launcher/plugins/calculator.md b/doc/devdocs/modules/launcher/plugins/calculator.md index 79ffe208a840..d9ac8e1e56aa 100644 --- a/doc/devdocs/modules/launcher/plugins/calculator.md +++ b/doc/devdocs/modules/launcher/plugins/calculator.md @@ -21,7 +21,12 @@ The Calculator plugin as the name suggests is used to perform calculations on th ### [`CalculateHelper`](/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateHelper.cs) - The [`CalculateHelper.cs`](src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateHelper.cs) class checks to see if the user entered query is a valid input to the calculator and only if the input is valid does it perform the operation. -- It does so by matching the user query to a valid regex. + - It does so by matching the user query to a valid regex. +- This class also handles some human multiplication expression like `2(1+2)` and `(2+3)(3+4)` in order to be computed by `Mages` lib. + - It does so by matching some regex and inserting `'*'` where appropriate, e.g: `2(1+2) -> 2 * (1+2)` + - It takes into account the combination of numbers (`num`), constants (`const`), functions (`func`) and expressions in parentheses (`(exp)`). + - The blank spaces between them are also considered. + - Some combinations were not handled as they are not common such as `'const num'` or `'func const'` ### [`CalculateEngine`](src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateEngine.cs) - The main computation is done in the [`CalculateEngine.cs`](src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateEngine.cs) file using the `Mages` library. diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest/ExtendedCalculatorParserTests.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest/ExtendedCalculatorParserTests.cs index 929fa8489acf..9332eee8134b 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest/ExtendedCalculatorParserTests.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest/ExtendedCalculatorParserTests.cs @@ -37,8 +37,6 @@ public void Interpret_ThrowError_WhenCalledNullOrEmpty(string input) [DataTestMethod] [DataRow("test")] - [DataRow("pi(2)")] // Incorrect input, constant is being treated as a function. - [DataRow("e(2)")] [DataRow("[10,10]")] // '[10,10]' is interpreted as array by mages engine public void Interpret_NoResult_WhenCalled(string input) { diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest/QueryTests.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest/QueryTests.cs index 56dd89430a19..d5e11bbc8a7a 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest/QueryTests.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest/QueryTests.cs @@ -14,7 +14,6 @@ public class QueryTests { [DataTestMethod] [DataRow("=pi(9+)", "Expression wrong or incomplete (Did you forget some parentheses?)")] - [DataRow("=pi(9)", "Expression wrong or incomplete (Did you forget some parentheses?)")] [DataRow("=pi,", "Expression wrong or incomplete (Did you forget some parentheses?)")] [DataRow("=log()", "Expression wrong or incomplete (Did you forget some parentheses?)")] [DataRow("=0xf0x6", "Expression wrong or incomplete (Did you forget some parentheses?)")] @@ -41,7 +40,6 @@ public void ErrorResultOnInvalidKeywordQuery(string typedString, string expected [DataTestMethod] [DataRow("pi(9+)")] - [DataRow("pi(9)")] [DataRow("pi,")] [DataRow("log()")] [DataRow("0xf0x6")] @@ -113,5 +111,111 @@ public void NoErrorForDivisionByNumberWithDecimalDigits(string typedString) Assert.AreEqual(result, "Copy this number to the clipboard"); Assert.AreEqual(resultWithKeyword, "Copy this number to the clipboard"); } + + [DataTestMethod] + [DataRow("pie", "pi * e")] + [DataRow("eln(100)", "e * ln(100)")] + [DataRow("pi(1+1)", "pi * (1+1)")] + [DataRow("2pi", "2 * pi")] + [DataRow("2log10(100)", "2 * log10(100)")] + [DataRow("2(3+4)", "2 * (3+4)")] + [DataRow("sin(pi)cos(pi)", "sin(pi) * cos(pi)")] + [DataRow("log10(100)(2+3)", "log10(100) * (2+3)")] + [DataRow("(1+1)cos(pi)", "(1+1) * cos(pi)")] + [DataRow("(1+1)(2+2)", "(1+1) * (2+2)")] + [DataRow("2(1+1)", "2 * (1+1)")] + [DataRow("pi(1+1)", "pi * (1+1)")] + [DataRow("pilog(100)", "pi * log(100)")] + [DataRow("3log(100)", "3 * log(100)")] + [DataRow("2e", "2 * e")] + [DataRow("(1+1)(3+2)", "(1+1) * (3+2)")] + [DataRow("(1+1)cos(pi)", "(1+1) * cos(pi)")] + [DataRow("sin(pi)cos(pi)", "sin(pi) * cos(pi)")] + [DataRow("2 (1+1)", "2 * (1+1)")] + [DataRow("pi (1+1)", "pi * (1+1)")] + [DataRow("pi log(100)", "pi * log(100)")] + [DataRow("3 log(100)", "3 * log(100)")] + [DataRow("2 e", "2 * e")] + [DataRow("(1+1) (3+2)", "(1+1) * (3+2)")] + [DataRow("(1+1) cos(pi)", "(1+1) * cos(pi)")] + [DataRow("sin (pi) cos(pi)", "sin (pi) * cos(pi)")] + [DataRow("2picos(pi)(1+1)", "2 * pi * cos(pi) * (1+1)")] + [DataRow("pilog(100)log(1000)", "pi * log(100) * log(1000)")] + [DataRow("pipipie", "pi * pi * pi * e")] + [DataRow("(1+1)(3+2)(1+1)(1+1)", "(1+1) * (3+2) * (1+1) * (1+1)")] + [DataRow("(1+1) (3+2) (1+1)(1+1)", "(1+1) * (3+2) * (1+1) * (1+1)")] + public void RightHumanMultiplicationExpressionTransformation(string typedString, string expectedQuery) + { + // Setup + + // Act + var result = CalculateHelper.FixHumanMultiplicationExpressions(typedString); + + // Assert + Assert.AreEqual(expectedQuery, result); + } + + [DataTestMethod] + [DataRow("2(1+1)")] + [DataRow("pi(1+1)")] + [DataRow("pilog(100)")] + [DataRow("3log(100)")] + [DataRow("2e")] + [DataRow("(1+1)(3+2)")] + [DataRow("(1+1)cos(pi)")] + [DataRow("sin(pi)cos(pi)")] + [DataRow("2 (1+1)")] + [DataRow("pi (1+1)")] + [DataRow("pi log(100)")] + [DataRow("3 log(100)")] + [DataRow("2 e")] + [DataRow("(1+1) (3+2)")] + [DataRow("(1+1) cos(pi)")] + [DataRow("sin (pi) cos(pi)")] + [DataRow("2picos(pi)(1+1)")] + [DataRow("pilog(100)log(1000)")] + [DataRow("pipipie")] + [DataRow("(1+1)(3+2)(1+1)(1+1)")] + [DataRow("(1+1) (3+2) (1+1)(1+1)")] + public void NoErrorForHumanMultiplicationExpressions(string typedString) + { + // Setup + Mock
main = new(); + Query expectedQuery = new(typedString); + Query expectedQueryWithKeyword = new("=" + typedString, "="); + + // Act + var result = main.Object.Query(expectedQuery).FirstOrDefault()?.SubTitle; + var resultWithKeyword = main.Object.Query(expectedQueryWithKeyword).FirstOrDefault()?.SubTitle; + + // Assert + Assert.AreEqual("Copy this number to the clipboard", result); + Assert.AreEqual("Copy this number to the clipboard", resultWithKeyword); + } + + [DataTestMethod] + [DataRow("2(1+1)", "4")] + [DataRow("pi(1+1)", "6.2831853072")] + [DataRow("pilog(100)", "6.2831853072")] + [DataRow("3log(100)", "6")] + [DataRow("2e", "5.4365636569")] + [DataRow("(1+1)(3+2)", "10")] + [DataRow("(1+1)cos(pi)", "-2")] + [DataRow("log(100)cos(pi)", "-2")] + public void RightAnswerForHumanMultiplicationExpressions(string typedString, string answer) + { + // Setup + Mock
main = new(); + Query expectedQuery = new(typedString); + Query expectedQueryWithKeyword = new("=" + typedString, "="); + + // Act + var result = main.Object.Query(expectedQuery).FirstOrDefault()?.Title; + var resultWithKeyword = main.Object.Query(expectedQueryWithKeyword).FirstOrDefault()?.Title; + + // Assert + Assert.AreEqual(answer, result); + Assert.AreEqual(answer, resultWithKeyword); + } } } diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateEngine.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateEngine.cs index e69a5eee0518..eed608e24fed 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateEngine.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateEngine.cs @@ -49,6 +49,8 @@ public CalculateResult Interpret(string input, CultureInfo cultureInfo, out stri Replace("log(", "log10(", true, CultureInfo.CurrentCulture). Replace("ln(", "log(", true, CultureInfo.CurrentCulture); + input = CalculateHelper.FixHumanMultiplicationExpressions(input); + var result = _magesEngine.Interpret(input); // This could happen for some incorrect queries, like pi(2) diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateHelper.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateHelper.cs index d0908b3de0ba..50b94b1e541b 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateHelper.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.Calculator/CalculateHelper.cs @@ -48,5 +48,141 @@ public static bool InputValid(string input) return true; } + + public static string FixHumanMultiplicationExpressions(string input) + { + var output = CheckNumberOrConstantThenParenthesisExpr(input); + output = CheckNumberOrConstantThenFunc(output); + output = CheckParenthesisExprThenFunc(output); + output = CheckParenthesisExprThenParenthesisExpr(output); + output = CheckNumberThenConstant(output); + output = CheckConstantThenConstant(output); + return output; + } + + /* + * num (exp) + * const (exp) + */ + private static string CheckNumberOrConstantThenParenthesisExpr(string input) + { + var output = input; + do + { + input = output; + output = Regex.Replace(input, @"(\d+|pi|e)\s*(\()", m => + { + if (m.Index > 0 && char.IsLetter(input[m.Index - 1])) + { + return m.Value; + } + + return $"{m.Groups[1].Value} * {m.Groups[2].Value}"; + }); + } + while (output != input); + + return output; + } + + /* + * num func + * const func + */ + private static string CheckNumberOrConstantThenFunc(string input) + { + var output = input; + do + { + input = output; + output = Regex.Replace(input, @"(\d+|pi|e)\s*([a-zA-Z]+[0-9]*\s*\()", m => + { + if (input[m.Index] == 'e' && input[m.Index + 1] == 'x' && input[m.Index + 2] == 'p') + { + return m.Value; + } + + if (m.Index > 0 && char.IsLetter(input[m.Index - 1])) + { + return m.Value; + } + + return $"{m.Groups[1].Value} * {m.Groups[2].Value}"; + }); + } + while (output != input); + + return output; + } + + /* + * (exp) func + * func func + */ + private static string CheckParenthesisExprThenFunc(string input) + { + var p = @"(\))\s*([a-zA-Z]+[0-9]*\s*\()"; + var r = "$1 * $2"; + return Regex.Replace(input, p, r); + } + + /* + * (exp) (exp) + * func (exp) + */ + private static string CheckParenthesisExprThenParenthesisExpr(string input) + { + var p = @"(\))\s*(\()"; + var r = "$1 * $2"; + return Regex.Replace(input, p, r); + } + + /* + * num const + */ + private static string CheckNumberThenConstant(string input) + { + var output = input; + do + { + input = output; + output = Regex.Replace(input, @"(\d+)\s*(pi|e)", m => + { + if (m.Index > 0 && char.IsLetter(input[m.Index - 1])) + { + return m.Value; + } + + return $"{m.Groups[1].Value} * {m.Groups[2].Value}"; + }); + } + while (output != input); + + return output; + } + + /* + * const const + */ + private static string CheckConstantThenConstant(string input) + { + var output = input; + do + { + input = output; + output = Regex.Replace(input, @"(pi|e)\s*(pi|e)", m => + { + if (m.Index > 0 && char.IsLetter(input[m.Index - 1])) + { + return m.Value; + } + + return $"{m.Groups[1].Value} * {m.Groups[2].Value}"; + }); + } + while (output != input); + + return output; + } } }