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.
This commit is contained in:
dcog989 2025-07-22 23:48:11 +01:00
parent 1da7e1ef35
commit 113baac567
5 changed files with 129 additions and 134 deletions

View file

@ -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<Result>();
}
_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<Result>
{
@ -115,6 +113,121 @@ namespace Flow.Launcher.Plugin.Calculator
return new List<Result>();
}
/// <summary>
/// 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.
/// </summary>
/// <returns>A normalized number string with '.' as the decimal separator for the Mages engine.</returns>
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;

View file

@ -1,91 +0,0 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace Flow.Launcher.Plugin.Calculator
{
/// <summary>
/// Tries to convert all numbers in a text from one culture format to another.
/// </summary>
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);
}
/// <summary>
/// Create a new <see cref="NumberTranslator"/> - returns null if no number conversion
/// is required between the cultures.
/// </summary>
/// <param name="sourceCulture">source culture</param>
/// <param name="targetCulture">target culture</param>
/// <returns></returns>
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;
}
/// <summary>
/// Translate from source to target culture.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public string Translate(string input)
{
return this.Translate(input, this.sourceCulture, this.targetCulture, this.splitRegexForSource);
}
/// <summary>
/// Translate from target to source culture.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
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);
}
}
}

View file

@ -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">
<UserControl.Resources>

View file

@ -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;
}
}
}

View file

@ -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",