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 2 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
164 changes: 133 additions & 31 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 @@ -16,13 +17,16 @@
@"^(" +
@"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|" +

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

View workflow job for this annotation

GitHub Actions / Check Spelling

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

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

View workflow job for this annotation

GitHub Actions / Check Spelling

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

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

View workflow job for this annotation

GitHub Actions / Check Spelling

`eigval` is not a recognized word. (unrecognized-spelling)
@"bin2dec|hex2dec|oct2dec|" +
@"factorial|sign|isprime|isinfty|" +

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

View workflow job for this annotation

GitHub Actions / Check Spelling

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

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

View workflow job for this annotation

GitHub Actions / Check Spelling

`isprime` is not a recognized word. (unrecognized-spelling)
@"==|~=|&&|\|\||(?:\<|\>)=?|" +
@"[ei]|[0-9]|0x[\da-fA-F]+|[\+\%\-\*\/\^\., ""]|[\(\)\|\!\[\]]" +
@")+$", RegexOptions.Compiled);
private static readonly Regex RegBrackets = new Regex(@"[\(\)\[\]]", RegexOptions.Compiled);
private static readonly Regex ThousandGroupRegex = new Regex(@"\B(?=(\d{3})+(?!\d))");
private static readonly Regex NumberRegex = new Regex(@"[\d\.,]+", RegexOptions.Compiled);

private static Engine MagesEngine;
private const string comma = ",";
private const string dot = ".";
Expand All @@ -32,6 +36,16 @@
private static Settings _settings;
private static 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 @@ -54,20 +68,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 @@ -80,7 +85,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 @@ -115,6 +120,121 @@

return new List<Result>();
}

/// <summary>
/// Parses a string representation of a number, detecting its format. It uses structural analysis
/// (checking for 3-digit groups) and falls back to system culture for 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)
{
var systemFormat = CultureInfo.CurrentCulture.NumberFormat;
string systemDecimalSeparator = systemFormat.NumberDecimalSeparator;

bool hasDot = numberStr.Contains(dot);
bool hasComma = numberStr.Contains(comma);

// Unambiguous case: both separators are present. The last one wins as decimal separator.
if (hasDot && hasComma)
{
context.InputUsesGroupSeparators = true;
int lastDotPos = numberStr.LastIndexOf(dot);
int lastCommaPos = numberStr.LastIndexOf(comma);

if (lastDotPos > lastCommaPos) // e.g. 1,234.56
{
context.InputDecimalSeparator = dot;
return numberStr.Replace(comma, string.Empty);
}
else // e.g. 1.234,56
{
context.InputDecimalSeparator = comma;
return numberStr.Replace(dot, string.Empty).Replace(comma, dot);
}
}

if (hasComma)
{
string[] parts = numberStr.Split(',');
// If all parts after the first are 3 digits, it's a potential group separator.
bool isGroupCandidate = parts.Length > 1 && parts.Skip(1).All(p => p.Length == 3);

if (isGroupCandidate)
{
// Ambiguous case: "1,234". Resolve using culture.
if (systemDecimalSeparator == comma)
{
context.InputDecimalSeparator = comma;
return numberStr.Replace(comma, dot);
}
else
{
context.InputUsesGroupSeparators = true;
return numberStr.Replace(comma, string.Empty);
}
}
else
{
// Unambiguous decimal: "123,45" or "1,2,345"
context.InputDecimalSeparator = comma;
return numberStr.Replace(comma, dot);
}
}

if (hasDot)
{
string[] parts = numberStr.Split('.');
bool isGroupCandidate = parts.Length > 1 && parts.Skip(1).All(p => p.Length == 3);

if (isGroupCandidate)
{
if (systemDecimalSeparator == dot)
{
context.InputDecimalSeparator = dot;
return numberStr;
}
else
{
context.InputUsesGroupSeparators = true;
return numberStr.Replace(dot, string.Empty);
}
}
else
{
context.InputDecimalSeparator = dot;
return numberStr; // Already in Mages-compatible format
}
}

// No separators.
return numberStr;
}

private string FormatResult(decimal roundedResult, ParsingContext context)
{
// Use the detected decimal separator from the input; otherwise, fall back to settings.
string decimalSeparator = context.InputDecimalSeparator ?? GetDecimalSeparator();
string groupSeparator = decimalSeparator == dot ? comma : dot;

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 = ThousandGroupRegex.Replace(integerPart, groupSeparator);
}

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

return integerPart;
}

private bool CanCalculate(Query query)
{
Expand All @@ -134,27 +254,9 @@
return false;
}

if ((query.Search.Contains(dot) && GetDecimalSeparator() != dot) ||
(query.Search.Contains(comma) && GetDecimalSeparator() != comma))
return false;

return true;
}

private string ChangeDecimalSeparator(decimal value, string newDecimalSeparator)
{
if (String.IsNullOrEmpty(newDecimalSeparator))
{
return value.ToString();
}

var numberFormatInfo = new NumberFormatInfo
{
NumberDecimalSeparator = newDecimalSeparator
};
return value.ToString(numberFormatInfo);
}

private static string GetDecimalSeparator()
{
string systemDecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
Expand All @@ -169,9 +271,9 @@

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

Check warning on line 274 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 276 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 @@ -188,12 +290,12 @@

public string GetTranslatedPluginTitle()
{
return Context.API.GetTranslation("flowlauncher_plugin_caculator_plugin_name");

Check warning on line 293 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 Context.API.GetTranslation("flowlauncher_plugin_caculator_plugin_description");

Check warning on line 298 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
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
Expand Up @@ -10,7 +10,6 @@
xmlns:viewModels="clr-namespace:Flow.Launcher.Plugin.Calculator.ViewModels"
d:DesignHeight="450"
d:DesignWidth="800"
Loaded="CalculatorSettings_Loaded"
mc:Ignorable="d">

<UserControl.Resources>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Windows;
using System.Windows.Controls;
using Flow.Launcher.Plugin.Calculator.ViewModels;

Expand All @@ -19,13 +18,6 @@ public CalculatorSettings(SettingsViewModel viewModel)
DataContext = viewModel;
InitializeComponent();
}

private void CalculatorSettings_Loaded(object sender, RoutedEventArgs e)
{
DecimalSeparatorComboBox.SelectedItem = _settings.DecimalSeparator;
MaxDecimalPlaces.SelectedItem = _settings.MaxDecimalPlaces;
}
}


}
6 changes: 3 additions & 3 deletions Plugins/Flow.Launcher.Plugin.Calculator/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"ID": "CEA0FDFC6D3B4085823D60DC76F28855",
"ActionKeyword": "*",
"Name": "Calculator",
"Description": "Perform mathematical calculations (including hexadecimal values)",
"Author": "cxfksword",
"Version": "1.0.0",
"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",

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.1.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Calculator.dll",
Expand Down
Loading