Skip to content

Commit

Permalink
Allow date components to be omitted
Browse files Browse the repository at this point in the history
  • Loading branch information
pitdicker committed Jun 12, 2023
1 parent 9e5605b commit 8ed009e
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 47 deletions.
74 changes: 56 additions & 18 deletions src/format/parsed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@ impl Parsed {
/// - Year, week number counted from Sunday or Monday, day of the week.
/// - ISO week date.
///
/// If the day of the month or day of the week is missing, it will use the first day as
/// fallback.
///
/// Gregorian year and ISO week date year can have their century number (`*_div_100`) omitted,
/// the two-digit year is used to guess the century number then.
pub fn to_naive_date(&self) -> ParseResult<NaiveDate> {
Expand Down Expand Up @@ -349,36 +352,50 @@ impl Parsed {
// it is consistent with other given fields.
let (verified, parsed_date) = match (given_year, given_isoyear, self) {
(Some(year), _, &Parsed { month: Some(month), day: Some(day), .. }) => {
// year, month, day
let date = NaiveDate::from_ymd_opt(year, month, day).ok_or(OUT_OF_RANGE)?;
(verify_isoweekdate(date) && verify_ordinal(date), date)
}

(Some(year), _, &Parsed { ordinal: Some(ordinal), .. }) => {
// year, day of the year
let date = NaiveDate::from_yo_opt(year, ordinal).ok_or(OUT_OF_RANGE)?;
(verify_ymd(date) && verify_isoweekdate(date) && verify_ordinal(date), date)
}

(Some(year), _, &Parsed { week_from_sun: Some(week), weekday: Some(weekday), .. }) => {
// year, week (starting at 1st Sunday), day of the week
let date = resolve_week_number(year, week, Weekday::Sun, weekday)?;
(verify_ymd(date) && verify_isoweekdate(date) && verify_ordinal(date), date)
}

(Some(year), _, &Parsed { week_from_mon: Some(week), weekday: Some(weekday), .. }) => {
// year, week (starting at 1st Monday), day of the week
let date = resolve_week_number(year, week, Weekday::Mon, weekday)?;
(verify_ymd(date) && verify_isoweekdate(date) && verify_ordinal(date), date)
}

(_, Some(isoyear), &Parsed { isoweek: Some(isoweek), weekday: Some(weekday), .. }) => {
// ISO year, week, day of the week
let date = NaiveDate::from_isoywd_opt(isoyear, isoweek, weekday);
let date = date.ok_or(OUT_OF_RANGE)?;
let date =
NaiveDate::from_isoywd_opt(isoyear, isoweek, weekday).ok_or(OUT_OF_RANGE)?;
(verify_ymd(date) && verify_ordinal(date), date)
}
#[rustfmt::skip]
(Some(year), None, &Parsed { month: Some(month),
week_from_sun: None, week_from_mon: None, .. }) => {
let date = NaiveDate::from_ymd_opt(year, month, 1).ok_or(OUT_OF_RANGE)?;
(verify_isoweekdate(date) && verify_ordinal(date), date)
}
#[rustfmt::skip]
(Some(year), None, &Parsed { week_from_sun: Some(week),
week_from_mon: None, month: None, .. }) => {
let date = resolve_week_number(year, week, Weekday::Sun, Weekday::Sun)?;
(verify_ymd(date) && verify_isoweekdate(date) && verify_ordinal(date), date)
}
#[rustfmt::skip]
(Some(year), None, &Parsed { week_from_mon: Some(week),
week_from_sun: None, month: None, .. }) => {
let date = resolve_week_number(year, week, Weekday::Mon, Weekday::Mon)?;
(verify_ymd(date) && verify_isoweekdate(date) && verify_ordinal(date), date)
}
(None, Some(isoyear), &Parsed { isoweek: Some(isoweek), .. }) => {
let weekday = Weekday::Mon;
let date =
NaiveDate::from_isoywd_opt(isoyear, isoweek, weekday).ok_or(OUT_OF_RANGE)?;
(verify_ymd(date) && verify_ordinal(date), date)
}

(_, _, _) => return Err(NOT_ENOUGH),
};

Expand Down Expand Up @@ -775,12 +792,12 @@ mod tests {
// ymd: omission of fields
assert_eq!(parse!(), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 1984), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 1984, month: 1), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 1984, month: 1), ymd(1984, 1, 1));
assert_eq!(parse!(year: 1984, month: 1, day: 2), ymd(1984, 1, 2));
assert_eq!(parse!(year: 1984, day: 2), Err(NOT_ENOUGH));
assert_eq!(parse!(year_div_100: 19), Err(NOT_ENOUGH));
assert_eq!(parse!(year_div_100: 19, year_mod_100: 84), Err(NOT_ENOUGH));
assert_eq!(parse!(year_div_100: 19, year_mod_100: 84, month: 1), Err(NOT_ENOUGH));
assert_eq!(parse!(year_div_100: 19, year_mod_100: 84, month: 1), ymd(1984, 1, 1));
assert_eq!(parse!(year_div_100: 19, year_mod_100: 84, month: 1, day: 2), ymd(1984, 1, 2));
assert_eq!(parse!(year_div_100: 19, year_mod_100: 84, day: 2), Err(NOT_ENOUGH));
assert_eq!(parse!(year_div_100: 19, month: 1, day: 2), Err(NOT_ENOUGH));
Expand Down Expand Up @@ -857,8 +874,8 @@ mod tests {
assert_eq!(parse!(year: -1, year_mod_100: 99, month: 1, day: 1), Err(OUT_OF_RANGE));

// weekdates
assert_eq!(parse!(year: 2000, week_from_mon: 0), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 2000, week_from_sun: 0), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 2000, week_from_mon: 1), ymd(2000, 1, 3));
assert_eq!(parse!(year: 2000, week_from_sun: 1), ymd(2000, 1, 2));
assert_eq!(parse!(year: 2000, weekday: Sun), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 2000, week_from_mon: 0, weekday: Fri), Err(OUT_OF_RANGE));
assert_eq!(parse!(year: 2000, week_from_sun: 0, weekday: Fri), Err(OUT_OF_RANGE));
Expand Down Expand Up @@ -899,12 +916,12 @@ mod tests {
);

// ISO weekdates
assert_eq!(parse!(isoyear: 2004, isoweek: 53), Err(NOT_ENOUGH));
assert_eq!(parse!(isoyear: 2004, isoweek: 53, weekday: Fri), ymd(2004, 12, 31));
assert_eq!(parse!(isoyear: 2004, isoweek: 53, weekday: Sat), ymd(2005, 1, 1));
assert_eq!(parse!(isoyear: 2004, isoweek: 0xffffffff, weekday: Sat), Err(OUT_OF_RANGE));
assert_eq!(parse!(isoyear: 2005, isoweek: 0, weekday: Thu), Err(OUT_OF_RANGE));
assert_eq!(parse!(isoyear: 2005, isoweek: 5, weekday: Thu), ymd(2005, 2, 3));
assert_eq!(parse!(isoyear: 2004, isoweek: 53), ymd(2004, 12, 27));
assert_eq!(parse!(isoyear: 2005, weekday: Thu), Err(NOT_ENOUGH));

// year and ordinal
Expand Down Expand Up @@ -949,8 +966,29 @@ mod tests {
// technically unique (2014-12-31) but Chrono gives up

// incomplete year but complete date
assert_eq!(parse!(year_div_100: 20, isoyear: 2023, isoweek: 1, weekday: Tue), ymd(2023, 1, 3));
assert_eq!(
parse!(year_div_100: 20, isoyear: 2023, isoweek: 1, weekday: Tue),
ymd(2023, 1, 3)
);
assert_eq!(parse!(isoyear_div_100: 20, year: 2023, ordinal: 3), ymd(2023, 1, 3));

// If there are two (or more) combinations for which we could pick the first day as
// fallback, give up as ambiguous.
assert_eq!(parse!(year: 2023, month: 6, week_from_sun: 22), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 2023, month: 6, week_from_mon: 22), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 2023, week_from_sun: 22, week_from_mon: 22), Err(NOT_ENOUGH));
assert_eq!(parse!(year: 2023, month: 6, isoyear: 2023, isoweek: 23), Err(NOT_ENOUGH));
assert_eq!(
parse!(year: 2023, week_from_sun: 22, isoyear: 2023, isoweek: 23),
Err(NOT_ENOUGH)
);
assert_eq!(
parse!(year: 2023, week_from_mon: 22, isoyear: 2023, isoweek: 23),
Err(NOT_ENOUGH)
);
// not ambiguous
assert_eq!(parse!(year: 2023, month: 6, isoweek: 22), ymd(2023, 6, 1));
assert_eq!(parse!(isoyear: 2023, month: 6, isoweek: 23), ymd(2023, 6, 5));
}

#[test]
Expand Down
54 changes: 27 additions & 27 deletions src/naive/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,12 +552,24 @@ impl NaiveDate {
/// Ok(NaiveDate::from_ymd_opt(2014, 5, 17).unwrap()));
/// ```
///
/// Out-of-bound dates or insufficient fields are errors.
/// If the format string doesn't contain a day field, it defaults to the first day.
/// If year and month are given it defaults to the first day of the month.
/// If year and week are given it defaults to the first day of the week.
///
/// ```
/// # use chrono::NaiveDate;
/// # let parse_from_str = NaiveDate::parse_from_str;
/// assert_eq!(parse_from_str("2015/9", "%Y/%m"),
/// Ok(NaiveDate::from_ymd_opt(2015, 9, 1).unwrap()));
/// assert_eq!(parse_from_str("2015 week 6", "%Y week %W"),
/// Ok(NaiveDate::from_ymd_opt(2015, 2, 9).unwrap()));
/// ```
///
/// Out-of-bound dates are invalid.
///
/// ```
/// # use chrono::NaiveDate;
/// # let parse_from_str = NaiveDate::parse_from_str;
/// assert!(parse_from_str("2015/9", "%Y/%m").is_err());
/// assert!(parse_from_str("2015/9/31", "%Y/%m/%d").is_err());
/// ```
///
Expand Down Expand Up @@ -3012,31 +3024,19 @@ mod tests {
#[test]
fn test_date_parse_from_str() {
let ymd = |y, m, d| NaiveDate::from_ymd_opt(y, m, d).unwrap();
assert_eq!(
NaiveDate::parse_from_str("2014-5-7T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"),
Ok(ymd(2014, 5, 7))
); // ignore time and offset
assert_eq!(
NaiveDate::parse_from_str("2015-W06-1=2015-033", "%G-W%V-%u = %Y-%j"),
Ok(ymd(2015, 2, 2))
);
assert_eq!(
NaiveDate::parse_from_str("Fri, 09 Aug 13", "%a, %d %b %y"),
Ok(ymd(2013, 8, 9))
);
assert!(NaiveDate::parse_from_str("Sat, 09 Aug 2013", "%a, %d %b %Y").is_err());
assert!(NaiveDate::parse_from_str("2014-57", "%Y-%m-%d").is_err());
assert!(NaiveDate::parse_from_str("2014", "%Y").is_err()); // insufficient

assert_eq!(
NaiveDate::parse_from_str("2020-01-0", "%Y-%W-%w").ok(),
NaiveDate::from_ymd_opt(2020, 1, 12),
);

assert_eq!(
NaiveDate::parse_from_str("2019-01-0", "%Y-%W-%w").ok(),
NaiveDate::from_ymd_opt(2019, 1, 13),
);
let parse = NaiveDate::parse_from_str;
assert_eq!(parse("2015-W06-1=2015-033", "%G-W%V-%u = %Y-%j"), Ok(ymd(2015, 2, 2)));
assert_eq!(parse("Fri, 09 Aug 13", "%a, %d %b %y"), Ok(ymd(2013, 8, 9)));
assert!(parse("Sat, 09 Aug 2013", "%a, %d %b %Y").is_err());
assert!(parse("2014-57", "%Y-%m-%d").is_err());
assert_eq!(parse("2020-01-0", "%Y-%W-%w"), Ok(ymd(2020, 1, 12)));
assert_eq!(parse("2019-01-0", "%Y-%W-%w"), Ok(ymd(2019, 1, 13)));
// allow missing day
assert_eq!(parse("2023-01", "%Y-%m"), Ok(ymd(2023, 1, 1)));
assert_eq!(parse("2023-W01", "%G-W%V"), Ok(ymd(2023, 1, 2)));
assert_eq!(parse("2023-w01", "%Y-w%U"), Ok(ymd(2023, 1, 1)));
// ignore time and offset
assert_eq!(parse("2014-5-7T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"), Ok(ymd(2014, 5, 7)));
}

#[test]
Expand Down
6 changes: 4 additions & 2 deletions src/naive/datetime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,10 @@ impl NaiveDateTime {
/// Ok(NaiveDate::from_ymd_opt(2015, 7, 1).unwrap().and_hms_milli_opt(8, 59, 59, 1_123).unwrap()));
/// ```
///
/// Missing minutes, seconds and nanoseconds are assumed to be zero,
/// but out-of-bound times or insufficient fields are errors otherwise.
/// Missing hours, minutes, seconds and nanoseconds are assumed to be zero.
/// A missing day will, depending on the other available fields, default to the first day of
/// the month or the first day of the week.
/// Out-of-bound times or insufficient fields are errors otherwise.
///
/// ```
/// # use chrono::{NaiveDateTime, NaiveDate};
Expand Down

0 comments on commit 8ed009e

Please sign in to comment.