Skip to content

Commit b40086d

Browse files
VictoriousRaptorTBM13
authored andcommitted
Merge pull request Flow-Launcher#3859 from dcog989/Calculator-accepts-flexible-separator
Smart thousands and decimals for Calculator
1 parent 1ad764a commit b40086d

File tree

5 files changed

+158
-126
lines changed

5 files changed

+158
-126
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
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>
8+
<system:String x:Key="flowlauncher_plugin_calculator_not_a_number">Not a number (NaN)</system:String>
9+
<system:String x:Key="flowlauncher_plugin_calculator_expression_not_complete">Expression wrong or incomplete (Did you forget some parentheses?)</system:String>
810
<system:String x:Key="flowlauncher_plugin_calculator_copy_number_to_clipboard">Copy this number to the clipboard</system:String>
911
<system:String x:Key="flowlauncher_plugin_calculator_output_decimal_separator">Decimal separator</system:String>
1012
<system:String x:Key="flowlauncher_plugin_calculator_output_decimal_separator_help">The decimal separator to be used in the output.</system:String>

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

Lines changed: 147 additions & 32 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,26 +59,17 @@ 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
if (result?.ToString() != "NaN" && result is not Function && !string.IsNullOrEmpty(result?.ToString()))
6570
{
6671
decimal roundedResult = Math.Round(Convert.ToDecimal(result), _settings.MaxDecimalPlaces, MidpointRounding.AwayFromZero);
67-
string newResult = ChangeDecimalSeparator(roundedResult, GetDecimalSeparator());
72+
string newResult = FormatResult(roundedResult, context);
6873

6974
return new List<Result>
7075
{
@@ -100,43 +105,153 @@ public List<Result> Query(Query query)
100105
return new List<Result>();
101106
}
102107

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

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

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

121-
if ((query.Search.Contains(Dot) && GetDecimalSeparator() != Dot) ||
122-
(query.Search.Contains(Comma) && GetDecimalSeparator() != Comma))
123-
return false;
202+
// Case 4: No separators
203+
return numberStr;
204+
}
124205

125-
return true;
206+
private string FormatResult(decimal roundedResult, ParsingContext context)
207+
{
208+
string decimalSeparator = context.InputDecimalSeparator ?? GetDecimalSeparator();
209+
string groupSeparator = GetGroupSeparator(decimalSeparator);
210+
211+
string resultStr = roundedResult.ToString(CultureInfo.InvariantCulture);
212+
213+
string[] parts = resultStr.Split('.');
214+
string integerPart = parts[0];
215+
string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty;
216+
217+
if (context.InputUsesGroupSeparators && integerPart.Length > 3)
218+
{
219+
integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator);
220+
}
221+
222+
if (!string.IsNullOrEmpty(fractionalPart))
223+
{
224+
return integerPart + decimalSeparator + fractionalPart;
225+
}
226+
227+
return integerPart;
126228
}
127229

128-
private static string ChangeDecimalSeparator(decimal value, string newDecimalSeparator)
230+
private string GetGroupSeparator(string decimalSeparator)
129231
{
130-
if (string.IsNullOrEmpty(newDecimalSeparator))
232+
// This logic is now independent of the system's group separator
233+
// to ensure consistent output for unit testing.
234+
return decimalSeparator == Dot ? Comma : Dot;
235+
}
236+
237+
private bool CanCalculate(Query query)
238+
{
239+
if (query.Search.Length < 2)
131240
{
132-
return value.ToString();
241+
return false;
133242
}
134243

135-
var numberFormatInfo = new NumberFormatInfo
244+
if (!RegValidExpressChar.IsMatch(query.Search))
136245
{
137-
NumberDecimalSeparator = newDecimalSeparator
138-
};
139-
return value.ToString(numberFormatInfo);
246+
return false;
247+
}
248+
249+
if (!IsBracketComplete(query.Search))
250+
{
251+
return false;
252+
}
253+
254+
return true;
140255
}
141256

142257
private string GetDecimalSeparator()

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,10 @@ internal static partial class MainRegexHelper
1919
@"[\+\%\-\*\/\^\., ""]|[\(\)\|\!\[\]]" +
2020
@")+$", RegexOptions.Compiled)]
2121
public static partial Regex GetRegValidExpressChar();
22+
23+
[GeneratedRegex(@"[\d\.,]+", RegexOptions.Compiled)]
24+
public static partial Regex GetNumberRegex();
25+
26+
[GeneratedRegex(@"\B(?=(\d{3})+(?!\d))", RegexOptions.Compiled)]
27+
public static partial Regex GetThousandGroupRegex();
2228
}

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

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

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)