Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e107939
backout 'smart' digit grouping, mages fixes + workaround
dcog989 Sep 12, 2025
103d383
dead code, improve messages, group separator fix?
dcog989 Sep 12, 2025
190e0e1
Fix 'German' number formatting
dcog989 Sep 12, 2025
bd186e7
correct + extend description
dcog989 Sep 12, 2025
110f571
Rework solution for nested Mages
dcog989 Sep 13, 2025
6462023
Merge branch 'dev' into calculator-min-fix
dcog989 Sep 13, 2025
11f5ea5
Improve code quality
Jack251970 Sep 14, 2025
495ace1
Improve plugin description
Jack251970 Sep 14, 2025
daf35a4
Do not check bracket complete
Jack251970 Sep 14, 2025
c79e483
Merge branch 'Flow-Launcher:dev' into calculator-min-fix
dcog989 Sep 14, 2025
336e51d
IcoPath to const string
dcog989 Sep 14, 2025
e990e0f
Handle misplaced separators, Mages edge cases
dcog989 Sep 14, 2025
edc76fa
Review feedback, case insensitive, consistent separators
dcog989 Sep 14, 2025
9be8b71
review feedback, CultureInvariant, mild refactor
dcog989 Sep 14, 2025
b07420a
Use EmptyResults to improve code quality
Jack251970 Sep 15, 2025
cea1402
Improve code quality
Jack251970 Sep 15, 2025
1906d68
Add ShowErrorMessage setting
Jack251970 Sep 15, 2025
f9facda
Improve code quality
Jack251970 Sep 15, 2025
684fafd
Improve code quality
Jack251970 Sep 15, 2025
6841ad5
Add calculator unit testing
Jack251970 Sep 16, 2025
e10b925
Add workaround for log & ln function
Jack251970 Sep 16, 2025
552b654
Fix unit test result issue
Jack251970 Sep 16, 2025
8321e40
Fix test setting & Add instances to private fields
Jack251970 Sep 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 70 additions & 108 deletions Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
{
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();
Expand All @@ -27,16 +26,6 @@
private Settings _settings;
private SettingsViewModel _viewModel;

/// <summary>
/// Holds the formatting information for a single query.
/// This is used to ensure thread safety by keeping query state local.
/// </summary>
private class ParsingContext
{
public string InputDecimalSeparator { get; set; }
public bool InputUsesGroupSeparators { get; set; }
}

public void Init(PluginInitContext context)
{
Context = context;
Expand All @@ -59,24 +48,46 @@
return new List<Result>();
}

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)");

Check warning on line 63 in Plugins/Flow.Launcher.Plugin.Calculator/Main.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`bpow` is not a recognized word. (unrecognized-spelling)
} while (previous != expression);
// WORKAROUND END

var result = MagesEngine.Interpret(expression);

if (result?.ToString() == "NaN")
if (result == null || string.IsNullOrEmpty(result.ToString()))
{
return new List<Result>
{
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<Result>
{
Expand Down Expand Up @@ -106,113 +117,64 @@
}
catch (Exception)
{
// ignored
// Mages engine can throw various exceptions, for simplicity we catch them all and show a generic message.
return new List<Result>
{
new Result
{
Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(),
IcoPath = "Images/calculator.png"
}
};
}

return new List<Result>();
}

/// <summary>
/// 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.
/// </summary>
/// <returns>A normalized number string with '.' as the decimal separator for the Mages engine.</returns>
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;
var decimalSep = culture.NumberFormat.NumberDecimalSeparator;

// 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)
{
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);
}
}
// If validation passes, we can assume the separators are used correctly for numbers.
string processedStr = numberStr.Replace(groupSep, "");
processedStr = processedStr.Replace(decimalSep, ".");

// Case 4: No separators
return numberStr;
return processedStr;
}

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);
Expand All @@ -221,7 +183,7 @@
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);
}
Expand All @@ -236,19 +198,19 @@

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)
{
return false;
}

if (!RegValidExpressChar.IsMatch(query.Search))
if (string.IsNullOrWhiteSpace(query.Search))
{
return false;
}
Expand All @@ -275,9 +237,9 @@

private static bool IsBracketComplete(string query)
{
var matchs = RegBrackets.Matches(query);

Check warning on line 240 in Plugins/Flow.Launcher.Plugin.Calculator/Main.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`matchs` is not a recognized word. (unrecognized-spelling)
var leftBracketCount = 0;
foreach (Match match in matchs)

Check warning on line 242 in Plugins/Flow.Launcher.Plugin.Calculator/Main.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`matchs` is not a recognized word. (unrecognized-spelling)

Check warning on line 242 in Plugins/Flow.Launcher.Plugin.Calculator/Main.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`matchs` is not a recognized word. (unrecognized-spelling)
{
if (match.Value == "(" || match.Value == "[")
{
Expand Down
5 changes: 1 addition & 4 deletions Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ 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)]
[GeneratedRegex(@"-?[\d\.,]+", RegexOptions.Compiled)]
public static partial Regex GetNumberRegex();

[GeneratedRegex(@"\B(?=(\d{3})+(?!\d))", RegexOptions.Compiled)]
Expand Down
2 changes: 1 addition & 1 deletion Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"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",

Check warning on line 6 in Plugins/Flow.Launcher.Plugin.Calculator/plugin.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`dcog` is not a recognized word. (unrecognized-spelling)

Check warning on line 6 in Plugins/Flow.Launcher.Plugin.Calculator/plugin.json

View workflow job for this annotation

GitHub Actions / Check Spelling

`cxfksword` is not a recognized word. (unrecognized-spelling)
"Version": "1.0.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
Expand Down
Loading