Skip to content

Smart thousands and decimals for Calculator #3859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
xmlns:system="clr-namespace:System;assembly=mscorlib">

<system:String x:Key="flowlauncher_plugin_caculator_plugin_name">Calculator</system:String>
<system:String x:Key="flowlauncher_plugin_caculator_plugin_description">Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher)</system:String>
<system:String x:Key="flowlauncher_plugin_caculator_plugin_description">Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place.</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_not_a_number">Not a number (NaN)</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_expression_not_complete">Expression wrong or incomplete (Did you forget some parentheses?)</system:String>
<system:String x:Key="flowlauncher_plugin_calculator_copy_number_to_clipboard">Copy this number to the clipboard</system:String>
Expand Down
181 changes: 148 additions & 33 deletions Plugins/Flow.Launcher.Plugin.Calculator/Main.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,6 +15,9 @@
{
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();

private static Engine MagesEngine;
private const string Comma = ",";
private const string Dot = ".";
Expand All @@ -23,6 +27,16 @@
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 @@ -45,20 +59,11 @@
return new List<Result>();
}

var context = new ParsingContext();

try
{
string expression;

switch (_settings.DecimalSeparator)
{
case DecimalSeparator.Comma:
case DecimalSeparator.UseSystemLocale when CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator == ",":
expression = query.Search.Replace(",", ".");
break;
default:
expression = query.Search;
break;
}
var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value, context));

var result = MagesEngine.Interpret(expression);

Expand All @@ -71,7 +76,7 @@
if (!string.IsNullOrEmpty(result?.ToString()))
{
decimal roundedResult = Math.Round(Convert.ToDecimal(result), _settings.MaxDecimalPlaces, MidpointRounding.AwayFromZero);
string newResult = ChangeDecimalSeparator(roundedResult, GetDecimalSeparator());
string newResult = FormatResult(roundedResult, context);

return new List<Result>
{
Expand Down Expand Up @@ -107,46 +112,156 @@
return new List<Result>();
}

private bool CanCalculate(Query query)
/// <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.
/// </summary>
/// <returns>A normalized number string with '.' as the decimal separator for the Mages engine.</returns>
private string NormalizeNumber(string numberStr, ParsingContext context)
{
// Don't execute when user only input "e" or "i" keyword
if (query.Search.Length < 2)
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)
{
return false;
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);
}
}

if (!RegValidExpressChar.IsMatch(query.Search))
// Case 2: Only dots
if (dotCount > 0)
{
return false;
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)
{
context.InputUsesGroupSeparators = true;
return numberStr.Replace(Dot, string.Empty);
}
else
{
context.InputDecimalSeparator = Dot;
return numberStr;
}
}
else // Unambiguous decimal (e.g., "12.34" or "0.123" or ".123")
{
context.InputDecimalSeparator = Dot;
return numberStr;
}
}

if (!IsBracketComplete(query.Search))
// Case 3: Only commas
if (commaCount > 0)
{
return false;
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 ((query.Search.Contains(Dot) && GetDecimalSeparator() != Dot) ||
(query.Search.Contains(Comma) && GetDecimalSeparator() != Comma))
return false;
// Case 4: No separators
return numberStr;
}

return true;
private string FormatResult(decimal roundedResult, ParsingContext context)
{
string decimalSeparator = context.InputDecimalSeparator ?? GetDecimalSeparator();
string groupSeparator = GetGroupSeparator(decimalSeparator);

string resultStr = roundedResult.ToString(CultureInfo.InvariantCulture);

string[] parts = resultStr.Split('.');
string integerPart = parts[0];
string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty;

if (context.InputUsesGroupSeparators && integerPart.Length > 3)
{
integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator);
}

if (!string.IsNullOrEmpty(fractionalPart))
{
return integerPart + decimalSeparator + fractionalPart;
}

return integerPart;
}

private static string ChangeDecimalSeparator(decimal value, string newDecimalSeparator)
private string GetGroupSeparator(string decimalSeparator)
{
if (string.IsNullOrEmpty(newDecimalSeparator))
// This logic is now independent of the system's group separator
// to ensure consistent output for unit testing.
return decimalSeparator == Dot ? Comma : Dot;
}

private bool CanCalculate(Query query)
{
if (query.Search.Length < 2)
{
return value.ToString();
return false;
}

var numberFormatInfo = new NumberFormatInfo
if (!RegValidExpressChar.IsMatch(query.Search))
{
NumberDecimalSeparator = newDecimalSeparator
};
return value.ToString(numberFormatInfo);
return false;
}

if (!IsBracketComplete(query.Search))
{
return false;
}

return true;
}

private string GetDecimalSeparator()
private string GetDecimalSeparator()
{
string systemDecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
return _settings.DecimalSeparator switch
Expand All @@ -160,9 +275,9 @@

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

Check warning on line 278 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 280 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 All @@ -179,12 +294,12 @@

public string GetTranslatedPluginTitle()
{
return Localize.flowlauncher_plugin_caculator_plugin_name();

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

View workflow job for this annotation

GitHub Actions / Check Spelling

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

public string GetTranslatedPluginDescription()
{
return Localize.flowlauncher_plugin_caculator_plugin_description();

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

View workflow job for this annotation

GitHub Actions / Check Spelling

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

public Control CreateSettingPanel()
Expand Down
6 changes: 6 additions & 0 deletions Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
[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)]

Check warning on line 11 in Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

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

Check warning on line 11 in Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

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

Check warning on line 11 in Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

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

Check warning on line 11 in Plugins/Flow.Launcher.Plugin.Calculator/MainRegexHelper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`eigval` is not a recognized word. (unrecognized-spelling)
public static partial Regex GetRegValidExpressChar();

[GeneratedRegex(@"[\d\.,]+", RegexOptions.Compiled)]
public static partial Regex GetNumberRegex();

[GeneratedRegex(@"\B(?=(\d{3})+(?!\d))", RegexOptions.Compiled)]
public static partial Regex GetThousandGroupRegex();
}
91 changes: 0 additions & 91 deletions Plugins/Flow.Launcher.Plugin.Calculator/NumberTranslator.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls;
using Flow.Launcher.Plugin.Calculator.ViewModels;

namespace Flow.Launcher.Plugin.Calculator.Views
Expand Down
4 changes: 2 additions & 2 deletions 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)",
"Author": "cxfksword",
"Description": "Perform mathematical calculations (including hexadecimal values). Use ',' or '.' as thousand separator or decimal place.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jjw24 Not sure if this is needed here. Could you please take a look?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was significant enough to make it clear to the user???

"Author": "cxfksword, dcog989",
"Version": "1.0.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
Expand Down
Loading