Repository: chronotope/chrono (language: rust)

Issue:
UTC Offset is not Send (cannot send DateTime across mps::channel)
I have a struct with a `DateTime<UTC>` field that I would like to send across a channel, like this:

```
pub struct Signal {
  pub time: DateTime<UTC>,
  pub raw: String
}
```

But when I want to create the channel, `rustc` complains that

```
error: the trait `core::marker::Send` is not implemented for the type `<chrono::offset::utc::UTC as chrono::offset::TimeZone>::Offset` [    E0277]    

let (new_signal_sender, new_signal_rcvr) = channel::<Signal>();
                                           ^~~~~~~~~~~~~~~~~
note: `<chrono::offset::utc::UTC as chrono::offset::TimeZone>::Offset` cannot be sent between threads safely
let (new_signal_sender, new_signal_rcvr) = channel::<Signal>();
```

Now `Send` is easily the part of Rust I understand the least, but I would have expected a DateTime object to be "sendable".



Discussion / hints:
Thank you for reporting. Chrono 0.2.3 fixed this issue.


Fix patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 1b0af82aaf..173883b433 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -29,10 +29,10 @@ jobs:
           cargo clippy --target=x86_64-pc-windows-msvc --all-targets --color=always \
             -- -D warnings
       - run: |
-          cargo clippy --manifest-path fuzz/Cargo.toml --color=always \
+          cargo clippy --manifest-path fuzz/Cargo.toml --all-targets --color=always \
             -- -D warnings
       - run: |
-          cargo clippy --manifest-path bench/Cargo.toml --color=always \
+          cargo clippy --manifest-path bench/Cargo.toml --all-targets --color=always \
             -- -D warnings
         env:
           RUSTFLAGS: "-Dwarnings"
diff --git a/Cargo.toml b/Cargo.toml
index 09c4b1f4ee..03cd467060 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,7 +51,7 @@ js-sys = { version = "0.3", optional = true }       # contains FFI bindings for
 windows-targets = { version = "0.52", optional = true }
 
 [target.'cfg(windows)'.dev-dependencies]
-windows-bindgen = { version = "0.54" } # The MSRV of its windows-metatada 0.54 dependency is 1.70
+windows-bindgen = { version = "0.55" } # The MSRV of its windows-metatada dependency is 1.70
 
 [target.'cfg(unix)'.dependencies]
 iana-time-zone = { version = "0.1.45", optional = true, features = ["fallback"] }
diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs
index d59a4c6204..6d98c37030 100644
--- a/src/datetime/mod.rs
+++ b/src/datetime/mod.rs
@@ -3,7 +3,7 @@
 
 //! ISO 8601 date and time with time zone.
 
-#[cfg(all(not(feature = "std"), feature = "alloc"))]
+#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))]
 use alloc::string::String;
 use core::borrow::Borrow;
 use core::cmp::Ordering;
@@ -46,7 +46,7 @@ mod tests;
 /// There are some constructors implemented here (the `from_*` methods), but
 /// the general-purpose constructors are all via the methods on the
 /// [`TimeZone`](./offset/trait.TimeZone.html) implementations.
-#[derive(Clone)]
+#[derive(Copy, Clone)]
 #[cfg_attr(
     any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
     derive(Archive, Deserialize, Serialize),
@@ -1260,10 +1260,6 @@ impl<Tz: TimeZone> Timelike for DateTime<Tz> {
     }
 }
 
-// we need them as automatic impls cannot handle associated types
-impl<Tz: TimeZone> Copy for DateTime<Tz> where <Tz as TimeZone>::Offset: Copy {}
-unsafe impl<Tz: TimeZone> Send for DateTime<Tz> where <Tz as TimeZone>::Offset: Send {}
-
 impl<Tz: TimeZone, Tz2: TimeZone> PartialEq<DateTime<Tz2>> for DateTime<Tz> {
     fn eq(&self, other: &DateTime<Tz2>) -> bool {
         self.datetime == other.datetime
diff --git a/src/format/formatting.rs b/src/format/formatting.rs
index de6e0205ae..54fda69ee5 100644
--- a/src/format/formatting.rs
+++ b/src/format/formatting.rs
@@ -3,7 +3,7 @@
 
 //! Date and time formatting routines.
 
-#[cfg(all(not(feature = "std"), feature = "alloc"))]
+#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))]
 use alloc::string::{String, ToString};
 #[cfg(feature = "alloc")]
 use core::borrow::Borrow;
@@ -20,8 +20,6 @@ use crate::{NaiveDate, NaiveTime, Weekday};
 
 #[cfg(feature = "alloc")]
 use super::locales;
-#[cfg(all(feature = "unstable-locales", feature = "alloc"))]
-use super::Locale;
 #[cfg(any(feature = "alloc", feature = "serde", feature = "rustc-serialize"))]
 use super::{Colons, OffsetFormat, OffsetPrecision, Pad};
 #[cfg(feature = "alloc")]
diff --git a/src/format/mod.rs b/src/format/mod.rs
index a5e0ae5b41..d145356229 100644
--- a/src/format/mod.rs
+++ b/src/format/mod.rs
@@ -30,7 +30,7 @@
 //! # Ok::<(), chrono::ParseError>(())
 //! ```
 
-#[cfg(all(not(feature = "std"), feature = "alloc"))]
+#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))]
 use alloc::boxed::Box;
 use core::fmt;
 use core::str::FromStr;
diff --git a/src/format/strftime.rs b/src/format/strftime.rs
index 32c483dbc0..d928c7bac6 100644
--- a/src/format/strftime.rs
+++ b/src/format/strftime.rs
@@ -165,7 +165,7 @@ use super::{locales, Locale};
 use super::{Fixed, InternalInternal, Item, Numeric, Pad};
 #[cfg(any(feature = "alloc", feature = "std"))]
 use super::{ParseError, BAD_FORMAT};
-#[cfg(feature = "alloc")]
+#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))]
 use alloc::vec::Vec;
 
 /// Parsing iterator for `strftime`-like format strings.
diff --git a/src/offset/local/win_bindings.rs b/src/offset/local/win_bindings.rs
index 39060adadd..ca2def1e65 100644
--- a/src/offset/local/win_bindings.rs
+++ b/src/offset/local/win_bindings.rs
@@ -1,10 +1,8 @@
-// Bindings generated by `windows-bindgen` 0.54.0
-
 #![allow(non_snake_case, non_upper_case_globals, non_camel_case_types, dead_code, clippy::all)]
-::windows_targets::link!("kernel32.dll" "system" fn GetTimeZoneInformationForYear(wyear : u16, pdtzi : *const DYNAMIC_TIME_ZONE_INFORMATION, ptzi : *mut TIME_ZONE_INFORMATION) -> BOOL);
-::windows_targets::link!("kernel32.dll" "system" fn SystemTimeToFileTime(lpsystemtime : *const SYSTEMTIME, lpfiletime : *mut FILETIME) -> BOOL);
-::windows_targets::link!("kernel32.dll" "system" fn SystemTimeToTzSpecificLocalTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lpuniversaltime : *const SYSTEMTIME, lplocaltime : *mut SYSTEMTIME) -> BOOL);
-::windows_targets::link!("kernel32.dll" "system" fn TzSpecificLocalTimeToSystemTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lplocaltime : *const SYSTEMTIME, lpuniversaltime : *mut SYSTEMTIME) -> BOOL);
+windows_targets::link!("kernel32.dll" "system" fn GetTimeZoneInformationForYear(wyear : u16, pdtzi : *const DYNAMIC_TIME_ZONE_INFORMATION, ptzi : *mut TIME_ZONE_INFORMATION) -> BOOL);
+windows_targets::link!("kernel32.dll" "system" fn SystemTimeToFileTime(lpsystemtime : *const SYSTEMTIME, lpfiletime : *mut FILETIME) -> BOOL);
+windows_targets::link!("kernel32.dll" "system" fn SystemTimeToTzSpecificLocalTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lpuniversaltime : *const SYSTEMTIME, lplocaltime : *mut SYSTEMTIME) -> BOOL);
+windows_targets::link!("kernel32.dll" "system" fn TzSpecificLocalTimeToSystemTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lplocaltime : *const SYSTEMTIME, lpuniversaltime : *mut SYSTEMTIME) -> BOOL);
 pub type BOOL = i32;
 pub type BOOLEAN = u8;
 #[repr(C)]
@@ -19,8 +17,8 @@ pub struct DYNAMIC_TIME_ZONE_INFORMATION {
     pub TimeZoneKeyName: [u16; 128],
     pub DynamicDaylightTimeDisabled: BOOLEAN,
 }
-impl ::core::marker::Copy for DYNAMIC_TIME_ZONE_INFORMATION {}
-impl ::core::clone::Clone for DYNAMIC_TIME_ZONE_INFORMATION {
+impl Copy for DYNAMIC_TIME_ZONE_INFORMATION {}
+impl Clone for DYNAMIC_TIME_ZONE_INFORMATION {
     fn clone(&self) -> Self {
         *self
     }
@@ -30,8 +28,8 @@ pub struct FILETIME {
     pub dwLowDateTime: u32,
     pub dwHighDateTime: u32,
 }
-impl ::core::marker::Copy for FILETIME {}
-impl ::core::clone::Clone for FILETIME {
+impl Copy for FILETIME {}
+impl Clone for FILETIME {
     fn clone(&self) -> Self {
         *self
     }
@@ -47,8 +45,8 @@ pub struct SYSTEMTIME {
     pub wSecond: u16,
     pub wMilliseconds: u16,
 }
-impl ::core::marker::Copy for SYSTEMTIME {}
-impl ::core::clone::Clone for SYSTEMTIME {
+impl Copy for SYSTEMTIME {}
+impl Clone for SYSTEMTIME {
     fn clone(&self) -> Self {
         *self
     }
@@ -63,8 +61,8 @@ pub struct TIME_ZONE_INFORMATION {
     pub DaylightDate: SYSTEMTIME,
     pub DaylightBias: i32,
 }
-impl ::core::marker::Copy for TIME_ZONE_INFORMATION {}
-impl ::core::clone::Clone for TIME_ZONE_INFORMATION {
+impl Copy for TIME_ZONE_INFORMATION {}
+impl Clone for TIME_ZONE_INFORMATION {
     fn clone(&self) -> Self {
         *self
     }
diff --git a/src/offset/local/win_bindings.txt b/src/offset/local/win_bindings.txt
index 7fb3e2fa1c..fc00509cfb 100644
--- a/src/offset/local/win_bindings.txt
+++ b/src/offset/local/win_bindings.txt
@@ -1,5 +1,5 @@
 --out src/offset/local/win_bindings.rs
---config flatten sys
+--config flatten sys no-bindgen-comment
 --filter
     Windows.Win32.System.Time.GetTimeZoneInformationForYear
     Windows.Win32.System.Time.SystemTimeToFileTime
diff --git a/src/offset/local/windows.rs b/src/offset/local/windows.rs
index 1319ec6fea..2370297cc1 100644
--- a/src/offset/local/windows.rs
+++ b/src/offset/local/windows.rs
@@ -9,7 +9,6 @@
 // except according to those terms.
 
 use std::cmp::Ordering;
-use std::convert::TryFrom;
 use std::mem::MaybeUninit;
 use std::ptr;
 
@@ -141,18 +140,38 @@ impl TzInfo {
             }
             tz_info.assume_init()
         };
+        let std_offset = (tz_info.Bias)
+            .checked_add(tz_info.StandardBias)
+            .and_then(|o| o.checked_mul(60))
+            .and_then(|o| FixedOffset::west(o).ok())?;
+        let dst_offset = (tz_info.Bias)
+            .checked_add(tz_info.DaylightBias)
+            .and_then(|o| o.checked_mul(60))
+            .and_then(|o| FixedOffset::west(o).ok())?;
         Some(TzInfo {
-            std_offset: FixedOffset::west((tz_info.Bias + tz_info.StandardBias) * 60).ok()?,
-            dst_offset: FixedOffset::west((tz_info.Bias + tz_info.DaylightBias) * 60).ok()?,
-            std_transition: system_time_from_naive_date_time(tz_info.StandardDate, year),
-            dst_transition: system_time_from_naive_date_time(tz_info.DaylightDate, year),
+            std_offset,
+            dst_offset,
+            std_transition: naive_date_time_from_system_time(tz_info.StandardDate, year).ok()?,
+            dst_transition: naive_date_time_from_system_time(tz_info.DaylightDate, year).ok()?,
         })
     }
 }
 
-fn system_time_from_naive_date_time(st: SYSTEMTIME, year: i32) -> Option<NaiveDateTime> {
+/// Resolve a `SYSTEMTIME` object to an `Option<NaiveDateTime>`.
+///
+/// A `SYSTEMTIME` within a `TIME_ZONE_INFORMATION` struct can be zero to indicate there is no
+/// transition.
+/// If it has year, month and day values it is a concrete date.
+/// If the year is missing the `SYSTEMTIME` is a rule, which this method resolves for the provided
+/// year. A rule has a month, weekday, and nth weekday of the month as components.
+///
+/// Returns `Err` if any of the values is invalid, which should never happen.
+fn naive_date_time_from_system_time(
+    st: SYSTEMTIME,
+    year: i32,
+) -> Result<Option<NaiveDateTime>, ()> {
     if st.wYear == 0 && st.wMonth == 0 {
-        return None; // No DST transitions for this year in this timezone.
+        return Ok(None);
     }
     let time = NaiveTime::from_hms_milli(
         st.wHour as u32,
@@ -160,25 +179,35 @@ fn system_time_from_naive_date_time(st: SYSTEMTIME, year: i32) -> Option<NaiveDa
         st.wSecond as u32,
         st.wMilliseconds as u32,
     )
-    .ok()?;
-    // In Chrono's Weekday, Monday is 0 whereas in SYSTEMTIME Monday is 1 and Sunday is 0.
-    // Therefore we move back one day after converting the u16 value to a Weekday.
-    let day_of_week = Weekday::try_from(u8::try_from(st.wDayOfWeek).ok()?).ok()?.pred();
+    .map_err(|_| ())?;
+
     if st.wYear != 0 {
-        return NaiveDate::from_ymd(st.wYear as i32, st.wMonth as u32, st.wDay as u32)
-            .ok()
-            .map(|d| d.and_time(time));
+        // We have a concrete date.
+        let date = NaiveDate::from_ymd(st.wYear as i32, st.wMonth as u32, st.wDay as u32)
+            .map_err(|_| ())?;
+        return Ok(Some(date.and_time(time)));
     }
-    let date = if let Some(date) =
-        NaiveDate::from_weekday_of_month(year, st.wMonth as u32, day_of_week, st.wDay as u8)
-    {
-        date
-    } else if st.wDay == 5 {
-        NaiveDate::from_weekday_of_month(year, st.wMonth as u32, day_of_week, 4)?
-    } else {
-        return None;
+
+    // Resolve a rule with month, weekday, and nth weekday of the month to a date in the current
+    // year.
+    let weekday = match st.wDayOfWeek {
+        0 => Weekday::Sun,
+        1 => Weekday::Mon,
+        2 => Weekday::Tue,
+        3 => Weekday::Wed,
+        4 => Weekday::Thu,
+        5 => Weekday::Fri,
+        6 => Weekday::Sat,
+        _ => return Err(()),
+    };
+    let nth_day = match st.wDay {
+        1..=5 => st.wDay as u8,
+        _ => return Err(()),
     };
-    Some(date.and_time(time))
+    let date = NaiveDate::from_weekday_of_month(year, st.wMonth as u32, weekday, nth_day)
+        .or_else(|| NaiveDate::from_weekday_of_month(year, st.wMonth as u32, weekday, 4))
+        .ok_or(())?; // `st.wMonth` must be invalid
+    Ok(Some(date.and_time(time)))
 }
 
 #[cfg(test)]
diff --git a/src/round.rs b/src/round.rs
index f493ea4922..06fbcca849 100644
--- a/src/round.rs
+++ b/src/round.rs
@@ -6,7 +6,6 @@
 use crate::{DateTime, NaiveDateTime, TimeDelta, TimeZone, Timelike};
 use core::cmp::Ordering;
 use core::fmt;
-use core::marker::Sized;
 use core::ops::{Add, Sub};
 
 /// Extension trait for subsecond rounding or truncation to a maximum number


Test patch:
diff --git a/src/naive/date/tests.rs b/src/naive/date/tests.rs
index 734e393393..997345407b 100644
--- a/src/naive/date/tests.rs
+++ b/src/naive/date/tests.rs
@@ -442,98 +442,102 @@ fn test_date_pred() {
 }
 
 #[test]
-fn test_date_add() {
-    fn check((y1, m1, d1): (i32, u32, u32), rhs: TimeDelta, ymd: Option<(i32, u32, u32)>) {
-        let lhs = NaiveDate::from_ymd(y1, m1, d1).unwrap();
-        let sum = ymd.map(|(y, m, d)| NaiveDate::from_ymd(y, m, d).unwrap());
-        assert_eq!(lhs.checked_add_signed(rhs), sum);
-        assert_eq!(lhs.checked_sub_signed(-rhs), sum);
+fn test_date_checked_add_signed() {
+    fn check(lhs: Option<NaiveDate>, delta: TimeDelta, rhs: Option<NaiveDate>) {
+        assert_eq!(lhs.unwrap().checked_add_signed(delta), rhs);
+        assert_eq!(lhs.unwrap().checked_sub_signed(-delta), rhs);
     }
+    let ymd = |y, m, d| NaiveDate::from_ymd(y, m, d).ok();
 
-    check((2014, 1, 1), TimeDelta::zero(), Some((2014, 1, 1)));
-    check((2014, 1, 1), TimeDelta::seconds(86399).unwrap(), Some((2014, 1, 1)));
+    check(ymd(2014, 1, 1), TimeDelta::zero(), ymd(2014, 1, 1));
+    check(ymd(2014, 1, 1), TimeDelta::seconds(86399).unwrap(), ymd(2014, 1, 1));
     // always round towards zero
-    check((2014, 1, 1), TimeDelta::seconds(-86399).unwrap(), Some((2014, 1, 1)));
-    check((2014, 1, 1), TimeDelta::days(1).unwrap(), Some((2014, 1, 2)));
-    check((2014, 1, 1), TimeDelta::days(-1).unwrap(), Some((2013, 12, 31)));
-    check((2014, 1, 1), TimeDelta::days(364).unwrap(), Some((2014, 12, 31)));
-    check((2014, 1, 1), TimeDelta::days(365 * 4 + 1).unwrap(), Some((2018, 1, 1)));
-    check((2014, 1, 1), TimeDelta::days(365 * 400 + 97).unwrap(), Some((2414, 1, 1)));
+    check(ymd(2014, 1, 1), TimeDelta::seconds(-86399).unwrap(), ymd(2014, 1, 1));
+    check(ymd(2014, 1, 1), TimeDelta::days(1).unwrap(), ymd(2014, 1, 2));
+    check(ymd(2014, 1, 1), TimeDelta::days(-1).unwrap(), ymd(2013, 12, 31));
+    check(ymd(2014, 1, 1), TimeDelta::days(364).unwrap(), ymd(2014, 12, 31));
+    check(ymd(2014, 1, 1), TimeDelta::days(365 * 4 + 1).unwrap(), ymd(2018, 1, 1));
+    check(ymd(2014, 1, 1), TimeDelta::days(365 * 400 + 97).unwrap(), ymd(2414, 1, 1));
 
-    check((-7, 1, 1), TimeDelta::days(365 * 12 + 3).unwrap(), Some((5, 1, 1)));
+    check(ymd(-7, 1, 1), TimeDelta::days(365 * 12 + 3).unwrap(), ymd(5, 1, 1));
 
     // overflow check
     check(
-        (0, 1, 1),
+        ymd(0, 1, 1),
         TimeDelta::days(MAX_DAYS_FROM_YEAR_0 as i64).unwrap(),
-        Some((MAX_YEAR, 12, 31)),
+        ymd(MAX_YEAR, 12, 31),
     );
-    check((0, 1, 1), TimeDelta::days(MAX_DAYS_FROM_YEAR_0 as i64 + 1).unwrap(), None);
-    check((0, 1, 1), TimeDelta::max_value(), None);
-    check((0, 1, 1), TimeDelta::days(MIN_DAYS_FROM_YEAR_0 as i64).unwrap(), Some((MIN_YEAR, 1, 1)));
-    check((0, 1, 1), TimeDelta::days(MIN_DAYS_FROM_YEAR_0 as i64 - 1).unwrap(), None);
-    check((0, 1, 1), TimeDelta::min_value(), None);
+    check(ymd(0, 1, 1), TimeDelta::days(MAX_DAYS_FROM_YEAR_0 as i64 + 1).unwrap(), None);
+    check(ymd(0, 1, 1), TimeDelta::max_value(), None);
+    check(ymd(0, 1, 1), TimeDelta::days(MIN_DAYS_FROM_YEAR_0 as i64).unwrap(), ymd(MIN_YEAR, 1, 1));
+    check(ymd(0, 1, 1), TimeDelta::days(MIN_DAYS_FROM_YEAR_0 as i64 - 1).unwrap(), None);
+    check(ymd(0, 1, 1), TimeDelta::min_value(), None);
 }
 
 #[test]
-fn test_date_sub() {
-    fn check((y1, m1, d1): (i32, u32, u32), (y2, m2, d2): (i32, u32, u32), diff: TimeDelta) {
-        let lhs = NaiveDate::from_ymd(y1, m1, d1).unwrap();
-        let rhs = NaiveDate::from_ymd(y2, m2, d2).unwrap();
-        assert_eq!(lhs.signed_duration_since(rhs), diff);
-        assert_eq!(rhs.signed_duration_since(lhs), -diff);
+fn test_date_signed_duration_since() {
+    fn check(lhs: Option<NaiveDate>, rhs: Option<NaiveDate>, delta: TimeDelta) {
+        assert_eq!(lhs.unwrap().signed_duration_since(rhs.unwrap()), delta);
+        assert_eq!(rhs.unwrap().signed_duration_since(lhs.unwrap()), -delta);
     }
+    let ymd = |y, m, d| NaiveDate::from_ymd(y, m, d).ok();
 
-    check((2014, 1, 1), (2014, 1, 1), TimeDelta::zero());
-    check((2014, 1, 2), (2014, 1, 1), TimeDelta::days(1).unwrap());
-    check((2014, 12, 31), (2014, 1, 1), TimeDelta::days(364).unwrap());
-    check((2015, 1, 3), (2014, 1, 1), TimeDelta::days(365 + 2).unwrap());
-    check((2018, 1, 1), (2014, 1, 1), TimeDelta::days(365 * 4 + 1).unwrap());
-    check((2414, 1, 1), (2014, 1, 1), TimeDelta::days(365 * 400 + 97).unwrap());
+    check(ymd(2014, 1, 1), ymd(2014, 1, 1), TimeDelta::zero());
+    check(ymd(2014, 1, 2), ymd(2014, 1, 1), TimeDelta::days(1).unwrap());
+    check(ymd(2014, 12, 31), ymd(2014, 1, 1), TimeDelta::days(364).unwrap());
+    check(ymd(2015, 1, 3), ymd(2014, 1, 1), TimeDelta::days(365 + 2).unwrap());
+    check(ymd(2018, 1, 1), ymd(2014, 1, 1), TimeDelta::days(365 * 4 + 1).unwrap());
+    check(ymd(2414, 1, 1), ymd(2014, 1, 1), TimeDelta::days(365 * 400 + 97).unwrap());
 
-    check((MAX_YEAR, 12, 31), (0, 1, 1), TimeDelta::days(MAX_DAYS_FROM_YEAR_0 as i64).unwrap());
-    check((MIN_YEAR, 1, 1), (0, 1, 1), TimeDelta::days(MIN_DAYS_FROM_YEAR_0 as i64).unwrap());
+    check(
+        ymd(MAX_YEAR, 12, 31),
+        ymd(0, 1, 1),
+        TimeDelta::days(MAX_DAYS_FROM_YEAR_0 as i64).unwrap(),
+    );
+    check(ymd(MIN_YEAR, 1, 1), ymd(0, 1, 1), TimeDelta::days(MIN_DAYS_FROM_YEAR_0 as i64).unwrap());
 }
 
 #[test]
 fn test_date_add_days() {
-    fn check((y1, m1, d1): (i32, u32, u32), rhs: Days, ymd: Option<(i32, u32, u32)>) {
-        let lhs = NaiveDate::from_ymd(y1, m1, d1).unwrap();
-        let sum = ymd.map(|(y, m, d)| NaiveDate::from_ymd(y, m, d).unwrap());
-        assert_eq!(lhs.checked_add_days(rhs), sum);
+    fn check(lhs: Option<NaiveDate>, days: Days, rhs: Option<NaiveDate>) {
+        assert_eq!(lhs.unwrap().checked_add_days(days), rhs);
     }
+    let ymd = |y, m, d| NaiveDate::from_ymd(y, m, d).ok();
 
-    check((2014, 1, 1), Days::new(0), Some((2014, 1, 1)));
+    check(ymd(2014, 1, 1), Days::new(0), ymd(2014, 1, 1));
     // always round towards zero
-    check((2014, 1, 1), Days::new(1), Some((2014, 1, 2)));
-    check((2014, 1, 1), Days::new(364), Some((2014, 12, 31)));
-    check((2014, 1, 1), Days::new(365 * 4 + 1), Some((2018, 1, 1)));
-    check((2014, 1, 1), Days::new(365 * 400 + 97), Some((2414, 1, 1)));
+    check(ymd(2014, 1, 1), Days::new(1), ymd(2014, 1, 2));
+    check(ymd(2014, 1, 1), Days::new(364), ymd(2014, 12, 31));
+    check(ymd(2014, 1, 1), Days::new(365 * 4 + 1), ymd(2018, 1, 1));
+    check(ymd(2014, 1, 1), Days::new(365 * 400 + 97), ymd(2414, 1, 1));
 
-    check((-7, 1, 1), Days::new(365 * 12 + 3), Some((5, 1, 1)));
+    check(ymd(-7, 1, 1), Days::new(365 * 12 + 3), ymd(5, 1, 1));
 
     // overflow check
-    check((0, 1, 1), Days::new(MAX_DAYS_FROM_YEAR_0.try_into().unwrap()), Some((MAX_YEAR, 12, 31)));
-    check((0, 1, 1), Days::new(u64::try_from(MAX_DAYS_FROM_YEAR_0).unwrap() + 1), None);
+    check(ymd(0, 1, 1), Days::new(MAX_DAYS_FROM_YEAR_0.try_into().unwrap()), ymd(MAX_YEAR, 12, 31));
+    check(ymd(0, 1, 1), Days::new(u64::try_from(MAX_DAYS_FROM_YEAR_0).unwrap() + 1), None);
 }
 
 #[test]
 fn test_date_sub_days() {
-    fn check((y1, m1, d1): (i32, u32, u32), (y2, m2, d2): (i32, u32, u32), diff: Days) {
-        let lhs = NaiveDate::from_ymd(y1, m1, d1).unwrap();
-        let rhs = NaiveDate::from_ymd(y2, m2, d2).unwrap();
-        assert_eq!(lhs - diff, rhs);
+    fn check(lhs: Option<NaiveDate>, days: Days, rhs: Option<NaiveDate>) {
+        assert_eq!(lhs.unwrap().checked_sub_days(days), rhs);
     }
+    let ymd = |y, m, d| NaiveDate::from_ymd(y, m, d).ok();
 
-    check((2014, 1, 1), (2014, 1, 1), Days::new(0));
-    check((2014, 1, 2), (2014, 1, 1), Days::new(1));
-    check((2014, 12, 31), (2014, 1, 1), Days::new(364));
-    check((2015, 1, 3), (2014, 1, 1), Days::new(365 + 2));
-    check((2018, 1, 1), (2014, 1, 1), Days::new(365 * 4 + 1));
-    check((2414, 1, 1), (2014, 1, 1), Days::new(365 * 400 + 97));
+    check(ymd(2014, 1, 1), Days::new(0), ymd(2014, 1, 1));
+    check(ymd(2014, 1, 2), Days::new(1), ymd(2014, 1, 1));
+    check(ymd(2014, 12, 31), Days::new(364), ymd(2014, 1, 1));
+    check(ymd(2015, 1, 3), Days::new(365 + 2), ymd(2014, 1, 1));
+    check(ymd(2018, 1, 1), Days::new(365 * 4 + 1), ymd(2014, 1, 1));
+    check(ymd(2414, 1, 1), Days::new(365 * 400 + 97), ymd(2014, 1, 1));
 
-    check((MAX_YEAR, 12, 31), (0, 1, 1), Days::new(MAX_DAYS_FROM_YEAR_0.try_into().unwrap()));
-    check((0, 1, 1), (MIN_YEAR, 1, 1), Days::new((-MIN_DAYS_FROM_YEAR_0).try_into().unwrap()));
+    check(ymd(MAX_YEAR, 12, 31), Days::new(MAX_DAYS_FROM_YEAR_0.try_into().unwrap()), ymd(0, 1, 1));
+    check(
+        ymd(0, 1, 1),
+        Days::new((-MIN_DAYS_FROM_YEAR_0).try_into().unwrap()),
+        ymd(MIN_YEAR, 1, 1),
+    );
 }
 
 #[test]