11using System ;
22using System . Collections . Generic ;
33using System . Globalization ;
4+ using System . Linq ;
45using System . Runtime . InteropServices ;
56using System . Text . RegularExpressions ;
67using System . Windows . Controls ;
@@ -14,6 +15,9 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
1415 {
1516 private static readonly Regex RegValidExpressChar = MainRegexHelper . GetRegValidExpressChar ( ) ;
1617 private static readonly Regex RegBrackets = MainRegexHelper . GetRegBrackets ( ) ;
18+ private static readonly Regex ThousandGroupRegex = MainRegexHelper . GetThousandGroupRegex ( ) ;
19+ private static readonly Regex NumberRegex = MainRegexHelper . GetNumberRegex ( ) ;
20+
1721 private static Engine MagesEngine ;
1822 private const string Comma = "," ;
1923 private const string Dot = "." ;
@@ -23,6 +27,16 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
2327 private Settings _settings ;
2428 private SettingsViewModel _viewModel ;
2529
30+ /// <summary>
31+ /// Holds the formatting information for a single query.
32+ /// This is used to ensure thread safety by keeping query state local.
33+ /// </summary>
34+ private class ParsingContext
35+ {
36+ public string InputDecimalSeparator { get ; set ; }
37+ public bool InputUsesGroupSeparators { get ; set ; }
38+ }
39+
2640 public void Init ( PluginInitContext context )
2741 {
2842 Context = context ;
@@ -45,26 +59,17 @@ public List<Result> Query(Query query)
4559 return new List < Result > ( ) ;
4660 }
4761
62+ var context = new ParsingContext ( ) ;
63+
4864 try
4965 {
50- string expression ;
51-
52- switch ( _settings . DecimalSeparator )
53- {
54- case DecimalSeparator . Comma :
55- case DecimalSeparator . UseSystemLocale when CultureInfo . CurrentCulture . NumberFormat . NumberDecimalSeparator == "," :
56- expression = query . Search . Replace ( "," , "." ) ;
57- break ;
58- default :
59- expression = query . Search ;
60- break ;
61- }
66+ var expression = NumberRegex . Replace ( query . Search , m => NormalizeNumber ( m . Value , context ) ) ;
6267
6368 var result = MagesEngine . Interpret ( expression ) ;
6469 if ( result ? . ToString ( ) != "NaN" && result is not Function && ! string . IsNullOrEmpty ( result ? . ToString ( ) ) )
6570 {
6671 decimal roundedResult = Math . Round ( Convert . ToDecimal ( result ) , _settings . MaxDecimalPlaces , MidpointRounding . AwayFromZero ) ;
67- string newResult = ChangeDecimalSeparator ( roundedResult , GetDecimalSeparator ( ) ) ;
72+ string newResult = FormatResult ( roundedResult , context ) ;
6873
6974 return new List < Result >
7075 {
@@ -100,43 +105,153 @@ public List<Result> Query(Query query)
100105 return new List < Result > ( ) ;
101106 }
102107
103- private bool CanCalculate ( Query query )
108+ /// <summary>
109+ /// Parses a string representation of a number, detecting its format. It uses structural analysis
110+ /// and falls back to system culture for truly ambiguous cases (e.g., "1,234").
111+ /// It populates the provided ParsingContext with the detected format for later use.
112+ /// </summary>
113+ /// <returns>A normalized number string with '.' as the decimal separator for the Mages engine.</returns>
114+ private string NormalizeNumber ( string numberStr , ParsingContext context )
104115 {
105- // Don't execute when user only input "e" or "i" keyword
106- if ( query . Search . Length < 2 )
116+ var systemGroupSep = CultureInfo . CurrentCulture . NumberFormat . NumberGroupSeparator ;
117+ int dotCount = numberStr . Count ( f => f == '.' ) ;
118+ int commaCount = numberStr . Count ( f => f == ',' ) ;
119+
120+ // Case 1: Unambiguous mixed separators (e.g., "1.234,56")
121+ if ( dotCount > 0 && commaCount > 0 )
107122 {
108- return false ;
123+ context . InputUsesGroupSeparators = true ;
124+ if ( numberStr . LastIndexOf ( '.' ) > numberStr . LastIndexOf ( ',' ) )
125+ {
126+ context . InputDecimalSeparator = Dot ;
127+ return numberStr . Replace ( Comma , string . Empty ) ;
128+ }
129+ else
130+ {
131+ context . InputDecimalSeparator = Comma ;
132+ return numberStr . Replace ( Dot , string . Empty ) . Replace ( Comma , Dot ) ;
133+ }
109134 }
110135
111- if ( ! RegValidExpressChar . IsMatch ( query . Search ) )
136+ // Case 2: Only dots
137+ if ( dotCount > 0 )
112138 {
113- return false ;
139+ if ( dotCount > 1 )
140+ {
141+ context . InputUsesGroupSeparators = true ;
142+ return numberStr . Replace ( Dot , string . Empty ) ;
143+ }
144+ // A number is ambiguous if it has a single Dot in the thousands position,
145+ // and does not start with a "0." or "."
146+ bool isAmbiguous = numberStr . Length - numberStr . LastIndexOf ( '.' ) == 4
147+ && ! numberStr . StartsWith ( "0." )
148+ && ! numberStr . StartsWith ( "." ) ;
149+ if ( isAmbiguous )
150+ {
151+ if ( systemGroupSep == Dot )
152+ {
153+ context . InputUsesGroupSeparators = true ;
154+ return numberStr . Replace ( Dot , string . Empty ) ;
155+ }
156+ else
157+ {
158+ context . InputDecimalSeparator = Dot ;
159+ return numberStr ;
160+ }
161+ }
162+ else // Unambiguous decimal (e.g., "12.34" or "0.123" or ".123")
163+ {
164+ context . InputDecimalSeparator = Dot ;
165+ return numberStr ;
166+ }
114167 }
115168
116- if ( ! IsBracketComplete ( query . Search ) )
169+ // Case 3: Only commas
170+ if ( commaCount > 0 )
117171 {
118- return false ;
172+ if ( commaCount > 1 )
173+ {
174+ context . InputUsesGroupSeparators = true ;
175+ return numberStr . Replace ( Comma , string . Empty ) ;
176+ }
177+ // A number is ambiguous if it has a single Comma in the thousands position,
178+ // and does not start with a "0," or ","
179+ bool isAmbiguous = numberStr . Length - numberStr . LastIndexOf ( ',' ) == 4
180+ && ! numberStr . StartsWith ( "0," )
181+ && ! numberStr . StartsWith ( "," ) ;
182+ if ( isAmbiguous )
183+ {
184+ if ( systemGroupSep == Comma )
185+ {
186+ context . InputUsesGroupSeparators = true ;
187+ return numberStr . Replace ( Comma , string . Empty ) ;
188+ }
189+ else
190+ {
191+ context . InputDecimalSeparator = Comma ;
192+ return numberStr . Replace ( Comma , Dot ) ;
193+ }
194+ }
195+ else // Unambiguous decimal (e.g., "12,34" or "0,123" or ",123")
196+ {
197+ context . InputDecimalSeparator = Comma ;
198+ return numberStr . Replace ( Comma , Dot ) ;
199+ }
119200 }
120201
121- if ( ( query . Search . Contains ( Dot ) && GetDecimalSeparator ( ) != Dot ) ||
122- ( query . Search . Contains ( Comma ) && GetDecimalSeparator ( ) != Comma ) )
123- return false ;
202+ // Case 4: No separators
203+ return numberStr ;
204+ }
124205
125- return true ;
206+ private string FormatResult ( decimal roundedResult , ParsingContext context )
207+ {
208+ string decimalSeparator = context . InputDecimalSeparator ?? GetDecimalSeparator ( ) ;
209+ string groupSeparator = GetGroupSeparator ( decimalSeparator ) ;
210+
211+ string resultStr = roundedResult . ToString ( CultureInfo . InvariantCulture ) ;
212+
213+ string [ ] parts = resultStr . Split ( '.' ) ;
214+ string integerPart = parts [ 0 ] ;
215+ string fractionalPart = parts . Length > 1 ? parts [ 1 ] : string . Empty ;
216+
217+ if ( context . InputUsesGroupSeparators && integerPart . Length > 3 )
218+ {
219+ integerPart = ThousandGroupRegex . Replace ( integerPart , groupSeparator ) ;
220+ }
221+
222+ if ( ! string . IsNullOrEmpty ( fractionalPart ) )
223+ {
224+ return integerPart + decimalSeparator + fractionalPart ;
225+ }
226+
227+ return integerPart ;
126228 }
127229
128- private static string ChangeDecimalSeparator ( decimal value , string newDecimalSeparator )
230+ private string GetGroupSeparator ( string decimalSeparator )
129231 {
130- if ( string . IsNullOrEmpty ( newDecimalSeparator ) )
232+ // This logic is now independent of the system's group separator
233+ // to ensure consistent output for unit testing.
234+ return decimalSeparator == Dot ? Comma : Dot ;
235+ }
236+
237+ private bool CanCalculate ( Query query )
238+ {
239+ if ( query . Search . Length < 2 )
131240 {
132- return value . ToString ( ) ;
241+ return false ;
133242 }
134243
135- var numberFormatInfo = new NumberFormatInfo
244+ if ( ! RegValidExpressChar . IsMatch ( query . Search ) )
136245 {
137- NumberDecimalSeparator = newDecimalSeparator
138- } ;
139- return value . ToString ( numberFormatInfo ) ;
246+ return false ;
247+ }
248+
249+ if ( ! IsBracketComplete ( query . Search ) )
250+ {
251+ return false ;
252+ }
253+
254+ return true ;
140255 }
141256
142257 private string GetDecimalSeparator ( )
0 commit comments