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,26 +59,17 @@ 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
if ( result ? . ToString ( ) != "NaN" && result is not Function && ! string . IsNullOrEmpty ( result ? . ToString ( ) ) )
65
70
{
66
71
decimal roundedResult = Math . Round ( Convert . ToDecimal ( result ) , _settings . MaxDecimalPlaces , MidpointRounding . AwayFromZero ) ;
67
- string newResult = ChangeDecimalSeparator ( roundedResult , GetDecimalSeparator ( ) ) ;
72
+ string newResult = FormatResult ( roundedResult , context ) ;
68
73
69
74
return new List < Result >
70
75
{
@@ -100,43 +105,153 @@ public List<Result> Query(Query query)
100
105
return new List < Result > ( ) ;
101
106
}
102
107
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 )
104
115
{
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 )
107
122
{
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
+ }
109
134
}
110
135
111
- if ( ! RegValidExpressChar . IsMatch ( query . Search ) )
136
+ // Case 2: Only dots
137
+ if ( dotCount > 0 )
112
138
{
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
+ }
114
167
}
115
168
116
- if ( ! IsBracketComplete ( query . Search ) )
169
+ // Case 3: Only commas
170
+ if ( commaCount > 0 )
117
171
{
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
+ }
119
200
}
120
201
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
+ }
124
205
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 ;
126
228
}
127
229
128
- private static string ChangeDecimalSeparator ( decimal value , string newDecimalSeparator )
230
+ private string GetGroupSeparator ( string decimalSeparator )
129
231
{
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 )
131
240
{
132
- return value . ToString ( ) ;
241
+ return false ;
133
242
}
134
243
135
- var numberFormatInfo = new NumberFormatInfo
244
+ if ( ! RegValidExpressChar . IsMatch ( query . Search ) )
136
245
{
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 ;
140
255
}
141
256
142
257
private string GetDecimalSeparator ( )
0 commit comments