From 9f219f120d549f543eb4935df1d4c61ee9359412 Mon Sep 17 00:00:00 2001 From: Andrea Corradi Date: Fri, 3 Jan 2025 21:22:04 +0100 Subject: [PATCH 1/3] Add gap_info_from_local_datetime to get information about a gap --- chrono-tz/src/lib.rs | 95 +++++++++++++++++++++++++++++++++- chrono-tz/src/timezone_impl.rs | 69 +++++++++++++++++++++++- 2 files changed, 162 insertions(+), 2 deletions(-) diff --git a/chrono-tz/src/lib.rs b/chrono-tz/src/lib.rs index 578c9a1..1c1f75f 100644 --- a/chrono-tz/src/lib.rs +++ b/chrono-tz/src/lib.rs @@ -140,7 +140,7 @@ mod timezone_impl; mod timezones; pub use crate::directory::*; -pub use crate::timezone_impl::{OffsetComponents, OffsetName, TzOffset}; +pub use crate::timezone_impl::{GapInfo, OffsetComponents, OffsetName, TzOffset}; pub use crate::timezones::ParseError; pub use crate::timezones::Tz; pub use crate::timezones::TZ_VARIANTS; @@ -159,6 +159,7 @@ mod tests { use super::Europe::Moscow; use super::Europe::Vilnius; use super::Europe::Warsaw; + use super::GapInfo; use super::Pacific::Apia; use super::Pacific::Noumea; use super::Pacific::Tahiti; @@ -166,6 +167,7 @@ mod tests { use super::IANA_TZDB_VERSION; use super::US::Eastern; use super::UTC; + use chrono::NaiveDateTime; use chrono::{Duration, NaiveDate, TimeZone}; #[test] @@ -514,4 +516,95 @@ mod tests { assert_eq!(format!("{}", dt.offset()), "+0245"); assert_eq!(format!("{:?}", dt.offset()), "+0245"); } + + fn gap_info_test(tz: Tz, gap_begin: NaiveDateTime, gap_end: NaiveDateTime) { + let before = gap_begin - Duration::seconds(1); + let before_offset = tz.offset_from_local_datetime(&before).single().unwrap(); + + let gap_end = tz.from_local_datetime(&gap_end).single().unwrap(); + + let in_gap = gap_begin + Duration::seconds(1); + let GapInfo { begin, end } = tz.gap_info_from_local_datetime(&in_gap).unwrap(); + let (begin_time, begin_offset) = begin.unwrap(); + let end = end.unwrap(); + + assert_eq!(gap_begin, begin_time); + assert_eq!(before_offset, begin_offset); + assert_eq!(gap_end, end); + } + + #[test] + fn gap_info_europe_london() { + gap_info_test( + Tz::Europe__London, + NaiveDate::from_ymd_opt(2024, 3, 31) + .unwrap() + .and_hms_opt(1, 0, 0) + .unwrap(), + NaiveDate::from_ymd_opt(2024, 3, 31) + .unwrap() + .and_hms_opt(2, 0, 0) + .unwrap(), + ); + } + + #[test] + fn gap_info_europe_dublin() { + gap_info_test( + Tz::Europe__Dublin, + NaiveDate::from_ymd_opt(2024, 3, 31) + .unwrap() + .and_hms_opt(1, 0, 0) + .unwrap(), + NaiveDate::from_ymd_opt(2024, 3, 31) + .unwrap() + .and_hms_opt(2, 0, 0) + .unwrap(), + ); + } + + #[test] + fn gap_info_australia_adelaide() { + gap_info_test( + Tz::Australia__Adelaide, + NaiveDate::from_ymd_opt(2024, 10, 6) + .unwrap() + .and_hms_opt(2, 0, 0) + .unwrap(), + NaiveDate::from_ymd_opt(2024, 10, 6) + .unwrap() + .and_hms_opt(3, 0, 0) + .unwrap(), + ); + } + + #[test] + fn gap_info_samoa_skips_a_day() { + gap_info_test( + Tz::Pacific__Apia, + NaiveDate::from_ymd_opt(2011, 12, 30) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(), + NaiveDate::from_ymd_opt(2011, 12, 31) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(), + ); + } + + #[test] + fn gap_info_libya_2013() { + gap_info_test( + Tz::Libya, + NaiveDate::from_ymd_opt(2013, 3, 29) + .unwrap() + .and_hms_opt(1, 0, 0) + .unwrap(), + NaiveDate::from_ymd_opt(2013, 3, 29) + .unwrap() + .and_hms_opt(2, 0, 0) + .unwrap(), + ); + } } diff --git a/chrono-tz/src/timezone_impl.rs b/chrono-tz/src/timezone_impl.rs index 490c78d..50e2dc2 100644 --- a/chrono-tz/src/timezone_impl.rs +++ b/chrono-tz/src/timezone_impl.rs @@ -2,7 +2,8 @@ use core::cmp::Ordering; use core::fmt::{Debug, Display, Error, Formatter, Write}; use chrono::{ - Duration, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, + DateTime, Duration, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset, + TimeZone, }; use crate::binary_search::binary_search; @@ -403,3 +404,69 @@ impl TimeZone for Tz { TzOffset::new(*self, timespans.get(index)) } } + +/// Represents the information of a gap. +pub struct GapInfo { + /// When available it contains information about the beginning of the gap. + /// + /// The time represents the first instant in which the gap starts. + /// This means that it is the first instant that when used with [`TimeZone::from_local_datetime`] + /// it will return [`LocalResult::None`]. + /// + /// The offset represents the offset of the first instant before the gap. + pub begin: Option<(NaiveDateTime, TzOffset)>, + /// When available it contains the first instant after the gap. + pub end: Option>, +} + +impl Tz { + /// Returns information about a gap. + /// + /// It returns `None` if `local` is not in a gap for the current timezone. + /// + /// If `local` is at the limits of the known timestamps the fields `begin` or `end` in + /// [`GapInfo`] will be `None`. + pub fn gap_info_from_local_datetime(&self, local: &NaiveDateTime) -> Option { + let timestamp = local.and_utc().timestamp(); + let timespans = self.timespans(); + let index = binary_search(0, timespans.len(), |i| { + timespans.local_span(i).cmp(timestamp) + }); + + match index { + Ok(_) => None, + Err(end_idx) => { + let begin = if end_idx == 0 { + None + } else { + let start_idx = end_idx - 1; + + timespans + .local_span(start_idx) + .end + .and_then(|start_time| DateTime::from_timestamp(start_time, 0)) + .map(|start_time| { + ( + start_time.naive_local(), + TzOffset::new(*self, timespans.get(start_idx)), + ) + }) + }; + let end = if end_idx == timespans.len() { + None + } else { + timespans + .local_span(end_idx) + .begin + .and_then(|end_time| DateTime::from_timestamp(end_time, 0)) + .and_then(|date_time| { + // we create the DateTime from a timestamp that exists in the timezone + self.from_local_datetime(&date_time.naive_local()).single() + }) + }; + + Some(GapInfo { begin, end }) + } + } + } +} From c0044194e0e7e549ceee9a9d61c17e4c3648017e Mon Sep 17 00:00:00 2001 From: Andrea Corradi Date: Thu, 16 Jan 2025 19:12:44 +0100 Subject: [PATCH 2/3] changes from review comments --- chrono-tz/src/lib.rs | 2 +- chrono-tz/src/timezone_impl.rs | 82 ++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/chrono-tz/src/lib.rs b/chrono-tz/src/lib.rs index 1c1f75f..fa5495d 100644 --- a/chrono-tz/src/lib.rs +++ b/chrono-tz/src/lib.rs @@ -524,7 +524,7 @@ mod tests { let gap_end = tz.from_local_datetime(&gap_end).single().unwrap(); let in_gap = gap_begin + Duration::seconds(1); - let GapInfo { begin, end } = tz.gap_info_from_local_datetime(&in_gap).unwrap(); + let GapInfo { begin, end } = GapInfo::new(&in_gap, &tz).unwrap(); let (begin_time, begin_offset) = begin.unwrap(); let end = end.unwrap(); diff --git a/chrono-tz/src/timezone_impl.rs b/chrono-tz/src/timezone_impl.rs index 50e2dc2..0a47903 100644 --- a/chrono-tz/src/timezone_impl.rs +++ b/chrono-tz/src/timezone_impl.rs @@ -406,6 +406,10 @@ impl TimeZone for Tz { } /// Represents the information of a gap. +/// +/// This returns useful information that can be used when converting a local [`NaiveDateTime`] +/// to a timezone-aware [`DateTime`] with [`TimeZone::from_local_datetime`] and a gap +/// ([`LocalResult::None`]) is found. pub struct GapInfo { /// When available it contains information about the beginning of the gap. /// @@ -419,54 +423,54 @@ pub struct GapInfo { pub end: Option>, } -impl Tz { - /// Returns information about a gap. +impl GapInfo { + /// Return information about a gap. /// /// It returns `None` if `local` is not in a gap for the current timezone. /// /// If `local` is at the limits of the known timestamps the fields `begin` or `end` in /// [`GapInfo`] will be `None`. - pub fn gap_info_from_local_datetime(&self, local: &NaiveDateTime) -> Option { + pub fn new(local: &NaiveDateTime, tz: &Tz) -> Option { let timestamp = local.and_utc().timestamp(); - let timespans = self.timespans(); + let timespans = tz.timespans(); let index = binary_search(0, timespans.len(), |i| { timespans.local_span(i).cmp(timestamp) }); - match index { - Ok(_) => None, - Err(end_idx) => { - let begin = if end_idx == 0 { - None - } else { - let start_idx = end_idx - 1; - - timespans - .local_span(start_idx) - .end - .and_then(|start_time| DateTime::from_timestamp(start_time, 0)) - .map(|start_time| { - ( - start_time.naive_local(), - TzOffset::new(*self, timespans.get(start_idx)), - ) - }) - }; - let end = if end_idx == timespans.len() { - None - } else { - timespans - .local_span(end_idx) - .begin - .and_then(|end_time| DateTime::from_timestamp(end_time, 0)) - .and_then(|date_time| { - // we create the DateTime from a timestamp that exists in the timezone - self.from_local_datetime(&date_time.naive_local()).single() - }) - }; - - Some(GapInfo { begin, end }) - } - } + let Err(end_idx) = index else { + return None; + }; + + let begin = if end_idx == 0 { + None + } else { + let start_idx = end_idx - 1; + + timespans + .local_span(start_idx) + .end + .and_then(|start_time| DateTime::from_timestamp(start_time, 0)) + .map(|start_time| { + ( + start_time.naive_local(), + TzOffset::new(*tz, timespans.get(start_idx)), + ) + }) + }; + + let end = if end_idx >= timespans.len() { + None + } else { + timespans + .local_span(end_idx) + .begin + .and_then(|end_time| DateTime::from_timestamp(end_time, 0)) + .and_then(|date_time| { + // we create the DateTime from a timestamp that exists in the timezone + tz.from_local_datetime(&date_time.naive_local()).single() + }) + }; + + Some(Self { begin, end }) } } From b83db99810553e3e8b0593180a770553cd167c25 Mon Sep 17 00:00:00 2001 From: Andrea Corradi Date: Fri, 17 Jan 2025 17:47:44 +0100 Subject: [PATCH 3/3] Use match --- chrono-tz/src/timezone_impl.rs | 54 ++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/chrono-tz/src/timezone_impl.rs b/chrono-tz/src/timezone_impl.rs index 0a47903..7c971af 100644 --- a/chrono-tz/src/timezone_impl.rs +++ b/chrono-tz/src/timezone_impl.rs @@ -441,34 +441,36 @@ impl GapInfo { return None; }; - let begin = if end_idx == 0 { - None - } else { - let start_idx = end_idx - 1; - - timespans - .local_span(start_idx) - .end - .and_then(|start_time| DateTime::from_timestamp(start_time, 0)) - .map(|start_time| { - ( - start_time.naive_local(), - TzOffset::new(*tz, timespans.get(start_idx)), - ) - }) + let begin = match end_idx { + 0 => None, + _ => { + let start_idx = end_idx - 1; + + timespans + .local_span(start_idx) + .end + .and_then(|start_time| DateTime::from_timestamp(start_time, 0)) + .map(|start_time| { + ( + start_time.naive_local(), + TzOffset::new(*tz, timespans.get(start_idx)), + ) + }) + } }; - let end = if end_idx >= timespans.len() { - None - } else { - timespans - .local_span(end_idx) - .begin - .and_then(|end_time| DateTime::from_timestamp(end_time, 0)) - .and_then(|date_time| { - // we create the DateTime from a timestamp that exists in the timezone - tz.from_local_datetime(&date_time.naive_local()).single() - }) + let end = match end_idx { + _ if end_idx >= timespans.len() => None, + _ => { + timespans + .local_span(end_idx) + .begin + .and_then(|end_time| DateTime::from_timestamp(end_time, 0)) + .and_then(|date_time| { + // we create the DateTime from a timestamp that exists in the timezone + tz.from_local_datetime(&date_time.naive_local()).single() + }) + } }; Some(Self { begin, end })