@@ -22,7 +22,7 @@ public static class DateMath
22
22
{
23
23
// Match date math expressions with anchors and operations
24
24
internal static readonly Regex Parser = new (
25
- @"^(?<anchor>now|(?<date>\d{4}[-.] ?\d{2}[-.] ?\d{2}(?:[T\s](?:\d{1,2}(?::?\d{2}(?::?\d{2})?)?(?:\.\d{1,3})?)?(?:[+-]\d{2}:?\d{2}|Z)?)?)\|\|)" +
25
+ @"^(?<anchor>now|(?<date>\d{4}- ?\d{2}- ?\d{2}(?:[T\s](?:\d{1,2}(?::?\d{2}(?::?\d{2})?)?(?:\.\d{1,3})?)?(?:[+-]\d{2}:?\d{2}|Z)?)?)\|\|)" +
26
26
@"(?<operations>(?:[+\-/]\d*[yMwdhHms])*)$" ,
27
27
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
28
28
@@ -178,67 +178,60 @@ private static bool TryParseExplicitDate(string dateStr, TimeSpan offset, out Da
178
178
case 4 : // Built-in: year (yyyy)
179
179
return TryParseWithFormat ( dateStr , "yyyy" , offset , false , out result ) ;
180
180
181
- case 7 : // Built-in: year_month (yyyy-MM or yyyy.MM )
182
- if ( dateStr [ 4 ] is '-' or '. ')
183
- return TryParseWithFormat ( dateStr , dateStr [ 4 ] == '-' ? "yyyy-MM" : "yyyy. MM", offset , false , out result ) ;
181
+ case 7 : // Built-in: year_month (yyyy-MM)
182
+ if ( dateStr [ 4 ] == '-' )
183
+ return TryParseWithFormat ( dateStr , "yyyy-MM" , offset , false , out result ) ;
184
184
break ;
185
185
186
186
case 8 : // Built-in: basic_date (yyyyMMdd)
187
187
return TryParseWithFormat ( dateStr , "yyyyMMdd" , offset , false , out result ) ;
188
188
189
- case 10 : // Built-in: date (yyyy-MM-dd or yyyy.MM.dd )
190
- if ( dateStr [ 4 ] is '-' or '.' && dateStr [ 7 ] is '-' or '. ')
189
+ case 10 : // Built-in: date (yyyy-MM-dd)
190
+ if ( dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' )
191
191
{
192
- string format = dateStr [ 4 ] == '-' ? "yyyy-MM-dd" : "yyyy.MM.dd" ;
193
- return TryParseWithFormat ( dateStr , format , offset , false , out result ) ;
192
+ return TryParseWithFormat ( dateStr , "yyyy-MM-dd" , offset , false , out result ) ;
194
193
}
195
194
break ;
196
195
197
- case 13 : // Built-in: date_hour (yyyy-MM-ddTHH or yyyy.MM.ddTHH )
198
- if ( dateStr [ 4 ] is '-' or '.' && dateStr [ 7 ] is '-' or '. ' && dateStr [ 10 ] == 'T' )
196
+ case 13 : // Built-in: date_hour (yyyy-MM-ddTHH)
197
+ if ( dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' )
199
198
{
200
- string format = dateStr [ 4 ] == '-' ? "yyyy-MM-ddTHH" : "yyyy.MM.ddTHH" ;
201
- return TryParseWithFormat ( dateStr , format , offset , false , out result ) ;
199
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH" , offset , false , out result ) ;
202
200
}
203
201
break ;
204
202
205
- case 16 : // Built-in: date_hour_minute (yyyy-MM-ddTHH:mm or yyyy.MM.ddTHH:mm )
206
- if ( dateStr [ 4 ] is '-' or '.' && dateStr [ 7 ] is '-' or '. ' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' )
203
+ case 16 : // Built-in: date_hour_minute (yyyy-MM-ddTHH:mm)
204
+ if ( dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' )
207
205
{
208
- string format = dateStr [ 4 ] == '-' ? "yyyy-MM-ddTHH:mm" : "yyyy.MM.ddTHH:mm" ;
209
- return TryParseWithFormat ( dateStr , format , offset , false , out result ) ;
206
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm" , offset , false , out result ) ;
210
207
}
211
208
break ;
212
209
213
- case 19 : // Built-in: date_hour_minute_second (yyyy-MM-ddTHH:mm:ss or yyyy.MM.ddTHH:mm:ss )
214
- if ( dateStr [ 4 ] is '-' or '.' && dateStr [ 7 ] is '-' or '. ' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' )
210
+ case 19 : // Built-in: date_hour_minute_second (yyyy-MM-ddTHH:mm:ss)
211
+ if ( dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' )
215
212
{
216
- string format = dateStr [ 4 ] == '-' ? "yyyy-MM-ddTHH:mm:ss" : "yyyy.MM.ddTHH:mm:ss" ;
217
- return TryParseWithFormat ( dateStr , format , offset , false , out result ) ;
213
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:ss" , offset , false , out result ) ;
218
214
}
219
215
break ;
220
216
221
- case 20 : // Built-in: date_time_no_millis (yyyy-MM-ddTHH:mm:ssZ or yyyy.MM.ddTHH:mm:ssZ )
222
- if ( hasZ && dateStr [ 4 ] is '-' or '.' && dateStr [ 7 ] is '-' or '. ' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' )
217
+ case 20 : // Built-in: date_time_no_millis (yyyy-MM-ddTHH:mm:ssZ)
218
+ if ( hasZ && dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' )
223
219
{
224
- string format = dateStr [ 4 ] == '-' ? "yyyy-MM-ddTHH:mm:ssZ" : "yyyy.MM.ddTHH:mm:ssZ" ;
225
- return TryParseWithFormat ( dateStr , format , offset , true , out result ) ;
220
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:ssZ" , offset , true , out result ) ;
226
221
}
227
222
break ;
228
223
229
- case 23 : // Built-in: date_hour_minute_second_millis (yyyy-MM-ddTHH:mm:ss.fff or yyyy.MM.ddTHH:mm:ss.fff )
230
- if ( dateStr [ 4 ] is '-' or '.' && dateStr [ 7 ] is '-' or '. ' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' && dateStr [ 19 ] == '.' )
224
+ case 23 : // Built-in: date_hour_minute_second_millis (yyyy-MM-ddTHH:mm:ss.fff)
225
+ if ( dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' && dateStr [ 19 ] == '.' )
231
226
{
232
- string format = dateStr [ 4 ] == '-' ? "yyyy-MM-ddTHH:mm:ss.fff" : "yyyy.MM.ddTHH:mm:ss.fff" ;
233
- return TryParseWithFormat ( dateStr , format , offset , false , out result ) ;
227
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:ss.fff" , offset , false , out result ) ;
234
228
}
235
229
break ;
236
230
237
- case 24 : // Built-in: date_time (yyyy-MM-ddTHH:mm:ss.fffZ or yyyy.MM.ddTHH:mm:ss.fffZ )
238
- if ( hasZ && dateStr [ 4 ] is '-' or '.' && dateStr [ 7 ] is '-' or '. ' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' && dateStr [ 19 ] == '.' )
231
+ case 24 : // Built-in: date_time (yyyy-MM-ddTHH:mm:ss.fffZ)
232
+ if ( hasZ && dateStr [ 4 ] == '-' && dateStr [ 7 ] == '-' && dateStr [ 10 ] == 'T' && dateStr [ 13 ] == ':' && dateStr [ 16 ] == ':' && dateStr [ 19 ] == '.' )
239
233
{
240
- string format = dateStr [ 4 ] == '-' ? "yyyy-MM-ddTHH:mm:ss.fffZ" : "yyyy.MM.ddTHH:mm:ss.fffZ" ;
241
- return TryParseWithFormat ( dateStr , format , offset , true , out result ) ;
234
+ return TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:ss.fffZ" , offset , true , out result ) ;
242
235
}
243
236
break ;
244
237
}
@@ -247,30 +240,21 @@ private static bool TryParseExplicitDate(string dateStr, TimeSpan offset, out Da
247
240
// Note: .NET uses 'zzz' pattern for timezone offsets like +05:00
248
241
if ( hasTimezone && ! hasZ )
249
242
{
250
- // Determine the date separator for format construction
251
- char dateSeparator = ( len > 4 && dateStr [ 4 ] == '.' ) ? '.' : '-' ;
252
-
253
243
// Only try timezone formats for lengths that make sense
254
244
if ( len is >= 25 and <= 29 ) // +05:00 variants
255
245
{
256
246
if ( dateStr . Contains ( "." ) ) // with milliseconds
257
247
{
258
- // Try both separators: yyyy-MM-ddTHH:mm:ss.fff+05:00 or yyyy.MM.ddTHH:mm:ss.fff+05:00
259
- string format = dateSeparator == '.'
260
- ? "yyyy.MM.ddTHH:mm:ss.fffzzz"
261
- : "yyyy-MM-ddTHH:mm:ss.fffzzz" ;
262
- if ( TryParseWithFormat ( dateStr , format , offset , true , out result ) )
248
+ // yyyy-MM-ddTHH:mm:ss.fff+05:00
249
+ if ( TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:ss.fffzzz" , offset , true , out result ) )
263
250
return true ;
264
251
}
265
252
}
266
253
267
254
if ( len is >= 22 and <= 25 ) // without milliseconds
268
255
{
269
- // Try both separators: yyyy-MM-ddTHH:mm:ss+05:00 or yyyy.MM.ddTHH:mm:ss+05:00
270
- string format = dateSeparator == '.'
271
- ? "yyyy.MM.ddTHH:mm:sszzz"
272
- : "yyyy-MM-ddTHH:mm:sszzz" ;
273
- if ( TryParseWithFormat ( dateStr , format , offset , true , out result ) )
256
+ // yyyy-MM-ddTHH:mm:ss+05:00
257
+ if ( TryParseWithFormat ( dateStr , "yyyy-MM-ddTHH:mm:sszzz" , offset , true , out result ) )
274
258
return true ;
275
259
}
276
260
}
@@ -327,6 +311,28 @@ public static DateTimeOffset ApplyOperations(DateTimeOffset baseTime, string ope
327
311
throw new ArgumentException ( "Invalid operations" ) ;
328
312
}
329
313
314
+ // Validate that rounding operations (/) are only at the end
315
+ // According to Elasticsearch spec, rounding must be the final operation
316
+ bool foundRounding = false ;
317
+ for ( int i = 0 ; i < matches . Count ; i ++ )
318
+ {
319
+ string operation = matches [ i ] . Groups [ 1 ] . Value ;
320
+ if ( operation == "/" )
321
+ {
322
+ if ( foundRounding )
323
+ {
324
+ // Multiple rounding operations are not allowed
325
+ throw new ArgumentException ( "Multiple rounding operations are not allowed" ) ;
326
+ }
327
+ if ( i != matches . Count - 1 )
328
+ {
329
+ // Rounding operation must be the last operation
330
+ throw new ArgumentException ( "Rounding operation must be the final operation" ) ;
331
+ }
332
+ foundRounding = true ;
333
+ }
334
+ }
335
+
330
336
foreach ( Match opMatch in matches )
331
337
{
332
338
string operation = opMatch . Groups [ 1 ] . Value ;
0 commit comments