Skip to content

Commit

Permalink
Fix panic in default impl of TimeZone::from_local_datetime
Browse files Browse the repository at this point in the history
  • Loading branch information
pitdicker committed Sep 26, 2023
1 parent 652bd78 commit cf62e46
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 4 deletions.
24 changes: 22 additions & 2 deletions src/naive/datetime/tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use super::NaiveDateTime;
use crate::duration::Duration as OldDuration;
use crate::NaiveDate;
use crate::{Datelike, FixedOffset, Utc};
use crate::{Datelike, FixedOffset, LocalResult, NaiveDate, Utc};

#[test]
fn test_datetime_from_timestamp_millis() {
Expand Down Expand Up @@ -490,3 +489,24 @@ fn test_checked_sub_offset() {
assert_eq!(dt.checked_add_offset(positive_offset), Some(dt + positive_offset));
assert_eq!(dt.checked_sub_offset(positive_offset), Some(dt - positive_offset));
}

#[test]
fn test_and_timezone_min_max_dates() {
for offset_hour in -23..=23 {
dbg!(offset_hour);
let offset = FixedOffset::east_opt(offset_hour * 60 * 60).unwrap();

let local_max = NaiveDateTime::MAX.and_local_timezone(offset);
if offset_hour >= 0 {
assert_eq!(local_max.unwrap().naive_local(), NaiveDateTime::MAX);
} else {
assert_eq!(local_max, LocalResult::None);
}
let local_min = NaiveDateTime::MIN.and_local_timezone(offset);
if offset_hour <= 0 {
assert_eq!(local_min.unwrap().naive_local(), NaiveDateTime::MIN);
} else {
assert_eq!(local_min, LocalResult::None);
}
}
}
46 changes: 44 additions & 2 deletions src/offset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,24 @@ pub trait TimeZone: Sized + Clone {
/// Converts the local `NaiveDateTime` to the timezone-aware `DateTime` if possible.
#[allow(clippy::wrong_self_convention)]
fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<DateTime<Self>> {
self.offset_from_local_datetime(local)
.map(|offset| DateTime::from_naive_utc_and_offset(*local - offset.fix(), offset))
// Return `LocalResult::None` when the offset pushes a value out of range, instead of
// panicking.
match self.offset_from_local_datetime(local) {
LocalResult::None => LocalResult::None,
LocalResult::Single(offset) => match local.checked_sub_offset(offset.fix()) {
Some(dt) => LocalResult::Single(DateTime::from_naive_utc_and_offset(dt, offset)),
None => LocalResult::None,
},
LocalResult::Ambiguous(o1, o2) => {
match (local.checked_sub_offset(o1.fix()), local.checked_sub_offset(o2.fix())) {
(Some(d1), Some(d2)) => LocalResult::Ambiguous(
DateTime::from_naive_utc_and_offset(d1, o1),
DateTime::from_naive_utc_and_offset(d2, o2),
),
_ => LocalResult::None,
}
}
}
}

/// Creates the offset for given UTC `NaiveDate`. This cannot fail.
Expand Down Expand Up @@ -531,6 +547,32 @@ pub trait TimeZone: Sized + Clone {
mod tests {
use super::*;

#[test]
fn test_fixed_offset_min_max_dates() {
for offset_hour in -23..=23 {
dbg!(offset_hour);
let offset = FixedOffset::east_opt(offset_hour * 60 * 60).unwrap();

let local_max = offset.from_utc_datetime(&NaiveDateTime::MAX);
assert_eq!(local_max.naive_utc(), NaiveDateTime::MAX);
let local_min = offset.from_utc_datetime(&NaiveDateTime::MIN);
assert_eq!(local_min.naive_utc(), NaiveDateTime::MIN);

let local_max = offset.from_local_datetime(&NaiveDateTime::MAX);
if offset_hour >= 0 {
assert_eq!(local_max.unwrap().naive_local(), NaiveDateTime::MAX);
} else {
assert_eq!(local_max, LocalResult::None);
}
let local_min = offset.from_local_datetime(&NaiveDateTime::MIN);
if offset_hour <= 0 {
assert_eq!(local_min.unwrap().naive_local(), NaiveDateTime::MIN);
} else {
assert_eq!(local_min, LocalResult::None);
}
}
}

#[test]
fn test_negative_millis() {
let dt = Utc.timestamp_millis_opt(-1000).unwrap();
Expand Down

0 comments on commit cf62e46

Please sign in to comment.