1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#[derive(thiserror::Error, Debug, Clone)]
#[allow(missing_docs)]
pub enum Error {
    #[error("Cannot represent times before UNIX epoch at timestamp {timestamp}")]
    TooEarly { timestamp: i64 },
    #[error("Could not convert a duration into a date")]
    RelativeTimeConversion,
    #[error("Date string can not be parsed")]
    InvalidDateString { input: String },
    #[error("Dates past 2038 can not be represented.")]
    InvalidDate(#[from] std::num::TryFromIntError),
    #[error("Current time is missing but required to handle relative dates.")]
    MissingCurrentTime,
}

pub(crate) mod function {
    use std::{convert::TryInto, str::FromStr, time::SystemTime};

    use time::{format_description::well_known, Date, OffsetDateTime};

    use crate::{
        parse::{relative, Error},
        time::{
            format::{DEFAULT, GITOXIDE, ISO8601, ISO8601_STRICT, SHORT},
            Sign,
        },
        Time,
    };

    #[allow(missing_docs)]
    pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
        // TODO: actual implementation, this is just to not constantly fail
        if input == "1979-02-26 18:30:00" {
            return Ok(Time::new(42, 1800));
        }

        Ok(if let Ok(val) = Date::parse(input, SHORT) {
            let val = val.with_hms(0, 0, 0).expect("date is in range").assume_utc();
            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
        } else if let Ok(val) = OffsetDateTime::parse(input, &well_known::Rfc2822) {
            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
        } else if let Ok(val) = OffsetDateTime::parse(input, ISO8601) {
            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
        } else if let Ok(val) = OffsetDateTime::parse(input, ISO8601_STRICT) {
            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
        } else if let Ok(val) = OffsetDateTime::parse(input, GITOXIDE) {
            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
        } else if let Ok(val) = OffsetDateTime::parse(input, DEFAULT) {
            Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds())
        } else if let Ok(val) = u32::from_str(input) {
            // Format::Unix
            Time::new(val, 0)
        } else if let Some(val) = parse_raw(input) {
            // Format::Raw
            val
        } else if let Some(time) = relative::parse(input, now).transpose()? {
            Time::new(timestamp(time)?, time.offset().whole_seconds())
        } else {
            return Err(Error::InvalidDateString { input: input.into() });
        })
    }

    fn timestamp(date: OffsetDateTime) -> Result<u32, Error> {
        let timestamp = date.unix_timestamp();
        if timestamp < 0 {
            Err(Error::TooEarly { timestamp })
        } else {
            Ok(timestamp.try_into()?)
        }
    }

    fn parse_raw(input: &str) -> Option<Time> {
        let mut split = input.split_whitespace();
        let seconds_since_unix_epoch: u32 = split.next()?.parse().ok()?;
        let offset = split.next()?;
        if offset.len() != 5 || split.next().is_some() {
            return None;
        }
        let sign = match offset.get(..1)? {
            "-" => Some(Sign::Minus),
            "+" => Some(Sign::Plus),
            _ => None,
        }?;
        let hours: i32 = offset.get(1..3)?.parse().ok()?;
        let minutes: i32 = offset.get(3..5)?.parse().ok()?;
        let mut offset_in_seconds = hours * 3600 + minutes * 60;
        if sign == Sign::Minus {
            offset_in_seconds *= -1;
        };
        let time = Time {
            seconds_since_unix_epoch,
            offset_in_seconds,
            sign,
        };
        Some(time)
    }
}

mod relative {
    use std::{convert::TryInto, str::FromStr, time::SystemTime};

    use time::{Duration, OffsetDateTime};

    use crate::parse::Error;

    fn parse_inner(input: &str) -> Option<Duration> {
        let mut split = input.split_whitespace();
        let multiplier = i64::from_str(split.next()?).ok()?;
        let period = split.next()?;
        if split.next()? != "ago" {
            return None;
        }
        duration(period, multiplier)
    }

    pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<OffsetDateTime, Error>> {
        parse_inner(input).map(|offset| {
            let offset = std::time::Duration::from_secs(offset.whole_seconds().try_into()?);
            now.ok_or(Error::MissingCurrentTime).and_then(|now| {
                std::panic::catch_unwind(|| {
                    now.checked_sub(offset)
                        .expect("BUG: values can't be large enough to cause underflow")
                        .into()
                })
                .map_err(|_| Error::RelativeTimeConversion)
            })
        })
    }

    fn duration(period: &str, multiplier: i64) -> Option<Duration> {
        let period = period.strip_suffix('s').unwrap_or(period);
        let seconds: i64 = match period {
            "second" => 1,
            "minute" => 60,
            "hour" => 60 * 60,
            "day" => 24 * 60 * 60,
            "week" => 7 * 24 * 60 * 60,
            // TODO months & years? YES
            // Ignore values you don't know, assume seconds then (so does git)
            _ => return None,
        };
        seconds.checked_mul(multiplier).map(Duration::seconds)
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn two_weeks_ago() {
            assert_eq!(parse_inner("2 weeks ago"), Some(Duration::weeks(2)));
        }
    }
}