#[cfg(test)]
mod tests;
mod error;
mod local_datetime;
pub use self::{error::Error, local_datetime::LocalDateTime};
use std::borrow::Borrow;
use std::collections::HashMap;
use std::str;
use std::sync::LazyLock;
use chrono::offset::Utc;
use chrono::{Days, Duration};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
const USEC_PER_USEC: i64 = 1;
const USEC_PER_MSEC: i64 = 1_000 * USEC_PER_USEC;
const USEC_PER_SEC: i64 = 1_000 * USEC_PER_MSEC;
const USEC_PER_MINUTE: i64 = 60 * USEC_PER_SEC;
const USEC_PER_HOUR: i64 = 60 * USEC_PER_MINUTE;
const USEC_PER_DAY: i64 = 24 * USEC_PER_HOUR;
const USEC_PER_WEEK: i64 = 7 * USEC_PER_DAY;
const USEC_PER_MONTH: i64 = 2_629_800 * USEC_PER_SEC;
const USEC_PER_YEAR: i64 = 31_557_600 * USEC_PER_SEC;
#[rustfmt::skip]
static USEC_MULTIPLIER: LazyLock<HashMap<&'static str, i64>> = LazyLock::new(|| {
HashMap::from_iter([
("us", USEC_PER_USEC),
("usec", USEC_PER_USEC),
("µs", USEC_PER_USEC),
("ms", USEC_PER_MSEC),
("msec", USEC_PER_MSEC),
("s", USEC_PER_SEC),
("sec", USEC_PER_SEC),
("second", USEC_PER_SEC),
("seconds", USEC_PER_SEC),
("m", USEC_PER_MINUTE),
("min", USEC_PER_MINUTE),
("minute", USEC_PER_MINUTE),
("minutes", USEC_PER_MINUTE),
("h", USEC_PER_HOUR),
("hour", USEC_PER_HOUR),
("hours", USEC_PER_HOUR),
("hr", USEC_PER_HOUR),
("d", USEC_PER_DAY),
("day", USEC_PER_DAY),
("days", USEC_PER_DAY),
("M", USEC_PER_MONTH),
("month", USEC_PER_MONTH),
("months", USEC_PER_MONTH),
("w", USEC_PER_WEEK),
("week", USEC_PER_WEEK),
("weeks", USEC_PER_WEEK),
("y", USEC_PER_YEAR),
("year", USEC_PER_YEAR),
("years", USEC_PER_YEAR),
])
});
pub fn parse_timestamp_tz<S, T, Tz>(timestamp: S, timezone: T) -> Result<LocalDateTime<Tz>, Error>
where
S: AsRef<str>,
T: Borrow<Tz>,
Tz: TimeZone,
{
let tz = timezone.borrow();
let ts = timestamp.as_ref();
let ts_nw = ts
.chars()
.filter(|&c| !c.is_whitespace())
.collect::<String>();
if ts_nw.is_empty() {
return Err(Error::Format("Timestamp cannot be empty".to_owned()));
}
if ts.starts_with('+') {
let now = Utc::now().with_timezone(tz);
let offset = parse_offset(&ts_nw[1..])?;
return Ok(LocalDateTime::Single(now + offset));
}
if ts.ends_with(" left") {
let now = Utc::now().with_timezone(tz);
let offset = parse_offset(&ts_nw[..(ts_nw.len() - 4)])?;
return Ok(LocalDateTime::Single(now + offset));
}
if ts.starts_with('-') {
let now = Utc::now().with_timezone(tz);
let offset = parse_offset(&ts_nw[1..])?;
return Ok(LocalDateTime::Single(now - offset));
}
if ts.ends_with(" ago") {
let now = Utc::now().with_timezone(tz);
let offset = parse_offset(&ts_nw[..(ts_nw.len() - 3)])?;
return Ok(LocalDateTime::Single(now - offset));
}
if ts.starts_with('@') {
let epoch = tz.timestamp_opt(0, 0).unwrap();
let offset = parse_offset(&ts_nw[1..])?;
return Ok(LocalDateTime::Single(epoch + offset));
}
match (ts.find(" +"), ts.find(" -")) {
(Some(_), Some(_)) => Err(Error::Format(
"Timestamp cannot contain both a `+` and `-`".to_owned(),
)),
(Some(p), None) => {
let p_nw = ts_nw.find('+').unwrap();
let time = parse_time(&ts[..p], tz)?;
let offset = parse_offset(&ts_nw[(p_nw + 1)..])?;
Ok(time + offset)
}
(None, Some(m)) => {
let m_nw = ts_nw.rfind('-').unwrap();
let time = parse_time(&ts[..m], tz)?;
let offset = parse_offset(&ts_nw[(m_nw + 1)..])?;
Ok(time - offset)
}
(None, None) => {
let time = parse_time(ts, tz)?;
Ok(time)
}
}
}
fn parse_time<Tz: TimeZone>(ts: &str, tz: &Tz) -> Result<LocalDateTime<Tz>, Error> {
let dt = match ts {
"now" => LocalDateTime::Single(Utc::now().with_timezone(tz)),
"epoch" => LocalDateTime::Single(tz.timestamp_opt(0, 0).unwrap()),
"today" => LocalDateTime::from_date(naive_today(tz), tz)?,
"yesterday" => LocalDateTime::from_date(naive_today(tz) - Days::new(1), tz)?,
"tomorrow" => LocalDateTime::from_date(naive_today(tz) + Days::new(1), tz)?,
ts => match ts.find('.') {
Some(p) => {
let ts_t = &ts[..p];
let ndt = NaiveDateTime::parse_from_str(ts_t, "%y-%m-%d %H:%M:%S")
.or_else(|_| NaiveDateTime::parse_from_str(ts_t, "%Y-%m-%d %H:%M:%S"))
.or_else(|_| {
NaiveTime::parse_from_str(ts_t, "%H:%M:%S")
.map(|nt| naive_today(tz).and_time(nt))
})
.map_err(|_| {
Error::Format(format!("Cannot parse `{ts_t}` before '.' into a time"))
})?;
let ts_u = &ts[(p + 1)..];
let usecs: i64 = ts_u.parse().map_err(|e| {
Error::Number(format!(
"Cannot parse `{ts_u}` after '.' into a number: {e}"
))
})?;
let ndt = ndt + Duration::microseconds(usecs);
LocalDateTime::from_datetime(ndt, tz)?
}
None => NaiveDateTime::parse_from_str(ts, "%y-%m-%d %H:%M:%S")
.or_else(|_| NaiveDateTime::parse_from_str(ts, "%Y-%m-%d %H:%M:%S"))
.or_else(|_| NaiveDateTime::parse_from_str(ts, "%y-%m-%d %H:%M"))
.or_else(|_| NaiveDateTime::parse_from_str(ts, "%Y-%m-%d %H:%M"))
.or_else(|_| {
NaiveDate::parse_from_str(ts, "%y-%m-%d")
.map(|nd| nd.and_hms_opt(0, 0, 0).unwrap())
})
.or_else(|_| {
NaiveDate::parse_from_str(ts, "%Y-%m-%d")
.map(|nd| nd.and_hms_opt(0, 0, 0).unwrap())
})
.or_else(|_| {
NaiveTime::parse_from_str(ts, "%H:%M:%S").map(|nt| naive_today(tz).and_time(nt))
})
.or_else(|_| {
NaiveTime::parse_from_str(ts, "%H:%M").map(|nt| naive_today(tz).and_time(nt))
})
.map_err(|_| Error::Format(format!("Cannot parse `{ts}` into a time")))
.and_then(|ndt| LocalDateTime::from_datetime(ndt, tz))?,
},
};
Ok(dt)
}
fn parse_offset(mut ts_nw: &str) -> Result<Duration, Error> {
let mut total_usecs: i64 = 0;
loop {
if ts_nw.is_empty() {
return Ok(Duration::microseconds(total_usecs));
}
let (digits, ts_tail) = partition_predicate(ts_nw, |c| c.is_ascii_digit());
let (letters, ts_tail) = partition_predicate(ts_tail, char::is_alphabetic);
ts_nw = ts_tail;
let number: i64 = digits
.parse()
.map_err(|e| Error::Number(format!("Cannot parse `{digits}` into a number: {e}")))?;
let Some(&multiplier) = USEC_MULTIPLIER.get(letters) else {
return Err(Error::TimeUnit(letters.to_owned()));
};
let Some(usecs) = number
.checked_mul(multiplier)
.and_then(|usec| usec.checked_add(total_usecs))
else {
return Err(Error::Number(format!(
"Offset microseconds overflowed: total_usecs `{total_usecs}` number `{number}` multiplier `{multiplier}`"
)));
};
total_usecs = usecs;
}
}
fn naive_today<Tz: TimeZone>(tz: &Tz) -> NaiveDate {
Utc::now().with_timezone(tz).date_naive()
}
fn partition_predicate<P>(ts: &str, predicate: P) -> (&str, &str)
where
P: Fn(char) -> bool,
{
ts.find(|c: char| !predicate(c))
.map(|p| ts.split_at(p))
.unwrap_or((ts, ""))
}