diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml
index 502c07238e6..25aaf97e71c 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml
@@ -4,7 +4,7 @@
xmlns:system="clr-namespace:System;assembly=mscorlib">
Calculator
- Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher)
+ Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place.
Not a number (NaN)
Expression wrong or incomplete (Did you forget some parentheses?)
Copy this number to the clipboard
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
index 195d63e4772..0744024f4d2 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows.Controls;
@@ -14,6 +15,9 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
{
private static readonly Regex RegValidExpressChar = MainRegexHelper.GetRegValidExpressChar();
private static readonly Regex RegBrackets = MainRegexHelper.GetRegBrackets();
+ private static readonly Regex ThousandGroupRegex = MainRegexHelper.GetThousandGroupRegex();
+ private static readonly Regex NumberRegex = MainRegexHelper.GetNumberRegex();
+
private static Engine MagesEngine;
private const string Comma = ",";
private const string Dot = ".";
@@ -23,6 +27,16 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
private Settings _settings;
private SettingsViewModel _viewModel;
+ ///
+ /// Holds the formatting information for a single query.
+ /// This is used to ensure thread safety by keeping query state local.
+ ///
+ private class ParsingContext
+ {
+ public string InputDecimalSeparator { get; set; }
+ public bool InputUsesGroupSeparators { get; set; }
+ }
+
public void Init(PluginInitContext context)
{
Context = context;
@@ -45,20 +59,11 @@ public List Query(Query query)
return new List();
}
+ var context = new ParsingContext();
+
try
{
- string expression;
-
- switch (_settings.DecimalSeparator)
- {
- case DecimalSeparator.Comma:
- case DecimalSeparator.UseSystemLocale when CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator == ",":
- expression = query.Search.Replace(",", ".");
- break;
- default:
- expression = query.Search;
- break;
- }
+ var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value, context));
var result = MagesEngine.Interpret(expression);
@@ -71,7 +76,7 @@ public List Query(Query query)
if (!string.IsNullOrEmpty(result?.ToString()))
{
decimal roundedResult = Math.Round(Convert.ToDecimal(result), _settings.MaxDecimalPlaces, MidpointRounding.AwayFromZero);
- string newResult = ChangeDecimalSeparator(roundedResult, GetDecimalSeparator());
+ string newResult = FormatResult(roundedResult, context);
return new List
{
@@ -107,46 +112,156 @@ public List Query(Query query)
return new List();
}
- private bool CanCalculate(Query query)
+ ///
+ /// Parses a string representation of a number, detecting its format. It uses structural analysis
+ /// and falls back to system culture for truly ambiguous cases (e.g., "1,234").
+ /// It populates the provided ParsingContext with the detected format for later use.
+ ///
+ /// A normalized number string with '.' as the decimal separator for the Mages engine.
+ private string NormalizeNumber(string numberStr, ParsingContext context)
{
- // Don't execute when user only input "e" or "i" keyword
- if (query.Search.Length < 2)
+ var systemGroupSep = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator;
+ int dotCount = numberStr.Count(f => f == '.');
+ int commaCount = numberStr.Count(f => f == ',');
+
+ // Case 1: Unambiguous mixed separators (e.g., "1.234,56")
+ if (dotCount > 0 && commaCount > 0)
{
- return false;
+ context.InputUsesGroupSeparators = true;
+ if (numberStr.LastIndexOf('.') > numberStr.LastIndexOf(','))
+ {
+ context.InputDecimalSeparator = Dot;
+ return numberStr.Replace(Comma, string.Empty);
+ }
+ else
+ {
+ context.InputDecimalSeparator = Comma;
+ return numberStr.Replace(Dot, string.Empty).Replace(Comma, Dot);
+ }
}
- if (!RegValidExpressChar.IsMatch(query.Search))
+ // Case 2: Only dots
+ if (dotCount > 0)
{
- return false;
+ if (dotCount > 1)
+ {
+ context.InputUsesGroupSeparators = true;
+ return numberStr.Replace(Dot, string.Empty);
+ }
+ // A number is ambiguous if it has a single Dot in the thousands position,
+ // and does not start with a "0." or "."
+ bool isAmbiguous = numberStr.Length - numberStr.LastIndexOf('.') == 4
+ && !numberStr.StartsWith("0.")
+ && !numberStr.StartsWith(".");
+ if (isAmbiguous)
+ {
+ if (systemGroupSep == Dot)
+ {
+ context.InputUsesGroupSeparators = true;
+ return numberStr.Replace(Dot, string.Empty);
+ }
+ else
+ {
+ context.InputDecimalSeparator = Dot;
+ return numberStr;
+ }
+ }
+ else // Unambiguous decimal (e.g., "12.34" or "0.123" or ".123")
+ {
+ context.InputDecimalSeparator = Dot;
+ return numberStr;
+ }
}
- if (!IsBracketComplete(query.Search))
+ // Case 3: Only commas
+ if (commaCount > 0)
{
- return false;
+ if (commaCount > 1)
+ {
+ context.InputUsesGroupSeparators = true;
+ return numberStr.Replace(Comma, string.Empty);
+ }
+ // A number is ambiguous if it has a single Comma in the thousands position,
+ // and does not start with a "0," or ","
+ bool isAmbiguous = numberStr.Length - numberStr.LastIndexOf(',') == 4
+ && !numberStr.StartsWith("0,")
+ && !numberStr.StartsWith(",");
+ if (isAmbiguous)
+ {
+ if (systemGroupSep == Comma)
+ {
+ context.InputUsesGroupSeparators = true;
+ return numberStr.Replace(Comma, string.Empty);
+ }
+ else
+ {
+ context.InputDecimalSeparator = Comma;
+ return numberStr.Replace(Comma, Dot);
+ }
+ }
+ else // Unambiguous decimal (e.g., "12,34" or "0,123" or ",123")
+ {
+ context.InputDecimalSeparator = Comma;
+ return numberStr.Replace(Comma, Dot);
+ }
}
- if ((query.Search.Contains(Dot) && GetDecimalSeparator() != Dot) ||
- (query.Search.Contains(Comma) && GetDecimalSeparator() != Comma))
- return false;
+ // Case 4: No separators
+ return numberStr;
+ }
- return true;
+ private string FormatResult(decimal roundedResult, ParsingContext context)
+ {
+ string decimalSeparator = context.InputDecimalSeparator ?? GetDecimalSeparator();
+ string groupSeparator = GetGroupSeparator(decimalSeparator);
+
+ string resultStr = roundedResult.ToString(CultureInfo.InvariantCulture);
+
+ string[] parts = resultStr.Split('.');
+ string integerPart = parts[0];
+ string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty;
+
+ if (context.InputUsesGroupSeparators && integerPart.Length > 3)
+ {
+ integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator);
+ }
+
+ if (!string.IsNullOrEmpty(fractionalPart))
+ {
+ return integerPart + decimalSeparator + fractionalPart;
+ }
+
+ return integerPart;
}
- private static string ChangeDecimalSeparator(decimal value, string newDecimalSeparator)
+ private string GetGroupSeparator(string decimalSeparator)
{
- if (string.IsNullOrEmpty(newDecimalSeparator))
+ // This logic is now independent of the system's group separator
+ // to ensure consistent output for unit testing.
+ return decimalSeparator == Dot ? Comma : Dot;
+ }
+
+ private bool CanCalculate(Query query)
+ {
+ if (query.Search.Length < 2)
{
- return value.ToString();
+ return false;
}
- var numberFormatInfo = new NumberFormatInfo
+ if (!RegValidExpressChar.IsMatch(query.Search))
{
- NumberDecimalSeparator = newDecimalSeparator
- };
- return value.ToString(numberFormatInfo);
+ return false;
+ }
+
+ if (!IsBracketComplete(query.Search))
+ {
+ return false;
+ }
+
+ return true;
}
- private string GetDecimalSeparator()
+ private string GetDecimalSeparator()
{
string systemDecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
return _settings.DecimalSeparator switch
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs
index 8ffc547d10d..f4e2090e740 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs
@@ -10,4 +10,10 @@ internal static partial class MainRegexHelper
[GeneratedRegex(@"^(ceil|floor|exp|pi|e|max|min|det|abs|log|ln|sqrt|sin|cos|tan|arcsin|arccos|arctan|eigval|eigvec|eig|sum|polar|plot|round|sort|real|zeta|bin2dec|hex2dec|oct2dec|factorial|sign|isprime|isinfty|==|~=|&&|\|\||(?:\<|\>)=?|[ei]|[0-9]|0x[\da-fA-F]+|[\+\%\-\*\/\^\., ""]|[\(\)\|\!\[\]])+$", RegexOptions.Compiled)]
public static partial Regex GetRegValidExpressChar();
+
+ [GeneratedRegex(@"[\d\.,]+", RegexOptions.Compiled)]
+ public static partial Regex GetNumberRegex();
+
+ [GeneratedRegex(@"\B(?=(\d{3})+(?!\d))", RegexOptions.Compiled)]
+ public static partial Regex GetThousandGroupRegex();
}
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs b/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs
deleted file mode 100644
index 4eacb9d349c..00000000000
--- a/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-using System.Globalization;
-using System.Text;
-using System.Text.RegularExpressions;
-
-namespace Flow.Launcher.Plugin.Calculator
-{
- ///
- /// Tries to convert all numbers in a text from one culture format to another.
- ///
- public class NumberTranslator
- {
- private readonly CultureInfo sourceCulture;
- private readonly CultureInfo targetCulture;
- private readonly Regex splitRegexForSource;
- private readonly Regex splitRegexForTarget;
-
- private NumberTranslator(CultureInfo sourceCulture, CultureInfo targetCulture)
- {
- this.sourceCulture = sourceCulture;
- this.targetCulture = targetCulture;
-
- this.splitRegexForSource = GetSplitRegex(this.sourceCulture);
- this.splitRegexForTarget = GetSplitRegex(this.targetCulture);
- }
-
- ///
- /// Create a new - returns null if no number conversion
- /// is required between the cultures.
- ///
- /// source culture
- /// target culture
- ///
- public static NumberTranslator Create(CultureInfo sourceCulture, CultureInfo targetCulture)
- {
- bool conversionRequired = sourceCulture.NumberFormat.NumberDecimalSeparator != targetCulture.NumberFormat.NumberDecimalSeparator
- || sourceCulture.NumberFormat.PercentGroupSeparator != targetCulture.NumberFormat.PercentGroupSeparator
- || sourceCulture.NumberFormat.NumberGroupSizes != targetCulture.NumberFormat.NumberGroupSizes;
- return conversionRequired
- ? new NumberTranslator(sourceCulture, targetCulture)
- : null;
- }
-
- ///
- /// Translate from source to target culture.
- ///
- ///
- ///
- public string Translate(string input)
- {
- return this.Translate(input, this.sourceCulture, this.targetCulture, this.splitRegexForSource);
- }
-
- ///
- /// Translate from target to source culture.
- ///
- ///
- ///
- public string TranslateBack(string input)
- {
- return this.Translate(input, this.targetCulture, this.sourceCulture, this.splitRegexForTarget);
- }
-
- private string Translate(string input, CultureInfo cultureFrom, CultureInfo cultureTo, Regex splitRegex)
- {
- var outputBuilder = new StringBuilder();
-
- string[] tokens = splitRegex.Split(input);
- foreach (string token in tokens)
- {
- decimal number;
- outputBuilder.Append(
- decimal.TryParse(token, NumberStyles.Number, cultureFrom, out number)
- ? number.ToString(cultureTo)
- : token);
- }
-
- return outputBuilder.ToString();
- }
-
- private Regex GetSplitRegex(CultureInfo culture)
- {
- var splitPattern = $"((?:\\d|{Regex.Escape(culture.NumberFormat.NumberDecimalSeparator)}";
- if (!string.IsNullOrEmpty(culture.NumberFormat.NumberGroupSeparator))
- {
- splitPattern += $"|{Regex.Escape(culture.NumberFormat.NumberGroupSeparator)}";
- }
- splitPattern += ")+)";
- return new Regex(splitPattern);
- }
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs
index 77cb4627dd8..7bc307d111c 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs
@@ -1,5 +1,4 @@
-using System.Windows;
-using System.Windows.Controls;
+using System.Windows.Controls;
using Flow.Launcher.Plugin.Calculator.ViewModels;
namespace Flow.Launcher.Plugin.Calculator.Views
diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
index 3168edfccb7..c9435e04315 100644
--- a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
+++ b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
@@ -2,8 +2,8 @@
"ID": "CEA0FDFC6D3B4085823D60DC76F28855",
"ActionKeyword": "*",
"Name": "Calculator",
- "Description": "Perform mathematical calculations (including hexadecimal values)",
- "Author": "cxfksword",
+ "Description": "Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place.",
+ "Author": "cxfksword, dcog989",
"Version": "1.0.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",