diff --git a/bench/benches/chrono.rs b/bench/benches/chrono.rs index 38f8513d4..4277cb2cb 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) { @@ -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) { diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 24a344d9a..182b705a7 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -523,22 +523,114 @@ 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 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() + } + + /// 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(); @@ -547,12 +639,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 9e41a5109..fe1d15cf9 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -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!( @@ -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"), @@ -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!( @@ -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"), @@ -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(); +} + +#[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] @@ -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!( @@ -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"), @@ -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"), @@ -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()); @@ -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" @@ -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" diff --git a/src/format/formatting.rs b/src/format/formatting.rs index 9b055289c..3f9b58831 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( @@ -595,19 +599,13 @@ pub(crate) fn write_rfc3339( .format(w, off) } +/// Write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, similar to `%a, %d %b %Y %H:%M:%S %z`. #[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, 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])?; @@ -621,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 fda4d425b..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, @@ -240,8 +246,16 @@ 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. + /// + /// 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 da8e00d17..e2839756f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -236,8 +236,9 @@ //! //! 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_rfc2822().unwrap(), "Fri, 28 Nov 2014 12:00:09 +0000"); +//! 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 @@ -325,7 +326,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(); 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 {