@@ -27,6 +27,9 @@ public class DateMathPartParser : IPartParser
27
27
@"(?<operations>(?:[+\-/]\d*[yMwdhHms])*)$" ,
28
28
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
29
29
30
+ // Pre-compiled regex for operation parsing to avoid repeated compilation
31
+ private static readonly Regex _operationRegex = new ( @"([+\-/])(\d*)([yMwdhHms])" , RegexOptions . Compiled ) ;
32
+
30
33
public Regex Regex => _parser ;
31
34
32
35
public DateTimeOffset ? Parse ( Match match , DateTimeOffset relativeBaseTime , bool isUpperLimit )
@@ -54,13 +57,10 @@ public class DateMathPartParser : IPartParser
54
57
55
58
// Parse and apply operations
56
59
string operations = match . Groups [ "operations" ] . Value ;
57
- var result = ApplyOperations ( baseTime , operations , isUpperLimit ) ;
58
-
59
- return result ;
60
+ return ApplyOperations ( baseTime , operations , isUpperLimit ) ;
60
61
}
61
62
catch
62
63
{
63
- // Return null for any parsing errors to maintain robustness
64
64
return null ;
65
65
}
66
66
}
@@ -69,6 +69,8 @@ public class DateMathPartParser : IPartParser
69
69
/// Attempts to parse an explicit date string with proper timezone handling.
70
70
/// Supports various Elasticsearch-compatible date formats with optional timezone information.
71
71
///
72
+ /// Performance-optimized with length checks and format ordering by likelihood.
73
+ ///
72
74
/// Timezone Behavior:
73
75
/// - If timezone is specified (Z, +HH:MM, -HH:MM): Preserved from input
74
76
/// - If no timezone specified: Uses the provided fallback offset
@@ -83,80 +85,148 @@ private static bool TryParseExplicitDate(string dateStr, TimeSpan offset, out Da
83
85
{
84
86
result = default ;
85
87
86
- if ( string . IsNullOrEmpty ( dateStr ) )
88
+ if ( String . IsNullOrEmpty ( dateStr ) )
87
89
return false ;
88
90
89
- // Try various formats that Elasticsearch supports
90
- string [ ] formats = {
91
- "yyyy-MM-dd" ,
92
- "yyyy-MM-ddTHH:mm:ss" ,
93
- "yyyy-MM-ddTHH:mm" ,
94
- "yyyy-MM-ddTHH" ,
95
- "yyyy-MM-ddTHH:mm:ssZ" ,
96
- "yyyy-MM-ddTHH:mm:ss.fff" ,
97
- "yyyy-MM-ddTHH:mm:ss.fffZ" ,
98
- "yyyy-MM-ddTHH:mm:sszzz" ,
99
- "yyyy-MM-ddTHH:mm:ss.fffzzz" ,
100
- "yyyy.MM.dd" ,
101
- "yyyy.MM.ddTHH:mm:ss" ,
102
- "yyyy.MM.ddTHH:mm" ,
103
- "yyyy.MM.ddTHH" ,
104
- "yyyy.MM.ddTHH:mm:ssZ" ,
105
- "yyyy.MM.ddTHH:mm:ss.fff" ,
106
- "yyyy.MM.ddTHH:mm:ss.fffZ" ,
107
- "yyyy.MM.ddTHH:mm:sszzz" ,
108
- "yyyy.MM.ddTHH:mm:ss.fffzzz" ,
109
- "yyyyMMdd" ,
110
- "yyyyMMddTHHmmss" ,
111
- "yyyyMMddTHHmm" ,
112
- "yyyyMMddTHH" ,
113
- "yyyyMMddTHHmmssZ" ,
114
- "yyyyMMddTHHmmss.fff" ,
115
- "yyyyMMddTHHmmss.fffZ" ,
116
- "yyyyMMddTHHmmsszzz" ,
117
- "yyyyMMddTHHmmss.fffzzz"
118
- } ;
91
+ int len = dateStr . Length ;
92
+
93
+ // Early exit for obviously invalid lengths
94
+ if ( len is < 4 or > 29 ) // Min: yyyy (4), Max: yyyy-MM-ddTHH:mm:ss.fffzzz (29)
95
+ return false ;
119
96
120
- foreach ( string format in formats )
97
+ // Fast character validation for year digits
98
+ if ( ! Char . IsDigit ( dateStr [ 0 ] ) || ! Char . IsDigit ( dateStr [ 1 ] ) ||
99
+ ! Char . IsDigit ( dateStr [ 2 ] ) || ! Char . IsDigit ( dateStr [ 3 ] ) )
100
+ return false ;
101
+
102
+ // Detect timezone presence for smart format selection
103
+ bool hasZ = dateStr [ len - 1 ] == 'Z' ;
104
+ bool hasTimezone = hasZ ;
105
+ if ( ! hasTimezone && len > 10 ) // Check for +/-HH:mm timezone format
121
106
{
122
- // Handle timezone-aware formats differently from timezone-naive formats
123
- if ( format . EndsWith ( "Z" ) || format . Contains ( "zzz" ) )
107
+ for ( int index = Math . Max ( 10 , len - 6 ) ; index < len - 1 ; index ++ )
124
108
{
125
- // Try parsing with timezone information preserved
126
- if ( DateTimeOffset . TryParseExact ( dateStr , format , CultureInfo . InvariantCulture ,
127
- DateTimeStyles . None , out result ) )
109
+ if ( dateStr [ index ] is '+' or '-' && index + 1 < len && Char . IsDigit ( dateStr [ index + 1 ] ) )
128
110
{
129
- return true ;
111
+ hasTimezone = true ;
112
+ break ;
130
113
}
131
114
}
132
- else
115
+ }
116
+
117
+ // Length-based format selection for maximum performance
118
+ // Only try formats that match the exact length to avoid unnecessary parsing attempts
119
+ switch ( len )
120
+ {
121
+ case 4 : // Built-in: year (yyyy)
122
+ return TryParseWithFormat ( dateStr , "yyyy" , offset , false , out result ) ;
123
+
124
+ case 7 : // Built-in: year_month (yyyy-MM)
125
+ if ( dateStr [ 4 ] == '-' )
126
+ return TryParseWithFormat ( dateStr , "yyyy-MM" , offset , false , out result ) ;
127
+ break ;
128
+
129
+ case 8 : // Built-in: basic_date (yyyyMMdd)
130
+ return TryParseWithFormat ( dateStr , "yyyyMMdd" , offset , false , out result ) ;
131
+
132
+ case 10 : // Built-in: date (yyyy-MM-dd)
133
+ if ( dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' )
134
+ return TryParseWithFormat ( dateStr , "yyyy-MM-dd" , offset , false , out result ) ;
135
+ break ;
136
+
137
+ case 13 : // Built-in: date_hour (yyyy-MM-ddTHH)
138
+ if ( dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' )
139
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH" , offset , false , out result ) ;
140
+ break ;
141
+
142
+ case 16 : // Built-in: date_hour_minute (yyyy-MM-ddTHH:mm)
143
+ if ( dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' )
144
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm" , offset , false , out result ) ;
145
+ break ;
146
+
147
+ case 19 : // Built-in: date_hour_minute_second (yyyy-MM-ddTHH:mm:ss)
148
+ if ( dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' )
149
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:ss" , offset , false , out result ) ;
150
+ break ;
151
+
152
+ case 20 : // Built-in: date_time_no_millis (yyyy-MM-ddTHH:mm:ssZ)
153
+ if ( hasZ && dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' )
154
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:ssZ" , offset , true , out result ) ;
155
+ break ;
156
+
157
+ case 23 : // Built-in: date_hour_minute_second_millis (yyyy-MM-ddTHH:mm:ss.fff)
158
+ if ( dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' && dateStr [ 19 ] == '.' )
159
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:ss.fff" , offset , false , out result ) ;
160
+ break ;
161
+
162
+ case 24 : // Built-in: date_time (yyyy-MM-ddTHH:mm:ss.fffZ)
163
+ if ( hasZ && dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' && dateStr [ 19 ] == '.' )
164
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:ss.fffZ" , offset , true , out result ) ;
165
+ break ;
166
+ }
167
+
168
+ // Handle RFC 822 timezone offset formats (variable lengths: +05:00, +0500, etc.)
169
+ // Note: .NET uses 'zzz' pattern for timezone offsets like +05:00
170
+ if ( hasTimezone && ! hasZ )
171
+ {
172
+ // Only try timezone formats for lengths that make sense
173
+ if ( len is >= 25 and <= 29 ) // +05:00 variants
133
174
{
134
- // For formats without timezone, parse as DateTime and treat as if already in target timezone
135
- if ( DateTime . TryParseExact ( dateStr , format , CultureInfo . InvariantCulture ,
136
- DateTimeStyles . None , out DateTime dateTime ) )
175
+ if ( dateStr . Contains ( "." ) ) // with milliseconds
137
176
{
138
- // Treat the parsed DateTime as if it's already in the target timezone
139
- // This avoids any conversion issues
140
- result = new DateTimeOffset ( dateTime . Ticks , offset ) ;
141
- return true ;
177
+ // Try: yyyy-MM-ddTHH:mm:ss.fff+05:00
178
+ if ( TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:ss.fffzzz" , offset , true , out result ) )
179
+ return true ;
142
180
}
143
181
}
182
+
183
+ if ( len is >= 22 and <= 25 ) // without milliseconds
184
+ {
185
+ // Try: yyyy-MM-ddTHH:mm:ss+05:00
186
+ if ( TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:sszzz" , offset , true , out result ) )
187
+ return true ;
188
+ }
189
+ }
190
+
191
+ return false ;
192
+ }
193
+
194
+ /// <summary>
195
+ /// Helper method to parse with a specific format, handling timezone appropriately.
196
+ /// </summary>
197
+ private static bool TryParseWithFormat ( string dateStr , string format , TimeSpan offset , bool hasTimezone , out DateTimeOffset result )
198
+ {
199
+ result = default ;
200
+
201
+ if ( hasTimezone )
202
+ {
203
+ // Try parsing with timezone information preserved
204
+ return DateTimeOffset . TryParseExact ( dateStr , format , CultureInfo . InvariantCulture ,
205
+ DateTimeStyles . None , out result ) ;
206
+ }
207
+
208
+ // For formats without timezone, parse as DateTime and treat as if already in target timezone
209
+ if ( DateTime . TryParseExact ( dateStr , format , CultureInfo . InvariantCulture ,
210
+ DateTimeStyles . None , out DateTime dateTime ) )
211
+ {
212
+ // Treat the parsed DateTime as if it's already in the target timezone
213
+ result = new DateTimeOffset ( dateTime . Ticks , offset ) ;
214
+ return true ;
144
215
}
145
216
146
217
return false ;
147
218
}
148
219
149
220
private static DateTimeOffset ApplyOperations ( DateTimeOffset baseTime , string operations , bool isUpperLimit )
150
221
{
151
- if ( string . IsNullOrEmpty ( operations ) )
222
+ if ( String . IsNullOrEmpty ( operations ) )
152
223
return baseTime ;
153
224
154
225
var result = baseTime ;
155
- var operationRegex = new Regex ( @"([+\-/])(\d*)([yMwdhHms])" , RegexOptions . Compiled ) ;
156
- var matches = operationRegex . Matches ( operations ) ;
226
+ var matches = _operationRegex . Matches ( operations ) ;
157
227
158
228
// Validate that all operations were matched properly
159
- var totalMatchLength = matches . Cast < Match > ( ) . Sum ( m => m . Length ) ;
229
+ int totalMatchLength = matches . Cast < Match > ( ) . Sum ( m => m . Length ) ;
160
230
if ( totalMatchLength != operations . Length )
161
231
{
162
232
// If not all operations were matched, there are invalid operations
@@ -170,7 +240,7 @@ private static DateTimeOffset ApplyOperations(DateTimeOffset baseTime, string op
170
240
string unit = opMatch . Groups [ 3 ] . Value ;
171
241
172
242
// Default amount is 1 if not specified
173
- int amount = string . IsNullOrEmpty ( amountStr ) ? 1 : int . Parse ( amountStr ) ;
243
+ int amount = String . IsNullOrEmpty ( amountStr ) ? 1 : Int32 . Parse ( amountStr ) ;
174
244
175
245
switch ( operation )
176
246
{
@@ -199,7 +269,7 @@ private static DateTimeOffset AddTimeUnit(DateTimeOffset dateTime, int amount, s
199
269
"M" => dateTime . AddMonths ( amount ) , // Capital M for months
200
270
"m" => dateTime . AddMinutes ( amount ) , // Lowercase m for minutes
201
271
"w" => dateTime . AddDays ( amount * 7 ) ,
202
- "d" => dateTime . AddDays ( amount ) ,
272
+ "d" => dateTime . AddDays ( amount ) , // Only lowercase d for days
203
273
"h" or "H" => dateTime . AddHours ( amount ) ,
204
274
"s" => dateTime . AddSeconds ( amount ) ,
205
275
_ => throw new ArgumentException ( $ "Invalid time unit: { unit } ")
@@ -219,7 +289,7 @@ private static DateTimeOffset RoundToUnit(DateTimeOffset dateTime, string unit,
219
289
"y" => isUpperLimit ? dateTime . EndOfYear ( ) : dateTime . StartOfYear ( ) ,
220
290
"M" => isUpperLimit ? dateTime . EndOfMonth ( ) : dateTime . StartOfMonth ( ) ,
221
291
"w" => isUpperLimit ? dateTime . EndOfWeek ( ) : dateTime . StartOfWeek ( ) ,
222
- "d" => isUpperLimit ? dateTime . EndOfDay ( ) : dateTime . StartOfDay ( ) ,
292
+ "d" => isUpperLimit ? dateTime . EndOfDay ( ) : dateTime . StartOfDay ( ) , // Only lowercase d for days
223
293
"h" or "H" => isUpperLimit ? dateTime . EndOfHour ( ) : dateTime . StartOfHour ( ) ,
224
294
"m" => isUpperLimit ? dateTime . EndOfMinute ( ) : dateTime . StartOfMinute ( ) ,
225
295
"s" => isUpperLimit ? dateTime . EndOfSecond ( ) : dateTime . StartOfSecond ( ) ,
0 commit comments