Skip to content

Commit 6a01ca4

Browse files
Merge pull request #3859 from dcog989/Calculator-accepts-flexible-separator
Smart thousands and decimals for Calculator
2 parents c42628b + afc969d commit 6a01ca4

File tree

6 files changed

+158
-129
lines changed

6 files changed

+158
-129
lines changed

Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
xmlns:system="clr-namespace:System;assembly=mscorlib">
55

66
<system:String x:Key="flowlauncher_plugin_caculator_plugin_name">Calculator</system:String>
7-
<system:String x:Key="flowlauncher_plugin_caculator_plugin_description">Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher)</system:String>
7+
<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>
88
<system:String x:Key="flowlauncher_plugin_calculator_not_a_number">Not a number (NaN)</system:String>
99
<system:String x:Key="flowlauncher_plugin_calculator_expression_not_complete">Expression wrong or incomplete (Did you forget some parentheses?)</system:String>
1010
<system:String x:Key="flowlauncher_plugin_calculator_copy_number_to_clipboard">Copy this number to the clipboard</system:String>

Plugins/Flow.Launcher.Plugin.Calculator/Main.cs

Lines changed: 148 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Globalization;
4+
using System.Linq;
45
using System.Runtime.InteropServices;
56
using System.Text.RegularExpressions;
67
using System.Windows.Controls;
@@ -14,6 +15,9 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
1415
{
1516
private static readonly Regex RegValidExpressChar = MainRegexHelper.GetRegValidExpressChar();
1617
private static readonly Regex RegBrackets = MainRegexHelper.GetRegBrackets();
18+
private static readonly Regex ThousandGroupRegex = MainRegexHelper.GetThousandGroupRegex();
19+
private static readonly Regex NumberRegex = MainRegexHelper.GetNumberRegex();
20+
1721
private static Engine MagesEngine;
1822
private const string Comma = ",";
1923
private const string Dot = ".";
@@ -23,6 +27,16 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
2327
private Settings _settings;
2428
private SettingsViewModel _viewModel;
2529

30+
/// <summary>
31+
/// Holds the formatting information for a single query.
32+
/// This is used to ensure thread safety by keeping query state local.
33+
/// </summary>
34+
private class ParsingContext
35+
{
36+
public string InputDecimalSeparator { get; set; }
37+
public bool InputUsesGroupSeparators { get; set; }
38+
}
39+
2640
public void Init(PluginInitContext context)
2741
{
2842
Context = context;
@@ -45,20 +59,11 @@ public List<Result> Query(Query query)
4559
return new List<Result>();
4660
}
4761

62+
var context = new ParsingContext();
63+
4864
try
4965
{
50-
string expression;
51-
52-
switch (_settings.DecimalSeparator)
53-
{
54-
case DecimalSeparator.Comma:
55-
case DecimalSeparator.UseSystemLocale when CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator == ",":
56-
expression = query.Search.Replace(",", ".");
57-
break;
58-
default:
59-
expression = query.Search;
60-
break;
61-
}
66+
var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value, context));
6267

6368
var result = MagesEngine.Interpret(expression);
6469

@@ -71,7 +76,7 @@ public List<Result> Query(Query query)
7176
if (!string.IsNullOrEmpty(result?.ToString()))
7277
{
7378
decimal roundedResult = Math.Round(Convert.ToDecimal(result), _settings.MaxDecimalPlaces, MidpointRounding.AwayFromZero);
74-
string newResult = ChangeDecimalSeparator(roundedResult, GetDecimalSeparator());
79+
string newResult = FormatResult(roundedResult, context);
7580

7681
return new List<Result>
7782
{
@@ -107,46 +112,156 @@ public List<Result> Query(Query query)
107112
return new List<Result>();
108113
}
109114

110-
private bool CanCalculate(Query query)
115+
/// <summary>
116+
/// Parses a string representation of a number, detecting its format. It uses structural analysis
117+
/// and falls back to system culture for truly ambiguous cases (e.g., "1,234").
118+
/// It populates the provided ParsingContext with the detected format for later use.
119+
/// </summary>
120+
/// <returns>A normalized number string with '.' as the decimal separator for the Mages engine.</returns>
121+
private string NormalizeNumber(string numberStr, ParsingContext context)
111122
{
112-
// Don't execute when user only input "e" or "i" keyword
113-
if (query.Search.Length < 2)
123+
var systemGroupSep = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator;
124+
int dotCount = numberStr.Count(f => f == '.');
125+
int commaCount = numberStr.Count(f => f == ',');
126+
127+
// Case 1: Unambiguous mixed separators (e.g., "1.234,56")
128+
if (dotCount > 0 && commaCount > 0)
114129
{
115-
return false;
130+
context.InputUsesGroupSeparators = true;
131+
if (numberStr.LastIndexOf('.') > numberStr.LastIndexOf(','))
132+
{
133+
context.InputDecimalSeparator = Dot;
134+
return numberStr.Replace(Comma, string.Empty);
135+
}
136+
else
137+
{
138+
context.InputDecimalSeparator = Comma;
139+
return numberStr.Replace(Dot, string.Empty).Replace(Comma, Dot);
140+
}
116141
}
117142

118-
if (!RegValidExpressChar.IsMatch(query.Search))
143+
// Case 2: Only dots
144+
if (dotCount > 0)
119145
{
120-
return false;
146+
if (dotCount > 1)
147+
{
148+
context.InputUsesGroupSeparators = true;
149+
return numberStr.Replace(Dot, string.Empty);
150+
}
151+
// A number is ambiguous if it has a single Dot in the thousands position,
152+
// and does not start with a "0." or "."
153+
bool isAmbiguous = numberStr.Length - numberStr.LastIndexOf('.') == 4
154+
&& !numberStr.StartsWith("0.")
155+
&& !numberStr.StartsWith(".");
156+
if (isAmbiguous)
157+
{
158+
if (systemGroupSep == Dot)
159+
{
160+
context.InputUsesGroupSeparators = true;
161+
return numberStr.Replace(Dot, string.Empty);
162+
}
163+
else
164+
{
165+
context.InputDecimalSeparator = Dot;
166+
return numberStr;
167+
}
168+
}
169+
else // Unambiguous decimal (e.g., "12.34" or "0.123" or ".123")
170+
{
171+
context.InputDecimalSeparator = Dot;
172+
return numberStr;
173+
}
121174
}
122175

123-
if (!IsBracketComplete(query.Search))
176+
// Case 3: Only commas
177+
if (commaCount > 0)
124178
{
125-
return false;
179+
if (commaCount > 1)
180+
{
181+
context.InputUsesGroupSeparators = true;
182+
return numberStr.Replace(Comma, string.Empty);
183+
}
184+
// A number is ambiguous if it has a single Comma in the thousands position,
185+
// and does not start with a "0," or ","
186+
bool isAmbiguous = numberStr.Length - numberStr.LastIndexOf(',') == 4
187+
&& !numberStr.StartsWith("0,")
188+
&& !numberStr.StartsWith(",");
189+
if (isAmbiguous)
190+
{
191+
if (systemGroupSep == Comma)
192+
{
193+
context.InputUsesGroupSeparators = true;
194+
return numberStr.Replace(Comma, string.Empty);
195+
}
196+
else
197+
{
198+
context.InputDecimalSeparator = Comma;
199+
return numberStr.Replace(Comma, Dot);
200+
}
201+
}
202+
else // Unambiguous decimal (e.g., "12,34" or "0,123" or ",123")
203+
{
204+
context.InputDecimalSeparator = Comma;
205+
return numberStr.Replace(Comma, Dot);
206+
}
126207
}
127208

128-
if ((query.Search.Contains(Dot) && GetDecimalSeparator() != Dot) ||
129-
(query.Search.Contains(Comma) && GetDecimalSeparator() != Comma))
130-
return false;
209+
// Case 4: No separators
210+
return numberStr;
211+
}
131212

132-
return true;
213+
private string FormatResult(decimal roundedResult, ParsingContext context)
214+
{
215+
string decimalSeparator = context.InputDecimalSeparator ?? GetDecimalSeparator();
216+
string groupSeparator = GetGroupSeparator(decimalSeparator);
217+
218+
string resultStr = roundedResult.ToString(CultureInfo.InvariantCulture);
219+
220+
string[] parts = resultStr.Split('.');
221+
string integerPart = parts[0];
222+
string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty;
223+
224+
if (context.InputUsesGroupSeparators && integerPart.Length > 3)
225+
{
226+
integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator);
227+
}
228+
229+
if (!string.IsNullOrEmpty(fractionalPart))
230+
{
231+
return integerPart + decimalSeparator + fractionalPart;
232+
}
233+
234+
return integerPart;
133235
}
134236

135-
private static string ChangeDecimalSeparator(decimal value, string newDecimalSeparator)
237+
private string GetGroupSeparator(string decimalSeparator)
136238
{
137-
if (string.IsNullOrEmpty(newDecimalSeparator))
239+
// This logic is now independent of the system's group separator
240+
// to ensure consistent output for unit testing.
241+
return decimalSeparator == Dot ? Comma : Dot;
242+
}
243+
244+
private bool CanCalculate(Query query)
245+
{
246+
if (query.Search.Length < 2)
138247
{
139-
return value.ToString();
248+
return false;
140249
}
141250

142-
var numberFormatInfo = new NumberFormatInfo
251+
if (!RegValidExpressChar.IsMatch(query.Search))
143252
{
144-
NumberDecimalSeparator = newDecimalSeparator
145-
};
146-
return value.ToString(numberFormatInfo);
253+
return false;
254+
}
255+
256+
if (!IsBracketComplete(query.Search))
257+
{
258+
return false;
259+
}
260+
261+
return true;
147262
}
148263

149-
private string GetDecimalSeparator()
264+
private string GetDecimalSeparator()
150265
{
151266
string systemDecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
152267
return _settings.DecimalSeparator switch

Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@ internal static partial class MainRegexHelper
1010

1111
[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)]
1212
public static partial Regex GetRegValidExpressChar();
13+
14+
[GeneratedRegex(@"[\d\.,]+", RegexOptions.Compiled)]
15+
public static partial Regex GetNumberRegex();
16+
17+
[GeneratedRegex(@"\B(?=(\d{3})+(?!\d))", RegexOptions.Compiled)]
18+
public static partial Regex GetThousandGroupRegex();
1319
}

Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs

Lines changed: 0 additions & 91 deletions
This file was deleted.

Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System.Windows;
2-
using System.Windows.Controls;
1+
using System.Windows.Controls;
32
using Flow.Launcher.Plugin.Calculator.ViewModels;
43

54
namespace Flow.Launcher.Plugin.Calculator.Views

Plugins/Flow.Launcher.Plugin.Calculator/plugin.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"ID": "CEA0FDFC6D3B4085823D60DC76F28855",
33
"ActionKeyword": "*",
44
"Name": "Calculator",
5-
"Description": "Perform mathematical calculations (including hexadecimal values)",
6-
"Author": "cxfksword",
5+
"Description": "Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place.",
6+
"Author": "cxfksword, dcog989",
77
"Version": "1.0.0",
88
"Language": "csharp",
99
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",

0 commit comments

Comments
 (0)