@@ -22,7 +22,7 @@ public static class DateMath
2222{
2323 // Match date math expressions with anchors and operations
2424 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)?)?)\|\|)" +
2626 @"(?<operations>(?:[+\-/]\d*[yMwdhHms])*)$" ,
2727 RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
2828
@@ -178,67 +178,60 @@ private static bool TryParseExplicitDate(string dateStr, TimeSpan offset, out Da
178178 case 4 : // Built-in: year (yyyy)
179179 return TryParseWithFormat ( dateStr , "yyyy" , offset , false , out result ) ;
180180
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 ) ;
184184 break ;
185185
186186 case 8 : // Built-in: basic_date (yyyyMMdd)
187187 return TryParseWithFormat ( dateStr , "yyyyMMdd" , offset , false , out result ) ;
188188
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 ] == '-' )
191191 {
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 ) ;
194193 }
195194 break ;
196195
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' )
199198 {
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 ) ;
202200 }
203201 break ;
204202
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 ] == ':' )
207205 {
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 ) ;
210207 }
211208 break ;
212209
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 ] == ':' )
215212 {
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 ) ;
218214 }
219215 break ;
220216
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 ] == ':' )
223219 {
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 ) ;
226221 }
227222 break ;
228223
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 ] == '.' )
231226 {
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 ) ;
234228 }
235229 break ;
236230
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 ] == '.' )
239233 {
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 ) ;
242235 }
243236 break ;
244237 }
@@ -247,30 +240,21 @@ private static bool TryParseExplicitDate(string dateStr, TimeSpan offset, out Da
247240 // Note: .NET uses 'zzz' pattern for timezone offsets like +05:00
248241 if ( hasTimezone && ! hasZ )
249242 {
250- // Determine the date separator for format construction
251- char dateSeparator = ( len > 4 && dateStr [ 4 ] == '.' ) ? '.' : '-' ;
252-
253243 // Only try timezone formats for lengths that make sense
254244 if ( len is >= 25 and <= 29 ) // +05:00 variants
255245 {
256246 if ( dateStr . Contains ( "." ) ) // with milliseconds
257247 {
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 ) )
263250 return true ;
264251 }
265252 }
266253
267254 if ( len is >= 22 and <= 25 ) // without milliseconds
268255 {
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 ) )
274258 return true ;
275259 }
276260 }
@@ -327,6 +311,28 @@ public static DateTimeOffset ApplyOperations(DateTimeOffset baseTime, string ope
327311 throw new ArgumentException ( "Invalid operations" ) ;
328312 }
329313
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+
330336 foreach ( Match opMatch in matches )
331337 {
332338 string operation = opMatch . Groups [ 1 ] . Value ;
0 commit comments