@@ -80,6 +80,7 @@ mod format {
8080 pub const YYYYMMDDHHMMSS_HYPHENATED_OFFSET : & str = "%Y-%m-%d %H:%M:%S %#z" ;
8181 pub const YYYYMMDDHHMMSS_HYPHENATED_ZULU : & str = "%Y-%m-%d %H:%M:%SZ" ;
8282 pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_OFFSET : & str = "%Y-%m-%dT%H:%M:%S%#z" ;
83+ pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_ZULU : & str = "%Y-%m-%dT%H:%M:%SZ" ;
8384 pub const YYYYMMDDHHMMSS_T_SEP_HYPHENATED_SPACE_OFFSET : & str = "%Y-%m-%dT%H:%M:%S %#z" ;
8485 pub const YYYYMMDDHHMMS_T_SEP : & str = "%Y-%m-%dT%H:%M:%S" ;
8586 pub const UTC_OFFSET : & str = "UTC%#z" ;
@@ -88,7 +89,7 @@ mod format {
8889
8990 /// Whether the pattern ends in the character `Z`.
9091 pub ( crate ) fn is_zulu ( pattern : & str ) -> bool {
91- pattern == YYYYMMDDHHMMSS_HYPHENATED_ZULU
92+ pattern. ends_with ( 'Z' )
9293 }
9394
9495 /// Patterns for datetimes with timezones.
@@ -113,10 +114,11 @@ mod format {
113114 /// Patterns for datetimes without timezones.
114115 ///
115116 /// These are in decreasing order of length.
116- pub ( crate ) const PATTERNS_NO_TZ : [ ( & str , usize ) ; 8 ] = [
117+ pub ( crate ) const PATTERNS_NO_TZ : [ ( & str , usize ) ; 9 ] = [
117118 ( YYYYMMDDHHMMSS , 29 ) ,
118119 ( POSIX_LOCALE , 24 ) ,
119120 ( YYYYMMDDHHMMSS_HYPHENATED_ZULU , 20 ) ,
121+ ( YYYYMMDDHHMMSS_T_SEP_HYPHENATED_ZULU , 20 ) ,
120122 ( YYYYMMDDHHMMS_T_SEP , 19 ) ,
121123 ( YYYYMMDDHHMMS , 19 ) ,
122124 ( YYYY_MM_DD_HH_MM , 16 ) ,
@@ -232,8 +234,34 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
232234 // TODO: Replace with a proper customiseable parsing solution using `nom`, `grmtools`, or
233235 // similar
234236
235- // Formats with offsets don't require NaiveDateTime workaround
236- //
237+ // Try to parse a reference date first. Try parsing from longest
238+ // pattern to shortest pattern. If a reference date can be parsed,
239+ // then try to parse a time delta from the remaining slice. If no
240+ // reference date could be parsed, then try to parse the entire
241+ // string as a time delta. If no time delta could be parsed,
242+ // return an error.
243+ let ( ref_date, n) = match parse_reference_date ( date, s. as_ref ( ) ) {
244+ Some ( ( ref_date, n) ) => ( ref_date, n) ,
245+ None => {
246+ let tz = TimeZone :: from_offset ( date. offset ( ) ) ;
247+ match date. naive_local ( ) . and_local_timezone ( tz) {
248+ MappedLocalTime :: Single ( ref_date) => ( ref_date, 0 ) ,
249+ _ => return Err ( ParseDateTimeError :: InvalidInput ) ,
250+ }
251+ }
252+ } ;
253+ parse_relative_time_at_date ( ref_date, & s. as_ref ( ) [ n..] )
254+ }
255+
256+ /// Parse an absolute datetime from a prefix of s, if possible.
257+ ///
258+ /// Try to parse the longest possible absolute datetime at the beginning
259+ /// of string `s`. Return the parsed datetime and the index in `s` at
260+ /// which the datetime ended.
261+ fn parse_reference_date < S > ( date : DateTime < Local > , s : S ) -> Option < ( DateTime < FixedOffset > , usize ) >
262+ where
263+ S : AsRef < str > ,
264+ {
237265 // HACK: if the string ends with a single digit preceded by a + or -
238266 // sign, then insert a 0 between the sign and the digit to make it
239267 // possible for `chrono` to parse it.
@@ -242,7 +270,11 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
242270 for ( fmt, n) in format:: PATTERNS_TZ {
243271 if tmp_s. len ( ) >= n {
244272 if let Ok ( parsed) = DateTime :: parse_from_str ( & tmp_s[ 0 ..n] , fmt) {
245- return Ok ( parsed) ;
273+ if tmp_s == s. as_ref ( ) {
274+ return Some ( ( parsed, n) ) ;
275+ } else {
276+ return Some ( ( parsed, n - 1 ) ) ;
277+ }
246278 }
247279 }
248280 }
@@ -259,11 +291,11 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
259291 . unwrap ( )
260292 . from_local_datetime ( & parsed)
261293 {
262- MappedLocalTime :: Single ( datetime) => return Ok ( datetime) ,
263- _ => return Err ( ParseDateTimeError :: InvalidInput ) ,
294+ MappedLocalTime :: Single ( datetime) => return Some ( ( datetime, n ) ) ,
295+ _ => return None ,
264296 }
265297 } else if let Ok ( dt) = naive_dt_to_fixed_offset ( date, parsed) {
266- return Ok ( dt ) ;
298+ return Some ( ( dt , n ) ) ;
267299 }
268300 }
269301 }
@@ -287,13 +319,13 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
287319
288320 let dt = DateTime :: < FixedOffset > :: from ( beginning_of_day) ;
289321
290- return Ok ( dt ) ;
322+ return Some ( ( dt , s . as_ref ( ) . len ( ) ) ) ;
291323 }
292324
293325 // Parse epoch seconds
294326 if let Ok ( timestamp) = parse_timestamp ( s. as_ref ( ) ) {
295327 if let Some ( timestamp_date) = DateTime :: from_timestamp ( timestamp, 0 ) {
296- return Ok ( timestamp_date. into ( ) ) ;
328+ return Some ( ( timestamp_date. into ( ) , s . as_ref ( ) . len ( ) ) ) ;
297329 }
298330 }
299331
@@ -303,7 +335,7 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
303335 if let Ok ( parsed) = NaiveDate :: parse_from_str ( & s. as_ref ( ) [ 0 ..n] , fmt) {
304336 let datetime = parsed. and_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ;
305337 if let Ok ( dt) = naive_dt_to_fixed_offset ( date, datetime) {
306- return Ok ( dt ) ;
338+ return Some ( ( dt , n ) ) ;
307339 }
308340 }
309341 }
@@ -318,25 +350,21 @@ pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
318350 if ts. len ( ) == n + 12 {
319351 let f = format:: YYYYMMDDHHMM . to_owned ( ) + fmt;
320352 if let Ok ( parsed) = DateTime :: parse_from_str ( & ts, & f) {
321- return Ok ( parsed) ;
353+ if tmp_s == s. as_ref ( ) {
354+ return Some ( ( parsed, n) ) ;
355+ } else {
356+ return Some ( ( parsed, n - 1 ) ) ;
357+ }
322358 }
323359 }
324360 }
325361
326- // Parse relative time.
327- if let Ok ( datetime) = parse_relative_time_at_date ( date, s. as_ref ( ) ) {
328- return Ok ( DateTime :: < FixedOffset > :: from ( datetime) ) ;
329- }
330-
331362 // parse time only dates
332363 if let Some ( date_time) = parse_time_only_str:: parse_time_only ( date, s. as_ref ( ) ) {
333- return Ok ( date_time) ;
364+ return Some ( ( date_time, s . as_ref ( ) . len ( ) ) ) ;
334365 }
335366
336- // Default parse and failure
337- s. as_ref ( )
338- . parse ( )
339- . map_err ( |_| ( ParseDateTimeError :: InvalidInput ) )
367+ None
340368}
341369
342370// Convert NaiveDateTime to DateTime<FixedOffset> by assuming the offset
@@ -662,14 +690,10 @@ mod tests {
662690 assert ! ( crate :: parse_datetime( "bogus +1 day" ) . is_err( ) ) ;
663691 }
664692
665- // TODO Re-enable this when we parse the absolute datetime and the
666- // time delta separately, see
667- // <https://github.com/uutils/parse_datetime/issues/104>.
668- //
669- // #[test]
670- // fn test_parse_invalid_delta() {
671- // assert!(crate::parse_datetime("1997-01-01 bogus").is_err());
672- // }
693+ #[ test]
694+ fn test_parse_invalid_delta ( ) {
695+ assert ! ( crate :: parse_datetime( "1997-01-01 bogus" ) . is_err( ) ) ;
696+ }
673697
674698 #[ test]
675699 fn test_parse_datetime_tz_nodelta ( ) {
@@ -741,6 +765,80 @@ mod tests {
741765 }
742766 }
743767
768+ #[ test]
769+ fn test_parse_datetime_tz_delta ( ) {
770+ std:: env:: set_var ( "TZ" , "UTC0" ) ;
771+
772+ // 1998-01-01
773+ let expected = chrono:: NaiveDate :: from_ymd_opt ( 1998 , 1 , 1 )
774+ . unwrap ( )
775+ . and_hms_opt ( 0 , 0 , 0 )
776+ . unwrap ( )
777+ . and_utc ( )
778+ . fixed_offset ( ) ;
779+
780+ for s in [
781+ "1997-01-01 00:00:00 +0000 +1 year" ,
782+ "1997-01-01 00:00:00 +00 +1 year" ,
783+ "199701010000 +0000 +1 year" ,
784+ "199701010000UTC+0000 +1 year" ,
785+ "199701010000Z+0000 +1 year" ,
786+ "1997-01-01T00:00:00Z +1 year" ,
787+ "1997-01-01 00:00 +0000 +1 year" ,
788+ "1997-01-01 00:00:00 +0000 +1 year" ,
789+ "1997-01-01T00:00:00+0000 +1 year" ,
790+ "1997-01-01T00:00:00+00 +1 year" ,
791+ ] {
792+ let actual = crate :: parse_datetime ( s) . unwrap ( ) ;
793+ assert_eq ! ( actual, expected) ;
794+ }
795+ }
796+
797+ #[ test]
798+ fn test_parse_datetime_notz_delta ( ) {
799+ std:: env:: set_var ( "TZ" , "UTC0" ) ;
800+ let expected = chrono:: NaiveDate :: from_ymd_opt ( 1998 , 1 , 1 )
801+ . unwrap ( )
802+ . and_hms_opt ( 0 , 0 , 0 )
803+ . unwrap ( )
804+ . and_utc ( )
805+ . fixed_offset ( ) ;
806+
807+ for s in [
808+ "1997-01-01 00:00:00.000000000 +1 year" ,
809+ "Wed Jan 1 00:00:00 1997 +1 year" ,
810+ "1997-01-01T00:00:00 +1 year" ,
811+ "1997-01-01 00:00:00 +1 year" ,
812+ "1997-01-01 00:00 +1 year" ,
813+ "199701010000.00 +1 year" ,
814+ "199701010000 +1 year" ,
815+ ] {
816+ let actual = crate :: parse_datetime ( s) . unwrap ( ) ;
817+ assert_eq ! ( actual, expected) ;
818+ }
819+ }
820+
821+ #[ test]
822+ fn test_parse_date_notz_delta ( ) {
823+ std:: env:: set_var ( "TZ" , "UTC0" ) ;
824+ let expected = chrono:: NaiveDate :: from_ymd_opt ( 1998 , 1 , 1 )
825+ . unwrap ( )
826+ . and_hms_opt ( 0 , 0 , 0 )
827+ . unwrap ( )
828+ . and_utc ( )
829+ . fixed_offset ( ) ;
830+
831+ for s in [
832+ "1997-01-01 +1 year" ,
833+ "19970101 +1 year" ,
834+ "01/01/1997 +1 year" ,
835+ "01/01/97 +1 year" ,
836+ ] {
837+ let actual = crate :: parse_datetime ( s) . unwrap ( ) ;
838+ assert_eq ! ( actual, expected) ;
839+ }
840+ }
841+
744842 #[ test]
745843 fn test_time_only ( ) {
746844 use chrono:: { FixedOffset , Local } ;
0 commit comments