mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
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:
parent
1da7e1ef35
commit
113baac567
5 changed files with 129 additions and 134 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue