From 675b4a84e94335c01d0965b5cf1e2d33228a3bc4 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Thu, 28 Sep 2023 07:16:06 +0200 Subject: [PATCH 1/7] Remove unnecessary feature gate --- src/datetime/tests.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 9e41a5109..42b014983 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -1341,9 +1341,9 @@ fn test_min_max_getters() { assert_eq!(format!("{:?}", beyond_min), "-262144-12-31T22:00:00-02:00"); // RFC 2822 doesn't support years with more than 4 digits. // assert_eq!(beyond_min.to_rfc2822(), ""); - #[cfg(any(feature = "alloc", feature = "std"))] + #[cfg(feature = "alloc")] assert_eq!(beyond_min.to_rfc3339(), "-262144-12-31T22:00:00-02:00"); - #[cfg(any(feature = "alloc", feature = "std"))] + #[cfg(feature = "alloc")] assert_eq!( beyond_min.format("%Y-%m-%dT%H:%M:%S%:z").to_string(), "-262144-12-31T22:00:00-02:00" @@ -1366,9 +1366,9 @@ fn test_min_max_getters() { assert_eq!(format!("{:?}", beyond_max), "+262143-01-01T01:59:59.999999999+02:00"); // RFC 2822 doesn't support years with more than 4 digits. // assert_eq!(beyond_max.to_rfc2822(), ""); - #[cfg(any(feature = "alloc", feature = "std"))] + #[cfg(feature = "alloc")] assert_eq!(beyond_max.to_rfc3339(), "+262143-01-01T01:59:59.999999999+02:00"); - #[cfg(any(feature = "alloc", feature = "std"))] + #[cfg(feature = "alloc")] assert_eq!( beyond_max.format("%Y-%m-%dT%H:%M:%S%.9f%:z").to_string(), "+262143-01-01T01:59:59.999999999+02:00" From 7623426963f34892088754e042ed64881fa12f3a Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Fri, 9 Jun 2023 09:08:58 +0200 Subject: [PATCH 2/7] Add `try_to_rfc2822`, deprecate `to_rfc2822` --- bench/benches/chrono.rs | 4 +++- src/datetime/mod.rs | 35 ++++++++++++++++++++++++++++++----- src/datetime/tests.rs | 30 ++++++++++++++++-------------- src/format/formatting.rs | 7 ++++++- src/lib.rs | 4 ++-- 5 files changed, 57 insertions(+), 23 deletions(-) diff --git a/bench/benches/chrono.rs b/bench/benches/chrono.rs index 38f8513d4..7c20d9f7b 100644 --- a/bench/benches/chrono.rs +++ b/bench/benches/chrono.rs @@ -46,7 +46,9 @@ fn bench_datetime_to_rfc2822(c: &mut Criterion) { .unwrap(), ) .unwrap(); - c.bench_function("bench_datetime_to_rfc2822", |b| b.iter(|| black_box(dt).to_rfc2822())); + c.bench_function("bench_datetime_to_rfc2822", |b| { + b.iter(|| black_box(dt).try_to_rfc2822().unwrap()) + }); } fn bench_datetime_to_rfc3339(c: &mut Criterion) { diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 24a344d9a..39e683368 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -523,15 +523,40 @@ impl DateTime { /// /// # Panics /// - /// Panics if the date can not be represented in this format: the year may not be negative and - /// can not have more than 4 digits. + /// RFC 2822 is only defined on years 0 through 9999, and this method panics on dates outside + /// of that range. #[cfg(feature = "alloc")] #[must_use] + #[deprecated( + since = "0.4.32", + note = "Can panic on years outside of the range 0..=9999. Use `try_to_rfc2822()` instead." + )] pub fn to_rfc2822(&self) -> String { + self.try_to_rfc2822().expect("date outside of defined range for rfc2822") + } + + /// Returns an RFC 2822 date and time string such as `Tue, 1 Jul 2003 10:52:37 +0200`. + /// + /// # Errors + /// + /// RFC 2822 is only defined on years 0 through 9999, and this method returns an error on dates + /// outside of that 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_rfc2822(), Some("Sat, 10 Jun 2023 09:18:25 +0000".to_owned())); + /// + /// let dt = Utc.with_ymd_and_hms(10_000, 1, 1, 0, 0, 0).unwrap(); + /// assert_eq!(dt.try_to_rfc2822(), None); + /// ``` + #[cfg(feature = "alloc")] + pub fn try_to_rfc2822(&self) -> Option { let mut result = String::with_capacity(32); - write_rfc2822(&mut result, self.overflowing_naive_local(), self.offset.fix()) - .expect("writing rfc2822 datetime to string should never fail"); - result + write_rfc2822(&mut result, self.overflowing_naive_local(), self.offset.fix()).ok()?; + Some(result) } /// Returns an RFC 3339 and ISO 8601 date and time string such as `1996-12-19T16:39:57-08:00`. diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 42b014983..65e51f77d 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -457,12 +457,12 @@ fn test_datetime_rfc2822() { // timezone 0 assert_eq!( - Utc.with_ymd_and_hms(2015, 2, 18, 23, 16, 9).unwrap().to_rfc2822(), - "Wed, 18 Feb 2015 23:16:09 +0000" + Utc.with_ymd_and_hms(2015, 2, 18, 23, 16, 9).unwrap().try_to_rfc2822().as_deref(), + Some("Wed, 18 Feb 2015 23:16:09 +0000") ); assert_eq!( - Utc.with_ymd_and_hms(2015, 2, 1, 23, 16, 9).unwrap().to_rfc2822(), - "Sun, 1 Feb 2015 23:16:09 +0000" + Utc.with_ymd_and_hms(2015, 2, 1, 23, 16, 9).unwrap().try_to_rfc2822().as_deref(), + Some("Sun, 1 Feb 2015 23:16:09 +0000") ); // timezone +05 assert_eq!( @@ -473,8 +473,9 @@ fn test_datetime_rfc2822() { .unwrap() ) .unwrap() - .to_rfc2822(), - "Wed, 18 Feb 2015 23:16:09 +0500" + .try_to_rfc2822() + .as_deref(), + Some("Wed, 18 Feb 2015 23:16:09 +0500") ); assert_eq!( DateTime::parse_from_rfc2822("Wed, 18 Feb 2015 23:59:60 +0500"), @@ -508,8 +509,9 @@ fn test_datetime_rfc2822() { .unwrap() ) .unwrap() - .to_rfc2822(), - "Wed, 18 Feb 2015 23:59:60 +0500" + .try_to_rfc2822() + .as_deref(), + Some("Wed, 18 Feb 2015 23:59:60 +0500") ); assert_eq!( @@ -521,8 +523,8 @@ fn test_datetime_rfc2822() { Ok(FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2015, 2, 18, 23, 16, 9).unwrap()) ); assert_eq!( - ymdhms_micro(&edt, 2015, 2, 18, 23, 59, 59, 1_234_567).to_rfc2822(), - "Wed, 18 Feb 2015 23:59:60 +0500" + ymdhms_micro(&edt, 2015, 2, 18, 23, 59, 59, 1_234_567).try_to_rfc2822().as_deref(), + Some("Wed, 18 Feb 2015 23:59:60 +0500") ); assert_eq!( DateTime::parse_from_rfc2822("Wed, 18 Feb 2015 23:59:58 +0500"), @@ -1339,8 +1341,8 @@ fn test_min_max_getters() { let beyond_max = offset_max.from_utc_datetime(&NaiveDateTime::MAX); assert_eq!(format!("{:?}", beyond_min), "-262144-12-31T22:00:00-02:00"); - // RFC 2822 doesn't support years with more than 4 digits. - // assert_eq!(beyond_min.to_rfc2822(), ""); + #[cfg(feature = "alloc")] + assert_eq!(beyond_min.try_to_rfc2822(), None); // doesn't support years with more than 4 digits. #[cfg(feature = "alloc")] assert_eq!(beyond_min.to_rfc3339(), "-262144-12-31T22:00:00-02:00"); #[cfg(feature = "alloc")] @@ -1364,8 +1366,8 @@ fn test_min_max_getters() { assert_eq!(beyond_min.nanosecond(), 0); assert_eq!(format!("{:?}", beyond_max), "+262143-01-01T01:59:59.999999999+02:00"); - // RFC 2822 doesn't support years with more than 4 digits. - // assert_eq!(beyond_max.to_rfc2822(), ""); + #[cfg(feature = "alloc")] + assert_eq!(beyond_max.try_to_rfc2822(), None); // doesn't support years with more than 4 digits. #[cfg(feature = "alloc")] assert_eq!(beyond_max.to_rfc3339(), "+262143-01-01T01:59:59.999999999+02:00"); #[cfg(feature = "alloc")] diff --git a/src/format/formatting.rs b/src/format/formatting.rs index 9b055289c..6128b82b7 100644 --- a/src/format/formatting.rs +++ b/src/format/formatting.rs @@ -595,8 +595,13 @@ pub(crate) fn write_rfc3339( .format(w, off) } +/// Write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, same as `%a, %d %b %Y %H:%M:%S %z` +/// +/// # Errors +/// +/// RFC 2822 is only defined on years 0 through 9999, and this function returns an error on dates +/// outside that range. #[cfg(feature = "alloc")] -/// write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, same as `%a, %d %b %Y %H:%M:%S %z` pub(crate) fn write_rfc2822( w: &mut impl Write, dt: NaiveDateTime, diff --git a/src/lib.rs b/src/lib.rs index da8e00d17..4c086df05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -236,7 +236,7 @@ //! //! 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.try_to_rfc2822().unwrap(), "Fri, 28 Nov 2014 12:00:09 +0000"); //! assert_eq!(dt.to_rfc3339(), "2014-11-28T12:00:09+00:00"); //! assert_eq!(format!("{:?}", dt), "2014-11-28T12:00:09Z"); //! @@ -325,7 +325,7 @@ //! //! // Construct a datetime from epoch: //! let dt: DateTime = DateTime::from_timestamp(1_500_000_000, 0).unwrap(); -//! assert_eq!(dt.to_rfc2822(), "Fri, 14 Jul 2017 02:40:00 +0000"); +//! assert_eq!(dt.try_to_rfc2822(), Some("Fri, 14 Jul 2017 02:40:00 +0000".to_owned())); //! //! // Get epoch value from a datetime: //! let dt = DateTime::parse_from_rfc2822("Fri, 14 Jul 2017 02:40:00 +0000").unwrap(); From 99444edb4f9d657accf09324ebba52bbf41c18e9 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Mon, 24 Jul 2023 09:51:35 +0200 Subject: [PATCH 3/7] Test panic in `to_rfc2822` --- src/datetime/tests.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 65e51f77d..f084e86e9 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -452,6 +452,7 @@ fn test_datetime_with_timezone() { #[test] #[cfg(feature = "alloc")] +#[allow(deprecated)] fn test_datetime_rfc2822() { let edt = FixedOffset::east_opt(5 * 60 * 60).unwrap(); @@ -576,6 +577,31 @@ fn test_datetime_rfc2822() { assert!(DateTime::parse_from_rfc2822("Wed. 18 Feb 2015 23:16:09 +0000").is_err()); // *trailing* space causes failure assert!(DateTime::parse_from_rfc2822("Wed, 18 Feb 2015 23:16:09 +0000 ").is_err()); + + const RFC_2822_YEAR_MAX: i32 = 9999; + const RFC_2822_YEAR_MIN: i32 = 0; + + let dt = Utc.with_ymd_and_hms(RFC_2822_YEAR_MAX, 1, 2, 3, 4, 5).unwrap(); + assert_eq!(dt.to_rfc2822(), "Sat, 2 Jan 9999 03:04:05 +0000"); + + let dt = Utc.with_ymd_and_hms(RFC_2822_YEAR_MIN, 1, 2, 3, 4, 5).unwrap(); + assert_eq!(dt.to_rfc2822(), "Sun, 2 Jan 0000 03:04:05 +0000"); +} + +#[test] +#[should_panic] +#[cfg(feature = "alloc")] +#[allow(deprecated)] +fn test_rfc_2822_year_range_panic_high() { + let _ = Utc.with_ymd_and_hms(10000, 1, 2, 3, 4, 5).unwrap().to_rfc2822(); +} + +#[test] +#[should_panic] +#[cfg(feature = "alloc")] +#[allow(deprecated)] +fn test_rfc_2822_year_range_panic_low() { + let _ = Utc.with_ymd_and_hms(-1, 1, 2, 3, 4, 5).unwrap().to_rfc2822(); } #[test] From b9c32fd235f9cfda20ec71145389c3e673a07c9e Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Fri, 9 Jun 2023 09:20:12 +0200 Subject: [PATCH 4/7] Add `try_to_rfc3339` and `to_iso8601`, deprecate `to_rfc3339` --- bench/benches/chrono.rs | 4 ++- src/datetime/mod.rs | 67 ++++++++++++++++++++++++++++++++++++++-- src/datetime/tests.rs | 39 +++++++++++++---------- src/format/formatting.rs | 4 +++ src/format/mod.rs | 4 +++ src/lib.rs | 3 +- src/offset/fixed.rs | 4 +-- 7 files changed, 103 insertions(+), 22 deletions(-) diff --git a/bench/benches/chrono.rs b/bench/benches/chrono.rs index 7c20d9f7b..4277cb2cb 100644 --- a/bench/benches/chrono.rs +++ b/bench/benches/chrono.rs @@ -61,7 +61,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 39e683368..ea4621a82 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -559,11 +559,74 @@ impl DateTime { Some(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.32", + 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, and returns an error 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(); diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index f084e86e9..fe1d15cf9 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -612,8 +612,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!( @@ -624,18 +624,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"), @@ -655,12 +655,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"), @@ -674,7 +674,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()); @@ -1370,7 +1373,9 @@ fn test_min_max_getters() { #[cfg(feature = "alloc")] assert_eq!(beyond_min.try_to_rfc2822(), None); // doesn't support years with more than 4 digits. #[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(), @@ -1395,7 +1400,9 @@ fn test_min_max_getters() { #[cfg(feature = "alloc")] assert_eq!(beyond_max.try_to_rfc2822(), None); // doesn't support years with more than 4 digits. #[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 6128b82b7..f80e1dc2d 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 fda4d425b..cc8e2938e 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -242,6 +242,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 4c086df05..e2839756f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -237,7 +237,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.try_to_rfc2822().unwrap(), "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 6d3aebdb6..980c8d206 100644 --- a/src/offset/fixed.rs +++ b/src/offset/fixed.rs @@ -52,7 +52,7 @@ impl FixedOffset { /// .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 { @@ -88,7 +88,7 @@ impl FixedOffset { /// .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 { From 4faa45030ee311d64bf365ac22c9d6530b512d18 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Sat, 10 Jun 2023 09:34:48 +0200 Subject: [PATCH 5/7] Add note to `to_rfc3339_opts` --- src/datetime/mod.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index ea4621a82..717992616 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -635,12 +635,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 true and the timezone is UTC (offset 0), uses `Z` as - /// per [`Fixed::TimezoneOffsetColonZ`]. If `use_z` is false, uses - /// [`Fixed::TimezoneOffsetColon`] + /// 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. + /// + /// 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 /// From 43a64a5e5c77e7def962b0d5dd962df1245b81a2 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Sat, 10 Jun 2023 10:26:01 +0200 Subject: [PATCH 6/7] Never return an error when formatting the `RFC2822` item. --- src/datetime/mod.rs | 6 +++++- src/format/formatting.rs | 22 ++++++++-------------- src/format/mod.rs | 4 ++++ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 717992616..182b705a7 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -554,8 +554,12 @@ impl DateTime { /// ``` #[cfg(feature = "alloc")] pub fn try_to_rfc2822(&self) -> Option { + let naive_local = self.overflowing_naive_local(); + if !(0..=9999).contains(&naive_local.year()) { + return None; + } let mut result = String::with_capacity(32); - write_rfc2822(&mut result, self.overflowing_naive_local(), self.offset.fix()).ok()?; + write_rfc2822(&mut result, naive_local, self.offset.fix()).ok()?; Some(result) } diff --git a/src/format/formatting.rs b/src/format/formatting.rs index f80e1dc2d..3f9b58831 100644 --- a/src/format/formatting.rs +++ b/src/format/formatting.rs @@ -599,24 +599,13 @@ pub(crate) fn write_rfc3339( .format(w, off) } -/// Write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, same as `%a, %d %b %Y %H:%M:%S %z` -/// -/// # Errors -/// -/// RFC 2822 is only defined on years 0 through 9999, and this function returns an error on dates -/// outside that range. +/// Write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, similar to `%a, %d %b %Y %H:%M:%S %z`. #[cfg(feature = "alloc")] pub(crate) fn write_rfc2822( w: &mut impl Write, dt: NaiveDateTime, off: FixedOffset, ) -> fmt::Result { - let year = dt.year(); - // RFC2822 is only defined on years 0 through 9999 - if !(0..=9999).contains(&year) { - return Err(fmt::Error); - } - let english = default_locale(); w.write_str(short_weekdays(english)[dt.weekday().num_days_from_sunday() as usize])?; @@ -630,8 +619,13 @@ pub(crate) fn write_rfc2822( w.write_char(' ')?; w.write_str(short_months(english)[dt.month0() as usize])?; w.write_char(' ')?; - write_hundreds(w, (year / 100) as u8)?; - write_hundreds(w, (year % 100) as u8)?; + let year = dt.year(); + if (0..=9999).contains(&year) { + write_hundreds(w, (year / 100) as u8)?; + write_hundreds(w, (year % 100) as u8)?; + } else { + write!(w, "{:04}", year)?; + } w.write_char(' ')?; let (hour, min, sec) = dt.time().hms(); diff --git a/src/format/mod.rs b/src/format/mod.rs index cc8e2938e..0e44a4ea5 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -240,6 +240,10 @@ pub enum Fixed { /// Parsing allows an optional colon. TimezoneOffsetZ, /// RFC 2822 date and time syntax. Commonly used for email and MIME date and time. + /// + /// This does not always output a strictly valid RFC 2822 string. RFC 2822 is only defined on + /// years 0 through 9999. We format a date outside these ranges anyway to prevent a panic when + /// formatting. RFC2822, /// RFC 3339 & ISO 8601 date and time syntax. /// From 4bc5075dd4013d5857cff0367a0b100d9566c277 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Sat, 10 Jun 2023 12:32:22 +0200 Subject: [PATCH 7/7] Correct documentation for `Numeric` year formatting items --- src/format/mod.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/format/mod.rs b/src/format/mod.rs index 0e44a4ea5..77274f324 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -102,16 +102,22 @@ pub enum Numeric { /// Full Gregorian year (FW=4, PW=∞). /// May accept years before 1 BCE or after 9999 CE, given an initial sign (+/-). Year, - /// Gregorian year divided by 100 (century number; FW=PW=2). Implies the non-negative year. + /// Gregorian year divided by 100 (century number; FW=2, PW=∞). + /// Parsing supports only positive centuries. + /// Formatting applies Euclidean division, year -1 is in century -1. YearDiv100, /// Gregorian year modulo 100 (FW=PW=2). Cannot be negative. + /// Formatting applies Euclidean modulus, year -1 is formatted as 99. YearMod100, /// Year in the ISO week date (FW=4, PW=∞). /// May accept years before 1 BCE or after 9999 CE, given an initial sign. IsoYear, - /// Year in the ISO week date, divided by 100 (FW=PW=2). Implies the non-negative year. + /// Year in the ISO week date, divided by 100 (FW=2, PW=∞). + /// Parsing supports only positive centuries. + /// Formatting applies Euclidean division, year -1 is in century -1. IsoYearDiv100, - /// Year in the ISO week date, modulo 100 (FW=PW=2). Cannot be negative. + /// Year in the ISO week date, modulo 100 (FW=PW=2). + /// Formatting applies Euclidean modulus, year -1 is formatted as 99. IsoYearMod100, /// Month (FW=PW=2). Month,