mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-01-20 16:03:17 +08:00
[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:
parent
302abf7082
commit
114f1b45e2
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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)
|
||||
|
@ -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)}";
|
||||
|
Loading…
Reference in New Issue
Block a user