Skip to content

Commit

Permalink
Fix add and sub days implemention and test with dst timezone
Browse files Browse the repository at this point in the history
This previously called the `.datetime` field on the datetime, however this is problematic as it is in UTC time. It now correctly uses the `.naive_local()` method to get the local time as a NaiveTime
  • Loading branch information
esheppa committed Nov 19, 2022
1 parent 9e5eb49 commit e5bb5ce
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 2 deletions.
4 changes: 2 additions & 2 deletions src/datetime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ impl<Tz: TimeZone> DateTime<Tz> {
///
/// Returns `None` if the resulting date would be out of range.
pub fn checked_add_days(self, days: Days) -> Option<Self> {
self.datetime
self.naive_local()
.checked_add_days(days)?
.and_local_timezone(TimeZone::from_offset(&self.offset))
.single()
Expand All @@ -370,7 +370,7 @@ impl<Tz: TimeZone> DateTime<Tz> {
///
/// Returns `None` if the resulting date would be out of range.
pub fn checked_sub_days(self, days: Days) -> Option<Self> {
self.datetime
self.naive_local()
.checked_sub_days(days)?
.and_local_timezone(TimeZone::from_offset(&self.offset))
.single()
Expand Down
233 changes: 233 additions & 0 deletions src/datetime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,239 @@ use crate::offset::{FixedOffset, TimeZone, Utc};
use crate::oldtime::Duration;
#[cfg(feature = "clock")]
use crate::Datelike;
use crate::{Days, LocalResult, Months, NaiveDateTime};

#[derive(Clone)]
struct DstTester;

impl DstTester {
fn winter_offset() -> FixedOffset {
FixedOffset::east_opt(8 * 60 * 60).unwrap()
}
fn summer_offset() -> FixedOffset {
FixedOffset::east_opt(9 * 60 * 60).unwrap()
}

const TO_WINTER_MONTH_DAY: (u32, u32) = (4, 15);
const TO_SUMMER_MONTH_DAY: (u32, u32) = (9, 15);

fn transition_start_local() -> NaiveTime {
NaiveTime::from_hms_opt(2, 0, 0).unwrap()
}
}

impl TimeZone for DstTester {
type Offset = FixedOffset;

fn from_offset(_: &Self::Offset) -> Self {
DstTester
}

fn offset_from_local_date(&self, _: &NaiveDate) -> crate::LocalResult<Self::Offset> {
unimplemented!()
}

fn offset_from_local_datetime(
&self,
local: &NaiveDateTime,
) -> crate::LocalResult<Self::Offset> {
let local_to_winter_transition_start = NaiveDate::from_ymd_opt(
local.year(),
DstTester::TO_WINTER_MONTH_DAY.0,
DstTester::TO_WINTER_MONTH_DAY.1,
)
.unwrap()
.and_time(DstTester::transition_start_local());

let local_to_winter_transition_end = NaiveDate::from_ymd_opt(
local.year(),
DstTester::TO_WINTER_MONTH_DAY.0,
DstTester::TO_WINTER_MONTH_DAY.1,
)
.unwrap()
.and_time(DstTester::transition_start_local() - Duration::hours(1));

let local_to_summer_transition_start = NaiveDate::from_ymd_opt(
local.year(),
DstTester::TO_SUMMER_MONTH_DAY.0,
DstTester::TO_SUMMER_MONTH_DAY.1,
)
.unwrap()
.and_time(DstTester::transition_start_local());

let local_to_summer_transition_end = NaiveDate::from_ymd_opt(
local.year(),
DstTester::TO_SUMMER_MONTH_DAY.0,
DstTester::TO_SUMMER_MONTH_DAY.1,
)
.unwrap()
.and_time(DstTester::transition_start_local() + Duration::hours(1));

if *local < local_to_winter_transition_end || *local >= local_to_summer_transition_end {
LocalResult::Single(DstTester::summer_offset())
} else if *local >= local_to_winter_transition_start
&& *local < local_to_summer_transition_start
{
LocalResult::Single(DstTester::winter_offset())
} else if *local >= local_to_winter_transition_end
&& *local < local_to_winter_transition_start
{
LocalResult::Ambiguous(DstTester::winter_offset(), DstTester::summer_offset())
} else if *local >= local_to_summer_transition_start
&& *local < local_to_summer_transition_end
{
LocalResult::None
} else {
panic!("Unexpected local time {}", local)
}
}

fn offset_from_utc_date(&self, _: &NaiveDate) -> Self::Offset {
unimplemented!()
}

fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
let utc_to_winter_transition = NaiveDate::from_ymd_opt(
utc.year(),
DstTester::TO_WINTER_MONTH_DAY.0,
DstTester::TO_WINTER_MONTH_DAY.1,
)
.unwrap()
.and_time(DstTester::transition_start_local())
- DstTester::summer_offset();

let utc_to_summer_transition = NaiveDate::from_ymd_opt(
utc.year(),
DstTester::TO_SUMMER_MONTH_DAY.0,
DstTester::TO_SUMMER_MONTH_DAY.1,
)
.unwrap()
.and_time(DstTester::transition_start_local())
- DstTester::winter_offset();

if *utc < utc_to_winter_transition || *utc >= utc_to_summer_transition {
DstTester::summer_offset()
} else if *utc >= utc_to_winter_transition && *utc < utc_to_summer_transition {
DstTester::winter_offset()
} else {
panic!("Unexpected utc time {}", utc)
}
}
}

#[test]
fn test_datetime_add_days() {
let est = FixedOffset::west_opt(5 * 60 * 60).unwrap();
let kst = FixedOffset::east_opt(9 * 60 * 60).unwrap();

assert_eq!(
format!("{}", est.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() + Days::new(5)),
"2014-05-11 07:08:09 -05:00"
);
assert_eq!(
format!("{}", kst.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() + Days::new(5)),
"2014-05-11 07:08:09 +09:00"
);

assert_eq!(
format!("{}", est.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() + Days::new(35)),
"2014-06-10 07:08:09 -05:00"
);
assert_eq!(
format!("{}", kst.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() + Days::new(35)),
"2014-06-10 07:08:09 +09:00"
);

assert_eq!(
format!("{}", DstTester.with_ymd_and_hms(2014, 4, 6, 7, 8, 9).unwrap() + Days::new(5)),
"2014-04-11 07:08:09 +09:00"
);
assert_eq!(
format!("{}", DstTester.with_ymd_and_hms(2014, 4, 6, 7, 8, 9).unwrap() + Days::new(10)),
"2014-04-16 07:08:09 +08:00"
);

assert_eq!(
format!("{}", DstTester.with_ymd_and_hms(2014, 9, 6, 7, 8, 9).unwrap() + Days::new(5)),
"2014-09-11 07:08:09 +08:00"
);
assert_eq!(
format!("{}", DstTester.with_ymd_and_hms(2014, 9, 6, 7, 8, 9).unwrap() + Days::new(10)),
"2014-09-16 07:08:09 +09:00"
);
}

#[test]
fn test_datetime_sub_days() {
let est = FixedOffset::west_opt(5 * 60 * 60).unwrap();
let kst = FixedOffset::east_opt(9 * 60 * 60).unwrap();

assert_eq!(
format!("{}", est.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() - Days::new(5)),
"2014-05-01 07:08:09 -05:00"
);
assert_eq!(
format!("{}", kst.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() - Days::new(5)),
"2014-05-01 07:08:09 +09:00"
);

assert_eq!(
format!("{}", est.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() - Days::new(35)),
"2014-04-01 07:08:09 -05:00"
);
assert_eq!(
format!("{}", kst.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() - Days::new(35)),
"2014-04-01 07:08:09 +09:00"
);
}

#[test]
fn test_datetime_add_months() {
let est = FixedOffset::west_opt(5 * 60 * 60).unwrap();
let kst = FixedOffset::east_opt(9 * 60 * 60).unwrap();

assert_eq!(
format!("{}", est.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() + Months::new(1)),
"2014-06-06 07:08:09 -05:00"
);
assert_eq!(
format!("{}", kst.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() + Months::new(1)),
"2014-06-06 07:08:09 +09:00"
);

assert_eq!(
format!("{}", est.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() + Months::new(5)),
"2014-10-06 07:08:09 -05:00"
);
assert_eq!(
format!("{}", kst.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() + Months::new(5)),
"2014-10-06 07:08:09 +09:00"
);
}

#[test]
fn test_datetime_sub_months() {
let est = FixedOffset::west_opt(5 * 60 * 60).unwrap();
let kst = FixedOffset::east_opt(9 * 60 * 60).unwrap();

assert_eq!(
format!("{}", est.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() - Months::new(1)),
"2014-04-06 07:08:09 -05:00"
);
assert_eq!(
format!("{}", kst.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() - Months::new(1)),
"2014-04-06 07:08:09 +09:00"
);

assert_eq!(
format!("{}", est.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() - Months::new(5)),
"2013-12-06 07:08:09 -05:00"
);
assert_eq!(
format!("{}", kst.with_ymd_and_hms(2014, 5, 6, 7, 8, 9).unwrap() - Months::new(5)),
"2013-12-06 07:08:09 +09:00"
);
}

#[test]
fn test_datetime_offset() {
Expand Down

0 comments on commit e5bb5ce

Please sign in to comment.