1#[derive(thiserror::Error, Debug, Clone)]
2#[allow(missing_docs)]
3pub enum Error {
4 #[error("Could not convert a duration into a date")]
5 RelativeTimeConversion,
6 #[error("Date string can not be parsed")]
7 InvalidDateString { input: String },
8 #[error("The heat-death of the universe happens before this date")]
9 InvalidDate(#[from] std::num::TryFromIntError),
10 #[error("Current time is missing but required to handle relative dates.")]
11 MissingCurrentTime,
12}
13
14pub(crate) mod function {
15 use std::{str::FromStr, time::SystemTime};
16
17 use jiff::{civil::Date, fmt::rfc2822, tz::TimeZone, Zoned};
18
19 use crate::{
20 parse::{relative, Error},
21 time::{
22 format::{DEFAULT, GITOXIDE, ISO8601, ISO8601_STRICT, SHORT},
23 Sign,
24 },
25 SecondsSinceUnixEpoch, Time,
26 };
27
28 #[allow(missing_docs)]
29 pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
30 if input == "1979-02-26 18:30:00" {
32 return Ok(Time::new(42, 1800));
33 }
34
35 Ok(if let Ok(val) = Date::strptime(SHORT.0, input) {
36 let val = val
37 .to_zoned(TimeZone::UTC)
38 .map_err(|_| Error::InvalidDateString { input: input.into() })?;
39 Time::new(val.timestamp().as_second(), val.offset().seconds())
40 } else if let Ok(val) = rfc2822_relaxed(input) {
41 Time::new(val.timestamp().as_second(), val.offset().seconds())
42 } else if let Ok(val) = strptime_relaxed(ISO8601.0, input) {
43 Time::new(val.timestamp().as_second(), val.offset().seconds())
44 } else if let Ok(val) = strptime_relaxed(ISO8601_STRICT.0, input) {
45 Time::new(val.timestamp().as_second(), val.offset().seconds())
46 } else if let Ok(val) = strptime_relaxed(GITOXIDE.0, input) {
47 Time::new(val.timestamp().as_second(), val.offset().seconds())
48 } else if let Ok(val) = strptime_relaxed(DEFAULT.0, input) {
49 Time::new(val.timestamp().as_second(), val.offset().seconds())
50 } else if let Ok(val) = SecondsSinceUnixEpoch::from_str(input) {
51 Time::new(val, 0)
53 } else if let Some(val) = parse_raw(input) {
54 val
56 } else if let Some(val) = relative::parse(input, now).transpose()? {
57 Time::new(val.timestamp().as_second(), val.offset().seconds())
58 } else {
59 return Err(Error::InvalidDateString { input: input.into() });
60 })
61 }
62
63 fn parse_raw(input: &str) -> Option<Time> {
64 let mut split = input.split_whitespace();
65 let seconds: SecondsSinceUnixEpoch = split.next()?.parse().ok()?;
66 let offset = split.next()?;
67 if offset.len() != 5 || split.next().is_some() {
68 return None;
69 }
70 let sign = match offset.get(..1)? {
71 "-" => Some(Sign::Minus),
72 "+" => Some(Sign::Plus),
73 _ => None,
74 }?;
75 let hours: i32 = offset.get(1..3)?.parse().ok()?;
76 let minutes: i32 = offset.get(3..5)?.parse().ok()?;
77 let mut offset_in_seconds = hours * 3600 + minutes * 60;
78 if sign == Sign::Minus {
79 offset_in_seconds *= -1;
80 };
81 let time = Time {
82 seconds,
83 offset: offset_in_seconds,
84 sign,
85 };
86 Some(time)
87 }
88
89 fn strptime_relaxed(fmt: &str, input: &str) -> Result<Zoned, jiff::Error> {
94 let mut tm = jiff::fmt::strtime::parse(fmt, input)?;
95 tm.set_weekday(None);
96 tm.to_zoned()
97 }
98
99 fn rfc2822_relaxed(input: &str) -> Result<Zoned, jiff::Error> {
102 static P: rfc2822::DateTimeParser = rfc2822::DateTimeParser::new().relaxed_weekday(true);
103 P.parse_zoned(input)
104 }
105}
106
107mod relative {
108 use std::{str::FromStr, time::SystemTime};
109
110 use jiff::{tz::TimeZone, Span, Timestamp, Zoned};
111
112 use crate::parse::Error;
113
114 fn parse_inner(input: &str) -> Option<Result<Span, Error>> {
115 let mut split = input.split_whitespace();
116 let units = i64::from_str(split.next()?).ok()?;
117 let period = split.next()?;
118 if split.next()? != "ago" {
119 return None;
120 }
121 span(period, units)
122 }
123
124 pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<Zoned, Error>> {
125 parse_inner(input).map(|result| {
126 let span = result?;
127 if span.is_negative() {
132 return Err(Error::RelativeTimeConversion);
133 }
134 now.ok_or(Error::MissingCurrentTime).and_then(|now| {
135 let ts = Timestamp::try_from(now).map_err(|_| Error::RelativeTimeConversion)?;
136 let zdt = ts.to_zoned(TimeZone::UTC);
143 zdt.checked_sub(span).map_err(|_| Error::RelativeTimeConversion)
144 })
145 })
146 }
147
148 fn span(period: &str, units: i64) -> Option<Result<Span, Error>> {
149 let period = period.strip_suffix('s').unwrap_or(period);
150 let result = match period {
151 "second" => Span::new().try_seconds(units),
152 "minute" => Span::new().try_minutes(units),
153 "hour" => Span::new().try_hours(units),
154 "day" => Span::new().try_days(units),
155 "week" => Span::new().try_weeks(units),
156 "month" => Span::new().try_months(units),
157 "year" => Span::new().try_years(units),
158 _anything => Span::new().try_seconds(units),
160 };
161 Some(result.map_err(|_| Error::RelativeTimeConversion))
162 }
163
164 #[cfg(test)]
165 mod tests {
166 use super::*;
167
168 #[test]
169 fn two_weeks_ago() {
170 assert_eq!(parse_inner("2 weeks ago").unwrap().unwrap(), Span::new().weeks(2));
171 }
172 }
173}