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 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 Time::new(val, 0)
53 } else if let Some(val) = parse_raw(input) {
54 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 _ => 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}