@@ -14,7 +14,7 @@ pub enum Error {
1414pub ( crate ) mod function {
1515 use std:: { str:: FromStr , time:: SystemTime } ;
1616
17- use time :: { format_description :: well_known , Date , OffsetDateTime } ;
17+ use jiff :: { civil :: Date , fmt :: rfc2822 , tz :: TimeZone , Zoned } ;
1818
1919 use crate :: {
2020 parse:: { relative, Error } ,
@@ -32,27 +32,27 @@ pub(crate) mod function {
3232 return Ok ( Time :: new ( 42 , 1800 ) ) ;
3333 }
3434
35- Ok ( if let Ok ( val) = Date :: parse ( input , SHORT . 0 ) {
36- let val = val. with_hms ( 0 , 0 , 0 ) . expect ( "date is in range" ) . assume_utc ( ) ;
37- Time :: new ( val. unix_timestamp ( ) , val. offset ( ) . whole_seconds ( ) )
38- } else if let Ok ( val) = OffsetDateTime :: parse ( input, & well_known :: Rfc2822 ) {
39- Time :: new ( val. unix_timestamp ( ) , val. offset ( ) . whole_seconds ( ) )
40- } else if let Ok ( val) = OffsetDateTime :: parse ( input , ISO8601 . 0 ) {
41- Time :: new ( val. unix_timestamp ( ) , val. offset ( ) . whole_seconds ( ) )
42- } else if let Ok ( val) = OffsetDateTime :: parse ( input , ISO8601_STRICT . 0 ) {
43- Time :: new ( val. unix_timestamp ( ) , val. offset ( ) . whole_seconds ( ) )
44- } else if let Ok ( val) = OffsetDateTime :: parse ( input , GITOXIDE . 0 ) {
45- Time :: new ( val. unix_timestamp ( ) , val. offset ( ) . whole_seconds ( ) )
46- } else if let Ok ( val) = OffsetDateTime :: parse ( input , DEFAULT . 0 ) {
47- Time :: new ( val. unix_timestamp ( ) , val. offset ( ) . whole_seconds ( ) )
35+ Ok ( if let Ok ( val) = Date :: strptime ( SHORT . 0 , input ) {
36+ let val = val. to_zoned ( TimeZone :: UTC ) . expect ( "date is in range" ) ;
37+ Time :: new ( val. timestamp ( ) . as_second ( ) , val. offset ( ) . seconds ( ) )
38+ } else if let Ok ( val) = rfc2822_relaxed ( input) {
39+ Time :: new ( val. timestamp ( ) . as_second ( ) , val. offset ( ) . seconds ( ) )
40+ } else if let Ok ( val) = strptime_relaxed ( ISO8601 . 0 , input ) {
41+ Time :: new ( val. timestamp ( ) . as_second ( ) , val. offset ( ) . seconds ( ) )
42+ } else if let Ok ( val) = strptime_relaxed ( ISO8601_STRICT . 0 , input ) {
43+ Time :: new ( val. timestamp ( ) . as_second ( ) , val. offset ( ) . seconds ( ) )
44+ } else if let Ok ( val) = strptime_relaxed ( GITOXIDE . 0 , input ) {
45+ Time :: new ( val. timestamp ( ) . as_second ( ) , val. offset ( ) . seconds ( ) )
46+ } else if let Ok ( val) = strptime_relaxed ( DEFAULT . 0 , input ) {
47+ Time :: new ( val. timestamp ( ) . as_second ( ) , val. offset ( ) . seconds ( ) )
4848 } else if let Ok ( val) = SecondsSinceUnixEpoch :: from_str ( input) {
4949 // Format::Unix
5050 Time :: new ( val, 0 )
5151 } else if let Some ( val) = parse_raw ( input) {
5252 // Format::Raw
5353 val
54- } else if let Some ( time ) = relative:: parse ( input, now) . transpose ( ) ? {
55- Time :: new ( time . unix_timestamp ( ) , time . offset ( ) . whole_seconds ( ) )
54+ } else if let Some ( val ) = relative:: parse ( input, now) . transpose ( ) ? {
55+ Time :: new ( val . timestamp ( ) . as_second ( ) , val . offset ( ) . seconds ( ) )
5656 } else {
5757 return Err ( Error :: InvalidDateString { input : input. into ( ) } ) ;
5858 } )
@@ -83,52 +83,79 @@ pub(crate) mod function {
8383 } ;
8484 Some ( time)
8585 }
86+
87+ /// This is just like `Zoned::strptime`, but it allows parsing datetimes
88+ /// whose weekdays are inconsistent with the date. While the day-of-week
89+ /// still must be parsed, it is otherwise ignored. This seems to be
90+ /// consistent with how `git` behaves.
91+ fn strptime_relaxed ( fmt : & str , input : & str ) -> Result < Zoned , jiff:: Error > {
92+ let mut tm = jiff:: fmt:: strtime:: parse ( fmt, input) ?;
93+ tm. set_weekday ( None ) ;
94+ tm. to_zoned ( )
95+ }
96+
97+ /// This is just like strptime_relaxed, except for RFC 2822 parsing.
98+ /// Namely, it permits the weekday to be inconsistent with the date.
99+ fn rfc2822_relaxed ( input : & str ) -> Result < Zoned , jiff:: Error > {
100+ static P : rfc2822:: DateTimeParser = rfc2822:: DateTimeParser :: new ( ) . relaxed_weekday ( true ) ;
101+ P . parse_zoned ( input)
102+ }
86103}
87104
88105mod relative {
89106 use std:: { str:: FromStr , time:: SystemTime } ;
90107
91- use time :: { Duration , OffsetDateTime } ;
108+ use jiff :: { tz :: TimeZone , Span , Timestamp , Zoned } ;
92109
93110 use crate :: parse:: Error ;
94111
95- fn parse_inner ( input : & str ) -> Option < Duration > {
112+ fn parse_inner ( input : & str ) -> Option < Result < Span , Error > > {
96113 let mut split = input. split_whitespace ( ) ;
97- let multiplier = i64:: from_str ( split. next ( ) ?) . ok ( ) ?;
114+ let units = i64:: from_str ( split. next ( ) ?) . ok ( ) ?;
98115 let period = split. next ( ) ?;
99116 if split. next ( ) ? != "ago" {
100117 return None ;
101118 }
102- duration ( period, multiplier )
119+ span ( period, units )
103120 }
104121
105- pub ( crate ) fn parse ( input : & str , now : Option < SystemTime > ) -> Option < Result < OffsetDateTime , Error > > {
106- parse_inner ( input) . map ( |offset| {
107- let offset = std:: time:: Duration :: from_secs ( offset. whole_seconds ( ) . try_into ( ) ?) ;
122+ pub ( crate ) fn parse ( input : & str , now : Option < SystemTime > ) -> Option < Result < Zoned , Error > > {
123+ parse_inner ( input) . map ( |result| {
124+ let span = result?;
125+ // This was an error case in a previous version of this code, where
126+ // it would fail when converting from a negative signed integer
127+ // to an unsigned integer. This preserves that failure case even
128+ // though the code below handles it okay.
129+ if span. is_negative ( ) {
130+ return Err ( Error :: RelativeTimeConversion ) ;
131+ }
108132 now. ok_or ( Error :: MissingCurrentTime ) . and_then ( |now| {
109- std:: panic:: catch_unwind ( || {
110- now. checked_sub ( offset)
111- . expect ( "BUG: values can't be large enough to cause underflow" )
112- . into ( )
113- } )
114- . map_err ( |_| Error :: RelativeTimeConversion )
133+ let ts = Timestamp :: try_from ( now) . map_err ( |_| Error :: RelativeTimeConversion ) ?;
134+ // N.B. This matches the behavior of this code when it was
135+ // written with `time`, but we might consider using the system
136+ // time zone here. If we did, then it would implement "1 day
137+ // ago" correctly, even when it crosses DST transitions. Since
138+ // we're in the UTC time zone here, which has no DST, 1 day is
139+ // in practice always 24 hours. ---AG
140+ let zdt = ts. to_zoned ( TimeZone :: UTC ) ;
141+ zdt. checked_sub ( span) . map_err ( |_| Error :: RelativeTimeConversion )
115142 } )
116143 } )
117144 }
118145
119- fn duration ( period : & str , multiplier : i64 ) -> Option < Duration > {
146+ fn span ( period : & str , units : i64 ) -> Option < Result < Span , Error > > {
120147 let period = period. strip_suffix ( 's' ) . unwrap_or ( period) ;
121- let seconds : i64 = match period {
122- "second" => 1 ,
123- "minute" => 60 ,
124- "hour" => 60 * 60 ,
125- "day" => 24 * 60 * 60 ,
126- "week" => 7 * 24 * 60 * 60 ,
148+ let result = match period {
149+ "second" => Span :: new ( ) . try_seconds ( units ) ,
150+ "minute" => Span :: new ( ) . try_minutes ( units ) ,
151+ "hour" => Span :: new ( ) . try_hours ( units ) ,
152+ "day" => Span :: new ( ) . try_days ( units ) ,
153+ "week" => Span :: new ( ) . try_weeks ( units ) ,
127154 // TODO months & years? YES
128155 // Ignore values you don't know, assume seconds then (so does git)
129156 _ => return None ,
130157 } ;
131- seconds . checked_mul ( multiplier ) . map ( Duration :: seconds )
158+ Some ( result . map_err ( |_| Error :: RelativeTimeConversion ) )
132159 }
133160
134161 #[ cfg( test) ]
@@ -137,7 +164,7 @@ mod relative {
137164
138165 #[ test]
139166 fn two_weeks_ago ( ) {
140- assert_eq ! ( parse_inner( "2 weeks ago" ) , Some ( Duration :: weeks( 2 ) ) ) ;
167+ assert_eq ! ( parse_inner( "2 weeks ago" ) . unwrap ( ) . unwrap ( ) , Span :: new ( ) . weeks( 2 ) ) ;
141168 }
142169 }
143170}
0 commit comments