From e1079396c3bfb6e3a22ef4227900c91460cd300d Mon Sep 17 00:00:00 2001 From: dcog989 Date: Fri, 12 Sep 2025 19:20:19 +0100 Subject: [PATCH] backout 'smart' digit grouping, mages fixes + workaround Mages did not like the previous change to smart thousands / decimal so backed that out. Workaround for https://github.com/FlorianRappl/Mages/issues/132 --- .../Flow.Launcher.Plugin.Calculator/Main.cs | 168 +++++++----------- .../MainRegexHelper.cs | 2 +- 2 files changed, 65 insertions(+), 105 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 6878c54b4..d2e1ed821 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -13,7 +13,6 @@ namespace Flow.Launcher.Plugin.Calculator { public class Main : IPlugin, IPluginI18n, ISettingProvider { - 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(); @@ -27,16 +26,6 @@ namespace Flow.Launcher.Plugin.Calculator private Settings _settings; private SettingsViewModel _viewModel; - /// - /// Holds the formatting information for a single query. - /// This is used to ensure thread safety by keeping query state local. - /// - private class ParsingContext - { - public string InputDecimalSeparator { get; set; } - public bool InputUsesGroupSeparators { get; set; } - } - public void Init(PluginInitContext context) { Context = context; @@ -59,24 +48,46 @@ namespace Flow.Launcher.Plugin.Calculator return new List(); } - var context = new ParsingContext(); - try { - var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value, context)); + var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value)); + + // WORKAROUND START: The 'pow' function in Mages v3.0.0 is broken. + // https://github.com/FlorianRappl/Mages/issues/132 + // We bypass it by rewriting any pow(x,y) expression to the equivalent (x^y) expression + // before the engine sees it. This loop handles nested calls. + string previous; + do + { + previous = expression; + expression = Regex.Replace(previous, @"\bpow\s*\(\s*([^,]+?)\s*,\s*([^)]+?)\s*\)", "($1^$2)"); + } while (previous != expression); + // WORKAROUND END var result = MagesEngine.Interpret(expression); - if (result?.ToString() == "NaN") + if (result == null || string.IsNullOrEmpty(result.ToString())) + { + return new List + { + new Result + { + Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(), + IcoPath = "Images/calculator.png" + } + }; + } + + if (result.ToString() == "NaN") result = Localize.flowlauncher_plugin_calculator_not_a_number(); if (result is Function) result = Localize.flowlauncher_plugin_calculator_expression_not_complete(); - if (!string.IsNullOrEmpty(result?.ToString())) + if (!string.IsNullOrEmpty(result.ToString())) { decimal roundedResult = Math.Round(Convert.ToDecimal(result), _settings.MaxDecimalPlaces, MidpointRounding.AwayFromZero); - string newResult = FormatResult(roundedResult, context); + string newResult = FormatResult(roundedResult); return new List { @@ -104,115 +115,69 @@ namespace Flow.Launcher.Plugin.Calculator }; } } - catch (Exception) + catch (Exception e) { - // ignored + return new List + { + new Result + { + Title = e.Message, + SubTitle = "Calculator Exception", + IcoPath = "Images/calculator.png", + Score = 300 + } + }; } return new List(); } /// - /// 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. + /// Parses a string representation of a number using the system's current culture. /// /// A normalized number string with '.' as the decimal separator for the Mages engine. - private string NormalizeNumber(string numberStr, ParsingContext context) + private string NormalizeNumber(string numberStr) { - var systemGroupSep = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator; - int dotCount = numberStr.Count(f => f == '.'); - int commaCount = numberStr.Count(f => f == ','); + var culture = CultureInfo.CurrentCulture; + var groupSep = culture.NumberFormat.NumberGroupSeparator; - // Case 1: Unambiguous mixed separators (e.g., "1.234,56") - if (dotCount > 0 && commaCount > 0) + // If the string contains the group separator, check if it's used correctly. + if (!string.IsNullOrEmpty(groupSep) && numberStr.Contains(groupSep)) { - context.InputUsesGroupSeparators = true; - if (numberStr.LastIndexOf('.') > numberStr.LastIndexOf(',')) + var parts = numberStr.Split(groupSep); + // If any part after the first (excluding a possible last part with a decimal) + // does not have 3 digits, then it's not a valid use of a thousand separator. + for (int i = 1; i < parts.Length; i++) { - 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) + var part = parts[i]; + // The last part might contain a decimal separator. + if (i == parts.Length - 1 && part.Contains(culture.NumberFormat.NumberDecimalSeparator)) { - context.InputUsesGroupSeparators = true; - return numberStr.Replace(Dot, string.Empty); + part = part.Split(culture.NumberFormat.NumberDecimalSeparator)[0]; } - else + + if (part.Length != 3) { - context.InputDecimalSeparator = Dot; + // This is not a number with valid thousand separators, + // so it must be arguments to a function. Return it unmodified. 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) + // At this point, any group separators are in valid positions (or there are none). + // We can safely parse with the user's culture. + if (decimal.TryParse(numberStr, NumberStyles.Any, culture, out var number)) { - 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); - } + return number.ToString(CultureInfo.InvariantCulture); } - // Case 4: No separators return numberStr; } - private string FormatResult(decimal roundedResult, ParsingContext context) + private string FormatResult(decimal roundedResult) { - string decimalSeparator = context.InputDecimalSeparator ?? GetDecimalSeparator(); + string decimalSeparator = GetDecimalSeparator(); string groupSeparator = GetGroupSeparator(decimalSeparator); string resultStr = roundedResult.ToString(CultureInfo.InvariantCulture); @@ -221,7 +186,7 @@ namespace Flow.Launcher.Plugin.Calculator string integerPart = parts[0]; string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty; - if (context.InputUsesGroupSeparators && integerPart.Length > 3) + if (integerPart.Length > 3) { integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator); } @@ -248,11 +213,6 @@ namespace Flow.Launcher.Plugin.Calculator return false; } - if (!RegValidExpressChar.IsMatch(query.Search)) - { - return false; - } - if (!IsBracketComplete(query.Search)) { return false; diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs index f4e2090e7..85db0c2cd 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs @@ -11,7 +11,7 @@ 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)] + [GeneratedRegex(@"-?[\d\.,]+", RegexOptions.Compiled)] public static partial Regex GetNumberRegex(); [GeneratedRegex(@"\B(?=(\d{3})+(?!\d))", RegexOptions.Compiled)]