git_date/
parse.rs

1#[derive(thiserror::Error, Debug, Clone)]
2#[allow(missing_docs)]
3pub enum Error {
4    #[error("Cannot represent times before UNIX epoch at timestamp {timestamp}")]
5    TooEarly { timestamp: i64 },
6    #[error("Could not convert a duration into a date")]
7    RelativeTimeConversion,
8    #[error("Date string can not be parsed")]
9    InvalidDateString { input: String },
10    #[error("Dates past 2038 can not be represented.")]
11    InvalidDate(#[from] std::num::TryFromIntError),
12    #[error("Current time is missing but required to handle relative dates.")]
13    MissingCurrentTime,
14}
15
16pub(crate) mod function {
17    use std::{convert::TryInto, str::FromStr, time::SystemTime};
18
19    use time::{format_description::well_known, Date, OffsetDateTime};
20
21    use crate::{
22        parse::{relative, Error},
23        time::{
24            format::{DEFAULT, GITOXIDE, ISO8601, ISO8601_STRICT, SHORT},
25            Sign,
26        },
27        Time,
28    };
29
30    #[allow(missing_docs)]
31    pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
32        // TODO: actual implementation, this is just to not constantly fail
33        if input == "1979-02-26 18:30:00" {
34            return Ok(Time::new(42, 1800));
35        }
36
37        Ok(if let Ok(val) = Date::parse(input, SHORT) {
38            let val = val.with_hms(0, 0, 0).expect("date is in range").assume_utc();
39            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
40        } else if let Ok(val) = OffsetDateTime::parse(input, &well_known::Rfc2822) {
41            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
42        } else if let Ok(val) = OffsetDateTime::parse(input, ISO8601) {
43            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
44        } else if let Ok(val) = OffsetDateTime::parse(input, ISO8601_STRICT) {
45            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
46        } else if let Ok(val) = OffsetDateTime::parse(input, GITOXIDE) {
47            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
48        } else if let Ok(val) = OffsetDateTime::parse(input, DEFAULT) {
49            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
50        } else if let Ok(val) = u32::from_str(input) {
51            // Format::Unix
52            Time::new(val, 0)
53        } else if let Some(val) = parse_raw(input) {
54            // Format::Raw
55            val
56        } else if let Some(time) = relative::parse(input, now).transpose()? {
57            Time::new(timestamp(time)?, time.offset().whole_seconds())
58        } else {
59            return Err(Error::InvalidDateString { input: input.into() });
60        })
61    }
62
63    fn timestamp(date: OffsetDateTime) -> Result<u32, Error> {
64        let timestamp = date.unix_timestamp();
65        if timestamp < 0 {
66            Err(Error::TooEarly { timestamp })
67        } else {
68            Ok(timestamp.try_into()?)
69        }
70    }
71
72    fn parse_raw(input: &str) -> Option<Time> {
73        let mut split = input.split_whitespace();
74        let seconds_since_unix_epoch: u32 = split.next()?.parse().ok()?;
75        let offset = split.next()?;
76        if offset.len() != 5 || split.next().is_some() {
77            return None;
78        }
79        let sign = match offset.get(..1)? {
80            "-" => Some(Sign::Minus),
81            "+" => Some(Sign::Plus),
82            _ => None,
83        }?;
84        let hours: i32 = offset.get(1..3)?.parse().ok()?;
85        let minutes: i32 = offset.get(3..5)?.parse().ok()?;
86        let mut offset_in_seconds = hours * 3600 + minutes * 60;
87        if sign == Sign::Minus {
88            offset_in_seconds *= -1;
89        };
90        let time = Time {
91            seconds_since_unix_epoch,
92            offset_in_seconds,
93            sign,
94        };
95        Some(time)
96    }
97}
98
99mod relative {
100    use std::{convert::TryInto, str::FromStr, time::SystemTime};
101
102    use time::{Duration, OffsetDateTime};
103
104    use crate::parse::Error;
105
106    fn parse_inner(input: &str) -> Option<Duration> {
107        let mut split = input.split_whitespace();
108        let multiplier = i64::from_str(split.next()?).ok()?;
109        let period = split.next()?;
110        if split.next()? != "ago" {
111            return None;
112        }
113        duration(period, multiplier)
114    }
115
116    pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<OffsetDateTime, Error>> {
117        parse_inner(input).map(|offset| {
118            let offset = std::time::Duration::from_secs(offset.whole_seconds().try_into()?);
119            now.ok_or(Error::MissingCurrentTime).and_then(|now| {
120                std::panic::catch_unwind(|| {
121                    now.checked_sub(offset)
122                        .expect("BUG: values can't be large enough to cause underflow")
123                        .into()
124                })
125                .map_err(|_| Error::RelativeTimeConversion)
126            })
127        })
128    }
129
130    fn duration(period: &str, multiplier: i64) -> Option<Duration> {
131        let period = period.strip_suffix('s').unwrap_or(period);
132        let seconds: i64 = match period {
133            "second" => 1,
134            "minute" => 60,
135            "hour" => 60 * 60,
136            "day" => 24 * 60 * 60,
137            "week" => 7 * 24 * 60 * 60,
138            // TODO months & years? YES
139            // Ignore values you don't know, assume seconds then (so does git)
140            _ => return None,
141        };
142        seconds.checked_mul(multiplier).map(Duration::seconds)
143    }
144
145    #[cfg(test)]
146    mod tests {
147        use super::*;
148
149        #[test]
150        fn two_weeks_ago() {
151            assert_eq!(parse_inner("2 weeks ago"), Some(Duration::weeks(2)));
152        }
153    }
154}