diff --git a/bench/benches/chrono.rs b/bench/benches/chrono.rs index 925c2939f..0778b9858 100644 --- a/bench/benches/chrono.rs +++ b/bench/benches/chrono.rs @@ -69,7 +69,9 @@ fn bench_datetime_to_rfc3339(c: &mut Criterion) { .unwrap(), ) .unwrap(); - c.bench_function("bench_datetime_to_rfc3339", |b| b.iter(|| black_box(dt).to_rfc3339())); + c.bench_function("bench_datetime_to_rfc3339", |b| { + b.iter(|| black_box(dt).try_to_rfc3339().unwrap()) + }); } fn bench_datetime_to_rfc3339_opts(c: &mut Criterion) { diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index bb157f836..11b7f27a8 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -630,11 +630,74 @@ impl DateTime { result } - /// Returns an RFC 3339 and ISO 8601 date and time string such as `1996-12-19T16:39:57-08:00`. + /// Returns an RFC 3339 date and time string such as `1996-12-19T16:39:57-08:00`. + /// This is also valid ISO 8601. + /// + /// # Warning + /// + /// RFC 3339 is only defined on years 0 through 9999. This method switches to an ISO 8601 + /// representation on dates outside of that range, which is not supported by conforming RFC 3339 + /// parsers. #[cfg(feature = "alloc")] #[must_use] + #[deprecated( + since = "0.4.36", + note = "Produces invalid data on years outside of the range 0..=9999. Use `try_to_rfc3339()` or `to_iso8601` instead." + )] pub fn to_rfc3339(&self) -> String { - // For some reason a string with a capacity less than 32 is ca 20% slower when benchmarking. + self.to_iso8601() + } + + /// Returns an RFC 3339 date and time string such as `1996-12-19T16:39:57-08:00`. + /// This is also valid ISO 8601. + /// + /// # Errors + /// + /// RFC 3339 is only defined on years 0 through 9999. This method returns `None` on dates + /// outside of this range. + /// + /// # Example + /// + /// ```rust + /// # use chrono::{TimeZone, Utc}; + /// let dt = Utc.with_ymd_and_hms(2023, 6, 10, 9, 18, 25).unwrap(); + /// assert_eq!(dt.try_to_rfc3339(), Some("2023-06-10T09:18:25+00:00".to_owned())); + /// + /// let dt = Utc.with_ymd_and_hms(10_000, 1, 1, 0, 0, 0).unwrap(); + /// assert_eq!(dt.try_to_rfc3339(), None); + /// ``` + #[cfg(feature = "alloc")] + #[must_use] + pub fn try_to_rfc3339(&self) -> Option { + let year = self.year(); + if !(0..=9999).contains(&year) { + return None; + } + Some(self.to_iso8601()) + } + + /// Returns an ISO 8601 date and time string such as `1996-12-19T16:39:57-08:00`. + /// + /// Note that although the standard supports many different formats, we choose one that is + /// compatible with the RFC 3339 format for most common cases. + /// This format supports years outside of the range 0 through 9999, which RFC 3339 does not. + /// + /// # Example + /// + /// ```rust + /// # use chrono::{TimeZone, Utc}; + /// let dt = Utc.with_ymd_and_hms(2023, 6, 10, 9, 18, 25).unwrap(); + /// assert_eq!(dt.to_iso8601(), "2023-06-10T09:18:25+00:00"); + /// + /// let dt = Utc.with_ymd_and_hms(10_000, 1, 1, 0, 0, 0).unwrap(); + /// assert_eq!(dt.to_iso8601(), "+10000-01-01T00:00:00+00:00"); + /// + /// let dt = Utc.with_ymd_and_hms(-537, 6, 10, 9, 18, 25).unwrap(); + /// assert_eq!(dt.to_iso8601(), "-0537-06-10T09:18:25+00:00"); + /// ``` + #[cfg(feature = "alloc")] + #[must_use] + pub fn to_iso8601(&self) -> String { let mut result = String::with_capacity(32); let naive = self.overflowing_naive_local(); let offset = self.offset.fix(); @@ -643,12 +706,15 @@ impl DateTime { result } - /// Return an RFC 3339 and ISO 8601 date and time string with subseconds - /// formatted as per `SecondsFormat`. + /// Return an RFC 3339 and ISO 8601 date and time string with subseconds formatted as per + /// `SecondsFormat`. + /// + /// If `use_z` is `false` and the time zone is UTC the offset will be formatted as `+00:00`. + /// If `use_z` is `true` the offset will be formatted as `Z` instead. /// - /// If `use_z` is true and the timezone is UTC (offset 0), uses `Z` as - /// per [`Fixed::TimezoneOffsetColonZ`]. If `use_z` is false, uses - /// [`Fixed::TimezoneOffsetColon`] + /// Note that if the year of the `DateTime` is outside of the range 0 through 9999 then the date + /// while be formatted as an expanded representation according to ISO 8601. This makes the + /// string incompatible with RFC 3339. /// /// # Examples /// diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 81e5c1bb5..21a77b0d5 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -772,8 +772,8 @@ fn test_datetime_rfc3339() { // timezone 0 assert_eq!( - Utc.with_ymd_and_hms(2015, 2, 18, 23, 16, 9).unwrap().to_rfc3339(), - "2015-02-18T23:16:09+00:00" + Utc.with_ymd_and_hms(2015, 2, 18, 23, 16, 9).unwrap().try_to_rfc3339().as_deref(), + Some("2015-02-18T23:16:09+00:00") ); // timezone +05 assert_eq!( @@ -784,18 +784,18 @@ fn test_datetime_rfc3339() { .unwrap() ) .unwrap() - .to_rfc3339(), - "2015-02-18T23:16:09.150+05:00" + .try_to_rfc3339() + .as_deref(), + Some("2015-02-18T23:16:09.150+05:00") ); - assert_eq!(ymdhms_utc(2015, 2, 18, 23, 16, 9).to_rfc3339(), "2015-02-18T23:16:09+00:00"); assert_eq!( - ymdhms_milli(&edt5, 2015, 2, 18, 23, 16, 9, 150).to_rfc3339(), - "2015-02-18T23:16:09.150+05:00" + ymdhms_utc(2015, 2, 18, 23, 16, 9).try_to_rfc3339().as_deref(), + Some("2015-02-18T23:16:09+00:00") ); assert_eq!( - ymdhms_micro(&edt5, 2015, 2, 18, 23, 59, 59, 1_234_567).to_rfc3339(), - "2015-02-18T23:59:60.234567+05:00" + ymdhms_micro(&edt5, 2015, 2, 18, 23, 59, 59, 1_234_567).try_to_rfc3339().as_deref(), + Some("2015-02-18T23:59:60.234567+05:00") ); assert_eq!( DateTime::parse_from_rfc3339("2015-02-18T23:59:59.123+05:00"), @@ -815,12 +815,12 @@ fn test_datetime_rfc3339() { ); assert_eq!( - ymdhms_micro(&edt5, 2015, 2, 18, 23, 59, 59, 1_234_567).to_rfc3339(), - "2015-02-18T23:59:60.234567+05:00" + ymdhms_micro(&edt5, 2015, 2, 18, 23, 59, 59, 1_234_567).try_to_rfc3339().as_deref(), + Some("2015-02-18T23:59:60.234567+05:00") ); assert_eq!( - ymdhms_milli(&edt5, 2015, 2, 18, 23, 16, 9, 150).to_rfc3339(), - "2015-02-18T23:16:09.150+05:00" + ymdhms_milli(&edt5, 2015, 2, 18, 23, 16, 9, 150).try_to_rfc3339().as_deref(), + Some("2015-02-18T23:16:09.150+05:00") ); assert_eq!( DateTime::parse_from_rfc3339("2015-02-18T00:00:00.234567+05:00"), @@ -834,7 +834,10 @@ fn test_datetime_rfc3339() { DateTime::parse_from_rfc3339("2015-02-18 23:59:60.234567+05:00"), Ok(ymdhms_micro(&edt5, 2015, 2, 18, 23, 59, 59, 1_234_567)) ); - assert_eq!(ymdhms_utc(2015, 2, 18, 23, 16, 9).to_rfc3339(), "2015-02-18T23:16:09+00:00"); + assert_eq!( + ymdhms_utc(2015, 2, 18, 23, 16, 9).try_to_rfc3339().as_deref(), + Some("2015-02-18T23:16:09+00:00") + ); assert!(DateTime::parse_from_rfc3339("2015-02-18T23:59:60.234567 +05:00").is_err()); assert!(DateTime::parse_from_rfc3339("2015-02-18T23:059:60.234567+05:00").is_err()); @@ -1538,7 +1541,9 @@ fn test_min_max_getters() { // RFC 2822 doesn't support years with more than 4 digits. // assert_eq!(beyond_min.to_rfc2822(), ""); #[cfg(feature = "alloc")] - assert_eq!(beyond_min.to_rfc3339(), "-262144-12-31T22:00:00-02:00"); + assert_eq!(beyond_min.try_to_rfc3339(), None); // doesn't support years with more than 4 digits. + #[cfg(feature = "alloc")] + assert_eq!(beyond_min.to_iso8601(), "-262144-12-31T22:00:00-02:00"); #[cfg(feature = "alloc")] assert_eq!( beyond_min.format("%Y-%m-%dT%H:%M:%S%:z").to_string(), @@ -1563,7 +1568,9 @@ fn test_min_max_getters() { // RFC 2822 doesn't support years with more than 4 digits. // assert_eq!(beyond_max.to_rfc2822(), ""); #[cfg(feature = "alloc")] - assert_eq!(beyond_max.to_rfc3339(), "+262143-01-01T01:59:59.999999999+02:00"); + assert_eq!(beyond_max.try_to_rfc3339(), None); // doesn't support years with more than 4 digits. + #[cfg(feature = "alloc")] + assert_eq!(beyond_max.to_iso8601(), "+262143-01-01T01:59:59.999999999+02:00"); #[cfg(feature = "alloc")] assert_eq!( beyond_max.format("%Y-%m-%dT%H:%M:%S%.9f%:z").to_string(), diff --git a/src/format/formatting.rs b/src/format/formatting.rs index f3448f94f..522231d37 100644 --- a/src/format/formatting.rs +++ b/src/format/formatting.rs @@ -531,6 +531,10 @@ pub enum SecondsFormat { } /// Writes the date, time and offset to the string. same as `%Y-%m-%dT%H:%M:%S%.f%:z` +/// +/// This does not always output a valid RFC 3339 string. RFC 3339 is only defined on years +/// 0 through 9999. Instead we output an ISO 8601 string with a format that for common dates is +/// compatible with RFC 3339. #[inline] #[cfg(any(feature = "alloc", feature = "serde", feature = "rustc-serialize"))] pub(crate) fn write_rfc3339( diff --git a/src/format/mod.rs b/src/format/mod.rs index 75bc02ee3..8d7c8d0d0 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -244,6 +244,10 @@ pub enum Fixed { /// RFC 2822 date and time syntax. Commonly used for email and MIME date and time. RFC2822, /// RFC 3339 & ISO 8601 date and time syntax. + /// + /// Note that if the year of the `DateTime` is outside of the range 0 through 9999 then the date + /// while be formatted as an expanded representation according to ISO 8601. These dates are not + /// supported by, and incompatible with, RFC 3339. RFC3339, /// Internal uses only. diff --git a/src/lib.rs b/src/lib.rs index 917b39691..fafc8c9e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -305,7 +305,8 @@ //! assert_eq!(dt.format("%a %b %e %T %Y").to_string(), dt.format("%c").to_string()); //! assert_eq!(dt.to_string(), "2014-11-28 12:00:09 UTC"); //! assert_eq!(dt.to_rfc2822(), "Fri, 28 Nov 2014 12:00:09 +0000"); -//! assert_eq!(dt.to_rfc3339(), "2014-11-28T12:00:09+00:00"); +//! assert_eq!(dt.try_to_rfc3339().unwrap(), "2014-11-28T12:00:09+00:00"); +//! assert_eq!(dt.to_iso8601(), "2014-11-28T12:00:09+00:00"); //! assert_eq!(format!("{:?}", dt), "2014-11-28T12:00:09Z"); //! //! // Note that milli/nanoseconds are only printed if they are non-zero diff --git a/src/offset/fixed.rs b/src/offset/fixed.rs index 745dad29a..fa7d1c523 100644 --- a/src/offset/fixed.rs +++ b/src/offset/fixed.rs @@ -55,7 +55,7 @@ impl FixedOffset { /// let hour = 3600; /// let datetime = /// FixedOffset::east_opt(5 * hour).unwrap().with_ymd_and_hms(2016, 11, 08, 0, 0, 0).unwrap(); - /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00+05:00") + /// assert_eq!(&datetime.to_iso8601(), "2016-11-08T00:00:00+05:00") /// ``` #[must_use] pub const fn east_opt(secs: i32) -> Option { @@ -89,7 +89,7 @@ impl FixedOffset { /// let hour = 3600; /// let datetime = /// FixedOffset::west_opt(5 * hour).unwrap().with_ymd_and_hms(2016, 11, 08, 0, 0, 0).unwrap(); - /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00-05:00") + /// assert_eq!(&datetime.to_iso8601(), "2016-11-08T00:00:00-05:00") /// ``` #[must_use] pub const fn west_opt(secs: i32) -> Option {