1
1
using System ;
2
2
using System . Collections . Generic ;
3
3
using System . Globalization ;
4
+ using System . Linq ;
4
5
using System . Runtime . InteropServices ;
5
6
using System . Text . RegularExpressions ;
6
7
using System . Windows . Controls ;
@@ -14,6 +15,9 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
14
15
{
15
16
private static readonly Regex RegValidExpressChar = MainRegexHelper . GetRegValidExpressChar ( ) ;
16
17
private static readonly Regex RegBrackets = MainRegexHelper . GetRegBrackets ( ) ;
18
+ private static readonly Regex ThousandGroupRegex = MainRegexHelper . GetThousandGroupRegex ( ) ;
19
+ private static readonly Regex NumberRegex = MainRegexHelper . GetNumberRegex ( ) ;
20
+
17
21
private static Engine MagesEngine ;
18
22
private const string Comma = "," ;
19
23
private const string Dot = "." ;
@@ -23,6 +27,16 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider
23
27
private Settings _settings ;
24
28
private SettingsViewModel _viewModel ;
25
29
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
+
26
40
public void Init ( PluginInitContext context )
27
41
{
28
42
Context = context ;
@@ -45,20 +59,11 @@ public List<Result> Query(Query query)
45
59
return new List < Result > ( ) ;
46
60
}
47
61
62
+ var context = new ParsingContext ( ) ;
63
+
48
64
try
49
65
{
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 ) ) ;
62
67
63
68
var result = MagesEngine . Interpret ( expression ) ;
64
69
@@ -71,7 +76,7 @@ public List<Result> Query(Query query)
71
76
if ( ! string . IsNullOrEmpty ( result ? . ToString ( ) ) )
72
77
{
73
78
decimal roundedResult = Math . Round ( Convert . ToDecimal ( result ) , _settings . MaxDecimalPlaces , MidpointRounding . AwayFromZero ) ;
74
- string newResult = ChangeDecimalSeparator ( roundedResult , GetDecimalSeparator ( ) ) ;
79
+ string newResult = FormatResult ( roundedResult , context ) ;
75
80
76
81
return new List < Result >
77
82
{
@@ -107,46 +112,156 @@ public List<Result> Query(Query query)
107
112
return new List < Result > ( ) ;
108
113
}
109
114
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 )
111
122
{
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 )
114
129
{
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
+ }
116
141
}
117
142
118
- if ( ! RegValidExpressChar . IsMatch ( query . Search ) )
143
+ // Case 2: Only dots
144
+ if ( dotCount > 0 )
119
145
{
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
+ }
121
174
}
122
175
123
- if ( ! IsBracketComplete ( query . Search ) )
176
+ // Case 3: Only commas
177
+ if ( commaCount > 0 )
124
178
{
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
+ }
126
207
}
127
208
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
+ }
131
212
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 ;
133
235
}
134
236
135
- private static string ChangeDecimalSeparator ( decimal value , string newDecimalSeparator )
237
+ private string GetGroupSeparator ( string decimalSeparator )
136
238
{
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 )
138
247
{
139
- return value . ToString ( ) ;
248
+ return false ;
140
249
}
141
250
142
- var numberFormatInfo = new NumberFormatInfo
251
+ if ( ! RegValidExpressChar . IsMatch ( query . Search ) )
143
252
{
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 ;
147
262
}
148
263
149
- private string GetDecimalSeparator ( )
264
+ private string GetDecimalSeparator ( )
150
265
{
151
266
string systemDecimalSeparator = CultureInfo . CurrentCulture . NumberFormat . NumberDecimalSeparator ;
152
267
return _settings . DecimalSeparator switch
0 commit comments