#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
#![warn(clippy::pedantic)]
#![allow(
clippy::if_not_else,
clippy::missing_errors_doc,
clippy::module_name_repetitions,
clippy::too_many_lines,
clippy::cast_lossless,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss
)]
pub mod datetime;
mod errors;
mod parser;
mod types;
use datetime::DateTime;
pub use errors::{DateError, DateResult};
pub use types::Interval;
use types::{DateSpec, DateTimeSpec};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dialect {
Uk,
Us,
}
pub fn parse_date_string<Dt: DateTime>(s: &str, now: Dt, dialect: Dialect) -> DateResult<Dt> {
into_date_string(parser::DateParser::new(s).parse(dialect)?, now, dialect)
}
fn into_date_string<Dt: DateTime>(d: DateTimeSpec, now: Dt, dialect: Dialect) -> DateResult<Dt> {
if let Some(dspec) = d.date {
dspec
.into_date_time(now, d.time, dialect)
.ok_or(DateError::MissingDate)
} else if let Some(tspec) = d.time {
let (tz, date, _) = now.split();
tspec.into_date_time(tz, date).ok_or(DateError::MissingTime)
} else {
Err(DateError::MissingTime)
}
}
pub fn parse_duration(s: &str) -> DateResult<Interval> {
let d = parser::DateParser::new(s).parse(Dialect::Uk)?;
if d.time.is_some() {
return Err(DateError::UnexpectedTime);
}
match d.date {
Some(DateSpec::Relative(skip)) => Ok(skip),
Some(DateSpec::Absolute(_)) => Err(DateError::UnexpectedAbsoluteDate),
Some(DateSpec::FromName(..)) => Err(DateError::UnexpectedDate),
None => Err(DateError::MissingDate),
}
}
#[cfg(test)]
mod tests {
use crate::{parse_duration, DateError, Dialect, Interval};
#[cfg(feature = "chrono")]
#[track_caller]
fn format_chrono(d: &crate::types::DateTimeSpec, dialect: Dialect) -> String {
use chrono::{FixedOffset, TimeZone};
let base = FixedOffset::east(7200).ymd(2018, 3, 21).and_hms(11, 00, 00);
match crate::into_date_string(d.clone(), base, dialect) {
Err(e) => {
panic!("unexpected error attempting to format [chrono] {d:?}\n\t{e:?}")
}
Ok(date) => date.format("%+").to_string(),
}
}
#[cfg(feature = "time")]
#[track_caller]
fn format_time(d: &crate::types::DateTimeSpec, dialect: Dialect) -> String {
use time::{Date, Month, PrimitiveDateTime, Time, UtcOffset};
let base = PrimitiveDateTime::new(
Date::from_calendar_date(2018, Month::March, 21).unwrap(),
Time::from_hms(11, 00, 00).unwrap(),
)
.assume_offset(UtcOffset::from_whole_seconds(7200).unwrap());
match crate::into_date_string(d.clone(), base, dialect) {
Err(e) => {
panic!("unexpected error attempting to format [time] {d:?}\n\t{e:?}")
}
Ok(date) => {
let format = time::format_description::parse(
"[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]",
).unwrap();
date.format(&format).unwrap()
}
}
}
macro_rules! assert_date_string {
($s:literal, $dialect:ident, $expect:literal) => {
let dialect = Dialect::$dialect;
let input = $s;
let _date = match crate::parser::DateParser::new(input).parse(dialect) {
Err(e) => {
panic!("unexpected error attempting to parse [chrono] {input:?}\n\t{e:?}")
}
Ok(date) => date,
};
#[cfg(feature = "chrono")]
{
let output = format_chrono(&_date, dialect);
let expected: &str = $expect;
if output != expected {
panic!("unexpected output attempting to format [chrono] {input:?}.\nexpected: {expected:?}\n parsed: {_date:?}");
}
}
#[cfg(feature = "time")]
{
let output = format_time(&_date, dialect);
let expected: &str = $expect;
if output != expected {
panic!("unexpected output attempting to format [time] {input:?}.\nexpected: {expected:?}\n parsed: {_date:?}");
}
}
};
}
#[test]
fn basics() {
assert_date_string!("friday", Uk, "2018-03-23T00:00:00+02:00");
assert_date_string!("friday 10:30", Uk, "2018-03-23T10:30:00+02:00");
assert_date_string!("friday 8pm", Uk, "2018-03-23T20:00:00+02:00");
assert_date_string!("tues", Uk, "2018-03-27T00:00:00+02:00");
assert_date_string!("next mon", Us, "2018-03-26T00:00:00+02:00");
assert_date_string!("next mon", Uk, "2018-04-02T00:00:00+02:00");
assert_date_string!("last fri 9.30", Uk, "2018-03-16T09:30:00+02:00");
assert_date_string!("9/11", Us, "2018-09-11T00:00:00+02:00");
assert_date_string!("last 9/11", Us, "2017-09-11T00:00:00+02:00");
assert_date_string!("last 9/11 9am", Us, "2017-09-11T09:00:00+02:00");
assert_date_string!("April 1 8.30pm", Uk, "2018-04-01T20:30:00+02:00");
assert_date_string!("2d", Uk, "2018-03-23T11:00:00+02:00");
assert_date_string!("2d 03:00", Uk, "2018-03-23T03:00:00+02:00");
assert_date_string!("3 weeks", Uk, "2018-04-11T11:00:00+02:00");
assert_date_string!("3h", Uk, "2018-03-21T14:00:00+02:00");
assert_date_string!("6 months", Uk, "2018-09-21T00:00:00+02:00");
assert_date_string!("6 months ago", Uk, "2017-09-21T00:00:00+02:00");
assert_date_string!("3 hours ago", Uk, "2018-03-21T08:00:00+02:00");
assert_date_string!(" -3h", Uk, "2018-03-21T08:00:00+02:00");
assert_date_string!(" -3 month", Uk, "2017-12-21T00:00:00+02:00");
assert_date_string!("2017-06-30", Uk, "2017-06-30T00:00:00+02:00");
assert_date_string!("30/06/17", Uk, "2017-06-30T00:00:00+02:00");
assert_date_string!("06/30/17", Us, "2017-06-30T00:00:00+02:00");
assert_date_string!("2017-06-30 08:20:30", Uk, "2017-06-30T08:20:30+02:00");
assert_date_string!(
"2017-06-30 08:20:30 +04:00",
Uk,
"2017-06-30T06:20:30+02:00"
);
assert_date_string!("2017-06-30 08:20:30 +0400", Uk, "2017-06-30T06:20:30+02:00");
assert_date_string!("2017-06-30T08:20:30Z", Uk, "2017-06-30T10:20:30+02:00");
assert_date_string!("2017-06-30T08:20:30", Uk, "2017-06-30T08:20:30+02:00");
assert_date_string!("2017-06-30 8.20", Uk, "2017-06-30T08:20:00+02:00");
assert_date_string!("2017-06-30 8.30pm", Uk, "2017-06-30T20:30:00+02:00");
assert_date_string!("2017-06-30 8:30pm", Uk, "2017-06-30T20:30:00+02:00");
assert_date_string!("2017-06-30 2am", Uk, "2017-06-30T02:00:00+02:00");
assert_date_string!("30 June 2018", Uk, "2018-06-30T00:00:00+02:00");
assert_date_string!("June 30, 2018", Uk, "2018-06-30T00:00:00+02:00");
assert_date_string!("June 30, 2018", Uk, "2018-06-30T00:00:00+02:00");
}
#[test]
fn durations() {
macro_rules! assert_duration {
($s:literal, $expect:expr) => {
let dur = parse_duration($s).unwrap();
assert_eq!(dur, $expect);
};
}
macro_rules! assert_duration_err {
($s:literal, $expect:expr) => {
let err = parse_duration($s).unwrap_err();
assert_eq!(err, $expect);
};
}
assert_duration!("6h", Interval::Seconds(6 * 3600));
assert_duration!("4 hours ago", Interval::Seconds(-4 * 3600));
assert_duration!("5 min", Interval::Seconds(5 * 60));
assert_duration!("10m", Interval::Seconds(10 * 60));
assert_duration!("15m ago", Interval::Seconds(-15 * 60));
assert_duration!("1 day", Interval::Days(1));
assert_duration!("2 days ago", Interval::Days(-2));
assert_duration!("3 weeks", Interval::Days(21));
assert_duration!("2 weeks ago", Interval::Days(-14));
assert_duration!("1 month", Interval::Months(1));
assert_duration!("6 months", Interval::Months(6));
assert_duration!("8 years", Interval::Months(12 * 8));
assert_duration_err!("2020-01-01", DateError::UnexpectedAbsoluteDate);
assert_duration_err!("2 days 15:00", DateError::UnexpectedTime);
assert_duration_err!("tuesday", DateError::UnexpectedDate);
assert_duration_err!(
"bananas",
DateError::ExpectedToken("week day or month name", 0..7)
);
}
}