From e1079396c3bfb6e3a22ef4227900c91460cd300d Mon Sep 17 00:00:00 2001 From: dcog989 Date: Fri, 12 Sep 2025 19:20:19 +0100 Subject: [PATCH 01/21] 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 6878c54b4a8..d2e1ed82124 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 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider 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 @@ public List Query(Query query) 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 @@ public List Query(Query query) }; } } - 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 == ','); - - // 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); - } - } + var culture = CultureInfo.CurrentCulture; + var groupSep = culture.NumberFormat.NumberGroupSeparator; - // Case 2: Only dots - if (dotCount > 0) + // If the string contains the group separator, check if it's used correctly. + if (!string.IsNullOrEmpty(groupSep) && numberStr.Contains(groupSep)) { - 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) + 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++) { - 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 @@ private string FormatResult(decimal roundedResult, ParsingContext context) 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 @@ private bool CanCalculate(Query query) 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 f4e2090e740..85db0c2cd74 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)] From 103d3832a087a6a9d7bc341898d9291367fdd158 Mon Sep 17 00:00:00 2001 From: dcog989 Date: Fri, 12 Sep 2025 19:30:07 +0100 Subject: [PATCH 02/21] dead code, improve messages, group separator fix? --- .../Flow.Launcher.Plugin.Calculator/Main.cs | 18 +++++++++++------- .../MainRegexHelper.cs | 3 --- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index d2e1ed82124..85cd8a6fd2b 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -115,16 +115,15 @@ public List Query(Query query) }; } } - catch (Exception e) + catch (Exception) { + // Mages engine can throw various exceptions, for simplicity we catch them all and show a generic message. return new List { new Result { - Title = e.Message, - SubTitle = "Calculator Exception", - IcoPath = "Images/calculator.png", - Score = 300 + Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(), + IcoPath = "Images/calculator.png" } }; } @@ -201,14 +200,19 @@ private string FormatResult(decimal roundedResult) private string GetGroupSeparator(string decimalSeparator) { + if (_settings.DecimalSeparator == DecimalSeparator.UseSystemLocale) + { + return CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator; + } + // This logic is now independent of the system's group separator - // to ensure consistent output for unit testing. + // to ensure consistent output when a specific separator is chosen. return decimalSeparator == Dot ? Comma : Dot; } private bool CanCalculate(Query query) { - if (query.Search.Length < 2) + if (string.IsNullOrWhiteSpace(query.Search)) { return false; } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs index 85db0c2cd74..2e035361454 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs @@ -8,9 +8,6 @@ internal static partial class MainRegexHelper [GeneratedRegex(@"[\(\)\[\]]", RegexOptions.Compiled)] public static partial Regex GetRegBrackets(); - [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(); From 190e0e179f28ba12ad240347971cb5aec7454e07 Mon Sep 17 00:00:00 2001 From: dcog989 Date: Fri, 12 Sep 2025 20:01:55 +0100 Subject: [PATCH 03/21] Fix 'German' number formatting --- Plugins/Flow.Launcher.Plugin.Calculator/Main.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 85cd8a6fd2b..c81eb9b1c8c 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -139,6 +139,7 @@ private string NormalizeNumber(string numberStr) { var culture = CultureInfo.CurrentCulture; var groupSep = culture.NumberFormat.NumberGroupSeparator; + var decimalSep = culture.NumberFormat.NumberDecimalSeparator; // If the string contains the group separator, check if it's used correctly. if (!string.IsNullOrEmpty(groupSep) && numberStr.Contains(groupSep)) @@ -164,14 +165,11 @@ private string NormalizeNumber(string numberStr) } } - // 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)) - { - return number.ToString(CultureInfo.InvariantCulture); - } + // If validation passes, we can assume the separators are used correctly for numbers. + string processedStr = numberStr.Replace(groupSep, ""); + processedStr = processedStr.Replace(decimalSep, "."); - return numberStr; + return processedStr; } private string FormatResult(decimal roundedResult) From bd186e7fe1bf046d54311175767b648fd8913969 Mon Sep 17 00:00:00 2001 From: dcog989 Date: Fri, 12 Sep 2025 20:02:34 +0100 Subject: [PATCH 04/21] correct + extend description --- Plugins/Flow.Launcher.Plugin.Calculator/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json index c9435e04315..7b4b53cdb67 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json @@ -2,7 +2,7 @@ "ID": "CEA0FDFC6D3B4085823D60DC76F28855", "ActionKeyword": "*", "Name": "Calculator", - "Description": "Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place.", + "Description": "Perform mathematical calculations, including hex values and advanced math functions, such as 'min(1,2,3)', 'sqrt(123)', 'cos(123)', etc.. User locale determines thousand separator and decimal place.", "Author": "cxfksword, dcog989", "Version": "1.0.0", "Language": "csharp", From 110f571b40840e798237c304216b2435d9845796 Mon Sep 17 00:00:00 2001 From: dcog989 Date: Sat, 13 Sep 2025 13:17:08 +0100 Subject: [PATCH 05/21] Rework solution for nested Mages Previous solution missed e.g. `pow(min(2,3), 4)` --- .../Flow.Launcher.Plugin.Calculator/Main.cs | 89 +++++++++++++++---- 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index c81eb9b1c8c..99f0f83955f 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -16,6 +16,8 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider private static readonly Regex RegBrackets = MainRegexHelper.GetRegBrackets(); private static readonly Regex ThousandGroupRegex = MainRegexHelper.GetThousandGroupRegex(); private static readonly Regex NumberRegex = MainRegexHelper.GetNumberRegex(); + private static readonly Regex PowRegex = new(@"\bpow(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft); + private static Engine MagesEngine; private const string Comma = ","; @@ -43,11 +45,23 @@ public void Init(PluginInitContext context) public List Query(Query query) { - if (!CanCalculate(query)) + if (string.IsNullOrWhiteSpace(query.Search)) { return new List(); } + if (!IsBracketComplete(query.Search)) + { + return new List + { + new Result + { + Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(), + IcoPath = "Images/calculator.png" + } + }; + } + try { var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value)); @@ -60,7 +74,7 @@ public List Query(Query query) do { previous = expression; - expression = Regex.Replace(previous, @"\bpow\s*\(\s*([^,]+?)\s*,\s*([^)]+?)\s*\)", "($1^$2)"); + expression = PowRegex.Replace(previous, PowMatchEvaluator); } while (previous != expression); // WORKAROUND END @@ -131,6 +145,57 @@ public List Query(Query query) return new List(); } + private static string PowMatchEvaluator(Match m) + { + // m.Groups[1].Value will be `(...)` with parens + var contentWithParen = m.Groups[1].Value; + // remove outer parens. `(min(2,3), 4)` becomes `min(2,3), 4` + var argsContent = contentWithParen.Substring(1, contentWithParen.Length - 2); + + var bracketCount = 0; + var splitIndex = -1; + + // Find the top-level comma that separates the two arguments of pow. + for (var i = 0; i < argsContent.Length; i++) + { + switch (argsContent[i]) + { + case '(': + case '[': + bracketCount++; + break; + case ')': + case ']': + bracketCount--; + break; + case ',' when bracketCount == 0: + splitIndex = i; + break; + } + + if (splitIndex != -1) + break; + } + + if (splitIndex == -1) + { + // This indicates malformed arguments for pow, e.g., pow(5) or pow(). + // Return original string to let Mages handle the error. + return m.Value; + } + + var arg1 = argsContent.Substring(0, splitIndex).Trim(); + var arg2 = argsContent.Substring(splitIndex + 1).Trim(); + + // Check for empty arguments which can happen with stray commas, e.g., pow(,5) + if (string.IsNullOrEmpty(arg1) || string.IsNullOrEmpty(arg2)) + { + return m.Value; + } + + return $"({arg1}^{arg2})"; + } + /// /// Parses a string representation of a number using the system's current culture. /// @@ -208,21 +273,6 @@ private string GetGroupSeparator(string decimalSeparator) return decimalSeparator == Dot ? Comma : Dot; } - private bool CanCalculate(Query query) - { - if (string.IsNullOrWhiteSpace(query.Search)) - { - return false; - } - - if (!IsBracketComplete(query.Search)) - { - return false; - } - - return true; - } - private string GetDecimalSeparator() { string systemDecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; @@ -249,6 +299,11 @@ private static bool IsBracketComplete(string query) { leftBracketCount--; } + + if (leftBracketCount < 0) + { + return false; + } } return leftBracketCount == 0; From 11f5ea5074be16e3b712a05f4484b1c91e3bfa28 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 14 Sep 2025 12:33:46 +0800 Subject: [PATCH 06/21] Improve code quality --- Plugins/Flow.Launcher.Plugin.Calculator/Main.cs | 6 ++---- Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 99f0f83955f..0fd32555f3e 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Windows.Controls; @@ -16,8 +15,7 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider private static readonly Regex RegBrackets = MainRegexHelper.GetRegBrackets(); private static readonly Regex ThousandGroupRegex = MainRegexHelper.GetThousandGroupRegex(); private static readonly Regex NumberRegex = MainRegexHelper.GetNumberRegex(); - private static readonly Regex PowRegex = new(@"\bpow(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft); - + private static readonly Regex PowRegex = MainRegexHelper.GetPowRegex(); private static Engine MagesEngine; private const string Comma = ","; @@ -200,7 +198,7 @@ private static string PowMatchEvaluator(Match m) /// 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) + private static string NormalizeNumber(string numberStr) { var culture = CultureInfo.CurrentCulture; var groupSep = culture.NumberFormat.NumberGroupSeparator; diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs index 2e035361454..d8c2795dc85 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs @@ -4,7 +4,6 @@ namespace Flow.Launcher.Plugin.Calculator; internal static partial class MainRegexHelper { - [GeneratedRegex(@"[\(\)\[\]]", RegexOptions.Compiled)] public static partial Regex GetRegBrackets(); @@ -13,4 +12,7 @@ internal static partial class MainRegexHelper [GeneratedRegex(@"\B(?=(\d{3})+(?!\d))", RegexOptions.Compiled)] public static partial Regex GetThousandGroupRegex(); + + [GeneratedRegex(@"\bpow(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft)] + public static partial Regex GetPowRegex(); } From 495ace124687f86fd9828439ddaab2436079972d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 14 Sep 2025 12:39:11 +0800 Subject: [PATCH 07/21] Improve plugin description --- Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml | 2 +- Plugins/Flow.Launcher.Plugin.Calculator/plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml index b71e5d8a0e0..29a0ed26ff6 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml @@ -4,7 +4,7 @@ xmlns:system="clr-namespace:System;assembly=mscorlib"> Calculator - Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place. + Perform mathematical calculations, including hex values and advanced functions such as 'min(1,2,3)', 'sqrt(123)' and 'cos(123)'. Not a number (NaN) Expression wrong or incomplete (Did you forget some parentheses?) Copy this number to the clipboard diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json index 7b4b53cdb67..93df9ec72dd 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Calculator/plugin.json @@ -2,7 +2,7 @@ "ID": "CEA0FDFC6D3B4085823D60DC76F28855", "ActionKeyword": "*", "Name": "Calculator", - "Description": "Perform mathematical calculations, including hex values and advanced math functions, such as 'min(1,2,3)', 'sqrt(123)', 'cos(123)', etc.. User locale determines thousand separator and decimal place.", + "Description": "Perform mathematical calculations, including hex values and advanced functions such as 'min(1,2,3)', 'sqrt(123)' and 'cos(123)'.", "Author": "cxfksword, dcog989", "Version": "1.0.0", "Language": "csharp", From daf35a49725fab9107104238a81e72295ef7965f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 14 Sep 2025 15:54:35 +0800 Subject: [PATCH 08/21] Do not check bracket complete --- .../Flow.Launcher.Plugin.Calculator/Main.cs | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 0fd32555f3e..d19dbc9c27d 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -48,18 +48,6 @@ public List Query(Query query) return new List(); } - if (!IsBracketComplete(query.Search)) - { - return new List - { - new Result - { - Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(), - IcoPath = "Images/calculator.png" - } - }; - } - try { var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value)); @@ -283,30 +271,6 @@ private string GetDecimalSeparator() }; } - private static bool IsBracketComplete(string query) - { - var matchs = RegBrackets.Matches(query); - var leftBracketCount = 0; - foreach (Match match in matchs) - { - if (match.Value == "(" || match.Value == "[") - { - leftBracketCount++; - } - else - { - leftBracketCount--; - } - - if (leftBracketCount < 0) - { - return false; - } - } - - return leftBracketCount == 0; - } - public string GetTranslatedPluginTitle() { return Localize.flowlauncher_plugin_calculator_plugin_name(); From 336e51d1047f085b83fb5bb9b886b3b11b4dd0ed Mon Sep 17 00:00:00 2001 From: dcog989 Date: Sun, 14 Sep 2025 17:08:37 +0100 Subject: [PATCH 09/21] IcoPath to const string --- Plugins/Flow.Launcher.Plugin.Calculator/Main.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index d19dbc9c27d..54ac61ffb8f 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -20,6 +20,7 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider private static Engine MagesEngine; private const string Comma = ","; private const string Dot = "."; + private const string IcoPath = "Images/calculator.png"; internal static PluginInitContext Context { get; set; } = null!; @@ -73,7 +74,7 @@ public List Query(Query query) new Result { Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(), - IcoPath = "Images/calculator.png" + IcoPath = IcoPath } }; } @@ -94,7 +95,7 @@ public List Query(Query query) new Result { Title = newResult, - IcoPath = "Images/calculator.png", + IcoPath = IcoPath, Score = 300, SubTitle = Localize.flowlauncher_plugin_calculator_copy_number_to_clipboard(), CopyText = newResult, @@ -123,7 +124,7 @@ public List Query(Query query) new Result { Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(), - IcoPath = "Images/calculator.png" + IcoPath = IcoPath } }; } From e990e0ff5b38ead1cc1f9851a573aac7802951a8 Mon Sep 17 00:00:00 2001 From: dcog989 Date: Sun, 14 Sep 2025 19:39:06 +0100 Subject: [PATCH 10/21] 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. --- .../Flow.Launcher.Plugin.Calculator/Main.cs | 130 +++++++++++++----- .../MainRegexHelper.cs | 6 +- 2 files changed, 102 insertions(+), 34 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 54ac61ffb8f..a15420a632b 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -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; @@ -12,10 +13,10 @@ namespace Flow.Launcher.Plugin.Calculator { public class Main : IPlugin, IPluginI18n, ISettingProvider { - private static readonly Regex RegBrackets = MainRegexHelper.GetRegBrackets(); private static readonly Regex ThousandGroupRegex = MainRegexHelper.GetThousandGroupRegex(); private static readonly Regex NumberRegex = MainRegexHelper.GetNumberRegex(); private static readonly Regex PowRegex = MainRegexHelper.GetPowRegex(); + private static readonly Regex FunctionRegex = MainRegexHelper.GetFunctionRegex(); private static Engine MagesEngine; private const string Comma = ","; @@ -51,7 +52,8 @@ public List Query(Query query) try { - var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value)); + bool isFunctionPresent = FunctionRegex.IsMatch(query.Search); + var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value, isFunctionPresent)); // WORKAROUND START: The 'pow' function in Mages v3.0.0 is broken. // https://github.com/FlorianRappl/Mages/issues/132 @@ -183,47 +185,104 @@ private static string PowMatchEvaluator(Match m) return $"({arg1}^{arg2})"; } - /// - /// 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 static string NormalizeNumber(string numberStr) + private static string NormalizeNumber(string numberStr, bool isFunctionPresent) { var culture = CultureInfo.CurrentCulture; var groupSep = culture.NumberFormat.NumberGroupSeparator; var decimalSep = culture.NumberFormat.NumberDecimalSeparator; - // If the string contains the group separator, check if it's used correctly. - if (!string.IsNullOrEmpty(groupSep) && numberStr.Contains(groupSep)) + if (isFunctionPresent) { - 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++) + // STRICT MODE: When functions are present, ',' is ALWAYS an argument separator. + // It must not be normalized. + if (numberStr.Contains(',')) { - var part = parts[i]; - // The last part might contain a decimal separator. - if (i == parts.Length - 1 && part.Contains(culture.NumberFormat.NumberDecimalSeparator)) - { - part = part.Split(culture.NumberFormat.NumberDecimalSeparator)[0]; - } + return numberStr; + } - if (part.Length != 3) + // The string has no commas. It could have a '.' group separator (e.g. in de-DE) + // or a '.' decimal separator (e.g. in en-US). + // Since Mages' decimal separator is '.', we only need to strip the group separator. + if (groupSep == ".") + { + var parts = numberStr.Split('.'); + // A number with a dot group separator, e.g., "1.234" + if (parts.Length > 1) { - // This is not a number with valid thousand separators, - // so it must be arguments to a function. Return it unmodified. - return numberStr; + // Check if the parts after the first dot have the correct group length (usually 3). + for (int i = 1; i < parts.Length; i++) + { + if (parts[i].Length != 3) + { + // Malformed grouping, e.g., "1.23". This is likely a decimal number. + // Return as is and let Mages handle it. + return numberStr; + } + } + // Correct grouping, e.g., "1.234" or "1.234.567". Strip separators. + return numberStr.Replace(".", ""); } } + + // For any other case (e.g. en-US culture where group sep is ',' which was already handled), + // return the string as is. + return numberStr; } + else + { + // LENIENT MODE: No functions are present, so we can be flexible. + string processedStr = numberStr; + if (!string.IsNullOrEmpty(groupSep)) + { + processedStr = processedStr.Replace(groupSep, ""); + } + processedStr = processedStr.Replace(decimalSep, "."); + return processedStr; + } + } + + private static bool IsValidGrouping(string[] parts, int[] groupSizes) + { + if (parts.Length <= 1) return true; + + if (groupSizes is null || groupSizes.Length == 0 || groupSizes[0] == 0) + return false; // has groups, but culture defines none. + + var firstPart = parts[0]; + if (firstPart.StartsWith("-")) firstPart = firstPart.Substring(1); + if (firstPart.Length == 0) return false; // e.g. ",123" - // If validation passes, we can assume the separators are used correctly for numbers. - string processedStr = numberStr.Replace(groupSep, ""); - processedStr = processedStr.Replace(decimalSep, "."); + if (firstPart.Length > groupSizes[0]) return false; - return processedStr; + var lastGroupSize = groupSizes.Last(); + var canRepeatLastGroup = lastGroupSize != 0; + + int groupIndex = 0; + for (int i = parts.Length - 1; i > 0; i--) + { + int expectedSize; + if (groupIndex < groupSizes.Length) + { + expectedSize = groupSizes[groupIndex]; + } + else if(canRepeatLastGroup) + { + expectedSize = lastGroupSize; + } + else + { + return false; + } + + if (parts[i].Length != expectedSize) return false; + + groupIndex++; + } + + return true; } + private string FormatResult(decimal roundedResult) { string decimalSeparator = GetDecimalSeparator(); @@ -250,14 +309,23 @@ private string FormatResult(decimal roundedResult) private string GetGroupSeparator(string decimalSeparator) { + var culture = CultureInfo.CurrentCulture; + var systemGroupSeparator = culture.NumberFormat.NumberGroupSeparator; + if (_settings.DecimalSeparator == DecimalSeparator.UseSystemLocale) { - return CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator; + return systemGroupSeparator; + } + + // When a custom decimal separator is used, + // use the system's group separator unless it conflicts with the custom decimal separator. + if (decimalSeparator == systemGroupSeparator) + { + // Conflict: use the opposite of the decimal separator as a fallback. + return decimalSeparator == Dot ? Comma : Dot; } - // This logic is now independent of the system's group separator - // to ensure consistent output when a specific separator is chosen. - return decimalSeparator == Dot ? Comma : Dot; + return systemGroupSeparator; } private string GetDecimalSeparator() diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs index d8c2795dc85..5c2c19cf4b4 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs @@ -4,9 +4,6 @@ namespace Flow.Launcher.Plugin.Calculator; internal static partial class MainRegexHelper { - [GeneratedRegex(@"[\(\)\[\]]", RegexOptions.Compiled)] - public static partial Regex GetRegBrackets(); - [GeneratedRegex(@"-?[\d\.,]+", RegexOptions.Compiled)] public static partial Regex GetNumberRegex(); @@ -15,4 +12,7 @@ internal static partial class MainRegexHelper [GeneratedRegex(@"\bpow(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft)] public static partial Regex GetPowRegex(); + + [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)] + public static partial Regex GetFunctionRegex(); } From edc76faeb4c8860c3f676e5addb7da948a33b3ef Mon Sep 17 00:00:00 2001 From: dcog989 Date: Sun, 14 Sep 2025 21:51:47 +0100 Subject: [PATCH 11/21] Review feedback, case insensitive, consistent separators --- .../Flow.Launcher.Plugin.Calculator/Main.cs | 72 ++++++++++++------- .../MainRegexHelper.cs | 2 +- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index a15420a632b..545f343f285 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -52,8 +52,15 @@ public List Query(Query query) try { - bool isFunctionPresent = FunctionRegex.IsMatch(query.Search); - var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value, isFunctionPresent)); + var search = query.Search; + bool isFunctionPresent = FunctionRegex.IsMatch(search); + + // Mages is case sensitive, so we need to convert all function names to lower case. + search = FunctionRegex.Replace(search, m => m.Value.ToLowerInvariant()); + + var decimalSep = GetDecimalSeparator(); + var groupSep = GetGroupSeparator(decimalSep); + var expression = NumberRegex.Replace(search, m => NormalizeNumber(m.Value, isFunctionPresent, decimalSep, groupSep)); // WORKAROUND START: The 'pow' function in Mages v3.0.0 is broken. // https://github.com/FlorianRappl/Mages/issues/132 @@ -185,48 +192,56 @@ private static string PowMatchEvaluator(Match m) return $"({arg1}^{arg2})"; } - private static string NormalizeNumber(string numberStr, bool isFunctionPresent) + private static string NormalizeNumber(string numberStr, bool isFunctionPresent, string decimalSep, string groupSep) { - var culture = CultureInfo.CurrentCulture; - var groupSep = culture.NumberFormat.NumberGroupSeparator; - var decimalSep = culture.NumberFormat.NumberDecimalSeparator; - if (isFunctionPresent) { // STRICT MODE: When functions are present, ',' is ALWAYS an argument separator. - // It must not be normalized. if (numberStr.Contains(',')) { return numberStr; } - // The string has no commas. It could have a '.' group separator (e.g. in de-DE) - // or a '.' decimal separator (e.g. in en-US). - // Since Mages' decimal separator is '.', we only need to strip the group separator. - if (groupSep == ".") + string processedStr = numberStr; + + // Handle group separator, with special care for ambiguous dot. + if (!string.IsNullOrEmpty(groupSep)) { - var parts = numberStr.Split('.'); - // A number with a dot group separator, e.g., "1.234" - if (parts.Length > 1) + if (groupSep == ".") { - // Check if the parts after the first dot have the correct group length (usually 3). - for (int i = 1; i < parts.Length; i++) + var parts = processedStr.Split('.'); + if (parts.Length > 1) { - if (parts[i].Length != 3) + bool isGrouped = true; + for (var i = 1; i < parts.Length; i++) + { + if (parts[i].Length != 3) + { + isGrouped = false; + break; + } + } + + if (isGrouped) { - // Malformed grouping, e.g., "1.23". This is likely a decimal number. - // Return as is and let Mages handle it. - return numberStr; + processedStr = processedStr.Replace(groupSep, ""); } + // If not grouped, it's likely a decimal number, so we don't strip dots. } - // Correct grouping, e.g., "1.234" or "1.234.567". Strip separators. - return numberStr.Replace(".", ""); } + else + { + processedStr = processedStr.Replace(groupSep, ""); + } + } + + // Handle decimal separator. + if (decimalSep != ".") + { + processedStr = processedStr.Replace(decimalSep, "."); } - // For any other case (e.g. en-US culture where group sep is ',' which was already handled), - // return the string as is. - return numberStr; + return processedStr; } else { @@ -236,7 +251,10 @@ private static string NormalizeNumber(string numberStr, bool isFunctionPresent) { processedStr = processedStr.Replace(groupSep, ""); } - processedStr = processedStr.Replace(decimalSep, "."); + if (decimalSep != ".") + { + processedStr = processedStr.Replace(decimalSep, "."); + } return processedStr; } } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs index 5c2c19cf4b4..dacc249d9de 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs @@ -10,7 +10,7 @@ internal static partial class MainRegexHelper [GeneratedRegex(@"\B(?=(\d{3})+(?!\d))", RegexOptions.Compiled)] public static partial Regex GetThousandGroupRegex(); - [GeneratedRegex(@"\bpow(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft)] + [GeneratedRegex(@"\bpow(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase)] public static partial Regex GetPowRegex(); [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)] From 9be8b71f0992d16bc235efe7be8ed1b781a22131 Mon Sep 17 00:00:00 2001 From: dcog989 Date: Sun, 14 Sep 2025 22:28:37 +0100 Subject: [PATCH 12/21] review feedback, CultureInvariant, mild refactor --- Plugins/Flow.Launcher.Plugin.Calculator/Main.cs | 14 ++------------ .../MainRegexHelper.cs | 6 +++--- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 545f343f285..a4c01fc210f 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -212,17 +212,8 @@ private static string NormalizeNumber(string numberStr, bool isFunctionPresent, var parts = processedStr.Split('.'); if (parts.Length > 1) { - bool isGrouped = true; - for (var i = 1; i < parts.Length; i++) - { - if (parts[i].Length != 3) - { - isGrouped = false; - break; - } - } - - if (isGrouped) + var culture = CultureInfo.CurrentCulture; + if (IsValidGrouping(parts, culture.NumberFormat.NumberGroupSizes)) { processedStr = processedStr.Replace(groupSep, ""); } @@ -300,7 +291,6 @@ private static bool IsValidGrouping(string[] parts, int[] groupSizes) return true; } - private string FormatResult(decimal roundedResult) { string decimalSeparator = GetDecimalSeparator(); diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs index dacc249d9de..0746e45562a 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs @@ -4,15 +4,15 @@ namespace Flow.Launcher.Plugin.Calculator; internal static partial class MainRegexHelper { - [GeneratedRegex(@"-?[\d\.,]+", RegexOptions.Compiled)] + [GeneratedRegex(@"-?[\d\.,'\u00A0\u202F]+", RegexOptions.Compiled | RegexOptions.CultureInvariant)] public static partial Regex GetNumberRegex(); [GeneratedRegex(@"\B(?=(\d{3})+(?!\d))", RegexOptions.Compiled)] public static partial Regex GetThousandGroupRegex(); - [GeneratedRegex(@"\bpow(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase)] + [GeneratedRegex(@"\bpow(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] public static partial Regex GetPowRegex(); - [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)] + [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 | RegexOptions.CultureInvariant)] public static partial Regex GetFunctionRegex(); } From b07420a193c8c51468d404155a4680fe7253d5aa Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 15 Sep 2025 12:51:31 +0800 Subject: [PATCH 13/21] Use EmptyResults to improve code quality --- Plugins/Flow.Launcher.Plugin.Calculator/Main.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index a4c01fc210f..cd54a2155c5 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -22,6 +22,7 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider private const string Comma = ","; private const string Dot = "."; private const string IcoPath = "Images/calculator.png"; + private static readonly List EmptyResults = []; internal static PluginInitContext Context { get; set; } = null!; @@ -47,7 +48,7 @@ public List Query(Query query) { if (string.IsNullOrWhiteSpace(query.Search)) { - return new List(); + return EmptyResults; } try @@ -138,7 +139,7 @@ public List Query(Query query) }; } - return new List(); + return EmptyResults; } private static string PowMatchEvaluator(Match m) From cea1402dde4bddf71fef55ebbc01007089c8315f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 15 Sep 2025 12:53:17 +0800 Subject: [PATCH 14/21] Improve code quality --- Plugins/Flow.Launcher.Plugin.Calculator/Main.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index cd54a2155c5..5b04ae7e88e 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -181,8 +181,8 @@ private static string PowMatchEvaluator(Match m) return m.Value; } - var arg1 = argsContent.Substring(0, splitIndex).Trim(); - var arg2 = argsContent.Substring(splitIndex + 1).Trim(); + var arg1 = argsContent[..splitIndex].Trim(); + var arg2 = argsContent[(splitIndex + 1)..].Trim(); // Check for empty arguments which can happen with stray commas, e.g., pow(,5) if (string.IsNullOrEmpty(arg1) || string.IsNullOrEmpty(arg2)) @@ -259,7 +259,7 @@ private static bool IsValidGrouping(string[] parts, int[] groupSizes) return false; // has groups, but culture defines none. var firstPart = parts[0]; - if (firstPart.StartsWith("-")) firstPart = firstPart.Substring(1); + if (firstPart.StartsWith('-')) firstPart = firstPart[1..]; if (firstPart.Length == 0) return false; // e.g. ",123" if (firstPart.Length > groupSizes[0]) return false; From 1906d6854177b0960f0a861582254c31fe836439 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 15 Sep 2025 12:57:25 +0800 Subject: [PATCH 15/21] Add ShowErrorMessage setting --- .../Flow.Launcher.Plugin.Calculator/Languages/en.xaml | 1 + Plugins/Flow.Launcher.Plugin.Calculator/Main.cs | 6 ++++++ Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs | 5 ++++- .../Views/CalculatorSettings.xaml | 10 ++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml index 29a0ed26ff6..b12972b1b84 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml @@ -15,4 +15,5 @@ Dot (.) Max. decimal places Copy failed, please try later + Show error message when calculation fails \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 5b04ae7e88e..429b04979f4 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -79,6 +79,7 @@ public List Query(Query query) if (result == null || string.IsNullOrEmpty(result.ToString())) { + if (!_settings.ShowErrorMessage) return EmptyResults; return new List { new Result @@ -90,10 +91,14 @@ public List Query(Query query) } 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())) { @@ -129,6 +134,7 @@ public List Query(Query query) catch (Exception) { // Mages engine can throw various exceptions, for simplicity we catch them all and show a generic message. + if (!_settings.ShowErrorMessage) return EmptyResults; return new List { new Result diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs index 8354863b852..0f32b09a997 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs @@ -4,6 +4,9 @@ namespace Flow.Launcher.Plugin.Calculator public class Settings { public DecimalSeparator DecimalSeparator { get; set; } = DecimalSeparator.UseSystemLocale; - public int MaxDecimalPlaces { get; set; } = 10; + + public int MaxDecimalPlaces { get; set; } = 10; + + public bool ShowErrorMessage { get; set; } = false; } } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml index 8d240ef3971..9e7549b2df1 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml @@ -15,6 +15,7 @@ + @@ -58,5 +59,14 @@ ItemsSource="{Binding MaxDecimalPlacesRange}" SelectedItem="{Binding Settings.MaxDecimalPlaces}" /> + From f9facda5216369e393bb3f126b5f7fdc543fef12 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 15 Sep 2025 13:02:42 +0800 Subject: [PATCH 16/21] Improve code quality --- .../Flow.Launcher.Plugin.Calculator/Main.cs | 20 ++++++------ .../Settings.cs | 13 ++++---- .../ViewModels/SettingsViewModel.cs | 32 ++++++++----------- .../Views/CalculatorSettings.xaml.cs | 24 ++++++-------- 4 files changed, 38 insertions(+), 51 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 429b04979f4..17b4a85a459 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -80,14 +80,14 @@ public List Query(Query query) if (result == null || string.IsNullOrEmpty(result.ToString())) { if (!_settings.ShowErrorMessage) return EmptyResults; - return new List - { + return + [ new Result { Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(), IcoPath = IcoPath } - }; + ]; } if (result.ToString() == "NaN") @@ -105,8 +105,8 @@ public List Query(Query query) decimal roundedResult = Math.Round(Convert.ToDecimal(result), _settings.MaxDecimalPlaces, MidpointRounding.AwayFromZero); string newResult = FormatResult(roundedResult); - return new List - { + return + [ new Result { Title = newResult, @@ -128,21 +128,21 @@ public List Query(Query query) } } } - }; + ]; } } catch (Exception) { // Mages engine can throw various exceptions, for simplicity we catch them all and show a generic message. if (!_settings.ShowErrorMessage) return EmptyResults; - return new List - { + return + [ new Result { Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(), IcoPath = IcoPath } - }; + ]; } return EmptyResults; @@ -153,7 +153,7 @@ private static string PowMatchEvaluator(Match m) // m.Groups[1].Value will be `(...)` with parens var contentWithParen = m.Groups[1].Value; // remove outer parens. `(min(2,3), 4)` becomes `min(2,3), 4` - var argsContent = contentWithParen.Substring(1, contentWithParen.Length - 2); + var argsContent = contentWithParen[1..^1]; var bracketCount = 0; var splitIndex = -1; diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs index 0f32b09a997..cac0f308016 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs @@ -1,12 +1,11 @@  -namespace Flow.Launcher.Plugin.Calculator +namespace Flow.Launcher.Plugin.Calculator; + +public class Settings { - public class Settings - { - public DecimalSeparator DecimalSeparator { get; set; } = DecimalSeparator.UseSystemLocale; + public DecimalSeparator DecimalSeparator { get; set; } = DecimalSeparator.UseSystemLocale; - public int MaxDecimalPlaces { get; set; } = 10; + public int MaxDecimalPlaces { get; set; } = 10; - public bool ShowErrorMessage { get; set; } = false; - } + public bool ShowErrorMessage { get; set; } = false; } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs index 87ae72fb681..36c277f159a 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs @@ -1,31 +1,25 @@ using System.Collections.Generic; using System.Linq; -namespace Flow.Launcher.Plugin.Calculator.ViewModels -{ - public class SettingsViewModel : BaseModel - { - public SettingsViewModel(Settings settings) - { - Settings = settings; - } +namespace Flow.Launcher.Plugin.Calculator.ViewModels; - public Settings Settings { get; init; } +public class SettingsViewModel(Settings settings) : BaseModel +{ + public Settings Settings { get; init; } = settings; - public static IEnumerable MaxDecimalPlacesRange => Enumerable.Range(1, 20); + public static IEnumerable MaxDecimalPlacesRange => Enumerable.Range(1, 20); - public List AllDecimalSeparator { get; } = DecimalSeparatorLocalized.GetValues(); + public List AllDecimalSeparator { get; } = DecimalSeparatorLocalized.GetValues(); - public DecimalSeparator SelectedDecimalSeparator + public DecimalSeparator SelectedDecimalSeparator + { + get => Settings.DecimalSeparator; + set { - get => Settings.DecimalSeparator; - set + if (Settings.DecimalSeparator != value) { - if (Settings.DecimalSeparator != value) - { - Settings.DecimalSeparator = value; - OnPropertyChanged(); - } + Settings.DecimalSeparator = value; + OnPropertyChanged(); } } } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs index 7bc307d111c..9e75e7bfb3b 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml.cs @@ -1,22 +1,16 @@ using System.Windows.Controls; using Flow.Launcher.Plugin.Calculator.ViewModels; -namespace Flow.Launcher.Plugin.Calculator.Views +namespace Flow.Launcher.Plugin.Calculator.Views; + +public partial class CalculatorSettings : UserControl { - /// - /// Interaction logic for CalculatorSettings.xaml - /// - public partial class CalculatorSettings : UserControl - { - private readonly SettingsViewModel _viewModel; - private readonly Settings _settings; + private readonly SettingsViewModel _viewModel; - public CalculatorSettings(Settings settings) - { - _viewModel = new SettingsViewModel(settings); - _settings = _viewModel.Settings; - DataContext = _viewModel; - InitializeComponent(); - } + public CalculatorSettings(Settings settings) + { + _viewModel = new SettingsViewModel(settings); + DataContext = _viewModel; + InitializeComponent(); } } From 684fafdfddca9acfdaf6372a0a4546bc59e705e0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 15 Sep 2025 15:18:42 +0800 Subject: [PATCH 17/21] Improve code quality --- .../ViewModels/SettingsViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs index 36c277f159a..79236bdf8e5 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/ViewModels/SettingsViewModel.cs @@ -5,7 +5,7 @@ namespace Flow.Launcher.Plugin.Calculator.ViewModels; public class SettingsViewModel(Settings settings) : BaseModel { - public Settings Settings { get; init; } = settings; + public Settings Settings { get; } = settings; public static IEnumerable MaxDecimalPlacesRange => Enumerable.Range(1, 20); From 6841ad5410fd44c3402362a56723f4289cf1b7a0 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 16 Sep 2025 16:08:17 +0800 Subject: [PATCH 18/21] Add calculator unit testing --- Flow.Launcher.Test/Flow.Launcher.Test.csproj | 1 + Flow.Launcher.Test/Plugins/CalculatorTest.cs | 86 +++++++++++++++++++ .../Flow.Launcher.Plugin.Calculator/Main.cs | 3 +- 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 Flow.Launcher.Test/Plugins/CalculatorTest.cs diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index 1164e5ebea6..11ccff05b05 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -39,6 +39,7 @@ + diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs new file mode 100644 index 00000000000..3f403b24e3a --- /dev/null +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Flow.Launcher.Plugin.Calculator; +using Mages.Core; +using NUnit.Framework; +using NUnit.Framework.Legacy; + +namespace Flow.Launcher.Test.Plugins +{ + [TestFixture] + public class CalculatorPluginTest + { + private readonly Main _plugin; + + public CalculatorPluginTest() + { + _plugin = new Main(); + + var settingField = typeof(Main).GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance); + if (settingField == null) + Assert.Fail("Could not find field '_settings' on Flow.Launcher.Plugin.Calculator.Main"); + settingField.SetValue(_plugin, new Settings + { + ShowErrorMessage = false // Make sure we return the empty results when error occurs + }); + + var engineField = typeof(Main).GetField("MagesEngine", BindingFlags.NonPublic | BindingFlags.Static); + if (engineField == null) + Assert.Fail("Could not find static field 'MagesEngine' on Flow.Launcher.Plugin.Calculator.Main"); + engineField.SetValue(null, new Engine(new Configuration + { + Scope = new Dictionary + { + { "e", Math.E }, // e is not contained in the default mages engine + } + })); + } + + // Basic operations + [TestCase(@"1+1", "2")] + [TestCase(@"2-1", "1")] + [TestCase(@"2*2", "4")] + [TestCase(@"4/2", "2")] + [TestCase(@"2^3", "8")] + // Decimal places + [TestCase(@"10/3", "3.3333333333")] + // Parentheses + [TestCase(@"(1+2)*3", "9")] + [TestCase(@"2^(1+2)", "8")] + // Functions + [TestCase(@"pow(2,3)", "8")] + [TestCase(@"min(1,-1,-2)", "-2")] + [TestCase(@"max(1,-1,-2)", "1")] + [TestCase(@"sqrt(16)", "4")] + [TestCase(@"sin(pi)", "0")] + [TestCase(@"cos(0)", "1")] + [TestCase(@"tan(0)", "0")] + [TestCase(@"log(100)", "2")] + [TestCase(@"ln(e)", "1")] + [TestCase(@"abs(-5)", "5")] + // Constants + [TestCase(@"pi", "3.1415926536")] + // Complex expressions + [TestCase(@"(2+3)*sqrt(16)-log(100)/ln(e)", "19")] + [TestCase(@"sin(pi/2)+cos(0)+tan(0)", "2")] + // Error handling (should return empty result) + [TestCase(@"10/0", "")] + [TestCase(@"sqrt(-1)", "")] + [TestCase(@"log(0)", "")] + [TestCase(@"invalid_expression", "")] + public void CalculatorTest(string expression, string result) + { + ClassicAssert.AreEqual(GetCalculationResult(expression), result); + } + + private string GetCalculationResult(string expression) + { + var results = _plugin.Query(new Plugin.Query() + { + Search = expression + }); + return results.Count > 0 ? results[0].Title : string.Empty; + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 17b4a85a459..42cbafb43ab 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -112,7 +112,8 @@ public List Query(Query query) Title = newResult, IcoPath = IcoPath, Score = 300, - SubTitle = Localize.flowlauncher_plugin_calculator_copy_number_to_clipboard(), + // Check context nullability for unit testing + SubTitle = Context == null ? string.Empty : Localize.flowlauncher_plugin_calculator_copy_number_to_clipboard(), CopyText = newResult, Action = c => { From e10b9254ed6211ba8a8fdcc1047d9fe25724e7c4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 16 Sep 2025 16:31:06 +0800 Subject: [PATCH 19/21] Add workaround for log & ln function --- Flow.Launcher.Test/Plugins/CalculatorTest.cs | 4 +- .../Flow.Launcher.Plugin.Calculator/Main.cs | 63 +++++++++++++++++-- .../MainRegexHelper.cs | 6 ++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs index 3f403b24e3a..14655232364 100644 --- a/Flow.Launcher.Test/Plugins/CalculatorTest.cs +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -53,10 +53,12 @@ public CalculatorPluginTest() [TestCase(@"min(1,-1,-2)", "-2")] [TestCase(@"max(1,-1,-2)", "1")] [TestCase(@"sqrt(16)", "4")] - [TestCase(@"sin(pi)", "0")] + [TestCase(@"sin(pi)", "0.0000000000")] [TestCase(@"cos(0)", "1")] [TestCase(@"tan(0)", "0")] + [TestCase(@"log10(100)", "2")] [TestCase(@"log(100)", "2")] + [TestCase(@"log2(8)", "3")] [TestCase(@"ln(e)", "1")] [TestCase(@"abs(-5)", "5")] // Constants diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 42cbafb43ab..9d5e4700fff 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -16,6 +16,8 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider private static readonly Regex ThousandGroupRegex = MainRegexHelper.GetThousandGroupRegex(); private static readonly Regex NumberRegex = MainRegexHelper.GetNumberRegex(); private static readonly Regex PowRegex = MainRegexHelper.GetPowRegex(); + private static readonly Regex LogRegex = MainRegexHelper.GetLogRegex(); + private static readonly Regex LnRegex = MainRegexHelper.GetLnRegex(); private static readonly Regex FunctionRegex = MainRegexHelper.GetFunctionRegex(); private static Engine MagesEngine; @@ -67,12 +69,36 @@ public List Query(Query query) // 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 = PowRegex.Replace(previous, PowMatchEvaluator); - } while (previous != expression); + string previous; + do + { + previous = expression; + expression = PowRegex.Replace(previous, PowMatchEvaluator); + } while (previous != expression); + } + // WORKAROUND END + + // WORKAROUND START: The 'log' & 'ln' function in Mages v3.0.0 are broken. + // https://github.com/FlorianRappl/Mages/issues/137 + // We bypass it by rewriting any log & ln expression to the equivalent (log10 & log) expression + // before the engine sees it. This loop handles nested calls. + { + string previous; + do + { + previous = expression; + expression = LogRegex.Replace(previous, LogMatchEvaluator); + } while (previous != expression); + } + { + string previous; + do + { + previous = expression; + expression = LnRegex.Replace(previous, LnMatchEvaluator); + } while (previous != expression); + } // WORKAROUND END var result = MagesEngine.Interpret(expression); @@ -200,6 +226,33 @@ private static string PowMatchEvaluator(Match m) return $"({arg1}^{arg2})"; } + private static string LogMatchEvaluator(Match m) + { + // m.Groups[1].Value will be `(...)` with parens + var contentWithParen = m.Groups[1].Value; + var argsContent = contentWithParen[1..^1]; + + // log is unary — if malformed, return original to let Mages handle it + var arg = argsContent.Trim(); + if (string.IsNullOrEmpty(arg)) return m.Value; + + // log(x) -> log10(x) (natural log) + return $"(log10({arg}))"; + } + + private static string LnMatchEvaluator(Match m) + { + // m.Groups[1].Value will be `(...)` with parens + var contentWithParen = m.Groups[1].Value; + var argsContent = contentWithParen[1..^1]; + + // ln is unary — if malformed, return original to let Mages handle it + var arg = argsContent.Trim(); + if (string.IsNullOrEmpty(arg)) return m.Value; + + // ln(x) -> log(x) (natural log) + return $"(log({arg}))"; + } private static string NormalizeNumber(string numberStr, bool isFunctionPresent, string decimalSep, string groupSep) { if (isFunctionPresent) diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs index 0746e45562a..a8b582ccce5 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs @@ -13,6 +13,12 @@ internal static partial class MainRegexHelper [GeneratedRegex(@"\bpow(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] public static partial Regex GetPowRegex(); + [GeneratedRegex(@"\blog(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + public static partial Regex GetLogRegex(); + + [GeneratedRegex(@"\bln(\((?:[^()\[\]]|\((?)|\)(?<-Depth>)|\[(?)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + public static partial Regex GetLnRegex(); + [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 | RegexOptions.CultureInvariant)] public static partial Regex GetFunctionRegex(); } From 552b6547db09301c7ab57c0a1f000eb5ee5daf44 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 16 Sep 2025 16:40:36 +0800 Subject: [PATCH 20/21] Fix unit test result issue --- Flow.Launcher.Test/Plugins/CalculatorTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs index 14655232364..c5880000294 100644 --- a/Flow.Launcher.Test/Plugins/CalculatorTest.cs +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -64,7 +64,7 @@ public CalculatorPluginTest() // Constants [TestCase(@"pi", "3.1415926536")] // Complex expressions - [TestCase(@"(2+3)*sqrt(16)-log(100)/ln(e)", "19")] + [TestCase(@"(2+3)*sqrt(16)-log(100)/ln(e)", "18")] [TestCase(@"sin(pi/2)+cos(0)+tan(0)", "2")] // Error handling (should return empty result) [TestCase(@"10/0", "")] From 8321e400c1a6608c541a3e617967ece77df0860b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Tue, 16 Sep 2025 21:04:56 +0800 Subject: [PATCH 21/21] Fix test setting & Add instances to private fields --- Flow.Launcher.Test/Plugins/CalculatorTest.cs | 26 +++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs index c5880000294..b075813dbb6 100644 --- a/Flow.Launcher.Test/Plugins/CalculatorTest.cs +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -12,6 +12,19 @@ namespace Flow.Launcher.Test.Plugins public class CalculatorPluginTest { private readonly Main _plugin; + private readonly Settings _settings = new() + { + DecimalSeparator = DecimalSeparator.UseSystemLocale, + MaxDecimalPlaces = 10, + ShowErrorMessage = false // Make sure we return the empty results when error occurs + }; + private readonly Engine _engine = new(new Configuration + { + Scope = new Dictionary + { + { "e", Math.E }, // e is not contained in the default mages engine + } + }); public CalculatorPluginTest() { @@ -20,21 +33,12 @@ public CalculatorPluginTest() var settingField = typeof(Main).GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance); if (settingField == null) Assert.Fail("Could not find field '_settings' on Flow.Launcher.Plugin.Calculator.Main"); - settingField.SetValue(_plugin, new Settings - { - ShowErrorMessage = false // Make sure we return the empty results when error occurs - }); + settingField.SetValue(_plugin, _settings); var engineField = typeof(Main).GetField("MagesEngine", BindingFlags.NonPublic | BindingFlags.Static); if (engineField == null) Assert.Fail("Could not find static field 'MagesEngine' on Flow.Launcher.Plugin.Calculator.Main"); - engineField.SetValue(null, new Engine(new Configuration - { - Scope = new Dictionary - { - { "e", Math.E }, // e is not contained in the default mages engine - } - })); + engineField.SetValue(null, _engine); } // Basic operations