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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various fixes for formatting dates with a year outside of 0..=9999 #1144

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
8 changes: 6 additions & 2 deletions bench/benches/chrono.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -59,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) {
Expand Down
119 changes: 107 additions & 12 deletions src/datetime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,22 +523,114 @@
///
/// # 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<String> {
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())
.expect("writing rfc2822 datetime to string should never fail");
result
write_rfc2822(&mut result, 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`.
/// 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()
}

Check warning on line 582 in src/datetime/mod.rs

View check run for this annotation

Codecov / codecov/patch

src/datetime/mod.rs#L581-L582

Added lines #L581 - L582 were not covered by tests

/// Returns an RFC 3339 date and time string such as `1996-12-19T16:39:57-08:00`.
pitdicker marked this conversation as resolved.
Show resolved Hide resolved
/// 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<String> {
let year = self.year();
if !(0..=9999).contains(&year) {
pitdicker marked this conversation as resolved.
Show resolved Hide resolved
return None;
}
Some(self.to_iso8601())
}

/// Returns an ISO 8601 date and time string such as `1996-12-19T16:39:57-08:00`.
pitdicker marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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();
Expand All @@ -547,12 +639,15 @@
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
pitdicker marked this conversation as resolved.
Show resolved Hide resolved
/// string incompatible with RFC 3339.
///
/// # Examples
///
Expand Down
103 changes: 69 additions & 34 deletions src/datetime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,17 +452,18 @@ fn test_datetime_with_timezone() {

#[test]
#[cfg(feature = "alloc")]
#[allow(deprecated)]
fn test_datetime_rfc2822() {
let edt = FixedOffset::east_opt(5 * 60 * 60).unwrap();

// 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!(
Expand All @@ -473,8 +474,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"),
Expand Down Expand Up @@ -508,8 +510,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!(
Expand All @@ -521,8 +524,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"),
Expand Down Expand Up @@ -574,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();
pitdicker marked this conversation as resolved.
Show resolved Hide resolved
}

#[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]
Expand All @@ -584,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!(
Expand All @@ -596,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"),
Expand All @@ -627,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"),
Expand All @@ -646,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());
Expand Down Expand Up @@ -1339,11 +1370,13 @@ 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(any(feature = "alloc", feature = "std"))]
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.try_to_rfc2822(), None); // doesn't support years with more than 4 digits.
#[cfg(feature = "alloc")]
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(),
"-262144-12-31T22:00:00-02:00"
Expand All @@ -1364,11 +1397,13 @@ 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(any(feature = "alloc", feature = "std"))]
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.try_to_rfc2822(), None); // doesn't support years with more than 4 digits.
#[cfg(feature = "alloc")]
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(),
"+262143-01-01T01:59:59.999999999+02:00"
Expand Down