[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>
This commit is contained in:
Vignesh Dhanasekaran 2024-01-03 06:22:09 -05:00 committed by GitHub
parent 302abf7082
commit 114f1b45e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 24 deletions

View File

@ -245,5 +245,30 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator.UnitTests
Assert.IsNotNull(result);
Assert.AreEqual(expectedResult, result.Result);
}
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);
}
}
}

View File

@ -145,6 +145,15 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator.UnitTests
[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)")]
[DataRow("1.0E2", "(1.0 * 10^(2))")]
[DataRow("-1.0E-2", "(-1.0 * 10^(-2))")]
[DataRow("1.2E2", "(1.2 * 10^(2))")]
[DataRow("5/1.0E2", "5/(1.0 * 10^(2))")]
[DataRow("0.1E2", "(0.1 * 10^(2))")]
[DataRow(".1E2", "(.1 * 10^(2))")]
[DataRow(".1E2", "(.1 * 10^(2))")]
[DataRow("5/5E3", "5/(5 * 10^(3))")]
[DataRow("1.E2", "(1. * 10^(2))")]
public void RightHumanMultiplicationExpressionTransformation(string typedString, string expectedQuery)
{
// Setup
@ -200,6 +209,12 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator.UnitTests
[DataRow("pilog(100)", 6.2831853072)]
[DataRow("3log(100)", 6)]
[DataRow("2e", 5.4365636569)]
[DataRow("2E2", 200)]
[DataRow("2E-2", 0.02)]
[DataRow("1.2E2", 120)]
[DataRow("1.2E-1", 0.12)]
[DataRow("5/5E3", 0.001)]
[DataRow("-5/5E3", -0.001)]
[DataRow("(1+1)(3+2)", 10)]
[DataRow("(1+1)cos(pi)", -2)]
[DataRow("log(100)cos(pi)", -2)]

View File

@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Microsoft.PowerToys.Run.Plugin.Calculator
@ -18,6 +20,7 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator
@"sinh\s*\(|cosh\s*\(|tanh\s*\(|arsinh\s*\(|arcosh\s*\(|artanh\s*\(|" +
@"pi|" +
@"==|~=|&&|\|\||" +
@"((-?(\d+(\.\d*)?)|-?(\.\d+))[E](-?\d+))|" + /* expression from CheckScientificNotation between parenthesis */
@"e|[0-9]|0x[0-9a-fA-F]+|0b[01]+|[\+\-\*\/\^\., ""]|[\(\)\|\!\[\]]" +
@")+$",
RegexOptions.Compiled);
@ -51,7 +54,8 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator
public static string FixHumanMultiplicationExpressions(string input)
{
var output = CheckNumberOrConstantThenParenthesisExpr(input);
var output = CheckScientificNotation(input);
output = CheckNumberOrConstantThenParenthesisExpr(output);
output = CheckNumberOrConstantThenFunc(output);
output = CheckParenthesisExprThenFunc(output);
output = CheckParenthesisExprThenParenthesisExpr(output);
@ -60,6 +64,22 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator
return output;
}
private static string CheckScientificNotation(string input)
{
/**
* NOTE: By the time the expression gets to us, it's already in English format.
*
* Regex explanation:
* (-?(\d+({0}\d*)?)|-?({0}\d+)): Used to capture one of two types:
* -?(\d+({0}\d*)?): Captures a decimal number starting with a number (e.g. "-1.23")
* -?({0}\d+): Captures a decimal number without leading number (e.g. ".23")
* E: Captures capital 'E'
* (-?\d+): Captures an integer number (e.g. "-1" or "23")
*/
var p = @"(-?(\d+(\.\d*)?)|-?(\.\d+))E(-?\d+)";
return Regex.Replace(input, p, "($1 * 10^($5))");
}
/*
* num (exp)
* const (exp)

View File

@ -66,35 +66,47 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator
private static string Translate(string input, CultureInfo cultureFrom, CultureInfo cultureTo, Regex splitRegex)
{
var outputBuilder = new StringBuilder();
var hexRegex = new Regex(@"(?:(0x[\da-fA-F]+))");
string[] tokens = splitRegex.Split(input);
foreach (string token in tokens)
string[] hexTokens = hexRegex.Split(input);
foreach (string hexToken in hexTokens)
{
int leadingZeroCount = 0;
// Count leading zero characters.
foreach (char c in token)
if (hexToken.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase))
{
if (c != '0')
outputBuilder.Append(hexToken);
continue;
}
string[] tokens = splitRegex.Split(hexToken);
foreach (string token in tokens)
{
int leadingZeroCount = 0;
// Count leading zero characters.
foreach (char c in token)
{
break;
if (c != '0')
{
break;
}
leadingZeroCount++;
}
leadingZeroCount++;
// number is all zero characters. no need to add zero characters at the end.
if (token.Length == leadingZeroCount)
{
leadingZeroCount = 0;
}
decimal number;
outputBuilder.Append(
decimal.TryParse(token, NumberStyles.Number, cultureFrom, out number)
? (new string('0', leadingZeroCount) + number.ToString(cultureTo))
: token);
}
// number is all zero characters. no need to add zero characters at the end.
if (token.Length == leadingZeroCount)
{
leadingZeroCount = 0;
}
decimal number;
outputBuilder.Append(
decimal.TryParse(token, NumberStyles.Number, cultureFrom, out number)
? (new string('0', leadingZeroCount) + number.ToString(cultureTo))
: token);
}
return outputBuilder.ToString();
@ -102,7 +114,7 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator
private static Regex GetSplitRegex(CultureInfo culture)
{
var splitPattern = $"((?:\\d|[a-fA-F]|{Regex.Escape(culture.NumberFormat.NumberDecimalSeparator)}";
var splitPattern = $"((?:\\d|{Regex.Escape(culture.NumberFormat.NumberDecimalSeparator)}";
if (!string.IsNullOrEmpty(culture.NumberFormat.NumberGroupSeparator))
{
splitPattern += $"|{Regex.Escape(culture.NumberFormat.NumberGroupSeparator)}";