From 113baac567f91b30a7fb8164fa72eafb7cfb68e0 Mon Sep 17 00:00:00 2001 From: dcog989 Date: Tue, 22 Jul 2025 23:48:11 +0100 Subject: [PATCH] Smart thousands and decimals ## Core Logic * **Advanced Number Parsing:** We now process numbers with various decimal and thousand-separator formats (e.g., `1,234.56` and `1.234,56`). We distinguish between separator types based on their position and surrounding digits. * **Context-Aware Output Formatting:** We now mirror the output format based on the user's input. If a query includes thousand separators, the result will also have them. The decimal separator in the result will match the one used in the query. ## Code Cleanup * **Deleted Unused File:** `NumberTranslator.cs` was unused and therefore removed. * **Removed Redundant UI Code:** The `CalculatorSettings_Loaded` event handler in `CalculatorSettings.xaml.cs` (and its XAML registration) was removed. The functionality was already handled automatically by data binding. ## Maintainability * **Added Code Documentation:** An XML summary comment was added to the new `NormalizeNumber` method in `Main.cs` to clarify its purpose. So, the plugin is now much more flexible, and will accept whatever format the user gives it regardless of Windows region settings. --- .../Flow.Launcher.Plugin.Calculator/Main.cs | 157 ++++++++++++++---- .../NumberTranslator.cs | 91 ---------- .../Views/CalculatorSettings.xaml | 1 - .../Views/CalculatorSettings.xaml.cs | 8 - .../plugin.json | 6 +- 5 files changed, 129 insertions(+), 134 deletions(-) delete mode 100644 Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 3d06c4ce0..2787adbbc 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; @@ -23,6 +24,7 @@ namespace Flow.Launcher.Plugin.Calculator @"[ei]|[0-9]|0x[\da-fA-F]+|[\+\%\-\*\/\^\., ""]|[\(\)\|\!\[\]]" + @")+$", RegexOptions.Compiled); private static readonly Regex RegBrackets = new Regex(@"[\(\)\[\]]", RegexOptions.Compiled); + private static readonly Regex ThousandGroupRegex = new Regex(@"\B(?=(\d{3})+(?!\d))"); private static Engine MagesEngine; private const string comma = ","; private const string dot = "."; @@ -32,6 +34,9 @@ namespace Flow.Launcher.Plugin.Calculator private static Settings _settings; private static SettingsViewModel _viewModel; + private string _inputDecimalSeparator; + private bool _inputUsesGroupSeparators; + public void Init(PluginInitContext context) { Context = context; @@ -54,20 +59,13 @@ namespace Flow.Launcher.Plugin.Calculator return new List(); } + _inputDecimalSeparator = null; + _inputUsesGroupSeparators = false; + 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 numberRegex = new Regex(@"[\d\.,]+"); + var expression = numberRegex.Replace(query.Search, m => NormalizeNumber(m.Value)); var result = MagesEngine.Interpret(expression); @@ -80,7 +78,7 @@ namespace Flow.Launcher.Plugin.Calculator 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); return new List { @@ -115,6 +113,121 @@ namespace Flow.Launcher.Plugin.Calculator return new List(); } + + /// + /// Parses a string representation of a number, detecting its format. It uses structural analysis + /// (checking for 3-digit groups) and falls back to system culture for ambiguous cases (e.g., "1,234"). + /// It sets instance fields to remember the user's format for later output formatting. + /// + /// A normalized number string with '.' as the decimal separator for the Mages engine. + private string NormalizeNumber(string numberStr) + { + var systemFormat = CultureInfo.CurrentCulture.NumberFormat; + string systemDecimalSeparator = systemFormat.NumberDecimalSeparator; + + bool hasDot = numberStr.Contains(dot); + bool hasComma = numberStr.Contains(comma); + + // Unambiguous case: both separators are present. The last one wins as decimal separator. + if (hasDot && hasComma) + { + _inputUsesGroupSeparators = true; + int lastDotPos = numberStr.LastIndexOf(dot); + int lastCommaPos = numberStr.LastIndexOf(comma); + + if (lastDotPos > lastCommaPos) // e.g. 1,234.56 + { + _inputDecimalSeparator = dot; + return numberStr.Replace(comma, string.Empty); + } + else // e.g. 1.234,56 + { + _inputDecimalSeparator = comma; + return numberStr.Replace(dot, string.Empty).Replace(comma, dot); + } + } + + if (hasComma) + { + string[] parts = numberStr.Split(','); + // If all parts after the first are 3 digits, it's a potential group separator. + bool isGroupCandidate = parts.Length > 1 && parts.Skip(1).All(p => p.Length == 3); + + if (isGroupCandidate) + { + // Ambiguous case: "1,234". Resolve using culture. + if (systemDecimalSeparator == comma) + { + _inputDecimalSeparator = comma; + return numberStr.Replace(comma, dot); + } + else + { + _inputUsesGroupSeparators = true; + return numberStr.Replace(comma, string.Empty); + } + } + else + { + // Unambiguous decimal: "123,45" or "1,2,345" + _inputDecimalSeparator = comma; + return numberStr.Replace(comma, dot); + } + } + + if (hasDot) + { + string[] parts = numberStr.Split('.'); + bool isGroupCandidate = parts.Length > 1 && parts.Skip(1).All(p => p.Length == 3); + + if (isGroupCandidate) + { + if (systemDecimalSeparator == dot) + { + _inputDecimalSeparator = dot; + return numberStr; + } + else + { + _inputUsesGroupSeparators = true; + return numberStr.Replace(dot, string.Empty); + } + } + else + { + _inputDecimalSeparator = dot; + return numberStr; // Already in Mages-compatible format + } + } + + // No separators. + return numberStr; + } + + private string FormatResult(decimal roundedResult) + { + // Use the detected decimal separator from the input; otherwise, fall back to settings. + string decimalSeparator = _inputDecimalSeparator ?? GetDecimalSeparator(); + string groupSeparator = decimalSeparator == dot ? comma : dot; + + string resultStr = roundedResult.ToString(CultureInfo.InvariantCulture); + + string[] parts = resultStr.Split('.'); + string integerPart = parts[0]; + string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty; + + if (_inputUsesGroupSeparators) + { + integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator); + } + + if (!string.IsNullOrEmpty(fractionalPart)) + { + return integerPart + decimalSeparator + fractionalPart; + } + + return integerPart; + } private bool CanCalculate(Query query) { @@ -134,27 +247,9 @@ namespace Flow.Launcher.Plugin.Calculator return false; } - if ((query.Search.Contains(dot) && GetDecimalSeparator() != dot) || - (query.Search.Contains(comma) && GetDecimalSeparator() != comma)) - return false; - return true; } - private string ChangeDecimalSeparator(decimal value, string newDecimalSeparator) - { - if (String.IsNullOrEmpty(newDecimalSeparator)) - { - return value.ToString(); - } - - var numberFormatInfo = new NumberFormatInfo - { - NumberDecimalSeparator = newDecimalSeparator - }; - return value.ToString(numberFormatInfo); - } - private static string GetDecimalSeparator() { string systemDecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs b/Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs deleted file mode 100644 index 4eacb9d34..000000000 --- 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 b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml index ceee3897c..36beebc33 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml @@ -10,7 +10,6 @@ xmlns:viewModels="clr-namespace:Flow.Launcher.Plugin.Calculator.ViewModels" d:DesignHeight="450" d:DesignWidth="800" - Loaded="CalculatorSettings_Loaded" mc:Ignorable="d"> diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs index edb73470c..117a19b25 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs @@ -1,4 +1,3 @@ -using System.Windows; using System.Windows.Controls; using Flow.Launcher.Plugin.Calculator.ViewModels; @@ -19,13 +18,6 @@ namespace Flow.Launcher.Plugin.Calculator.Views DataContext = viewModel; InitializeComponent(); } - - private void CalculatorSettings_Loaded(object sender, RoutedEventArgs e) - { - DecimalSeparatorComboBox.SelectedItem = _settings.DecimalSeparator; - MaxDecimalPlaces.SelectedItem = _settings.MaxDecimalPlaces; - } } - } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json index 3168edfcc..739572930 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json @@ -2,9 +2,9 @@ "ID": "CEA0FDFC6D3B4085823D60DC76F28855", "ActionKeyword": "*", "Name": "Calculator", - "Description": "Perform mathematical calculations (including hexadecimal values)", - "Author": "cxfksword", - "Version": "1.0.0", + "Description": "Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place.", + "Author": "cxfksword, dcog989", + "Version": "1.1.0", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Calculator.dll",