mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge pull request #3971 from dcog989/calculator-min-fix
Backout 'smart' digit separation & Fix Mages functions for Calculator plugin
This commit is contained in:
commit
8bac530504
10 changed files with 420 additions and 204 deletions
|
|
@ -39,6 +39,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Calculator\Flow.Launcher.Plugin.Calculator.csproj" />
|
||||
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Explorer\Flow.Launcher.Plugin.Explorer.csproj" />
|
||||
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Program\Flow.Launcher.Plugin.Program.csproj" />
|
||||
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Url\Flow.Launcher.Plugin.Url.csproj" />
|
||||
|
|
|
|||
92
Flow.Launcher.Test/Plugins/CalculatorTest.cs
Normal file
92
Flow.Launcher.Test/Plugins/CalculatorTest.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
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;
|
||||
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<string, object>
|
||||
{
|
||||
{ "e", Math.E }, // e is not contained in the default mages engine
|
||||
}
|
||||
});
|
||||
|
||||
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, _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, _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.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
|
||||
[TestCase(@"pi", "3.1415926536")]
|
||||
// Complex expressions
|
||||
[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", "")]
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<system:String x:Key="flowlauncher_plugin_calculator_plugin_name">Calculator</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_calculator_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_plugin_description">Perform mathematical calculations, including hex values and advanced functions such as 'min(1,2,3)', 'sqrt(123)' and 'cos(123)'.</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>
|
||||
|
|
@ -15,4 +15,5 @@
|
|||
<system:String x:Key="flowlauncher_plugin_calculator_decimal_separator_dot">Dot (.)</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_calculator_max_decimal_places">Max. decimal places</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_calculator_failed_to_copy">Copy failed, please try later</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_calculator_show_error_message">Show error message when calculation fails</system:String>
|
||||
</ResourceDictionary>
|
||||
|
|
@ -13,30 +13,24 @@ 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();
|
||||
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;
|
||||
private const string Comma = ",";
|
||||
private const string Dot = ".";
|
||||
private const string IcoPath = "Images/calculator.png";
|
||||
private static readonly List<Result> EmptyResults = [];
|
||||
|
||||
internal static PluginInitContext Context { get; set; } = null!;
|
||||
|
||||
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;
|
||||
|
|
@ -54,38 +48,98 @@ namespace Flow.Launcher.Plugin.Calculator
|
|||
|
||||
public List<Result> Query(Query query)
|
||||
{
|
||||
if (!CanCalculate(query))
|
||||
if (string.IsNullOrWhiteSpace(query.Search))
|
||||
{
|
||||
return new List<Result>();
|
||||
return EmptyResults;
|
||||
}
|
||||
|
||||
var context = new ParsingContext();
|
||||
|
||||
try
|
||||
{
|
||||
var expression = NumberRegex.Replace(query.Search, m => NormalizeNumber(m.Value, context));
|
||||
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
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
|
||||
if (result?.ToString() == "NaN")
|
||||
if (result == null || string.IsNullOrEmpty(result.ToString()))
|
||||
{
|
||||
if (!_settings.ShowErrorMessage) return EmptyResults;
|
||||
return
|
||||
[
|
||||
new Result
|
||||
{
|
||||
Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(),
|
||||
IcoPath = IcoPath
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
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>
|
||||
{
|
||||
return
|
||||
[
|
||||
new Result
|
||||
{
|
||||
Title = newResult,
|
||||
IcoPath = "Images/calculator.png",
|
||||
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 =>
|
||||
{
|
||||
|
|
@ -101,118 +155,206 @@ namespace Flow.Launcher.Plugin.Calculator
|
|||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
];
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
// Mages engine can throw various exceptions, for simplicity we catch them all and show a generic message.
|
||||
if (!_settings.ShowErrorMessage) return EmptyResults;
|
||||
return
|
||||
[
|
||||
new Result
|
||||
{
|
||||
Title = Localize.flowlauncher_plugin_calculator_expression_not_complete(),
|
||||
IcoPath = IcoPath
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return new List<Result>();
|
||||
return EmptyResults;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
private static string PowMatchEvaluator(Match m)
|
||||
{
|
||||
var systemGroupSep = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator;
|
||||
int dotCount = numberStr.Count(f => f == '.');
|
||||
int commaCount = numberStr.Count(f => f == ',');
|
||||
// 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[1..^1];
|
||||
|
||||
// Case 1: Unambiguous mixed separators (e.g., "1.234,56")
|
||||
if (dotCount > 0 && commaCount > 0)
|
||||
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++)
|
||||
{
|
||||
context.InputUsesGroupSeparators = true;
|
||||
if (numberStr.LastIndexOf('.') > numberStr.LastIndexOf(','))
|
||||
switch (argsContent[i])
|
||||
{
|
||||
context.InputDecimalSeparator = Dot;
|
||||
return numberStr.Replace(Comma, string.Empty);
|
||||
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[..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))
|
||||
{
|
||||
return m.Value;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// STRICT MODE: When functions are present, ',' is ALWAYS an argument separator.
|
||||
if (numberStr.Contains(','))
|
||||
{
|
||||
return numberStr;
|
||||
}
|
||||
|
||||
string processedStr = numberStr;
|
||||
|
||||
// Handle group separator, with special care for ambiguous dot.
|
||||
if (!string.IsNullOrEmpty(groupSep))
|
||||
{
|
||||
if (groupSep == ".")
|
||||
{
|
||||
var parts = processedStr.Split('.');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
var culture = CultureInfo.CurrentCulture;
|
||||
if (IsValidGrouping(parts, culture.NumberFormat.NumberGroupSizes))
|
||||
{
|
||||
processedStr = processedStr.Replace(groupSep, "");
|
||||
}
|
||||
// If not grouped, it's likely a decimal number, so we don't strip dots.
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
processedStr = processedStr.Replace(groupSep, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle decimal separator.
|
||||
if (decimalSep != ".")
|
||||
{
|
||||
processedStr = processedStr.Replace(decimalSep, ".");
|
||||
}
|
||||
|
||||
return processedStr;
|
||||
}
|
||||
else
|
||||
{
|
||||
// LENIENT MODE: No functions are present, so we can be flexible.
|
||||
string processedStr = numberStr;
|
||||
if (!string.IsNullOrEmpty(groupSep))
|
||||
{
|
||||
processedStr = processedStr.Replace(groupSep, "");
|
||||
}
|
||||
if (decimalSep != ".")
|
||||
{
|
||||
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[1..];
|
||||
if (firstPart.Length == 0) return false; // e.g. ",123"
|
||||
|
||||
if (firstPart.Length > groupSizes[0]) return false;
|
||||
|
||||
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
|
||||
{
|
||||
context.InputDecimalSeparator = Comma;
|
||||
return numberStr.Replace(Dot, string.Empty).Replace(Comma, Dot);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parts[i].Length != expectedSize) return false;
|
||||
|
||||
groupIndex++;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Case 4: No separators
|
||||
return numberStr;
|
||||
return true;
|
||||
}
|
||||
|
||||
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 +363,7 @@ namespace Flow.Launcher.Plugin.Calculator
|
|||
string integerPart = parts[0];
|
||||
string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty;
|
||||
|
||||
if (context.InputUsesGroupSeparators && integerPart.Length > 3)
|
||||
if (integerPart.Length > 3)
|
||||
{
|
||||
integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator);
|
||||
}
|
||||
|
|
@ -236,29 +378,23 @@ namespace Flow.Launcher.Plugin.Calculator
|
|||
|
||||
private string GetGroupSeparator(string decimalSeparator)
|
||||
{
|
||||
// This logic is now independent of the system's group separator
|
||||
// to ensure consistent output for unit testing.
|
||||
return decimalSeparator == Dot ? Comma : Dot;
|
||||
}
|
||||
var culture = CultureInfo.CurrentCulture;
|
||||
var systemGroupSeparator = culture.NumberFormat.NumberGroupSeparator;
|
||||
|
||||
private bool CanCalculate(Query query)
|
||||
{
|
||||
if (query.Search.Length < 2)
|
||||
if (_settings.DecimalSeparator == DecimalSeparator.UseSystemLocale)
|
||||
{
|
||||
return false;
|
||||
return systemGroupSeparator;
|
||||
}
|
||||
|
||||
if (!RegValidExpressChar.IsMatch(query.Search))
|
||||
// When a custom decimal separator is used,
|
||||
// use the system's group separator unless it conflicts with the custom decimal separator.
|
||||
if (decimalSeparator == systemGroupSeparator)
|
||||
{
|
||||
return false;
|
||||
// Conflict: use the opposite of the decimal separator as a fallback.
|
||||
return decimalSeparator == Dot ? Comma : Dot;
|
||||
}
|
||||
|
||||
if (!IsBracketComplete(query.Search))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return systemGroupSeparator;
|
||||
}
|
||||
|
||||
private string GetDecimalSeparator()
|
||||
|
|
@ -273,25 +409,6 @@ namespace Flow.Launcher.Plugin.Calculator
|
|||
};
|
||||
}
|
||||
|
||||
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--;
|
||||
}
|
||||
}
|
||||
|
||||
return leftBracketCount == 0;
|
||||
}
|
||||
|
||||
public string GetTranslatedPluginTitle()
|
||||
{
|
||||
return Localize.flowlauncher_plugin_calculator_plugin_name();
|
||||
|
|
|
|||
|
|
@ -4,16 +4,21 @@ namespace Flow.Launcher.Plugin.Calculator;
|
|||
|
||||
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\.,'\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>)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
public static partial Regex GetPowRegex();
|
||||
|
||||
[GeneratedRegex(@"\blog(\((?:[^()\[\]]|\((?<Depth>)|\)(?<-Depth>)|\[(?<Depth>)|\](?<-Depth>))*(?(Depth)(?!))\))", RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
public static partial Regex GetLogRegex();
|
||||
|
||||
[GeneratedRegex(@"\bln(\((?:[^()\[\]]|\((?<Depth>)|\)(?<-Depth>)|\[(?<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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +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 int MaxDecimalPlaces { get; set; } = 10;
|
||||
}
|
||||
public DecimalSeparator DecimalSeparator { get; set; } = DecimalSeparator.UseSystemLocale;
|
||||
|
||||
public int MaxDecimalPlaces { get; set; } = 10;
|
||||
|
||||
public bool ShowErrorMessage { get; set; } = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,25 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Calculator.ViewModels
|
||||
namespace Flow.Launcher.Plugin.Calculator.ViewModels;
|
||||
|
||||
public class SettingsViewModel(Settings settings) : BaseModel
|
||||
{
|
||||
public class SettingsViewModel : BaseModel
|
||||
public Settings Settings { get; } = settings;
|
||||
|
||||
public static IEnumerable<int> MaxDecimalPlacesRange => Enumerable.Range(1, 20);
|
||||
|
||||
public List<DecimalSeparatorLocalized> AllDecimalSeparator { get; } = DecimalSeparatorLocalized.GetValues();
|
||||
|
||||
public DecimalSeparator SelectedDecimalSeparator
|
||||
{
|
||||
public SettingsViewModel(Settings settings)
|
||||
get => Settings.DecimalSeparator;
|
||||
set
|
||||
{
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public Settings Settings { get; init; }
|
||||
|
||||
public static IEnumerable<int> MaxDecimalPlacesRange => Enumerable.Range(1, 20);
|
||||
|
||||
public List<DecimalSeparatorLocalized> AllDecimalSeparator { get; } = DecimalSeparatorLocalized.GetValues();
|
||||
|
||||
public DecimalSeparator SelectedDecimalSeparator
|
||||
{
|
||||
get => Settings.DecimalSeparator;
|
||||
set
|
||||
if (Settings.DecimalSeparator != value)
|
||||
{
|
||||
if (Settings.DecimalSeparator != value)
|
||||
{
|
||||
Settings.DecimalSeparator = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
Settings.DecimalSeparator = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
|
|
@ -58,5 +59,14 @@
|
|||
ItemsSource="{Binding MaxDecimalPlacesRange}"
|
||||
SelectedItem="{Binding Settings.MaxDecimalPlaces}" />
|
||||
|
||||
<CheckBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Content="{DynamicResource flowlauncher_plugin_calculator_show_error_message}"
|
||||
IsChecked="{Binding Settings.ShowErrorMessage, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,16 @@
|
|||
using System.Windows.Controls;
|
||||
using Flow.Launcher.Plugin.Calculator.ViewModels;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Calculator.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for CalculatorSettings.xaml
|
||||
/// </summary>
|
||||
public partial class CalculatorSettings : UserControl
|
||||
{
|
||||
private readonly SettingsViewModel _viewModel;
|
||||
private readonly Settings _settings;
|
||||
namespace Flow.Launcher.Plugin.Calculator.Views;
|
||||
|
||||
public CalculatorSettings(Settings settings)
|
||||
{
|
||||
_viewModel = new SettingsViewModel(settings);
|
||||
_settings = _viewModel.Settings;
|
||||
DataContext = _viewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
public partial class CalculatorSettings : UserControl
|
||||
{
|
||||
private readonly SettingsViewModel _viewModel;
|
||||
|
||||
public CalculatorSettings(Settings settings)
|
||||
{
|
||||
_viewModel = new SettingsViewModel(settings);
|
||||
DataContext = _viewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 functions such as 'min(1,2,3)', 'sqrt(123)' and 'cos(123)'.",
|
||||
"Author": "cxfksword, dcog989",
|
||||
"Version": "1.0.0",
|
||||
"Language": "csharp",
|
||||
|
|
|
|||
Loading…
Reference in a new issue