mod combined;
mod date;
mod epoch;
mod offset;
mod pure;
mod relative;
mod time;
mod timezone;
mod weekday;
mod year;
mod builder;
mod ordinal;
mod primitive;
pub(crate) mod error;
use jiff::Zoned;
use primitive::space;
use winnow::{
combinator::{alt, eof, preceded, repeat_till, terminated, trace},
error::{AddContext, ContextError, ErrMode, StrContext, StrContextValue},
stream::Stream,
ModalResult, Parser,
};
use builder::DateTimeBuilder;
use error::Error;
#[derive(PartialEq, Debug)]
enum Item {
Timestamp(epoch::Timestamp),
DateTime(combined::DateTime),
Date(date::Date),
Time(time::Time),
Weekday(weekday::Weekday),
Relative(relative::Relative),
Offset(offset::Offset),
TimeZone(jiff::tz::TimeZone),
Pure(String),
}
pub(crate) fn parse_at_date<S: AsRef<str> + Clone>(base: Zoned, input: S) -> Result<Zoned, Error> {
match parse(&mut input.as_ref()) {
Ok(builder) => builder.set_base(base).build(),
Err(e) => Err(e.into()),
}
}
pub(crate) fn parse_at_local<S: AsRef<str> + Clone>(input: S) -> Result<Zoned, Error> {
match parse(&mut input.as_ref()) {
Ok(builder) => builder.build(), Err(e) => Err(e.into()),
}
}
fn parse(input: &mut &str) -> ModalResult<DateTimeBuilder> {
trace("parse", alt((parse_timestamp, parse_items))).parse_next(input)
}
fn parse_timestamp(input: &mut &str) -> ModalResult<DateTimeBuilder> {
let _ = timezone::parse(input);
trace(
"parse_timestamp",
terminated(epoch::parse.map(Item::Timestamp), preceded(space, eof)),
)
.verify_map(|item: Item| match item {
Item::Timestamp(ts) => DateTimeBuilder::new().set_timestamp(ts).ok(),
_ => None,
})
.parse_next(input)
}
fn parse_items(input: &mut &str) -> ModalResult<DateTimeBuilder> {
let tz = timezone::parse(input).map(Item::TimeZone);
let lower = input.to_ascii_lowercase();
let input = &mut lower.as_str();
let (mut items, _): (Vec<Item>, _) = trace(
"parse_items",
repeat_till(0.., parse_item, preceded(space, eof)),
)
.parse_next(input)?;
if let Ok(tz) = tz {
items.push(tz);
}
items.try_into().map_err(|e| expect_error(input, e))
}
fn parse_item(input: &mut &str) -> ModalResult<Item> {
trace(
"parse_item",
alt((
combined::parse.map(Item::DateTime),
date::parse.map(Item::Date),
time::parse.map(Item::Time),
relative::parse.map(Item::Relative),
weekday::parse.map(Item::Weekday),
offset::parse.map(Item::Offset),
pure::parse.map(Item::Pure),
)),
)
.parse_next(input)
}
fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode<ContextError> {
ErrMode::Cut(ContextError::new()).add_context(
input,
&input.checkpoint(),
StrContext::Expected(StrContextValue::Description(reason)),
)
}
#[cfg(test)]
mod tests {
use jiff::{civil::DateTime, tz::TimeZone, ToSpan, Zoned};
use super::*;
fn at_date(builder: DateTimeBuilder, base: Zoned) -> Zoned {
builder.set_base(base).build().unwrap()
}
fn at_utc(builder: DateTimeBuilder) -> Zoned {
at_date(builder, Zoned::now().with_time_zone(TimeZone::UTC))
}
fn test_eq_fmt(fmt: &str, input: &str) -> String {
let input = input.to_ascii_lowercase();
parse(&mut input.as_str())
.map(at_utc)
.map_err(|e| eprintln!("TEST FAILED AT:\n{e}"))
.expect("parsing failed during tests")
.strftime(fmt)
.to_string()
}
#[test]
fn date_and_time() {
assert_eq!(
"2022-12-12",
test_eq_fmt("%Y-%m-%d", " 10:10 2022-12-12 ")
);
assert_eq!("2024-01-02", test_eq_fmt("%Y-%m-%d", "2024-01-02"));
assert_eq!("2005-01-02", test_eq_fmt("%Y-%m-%d", "2005-01-01 +1 day"));
assert_eq!("Jul 16", test_eq_fmt("%b %d", "Jul 16"));
assert_eq!("0718061449", test_eq_fmt("%m%d%H%M%S", "Jul 18 06:14:49"));
assert_eq!(
"07182024061449",
test_eq_fmt("%m%d%Y%H%M%S", "Jul 18, 2024 06:14:49")
);
assert_eq!(
"07182024061449",
test_eq_fmt("%m%d%Y%H%M%S", "Jul 18 06:14:49 2024")
);
assert_eq!(
"2023-07-27T13:53:54+00:00",
test_eq_fmt("%Y-%m-%dT%H:%M:%S%:z", "@1690466034")
);
assert_eq!(
"2023-07-27T13:53:54+00:00",
test_eq_fmt("%Y-%m-%dT%H:%M:%S%:z", " @1690466034 ")
);
assert_eq!(
"2024-07-17 06:14:49 +00:00",
test_eq_fmt("%Y-%m-%d %H:%M:%S %:z", "Jul 17 06:14:49 2024 GMT"),
);
assert_eq!(
"2024-07-17 06:14:49.567 +00:00",
test_eq_fmt("%Y-%m-%d %H:%M:%S%.f %:z", "Jul 17 06:14:49.567 2024 GMT"),
);
assert_eq!(
"2024-07-17 06:14:49.567 +00:00",
test_eq_fmt("%Y-%m-%d %H:%M:%S%.f %:z", "Jul 17 06:14:49,567 2024 GMT"),
);
assert_eq!(
"2024-07-17 06:14:49 -03:00",
test_eq_fmt("%Y-%m-%d %H:%M:%S %:z", "Jul 17 06:14:49 2024 BRT"),
);
}
#[test]
fn empty() {
let result = parse(&mut "");
assert!(result.is_ok());
}
#[test]
fn invalid() {
let result = parse(&mut "2025-05-19 2024-05-20 06:14:49");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("date cannot appear more than once"));
let result = parse(&mut "2025-05-19 2024-05-20");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("date cannot appear more than once"));
let result = parse(&mut "06:14:49 06:14:49");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("time cannot appear more than once"));
let result = parse(&mut "2025-05-19 +00:00 +01:00");
assert!(result.is_err());
let result = parse(&mut "m1y");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("time offset cannot appear more than once"));
let result = parse(&mut "2025-05-19 abcdef");
assert!(result.is_err());
let result = parse(&mut "@1690466034 2025-05-19");
assert!(result.is_err());
let result = parse(&mut "2025-05-19 @1690466034");
assert!(result.is_err());
let result = parse(&mut "jul 18 12:30 10000");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("year must be no greater than 9999"));
let result = parse(&mut "01:02 12345");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("pure number must be 1-4 digits when interpreted as time"));
let result = parse(&mut "01:02 1234");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("time cannot appear more than once"));
let result = parse(&mut "jul 18 2025 2400");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("invalid hour in pure number"));
let result = parse(&mut "jul 18 2025 2360");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("invalid minute in pure number"));
}
#[test]
fn relative_weekday() {
let now = "2025-01-01 00:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
assert_eq!(
at_date(parse(&mut "last wed").unwrap(), now.clone()),
now.checked_sub(7.days()).unwrap()
);
assert_eq!(at_date(parse(&mut "this wed").unwrap(), now.clone()), now);
assert_eq!(
at_date(parse(&mut "next wed").unwrap(), now.clone()),
now.checked_add(7.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "last thu").unwrap(), now.clone()),
now.checked_sub(6.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "this thu").unwrap(), now.clone()),
now.checked_add(1.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "next thu").unwrap(), now.clone()),
now.checked_add(1.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "1 wed").unwrap(), now.clone()),
now.checked_add(7.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "1 thu").unwrap(), now.clone()),
now.checked_add(1.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "2 wed").unwrap(), now.clone()),
now.checked_add(14.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "2 thu").unwrap(), now.clone()),
now.checked_add(8.days()).unwrap()
);
}
#[test]
fn relative_date_time() {
let now = Zoned::now().with_time_zone(TimeZone::UTC);
let result = at_date(parse(&mut "2 days ago").unwrap(), now.clone());
assert_eq!(result, now.checked_sub(2.days()).unwrap());
assert_eq!(result.hour(), now.hour());
assert_eq!(result.minute(), now.minute());
assert_eq!(result.second(), now.second());
let result = at_date(parse(&mut "2 days 3 days ago").unwrap(), now.clone());
assert_eq!(result, now.checked_sub(1.days()).unwrap());
assert_eq!(result.hour(), now.hour());
assert_eq!(result.minute(), now.minute());
assert_eq!(result.second(), now.second());
let result = at_date(parse(&mut "2025-01-01 2 days ago").unwrap(), now.clone());
assert_eq!(result.hour(), 0);
assert_eq!(result.minute(), 0);
assert_eq!(result.second(), 0);
let result = at_date(parse(&mut "3 weeks").unwrap(), now.clone());
assert_eq!(result, now.checked_add(21.days()).unwrap());
assert_eq!(result.hour(), now.hour());
assert_eq!(result.minute(), now.minute());
assert_eq!(result.second(), now.second());
let result = at_date(parse(&mut "2025-01-01 3 weeks").unwrap(), now);
assert_eq!(result.hour(), 0);
assert_eq!(result.minute(), 0);
assert_eq!(result.second(), 0);
}
#[test]
fn pure() {
let now = Zoned::now().with_time_zone(TimeZone::UTC);
let result = at_date(parse(&mut "jul 18 12:30 2025").unwrap(), now.clone());
assert_eq!(result.year(), 2025);
let result = at_date(parse(&mut "1230").unwrap(), now.clone());
assert_eq!(result.hour(), 12);
assert_eq!(result.minute(), 30);
let result = at_date(parse(&mut "123").unwrap(), now.clone());
assert_eq!(result.hour(), 1);
assert_eq!(result.minute(), 23);
let result = at_date(parse(&mut "12").unwrap(), now.clone());
assert_eq!(result.hour(), 12);
assert_eq!(result.minute(), 0);
let result = at_date(parse(&mut "1").unwrap(), now.clone());
assert_eq!(result.hour(), 1);
assert_eq!(result.minute(), 0);
}
#[test]
fn timezone_rule() {
let parse_build = |mut s| parse(&mut s).unwrap().build().unwrap();
for (input, expected) in [
(
r#"TZ="Europe/Paris" 2025-01-02"#,
"2025-01-02 00:00:00[Europe/Paris]".parse().unwrap(),
),
(
r#"TZ="Europe/Paris" 2025-01-02 03:04:05"#,
"2025-01-02 03:04:05[Europe/Paris]".parse().unwrap(),
),
] {
assert_eq!(parse_build(input), expected, "{input}");
}
}
}