@@ -4,38 +4,65 @@ use crate::Error;
44use gix_error:: { ensure, Exn , ResultExt , ValidationError } ;
55use jiff:: { tz:: TimeZone , Span , Timestamp , Zoned } ;
66
7- fn parse_inner ( input : & str ) -> Option < Result < Span , Exn < Error > > > {
8- let mut split = input. split_whitespace ( ) ;
9- let units = i64:: from_str ( split. next ( ) ?) . ok ( ) ?;
10- let period = split. next ( ) ?;
11- if split. next ( ) ? != "ago" {
12- return None ;
7+ pub fn parse ( input : & str , now : Option < SystemTime > ) -> Option < Result < Zoned , Exn < Error > > > {
8+ // First try named dates
9+ if let Some ( result) = parse_named ( input, now) {
10+ return Some ( result) ;
1311 }
14- span ( period, units)
15- }
1612
17- pub fn parse ( input : & str , now : Option < SystemTime > ) -> Option < Result < Zoned , Exn < Error > > > {
18- parse_inner ( input) . map ( |result| -> Result < Zoned , Exn < Error > > {
13+ // Then try numeric relative dates
14+ parse_ago ( input) . map ( |result| -> Result < Zoned , Exn < Error > > {
1915 let span = result?;
2016 // This was an error case in a previous version of this code, where
2117 // it would fail when converting from a negative signed integer
2218 // to an unsigned integer. This preserves that failure case even
2319 // though the code below handles it okay.
2420 ensure ! ( !span. is_negative( ) , ValidationError :: new( "" ) ) ;
25- let now = now. ok_or ( ValidationError :: new ( "Missing current time" ) ) ?;
26- let ts: Timestamp = Timestamp :: try_from ( now) . or_raise ( || Error :: new ( "Could not convert current time" ) ) ?;
27- // N.B. This matches the behavior of this code when it was
28- // written with `time`, but we might consider using the system
29- // time zone here. If we did, then it would implement "1 day
30- // ago" correctly, even when it crosses DST transitions. Since
31- // we're in the UTC time zone here, which has no DST, 1 day is
32- // in practice always 24 hours. ---AG
33- let zdt = ts. to_zoned ( TimeZone :: UTC ) ;
34- zdt. checked_sub ( span)
35- . or_raise ( || Error :: new ( format ! ( "Failed to subtract {zdt} from {span}" ) ) )
21+ subtract_span ( now, span)
3622 } )
3723}
3824
25+ /// Parse named relative dates like "now", "today", "yesterday".
26+ fn parse_named ( input : & str , now : Option < SystemTime > ) -> Option < Result < Zoned , Exn < Error > > > {
27+ let input = input. trim ( ) ;
28+ let span = if input. eq_ignore_ascii_case ( "now" ) {
29+ Span :: new ( )
30+ } else if input. eq_ignore_ascii_case ( "today" ) {
31+ // "today" is treated the same as "now" (current time) for simplicity
32+ Span :: new ( )
33+ } else if input. eq_ignore_ascii_case ( "yesterday" ) {
34+ Span :: new ( ) . try_days ( 1 ) . ok ( ) ?
35+ } else {
36+ return None ;
37+ } ;
38+
39+ Some ( subtract_span ( now, span) )
40+ }
41+
42+ fn parse_ago ( input : & str ) -> Option < Result < Span , Exn < Error > > > {
43+ let mut split = input. split_whitespace ( ) ;
44+ let units = i64:: from_str ( split. next ( ) ?) . ok ( ) ?;
45+ let period = split. next ( ) ?;
46+ if split. next ( ) ? != "ago" {
47+ return None ;
48+ }
49+ span ( period, units)
50+ }
51+
52+ fn subtract_span ( now : Option < SystemTime > , span : Span ) -> Result < Zoned , Exn < ValidationError > > {
53+ let now = now. ok_or ( ValidationError :: new ( "Missing current time" ) ) ?;
54+ let ts: Timestamp = Timestamp :: try_from ( now) . or_raise ( || Error :: new ( "Could not convert current time" ) ) ?;
55+ // N.B. This matches the behavior of this code when it was
56+ // written with `time`, but we might consider using the system
57+ // time zone here. If we did, then it would implement "1 day
58+ // ago" correctly, even when it crosses DST transitions. Since
59+ // we're in the UTC time zone here, which has no DST, 1 day is
60+ // in practice always 24 hours. ---AG
61+ let zdt = ts. to_zoned ( TimeZone :: UTC ) ;
62+ zdt. checked_sub ( span)
63+ . or_raise ( || Error :: new ( format ! ( "Failed to subtract {zdt} from {span}" ) ) )
64+ }
65+
3966fn span ( period : & str , units : i64 ) -> Option < Result < Span , Exn < Error > > > {
4067 let period = period. strip_suffix ( 's' ) . unwrap_or ( period) ;
4168 let result = match period {
@@ -51,14 +78,3 @@ fn span(period: &str, units: i64) -> Option<Result<Span, Exn<Error>>> {
5178 } ;
5279 Some ( result. or_raise ( || Error :: new ( format ! ( "Couldn't parse span from '{period} {units}'" ) ) ) )
5380}
54-
55- #[ cfg( test) ]
56- mod tests {
57- use super :: * ;
58-
59- #[ test]
60- fn two_weeks_ago ( ) {
61- let actual = parse_inner ( "2 weeks ago" ) . unwrap ( ) . unwrap ( ) ;
62- assert_eq ! ( actual. fieldwise( ) , Span :: new( ) . weeks( 2 ) ) ;
63- }
64- }
0 commit comments