Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calculator - Human multiplication expressions #24655

Merged
merged 3 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion doc/devdocs/modules/launcher/plugins/calculator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?)")]
Expand All @@ -41,7 +40,6 @@ public void ErrorResultOnInvalidKeywordQuery(string typedString, string expected

[DataTestMethod]
[DataRow("pi(9+)")]
[DataRow("pi(9)")]
[DataRow("pi,")]
[DataRow("log()")]
[DataRow("0xf0x6")]
Expand Down Expand Up @@ -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> 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> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}