Skip to content
4 changes: 2 additions & 2 deletions src/items/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,11 +581,11 @@ mod tests {

fn date() -> date::Date {
let mut input = "2023-06-15";
date::parse(&mut input).unwrap()
date::iso1(&mut input).unwrap()
}

fn date_large(mut input: &str) -> date::Date {
date::parse(&mut input).unwrap()
date::iso1(&mut input).unwrap()
}

fn time() -> time::Time {
Expand Down
16 changes: 12 additions & 4 deletions src/items/combined.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ fn remaining_starts_with_meridiem(input: &str) -> bool {
|| trimmed.starts_with("p.m.")
}

pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {
let date = trace("iso_date", alt((date::iso1, date::iso2))).parse_next(input)?;
/// Parse the separator and time of a combined item whose ISO date has already
/// been parsed.
pub(crate) fn parse_time_after_date(input: &mut &str, date: date::Date) -> ModalResult<DateTime> {
// Note: the `T` is lowercased by the main parse function
alt((s('t').void(), (' ', space).void())).parse_next(input)?;

Expand All @@ -59,8 +60,15 @@ pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {

#[cfg(test)]
mod tests {
use super::{parse, DateTime};
use crate::items::{date::Date, time::Time};
use winnow::{combinator::alt, ModalResult, Parser};

use super::{parse_time_after_date, DateTime};
use crate::items::{date, date::Date, time::Time};

fn parse(input: &mut &str) -> ModalResult<DateTime> {
let date = alt((date::iso1, date::iso2)).parse_next(input)?;
parse_time_after_date(input, date)
}

#[test]
fn some_date() {
Expand Down
14 changes: 11 additions & 3 deletions src/items/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,10 @@ impl TryFrom<Date> for jiff::civil::Date {
}
}

pub(super) fn parse(input: &mut &str) -> ModalResult<Date> {
alt((iso1, iso2, us, literal1, literal2, literal3)).parse_next(input)
/// Parse a date that is not in ISO format. ISO dates are parsed by [`iso1`]
/// and [`iso2`], which the caller tries first.
pub(super) fn parse_non_iso(input: &mut &str) -> ModalResult<Date> {
alt((us, literal1, literal2, literal3)).parse_next(input)
}

/// Parse `[year]-[month]-[day]`
Expand Down Expand Up @@ -299,7 +301,13 @@ fn day_from_str(s: &str) -> ModalResult<u8> {

#[cfg(test)]
mod tests {
use super::{parse, Date};
use winnow::{combinator::alt, ModalResult, Parser};

use super::Date;

fn parse(input: &mut &str) -> ModalResult<Date> {
alt((super::iso1, super::iso2, super::parse_non_iso)).parse_next(input)
}

// Test cases from the GNU docs:
//
Expand Down
68 changes: 64 additions & 4 deletions src/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ mod primitive;

pub(crate) mod error;

use std::borrow::Cow;

use crate::ParsedDateTime;
use jiff::Zoned;
use primitive::space;
Expand Down Expand Up @@ -236,8 +238,12 @@ fn parse_items(input: &mut &str) -> ModalResult<DateTimeBuilder> {
let tz = timezone::parse(input).map(Item::TimeZone);

// Convert input to lowercase for case-insensitive parsing.
let lower = input.to_ascii_lowercase();
let input = &mut lower.as_str();
let lower: Cow<str> = if input.bytes().any(|b| b.is_ascii_uppercase()) {
input.to_ascii_lowercase().into()
} else {
(*input).into()
};
let input = &mut lower.as_ref();

let (mut items, _): (Vec<Item>, _) = trace(
"parse_items",
Expand All @@ -255,11 +261,26 @@ fn parse_items(input: &mut &str) -> ModalResult<DateTimeBuilder> {

/// Parse an item.
fn parse_item(input: &mut &str) -> ModalResult<Item> {
// An ISO date is either the start of a combined date and time item or a
// date item on its own. Parse it once instead of once per alternative.
let start = input.checkpoint();
if let Ok(date) = alt((date::iso1, date::iso2)).parse_next(input) {
let after_date = input.checkpoint();
match combined::parse_time_after_date(input, date.clone()) {
Ok(dt) => return Ok(Item::DateTime(dt)),
Err(ErrMode::Backtrack(_)) => {
input.reset(&after_date);
return Ok(Item::Date(date));
}
Err(e) => return Err(e),
}
}
input.reset(&start);

trace(
"parse_item",
alt((
combined::parse.map(Item::DateTime),
date::parse.map(Item::Date),
date::parse_non_iso.map(Item::Date),
time::parse.map(Item::Time),
relative::parse.map(Item::Relative),
weekday::parse.map(Item::Weekday),
Expand Down Expand Up @@ -743,6 +764,45 @@ mod tests {
assert_eq!(result.second(), 0);
}

#[test]
fn offset_followed_by_relative() {
let base = "2000-01-01 12:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();

// Both read "+8 years" as a relative item, not as an offset of +8.
for input in ["UTC +8 years", "+8 years"] {
let result = parse_at_date(base.clone(), input).unwrap();
assert_eq!(result.to_string(), "2008-01-01 12:00:00+00:00", "{input}");
}
}

#[test]
fn comments_and_ignored_signs() {
let base = "2000-06-15 12:34:56"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();

for (input, expected) in [
("1997-01-01 (foo) 10:00", "1997-01-01 10:00:00+00:00"),
("1997-01-01 (a (b)) 10:00", "1997-01-01 10:00:00+00:00"),
("(foo) 1997-01-01", "1997-01-01 00:00:00+00:00"),
("+ (c) 1997-01-01", "1997-01-01 00:00:00+00:00"),
("- (c) 1997-01-01", "1997-01-01 00:00:00+00:00"),
("1997-01-01 (x) + 1 day", "1997-01-02 00:00:00+00:00"),
] {
let result = parse_at_date(base.clone(), input).unwrap();
assert_eq!(result.to_string(), expected, "{input}");
}

// Like GNU, a hyphen followed by a digit is a sign, not ignored.
assert!(parse_at_date(base.clone(), "- 1997-01-01").is_err());
}

#[test]
fn pure() {
let now = Zoned::now().with_time_zone(TimeZone::UTC);
Expand Down
Loading
Loading