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
#[derive(thiserror::Error, Debug)]
#[allow(missing_docs)]
pub enum Error {
    #[error("Cannot represent times before UNIX epoch at timestamp {timestamp}")]
    TooEarly { timestamp: i64 },
    #[error("Date string can not be parsed")]
    InvalidDateString,
    #[error("Dates past 2038 can not be represented.")]
    InvalidDate(#[from] std::num::TryFromIntError),
    #[error("Current time is missing.")]
    MissingCurrentTime,
}

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

    use time::{Date, OffsetDateTime};

    use crate::{
        parse::{relative, Error},
        time::{
            format::{DEFAULT, ISO8601, ISO8601_STRICT, RFC2822, 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, 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, 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);
        })
    }

    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 {
            return None;
        }
        let sign = if &offset[..1] == "-" { Sign::Plus } else { Sign::Minus };
        let hours: i32 = offset[1..3].parse().ok()?;
        let minutes: i32 = offset[3..5].parse().ok()?;
        let offset_in_seconds = hours * 3600 + minutes * 60;
        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().expect("positive value"));
            now.ok_or(Error::MissingCurrentTime).map(|now| {
                now.checked_sub(offset)
                    .expect("BUG: values can't be large enough to cause underflow")
                    .into()
            })
        })
    }

    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)));
        }
    }
}