1use std::str::FromStr;
2
3use smallvec::SmallVec;
4
5use crate::Time;
6
7#[derive(thiserror::Error, Debug, Clone)]
8#[allow(missing_docs)]
9pub enum Error {
10 #[error("Could not convert a duration into a date")]
11 RelativeTimeConversion,
12 #[error("Date string can not be parsed")]
13 InvalidDateString { input: String },
14 #[error("The heat-death of the universe happens before this date")]
15 InvalidDate(#[from] std::num::TryFromIntError),
16 #[error("Current time is missing but required to handle relative dates.")]
17 MissingCurrentTime,
18}
19
20#[derive(Default, Clone)]
23pub struct TimeBuf {
24 buf: SmallVec<[u8; Time::MAX.size()]>,
25}
26
27impl TimeBuf {
28 pub fn as_str(&self) -> &str {
31 let time_bytes = self.buf.as_slice();
34 #[allow(unsafe_code)]
35 unsafe {
36 std::str::from_utf8_unchecked(time_bytes)
37 }
38 }
39
40 fn clear(&mut self) {
42 self.buf.clear();
43 }
44}
45
46impl std::io::Write for TimeBuf {
47 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
48 self.buf.write(buf)
49 }
50
51 fn flush(&mut self) -> std::io::Result<()> {
52 self.buf.flush()
53 }
54}
55
56impl Time {
57 pub fn to_str<'a>(&self, buf: &'a mut TimeBuf) -> &'a str {
60 buf.clear();
61 self.write_to(buf)
62 .expect("write to memory of just the right size cannot fail");
63 buf.as_str()
64 }
65}
66
67impl FromStr for Time {
68 type Err = Error;
69
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 crate::parse_header(s).ok_or_else(|| Error::InvalidDateString { input: s.into() })
72 }
73}
74
75pub(crate) mod function {
76 use std::{str::FromStr, time::SystemTime};
77
78 use jiff::{civil::Date, fmt::rfc2822, tz::TimeZone, Zoned};
79
80 use crate::{
81 parse::{relative, Error},
82 time::format::{DEFAULT, GITOXIDE, ISO8601, ISO8601_STRICT, SHORT},
83 OffsetInSeconds, SecondsSinceUnixEpoch, Time,
84 };
85
86 pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
148 if input == "1979-02-26 18:30:00" {
150 return Ok(Time::new(42, 1800));
151 }
152
153 Ok(if let Ok(val) = Date::strptime(SHORT.0, input) {
154 let val = val
155 .to_zoned(TimeZone::UTC)
156 .map_err(|_| Error::InvalidDateString { input: input.into() })?;
157 Time::new(val.timestamp().as_second(), val.offset().seconds())
158 } else if let Ok(val) = rfc2822_relaxed(input) {
159 Time::new(val.timestamp().as_second(), val.offset().seconds())
160 } else if let Ok(val) = strptime_relaxed(ISO8601.0, input) {
161 Time::new(val.timestamp().as_second(), val.offset().seconds())
162 } else if let Ok(val) = strptime_relaxed(ISO8601_STRICT.0, input) {
163 Time::new(val.timestamp().as_second(), val.offset().seconds())
164 } else if let Ok(val) = strptime_relaxed(GITOXIDE.0, input) {
165 Time::new(val.timestamp().as_second(), val.offset().seconds())
166 } else if let Ok(val) = strptime_relaxed(DEFAULT.0, input) {
167 Time::new(val.timestamp().as_second(), val.offset().seconds())
168 } else if let Ok(val) = SecondsSinceUnixEpoch::from_str(input) {
169 Time::new(val, 0)
171 } else if let Some(val) = relative::parse(input, now).transpose()? {
172 Time::new(val.timestamp().as_second(), val.offset().seconds())
173 } else if let Some(val) = parse_header(input) {
174 val
176 } else {
177 return Err(Error::InvalidDateString { input: input.into() });
178 })
179 }
180
181 pub fn parse_header(input: &str) -> Option<Time> {
187 pub enum Sign {
188 Plus,
189 Minus,
190 }
191 fn parse_offset(offset: &str) -> Option<OffsetInSeconds> {
192 if (offset.len() != 5) && (offset.len() != 7) {
193 return None;
194 }
195 let sign = match offset.get(..1)? {
196 "-" => Some(Sign::Minus),
197 "+" => Some(Sign::Plus),
198 _ => None,
199 }?;
200 if offset.as_bytes().get(1).is_some_and(|b| !b.is_ascii_digit()) {
201 return None;
202 }
203 let hours: i32 = offset.get(1..3)?.parse().ok()?;
204 let minutes: i32 = offset.get(3..5)?.parse().ok()?;
205 let offset_seconds: i32 = if offset.len() == 7 {
206 offset.get(5..7)?.parse().ok()?
207 } else {
208 0
209 };
210 let mut offset_in_seconds = hours * 3600 + minutes * 60 + offset_seconds;
211 if matches!(sign, Sign::Minus) {
212 offset_in_seconds *= -1;
213 }
214 Some(offset_in_seconds)
215 }
216
217 let mut split = input.split_whitespace();
218 let seconds = split.next()?;
219 let seconds = match seconds.parse::<SecondsSinceUnixEpoch>() {
220 Ok(s) => s,
221 Err(_err) => {
222 let first_digits: String = seconds.chars().take_while(char::is_ascii_digit).collect();
224 first_digits.parse().ok()?
225 }
226 };
227 let offset = match split.next() {
228 None => 0,
229 Some(offset) => {
230 if split.next().is_some() {
231 0
232 } else {
233 parse_offset(offset).unwrap_or_default()
234 }
235 }
236 };
237 let time = Time { seconds, offset };
238 Some(time)
239 }
240
241 fn strptime_relaxed(fmt: &str, input: &str) -> Result<Zoned, jiff::Error> {
246 let mut tm = jiff::fmt::strtime::parse(fmt, input)?;
247 tm.set_weekday(None);
248 tm.to_zoned()
249 }
250
251 fn rfc2822_relaxed(input: &str) -> Result<Zoned, jiff::Error> {
254 static P: rfc2822::DateTimeParser = rfc2822::DateTimeParser::new().relaxed_weekday(true);
255 P.parse_zoned(input)
256 }
257}
258
259mod relative {
260 use std::{str::FromStr, time::SystemTime};
261
262 use jiff::{tz::TimeZone, Span, Timestamp, Zoned};
263
264 use crate::parse::Error;
265
266 fn parse_inner(input: &str) -> Option<Result<Span, Error>> {
267 let mut split = input.split_whitespace();
268 let units = i64::from_str(split.next()?).ok()?;
269 let period = split.next()?;
270 if split.next()? != "ago" {
271 return None;
272 }
273 span(period, units)
274 }
275
276 pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<Zoned, Error>> {
277 parse_inner(input).map(|result| {
278 let span = result?;
279 if span.is_negative() {
284 return Err(Error::RelativeTimeConversion);
285 }
286 now.ok_or(Error::MissingCurrentTime).and_then(|now| {
287 let ts = Timestamp::try_from(now).map_err(|_| Error::RelativeTimeConversion)?;
288 let zdt = ts.to_zoned(TimeZone::UTC);
295 zdt.checked_sub(span).map_err(|_| Error::RelativeTimeConversion)
296 })
297 })
298 }
299
300 fn span(period: &str, units: i64) -> Option<Result<Span, Error>> {
301 let period = period.strip_suffix('s').unwrap_or(period);
302 let result = match period {
303 "second" => Span::new().try_seconds(units),
304 "minute" => Span::new().try_minutes(units),
305 "hour" => Span::new().try_hours(units),
306 "day" => Span::new().try_days(units),
307 "week" => Span::new().try_weeks(units),
308 "month" => Span::new().try_months(units),
309 "year" => Span::new().try_years(units),
310 _anything => Span::new().try_seconds(units),
312 };
313 Some(result.map_err(|_| Error::RelativeTimeConversion))
314 }
315
316 #[cfg(test)]
317 mod tests {
318 use super::*;
319
320 #[test]
321 fn two_weeks_ago() {
322 let actual = parse_inner("2 weeks ago").unwrap().unwrap();
323 assert_eq!(actual.fieldwise(), Span::new().weeks(2));
324 }
325 }
326}