#[derive(thiserror::Error, Debug, Clone)]
#[allow(missing_docs)]
pub enum Error {
#[error("Could not convert a duration into a date")]
RelativeTimeConversion,
#[error("Date string can not be parsed")]
InvalidDateString { input: String },
#[error("The heat-death of the universe happens before this date")]
InvalidDate(#[from] std::num::TryFromIntError),
#[error("Current time is missing but required to handle relative dates.")]
MissingCurrentTime,
}
pub(crate) mod function {
use std::{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,
},
SecondsSinceUnixEpoch, Time,
};
#[allow(missing_docs)]
pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
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(), val.offset().whole_seconds())
} else if let Ok(val) = OffsetDateTime::parse(input, &well_known::Rfc2822) {
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
} else if let Ok(val) = OffsetDateTime::parse(input, ISO8601) {
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
} else if let Ok(val) = OffsetDateTime::parse(input, ISO8601_STRICT) {
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
} else if let Ok(val) = OffsetDateTime::parse(input, GITOXIDE) {
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
} else if let Ok(val) = OffsetDateTime::parse(input, DEFAULT) {
Time::new(val.unix_timestamp(), val.offset().whole_seconds())
} else if let Ok(val) = SecondsSinceUnixEpoch::from_str(input) {
Time::new(val, 0)
} else if let Some(val) = parse_raw(input) {
val
} else if let Some(time) = relative::parse(input, now).transpose()? {
Time::new(time.unix_timestamp(), time.offset().whole_seconds())
} else {
return Err(Error::InvalidDateString { input: input.into() });
})
}
fn parse_raw(input: &str) -> Option<Time> {
let mut split = input.split_whitespace();
let seconds: SecondsSinceUnixEpoch = 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,
offset: offset_in_seconds,
sign,
};
Some(time)
}
}
mod relative {
use std::{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,
_ => 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)));
}
}
}