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,20 +59,11 @@ 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
@@ -71,7 +76,7 @@ public List<Result> Query(Query query)
7176 if ( ! string . IsNullOrEmpty ( result ? . ToString ( ) ) )
7277 {
7378 decimal roundedResult = Math . Round ( Convert . ToDecimal ( result ) , _settings . MaxDecimalPlaces , MidpointRounding . AwayFromZero ) ;
74- string newResult = ChangeDecimalSeparator ( roundedResult , GetDecimalSeparator ( ) ) ;
79+ string newResult = FormatResult ( roundedResult , context ) ;
7580
7681 return new List < Result >
7782 {
@@ -107,46 +112,156 @@ public List<Result> Query(Query query)
107112 return new List < Result > ( ) ;
108113 }
109114
110- private bool CanCalculate ( Query query )
115+ /// <summary>
116+ /// Parses a string representation of a number, detecting its format. It uses structural analysis
117+ /// and falls back to system culture for truly ambiguous cases (e.g., "1,234").
118+ /// It populates the provided ParsingContext with the detected format for later use.
119+ /// </summary>
120+ /// <returns>A normalized number string with '.' as the decimal separator for the Mages engine.</returns>
121+ private string NormalizeNumber ( string numberStr , ParsingContext context )
111122 {
112- // Don't execute when user only input "e" or "i" keyword
113- if ( query . Search . Length < 2 )
123+ var systemGroupSep = CultureInfo . CurrentCulture . NumberFormat . NumberGroupSeparator ;
124+ int dotCount = numberStr . Count ( f => f == '.' ) ;
125+ int commaCount = numberStr . Count ( f => f == ',' ) ;
126+
127+ // Case 1: Unambiguous mixed separators (e.g., "1.234,56")
128+ if ( dotCount > 0 && commaCount > 0 )
114129 {
115- return false ;
130+ context . InputUsesGroupSeparators = true ;
131+ if ( numberStr . LastIndexOf ( '.' ) > numberStr . LastIndexOf ( ',' ) )
132+ {
133+ context . InputDecimalSeparator = Dot ;
134+ return numberStr . Replace ( Comma , string . Empty ) ;
135+ }
136+ else
137+ {
138+ context . InputDecimalSeparator = Comma ;
139+ return numberStr . Replace ( Dot , string . Empty ) . Replace ( Comma , Dot ) ;
140+ }
116141 }
117142
118- if ( ! RegValidExpressChar . IsMatch ( query . Search ) )
143+ // Case 2: Only dots
144+ if ( dotCount > 0 )
119145 {
120- return false ;
146+ if ( dotCount > 1 )
147+ {
148+ context . InputUsesGroupSeparators = true ;
149+ return numberStr . Replace ( Dot , string . Empty ) ;
150+ }
151+ // A number is ambiguous if it has a single Dot in the thousands position,
152+ // and does not start with a "0." or "."
153+ bool isAmbiguous = numberStr . Length - numberStr . LastIndexOf ( '.' ) == 4
154+ && ! numberStr . StartsWith ( "0." )
155+ && ! numberStr . StartsWith ( "." ) ;
156+ if ( isAmbiguous )
157+ {
158+ if ( systemGroupSep == Dot )
159+ {
160+ context . InputUsesGroupSeparators = true ;
161+ return numberStr . Replace ( Dot , string . Empty ) ;
162+ }
163+ else
164+ {
165+ context . InputDecimalSeparator = Dot ;
166+ return numberStr ;
167+ }
168+ }
169+ else // Unambiguous decimal (e.g., "12.34" or "0.123" or ".123")
170+ {
171+ context . InputDecimalSeparator = Dot ;
172+ return numberStr ;
173+ }
121174 }
122175
123- if ( ! IsBracketComplete ( query . Search ) )
176+ // Case 3: Only commas
177+ if ( commaCount > 0 )
124178 {
125- return false ;
179+ if ( commaCount > 1 )
180+ {
181+ context . InputUsesGroupSeparators = true ;
182+ return numberStr . Replace ( Comma , string . Empty ) ;
183+ }
184+ // A number is ambiguous if it has a single Comma in the thousands position,
185+ // and does not start with a "0," or ","
186+ bool isAmbiguous = numberStr . Length - numberStr . LastIndexOf ( ',' ) == 4
187+ && ! numberStr . StartsWith ( "0," )
188+ && ! numberStr . StartsWith ( "," ) ;
189+ if ( isAmbiguous )
190+ {
191+ if ( systemGroupSep == Comma )
192+ {
193+ context . InputUsesGroupSeparators = true ;
194+ return numberStr . Replace ( Comma , string . Empty ) ;
195+ }
196+ else
197+ {
198+ context . InputDecimalSeparator = Comma ;
199+ return numberStr . Replace ( Comma , Dot ) ;
200+ }
201+ }
202+ else // Unambiguous decimal (e.g., "12,34" or "0,123" or ",123")
203+ {
204+ context . InputDecimalSeparator = Comma ;
205+ return numberStr . Replace ( Comma , Dot ) ;
206+ }
126207 }
127208
128- if ( ( query . Search . Contains ( Dot ) && GetDecimalSeparator ( ) != Dot ) ||
129- ( query . Search . Contains ( Comma ) && GetDecimalSeparator ( ) != Comma ) )
130- return false ;
209+ // Case 4: No separators
210+ return numberStr ;
211+ }
131212
132- return true ;
213+ private string FormatResult ( decimal roundedResult , ParsingContext context )
214+ {
215+ string decimalSeparator = context . InputDecimalSeparator ?? GetDecimalSeparator ( ) ;
216+ string groupSeparator = GetGroupSeparator ( decimalSeparator ) ;
217+
218+ string resultStr = roundedResult . ToString ( CultureInfo . InvariantCulture ) ;
219+
220+ string [ ] parts = resultStr . Split ( '.' ) ;
221+ string integerPart = parts [ 0 ] ;
222+ string fractionalPart = parts . Length > 1 ? parts [ 1 ] : string . Empty ;
223+
224+ if ( context . InputUsesGroupSeparators && integerPart . Length > 3 )
225+ {
226+ integerPart = ThousandGroupRegex . Replace ( integerPart , groupSeparator ) ;
227+ }
228+
229+ if ( ! string . IsNullOrEmpty ( fractionalPart ) )
230+ {
231+ return integerPart + decimalSeparator + fractionalPart ;
232+ }
233+
234+ return integerPart ;
133235 }
134236
135- private static string ChangeDecimalSeparator ( decimal value , string newDecimalSeparator )
237+ private string GetGroupSeparator ( string decimalSeparator )
136238 {
137- if ( string . IsNullOrEmpty ( newDecimalSeparator ) )
239+ // This logic is now independent of the system's group separator
240+ // to ensure consistent output for unit testing.
241+ return decimalSeparator == Dot ? Comma : Dot ;
242+ }
243+
244+ private bool CanCalculate ( Query query )
245+ {
246+ if ( query . Search . Length < 2 )
138247 {
139- return value . ToString ( ) ;
248+ return false ;
140249 }
141250
142- var numberFormatInfo = new NumberFormatInfo
251+ if ( ! RegValidExpressChar . IsMatch ( query . Search ) )
143252 {
144- NumberDecimalSeparator = newDecimalSeparator
145- } ;
146- return value . ToString ( numberFormatInfo ) ;
253+ return false ;
254+ }
255+
256+ if ( ! IsBracketComplete ( query . Search ) )
257+ {
258+ return false ;
259+ }
260+
261+ return true ;
147262 }
148263
149- private string GetDecimalSeparator ( )
264+ private string GetDecimalSeparator ( )
150265 {
151266 string systemDecimalSeparator = CultureInfo . CurrentCulture . NumberFormat . NumberDecimalSeparator ;
152267 return _settings . DecimalSeparator switch
0 commit comments