mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge pull request #3859 from dcog989/Calculator-accepts-flexible-separator
Smart thousands and decimals for Calculator
This commit is contained in:
commit
6a01ca403f
6 changed files with 158 additions and 129 deletions
|
|
@ -4,7 +4,7 @@
|
|||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<system:String x:Key="flowlauncher_plugin_caculator_plugin_name">Calculator</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_caculator_plugin_description">Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher)</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_caculator_plugin_description">Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place.</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_calculator_not_a_number">Not a number (NaN)</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_calculator_expression_not_complete">Expression wrong or incomplete (Did you forget some parentheses?)</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_calculator_copy_number_to_clipboard">Copy this number to the clipboard</system:String>
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Flow.Launcher.Plugin.Calculator
|
|||
{
|
||||
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 @@ namespace Flow.Launcher.Plugin.Calculator
|
|||
private Settings _settings;
|
||||
private SettingsViewModel _viewModel;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the formatting information for a single query.
|
||||
/// This is used to ensure thread safety by keeping query state local.
|
||||
/// </summary>
|
||||
private class ParsingContext
|
||||
{
|
||||
public string InputDecimalSeparator { get; set; }
|
||||
public bool InputUsesGroupSeparators { get; set; }
|
||||
}
|
||||
|
||||
public void Init(PluginInitContext context)
|
||||
{
|
||||
Context = context;
|
||||
|
|
@ -45,20 +59,11 @@ namespace Flow.Launcher.Plugin.Calculator
|
|||
return new List<Result>();
|
||||
}
|
||||
|
||||
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 @@ 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, context);
|
||||
|
||||
return new List<Result>
|
||||
{
|
||||
|
|
@ -107,9 +112,137 @@ namespace Flow.Launcher.Plugin.Calculator
|
|||
return new List<Result>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>A normalized number string with '.' as the decimal separator for the Mages engine.</returns>
|
||||
private string NormalizeNumber(string numberStr, ParsingContext context)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Only dots
|
||||
if (dotCount > 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Only commas
|
||||
if (commaCount > 0)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Case 4: No separators
|
||||
return numberStr;
|
||||
}
|
||||
|
||||
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 string GetGroupSeparator(string decimalSeparator)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
// Don't execute when user only input "e" or "i" keyword
|
||||
if (query.Search.Length < 2)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -125,28 +258,10 @@ 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 static string ChangeDecimalSeparator(decimal value, string newDecimalSeparator)
|
||||
{
|
||||
if (string.IsNullOrEmpty(newDecimalSeparator))
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
var numberFormatInfo = new NumberFormatInfo
|
||||
{
|
||||
NumberDecimalSeparator = newDecimalSeparator
|
||||
};
|
||||
return value.ToString(numberFormatInfo);
|
||||
}
|
||||
|
||||
private string GetDecimalSeparator()
|
||||
private string GetDecimalSeparator()
|
||||
{
|
||||
string systemDecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
|
||||
return _settings.DecimalSeparator switch
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue