Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISO 8601 parsers #1143

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
79 changes: 66 additions & 13 deletions src/datetime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use crate::duration::Duration as OldDuration;
#[cfg(feature = "unstable-locales")]
use crate::format::Locale;
use crate::format::{
parse, parse_and_remainder, parse_rfc3339, Fixed, Item, ParseError, ParseResult, Parsed,
StrftimeItems, TOO_LONG,
parse, parse_and_remainder, parse_iso8601, parse_rfc3339, Fixed, Item, ParseError, ParseResult,
Parsed, StrftimeItems, TOO_LONG,
};
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::format::{write_rfc3339, DelayedFormat};
Expand Down Expand Up @@ -770,17 +770,24 @@ impl DateTime<FixedOffset> {

/// Parses an RFC 3339 date-and-time string into a `DateTime<FixedOffset>` value.
///
/// Parses all valid RFC 3339 values (as well as the subset of valid ISO 8601 values that are
/// also valid RFC 3339 date-and-time values) and returns a new [`DateTime`] with a
/// [`FixedOffset`] corresponding to the parsed timezone. While RFC 3339 values come in a wide
/// variety of shapes and sizes, `1996-12-19T16:39:57-08:00` is an example of the most commonly
/// encountered variety of RFC 3339 formats.
///
/// Why isn't this named `parse_from_iso8601`? That's because ISO 8601 allows representing
/// values in a wide range of formats, only some of which represent actual date-and-time
/// instances (rather than periods, ranges, dates, or times). Some valid ISO 8601 values are
/// also simultaneously valid RFC 3339 values, but not all RFC 3339 values are valid ISO 8601
/// values (or the other way around).
/// This parses valid RFC 3339 datetime strings (such as `1996-12-19T16:39:57-08:00`)
/// and returns a new [`DateTime`] instance with the parsed timezone as the [`FixedOffset`].
///
/// RFC 3339 is a clearly defined subset or profile of ISO 8601.
///
/// # Example
///
/// ```
/// # use chrono::{DateTime, FixedOffset, TimeZone};
/// assert_eq!(
/// DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap(),
/// FixedOffset::east_opt(-8 * 3600).unwrap().with_ymd_and_hms(1996, 12, 19, 16, 39, 57).unwrap()
/// );
/// assert_eq!(
/// DateTime::parse_from_rfc3339("2023-06-10T07:15:00Z").unwrap(),
/// FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2023, 6, 10, 7, 15, 0).unwrap()
/// );
/// ```
pub fn parse_from_rfc3339(s: &str) -> ParseResult<DateTime<FixedOffset>> {
let mut parsed = Parsed::new();
let (s, _) = parse_rfc3339(&mut parsed, s)?;
Expand All @@ -790,6 +797,52 @@ impl DateTime<FixedOffset> {
parsed.to_datetime()
}

/// Parses an ISO 8601 date-and-time string into a `DateTime<FixedOffset>` value.
///
/// ISO 8601 allows representing values in a wide range of formats. Some valid ISO 8601 values
/// are also valid RFC 3339 values.
///
/// # Example
///
/// ```
/// # use chrono::{DateTime, FixedOffset, NaiveDate, TimeZone, Weekday};
/// // calendar date, regular time, basic format
/// assert_eq!(
/// DateTime::parse_from_iso8601("20230609T101530Z").unwrap(),
/// (FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2023, 6, 9, 10, 15, 30).unwrap(),
/// "")
/// );
/// // calendar date, regular time, extended format (is also valid RFC 3339)
/// assert_eq!(
/// DateTime::parse_from_iso8601("2023-06-09T10:15:30Z").unwrap(),
/// (FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2023, 6, 9, 10, 15, 30).unwrap(),
/// "")
/// );
/// // ordinal date, time with fraction of a second, extended format, `,` as decimal sign
/// assert_eq!(
/// DateTime::parse_from_iso8601("2023-160T10:15:30,25+01:00").unwrap(),
/// (NaiveDate::from_yo_opt(2023, 160)
/// .unwrap()
/// .and_hms_milli_opt(10, 15, 30, 250)
/// .unwrap()
/// .and_local_timezone(FixedOffset::east_opt(1 * 3600).unwrap())
/// .unwrap(), "")
/// );
/// // week date, time with fraction of an hour, basic format, `.` as decimal sign
/// assert_eq!(
/// DateTime::parse_from_iso8601("2023W235T10.25-01").unwrap(),
/// (NaiveDate::from_isoywd_opt(2023, 23, Weekday::Fri)
/// .unwrap()
/// .and_hms_opt(10, 15, 0)
/// .unwrap()
/// .and_local_timezone(FixedOffset::east_opt(-1 * 3600).unwrap())
/// .unwrap(), "")
/// );
/// ```
pub fn parse_from_iso8601(s: &str) -> ParseResult<(DateTime<FixedOffset>, &str)> {
parse_iso8601(s)
}

/// Parses a string from a user-specified format into a `DateTime<FixedOffset>` value.
///
/// Note that this method *requires a timezone* in the input string. See
Expand Down
39 changes: 39 additions & 0 deletions src/datetime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1519,3 +1519,42 @@ fn nano_roundrip() {
assert_eq!(nanos, nanos2);
}
}

#[test]
fn test_parse_from_iso8601() {
let parse = |s| DateTime::<FixedOffset>::parse_from_iso8601(s).map(|(dt, _)| dt);
let datetime = |y, m, d, h, n, s, nano, o| {
FixedOffset::east_opt(o)
.unwrap()
.with_ymd_and_hms(y, m, d, h, n, s)
.unwrap()
.with_nanosecond(nano)
.unwrap()
};

// Taken from ISO 8601
assert_eq!(parse("19850412T101530Z"), Ok(datetime(1985, 4, 12, 10, 15, 30, 0, 0)));
assert_eq!(parse("19850412T101530+0400"), Ok(datetime(1985, 4, 12, 10, 15, 30, 0, 14400)));
assert_eq!(parse("19850412T101530-04"), Ok(datetime(1985, 4, 12, 10, 15, 30, 0, -14400)));
assert_eq!(parse("1985-04-12T10:15:30Z"), Ok(datetime(1985, 4, 12, 10, 15, 30, 0, 0)));
assert_eq!(parse("1985-04-12T10:15:30+04:00"), Ok(datetime(1985, 4, 12, 10, 15, 30, 0, 14400)));
assert_eq!(parse("1985-04-12T10:15:30-04"), Ok(datetime(1985, 4, 12, 10, 15, 30, 0, -14400)));
assert_eq!(parse("1985W155T1015+0400"), Ok(datetime(1985, 4, 12, 10, 15, 0, 0, 14400)));
assert_eq!(parse("1985-W15-5T10:15+04"), Ok(datetime(1985, 4, 12, 10, 15, 0, 0, 14400)));
assert_eq!(parse("1985102T235030Z"), Ok(datetime(1985, 4, 12, 23, 50, 30, 0, 0)));
// With fractions
assert_eq!(
parse("1985102T235030,5+01"),
Ok(datetime(1985, 4, 12, 23, 50, 30, 500_000_000, 3600))
);
assert_eq!(parse("1985102T2350,5+01"), Ok(datetime(1985, 4, 12, 23, 50, 30, 0, 3600)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test test_parse_from_iso8601 should have asserts for errors on empty strings, unicode oddities, and a large variety of near-misses.

}

#[test]
fn test_iso8601_parses_debug() {
let parse = |s| DateTime::<FixedOffset>::parse_from_iso8601(s).map(|(dt, _)| dt);

let dt = FixedOffset::east_opt(3600).unwrap().with_ymd_and_hms(12345, 6, 7, 8, 9, 10).unwrap();
let debug = format!("{:?}", dt);
assert_eq!(parse(&debug), Ok(dt));
}
4 changes: 4 additions & 0 deletions src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ mod parsed;

// due to the size of parsing routines, they are in separate modules.
mod parse;
pub(crate) mod parse_iso8601;
pub(crate) mod scan;

pub mod strftime;
Expand Down Expand Up @@ -71,6 +72,9 @@ pub use locales::Locale;
pub(crate) use locales::Locale;
pub(crate) use parse::parse_rfc3339;
pub use parse::{parse, parse_and_remainder};
pub(crate) use parse_iso8601::{
parse_iso8601, parse_iso8601_date, parse_iso8601_datetime, parse_iso8601_time,
};
pub use parsed::Parsed;
pub use strftime::StrftimeItems;

Expand Down
2 changes: 1 addition & 1 deletion src/format/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn set_weekday_with_num_days_from_sunday(p: &mut Parsed, v: i64) -> ParseResult<
})
}

fn set_weekday_with_number_from_monday(p: &mut Parsed, v: i64) -> ParseResult<()> {
pub(super) fn set_weekday_with_number_from_monday(p: &mut Parsed, v: i64) -> ParseResult<()> {
p.set_weekday(match v {
1 => Weekday::Mon,
2 => Weekday::Tue,
Expand Down