Skip to content

Commit e990e0f

Browse files
committed
Handle misplaced separators, Mages edge cases
Allow for e.g. `25,00` when `,` used as digit grouping. Exclude Mages function from the above relaxed logic.
1 parent 336e51d commit e990e0f

File tree

2 files changed

+102
-34
lines changed

2 files changed

+102
-34
lines changed

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

Lines changed: 99 additions & 31 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;
@@ -12,10 +13,10 @@ namespace Flow.Launcher.Plugin.Calculator
1213
{
1314
public class Main : IPlugin, IPluginI18n, ISettingProvider
1415
{
15-
private static readonly Regex RegBrackets = MainRegexHelper.GetRegBrackets();
1616
private static readonly Regex ThousandGroupRegex = MainRegexHelper.GetThousandGroupRegex();
1717
private static readonly Regex NumberRegex = MainRegexHelper.GetNumberRegex();
1818
private static readonly Regex PowRegex = MainRegexHelper.GetPowRegex();
19+
private static readonly Regex FunctionRegex = MainRegexHelper.GetFunctionRegex();
1920

2021
private static Engine MagesEngine;
2122
private const string Comma = ",";
@@ -51,7 +52,8 @@ public List<Result> Query(Query query)
5152

5253
try
5354
{
54-
var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value));
55+
bool isFunctionPresent = FunctionRegex.IsMatch(query.Search);
56+
var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value, isFunctionPresent));
5557

5658
// WORKAROUND START: The 'pow' function in Mages v3.0.0 is broken.
5759
// https://github.com/FlorianRappl/Mages/issues/132
@@ -183,47 +185,104 @@ private static string PowMatchEvaluator(Match m)
183185
return $"({arg1}^{arg2})";
184186
}
185187

186-
/// <summary>
187-
/// Parses a string representation of a number using the system's current culture.
188-
/// </summary>
189-
/// <returns>A normalized number string with '.' as the decimal separator for the Mages engine.</returns>
190-
private static string NormalizeNumber(string numberStr)
188+
private static string NormalizeNumber(string numberStr, bool isFunctionPresent)
191189
{
192190
var culture = CultureInfo.CurrentCulture;
193191
var groupSep = culture.NumberFormat.NumberGroupSeparator;
194192
var decimalSep = culture.NumberFormat.NumberDecimalSeparator;
195193

196-
// If the string contains the group separator, check if it's used correctly.
197-
if (!string.IsNullOrEmpty(groupSep) && numberStr.Contains(groupSep))
194+
if (isFunctionPresent)
198195
{
199-
var parts = numberStr.Split(groupSep);
200-
// If any part after the first (excluding a possible last part with a decimal)
201-
// does not have 3 digits, then it's not a valid use of a thousand separator.
202-
for (int i = 1; i < parts.Length; i++)
196+
// STRICT MODE: When functions are present, ',' is ALWAYS an argument separator.
197+
// It must not be normalized.
198+
if (numberStr.Contains(','))
203199
{
204-
var part = parts[i];
205-
// The last part might contain a decimal separator.
206-
if (i == parts.Length - 1 && part.Contains(culture.NumberFormat.NumberDecimalSeparator))
207-
{
208-
part = part.Split(culture.NumberFormat.NumberDecimalSeparator)[0];
209-
}
200+
return numberStr;
201+
}
210202

211-
if (part.Length != 3)
203+
// The string has no commas. It could have a '.' group separator (e.g. in de-DE)
204+
// or a '.' decimal separator (e.g. in en-US).
205+
// Since Mages' decimal separator is '.', we only need to strip the group separator.
206+
if (groupSep == ".")
207+
{
208+
var parts = numberStr.Split('.');
209+
// A number with a dot group separator, e.g., "1.234"
210+
if (parts.Length > 1)
212211
{
213-
// This is not a number with valid thousand separators,
214-
// so it must be arguments to a function. Return it unmodified.
215-
return numberStr;
212+
// Check if the parts after the first dot have the correct group length (usually 3).
213+
for (int i = 1; i < parts.Length; i++)
214+
{
215+
if (parts[i].Length != 3)
216+
{
217+
// Malformed grouping, e.g., "1.23". This is likely a decimal number.
218+
// Return as is and let Mages handle it.
219+
return numberStr;
220+
}
221+
}
222+
// Correct grouping, e.g., "1.234" or "1.234.567". Strip separators.
223+
return numberStr.Replace(".", "");
216224
}
217225
}
226+
227+
// For any other case (e.g. en-US culture where group sep is ',' which was already handled),
228+
// return the string as is.
229+
return numberStr;
218230
}
231+
else
232+
{
233+
// LENIENT MODE: No functions are present, so we can be flexible.
234+
string processedStr = numberStr;
235+
if (!string.IsNullOrEmpty(groupSep))
236+
{
237+
processedStr = processedStr.Replace(groupSep, "");
238+
}
239+
processedStr = processedStr.Replace(decimalSep, ".");
240+
return processedStr;
241+
}
242+
}
243+
244+
private static bool IsValidGrouping(string[] parts, int[] groupSizes)
245+
{
246+
if (parts.Length <= 1) return true;
247+
248+
if (groupSizes is null || groupSizes.Length == 0 || groupSizes[0] == 0)
249+
return false; // has groups, but culture defines none.
250+
251+
var firstPart = parts[0];
252+
if (firstPart.StartsWith("-")) firstPart = firstPart.Substring(1);
253+
if (firstPart.Length == 0) return false; // e.g. ",123"
219254

220-
// If validation passes, we can assume the separators are used correctly for numbers.
221-
string processedStr = numberStr.Replace(groupSep, "");
222-
processedStr = processedStr.Replace(decimalSep, ".");
255+
if (firstPart.Length > groupSizes[0]) return false;
223256

224-
return processedStr;
257+
var lastGroupSize = groupSizes.Last();
258+
var canRepeatLastGroup = lastGroupSize != 0;
259+
260+
int groupIndex = 0;
261+
for (int i = parts.Length - 1; i > 0; i--)
262+
{
263+
int expectedSize;
264+
if (groupIndex < groupSizes.Length)
265+
{
266+
expectedSize = groupSizes[groupIndex];
267+
}
268+
else if(canRepeatLastGroup)
269+
{
270+
expectedSize = lastGroupSize;
271+
}
272+
else
273+
{
274+
return false;
275+
}
276+
277+
if (parts[i].Length != expectedSize) return false;
278+
279+
groupIndex++;
280+
}
281+
282+
return true;
225283
}
226284

285+
227286
private string FormatResult(decimal roundedResult)
228287
{
229288
string decimalSeparator = GetDecimalSeparator();
@@ -250,14 +309,23 @@ private string FormatResult(decimal roundedResult)
250309

251310
private string GetGroupSeparator(string decimalSeparator)
252311
{
312+
var culture = CultureInfo.CurrentCulture;
313+
var systemGroupSeparator = culture.NumberFormat.NumberGroupSeparator;
314+
253315
if (_settings.DecimalSeparator == DecimalSeparator.UseSystemLocale)
254316
{
255-
return CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator;
317+
return systemGroupSeparator;
318+
}
319+
320+
// When a custom decimal separator is used,
321+
// use the system's group separator unless it conflicts with the custom decimal separator.
322+
if (decimalSeparator == systemGroupSeparator)
323+
{
324+
// Conflict: use the opposite of the decimal separator as a fallback.
325+
return decimalSeparator == Dot ? Comma : Dot;
256326
}
257327

258-
// This logic is now independent of the system's group separator
259-
// to ensure consistent output when a specific separator is chosen.
260-
return decimalSeparator == Dot ? Comma : Dot;
328+
return systemGroupSeparator;
261329
}
262330

263331
private string GetDecimalSeparator()

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ namespace Flow.Launcher.Plugin.Calculator;
44

55
internal static partial class MainRegexHelper
66
{
7-
[GeneratedRegex(@"[\(\)\[\]]", RegexOptions.Compiled)]
8-
public static partial Regex GetRegBrackets();
9-
107
[GeneratedRegex(@"-?[\d\.,]+", RegexOptions.Compiled)]
118
public static partial Regex GetNumberRegex();
129

@@ -15,4 +12,7 @@ internal static partial class MainRegexHelper
1512

1613
[GeneratedRegex(@"\bpow(\((?:[^()\[\]]|\((?<Depth>)|\)(?<-Depth>)|\[(?<Depth>)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft)]
1714
public static partial Regex GetPowRegex();
15+
16+
[GeneratedRegex(@"\b(sqrt|pow|factorial|abs|sign|ceil|floor|round|exp|log|log2|log10|min|max|lt|eq|gt|sin|cos|tan|arcsin|arccos|arctan|isnan|isint|isprime|isinfty|rand|randi|type|is|as|length|throw|catch|eval|map|clamp|lerp|regex|shuffle)\s*\(", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
17+
public static partial Regex GetFunctionRegex();
1818
}

0 commit comments

Comments
 (0)