diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 5bd1ea0b37..3020c53236 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -366,6 +366,7 @@ impl DateTime { .checked_add_months(rhs)? .and_local_timezone(Tz::from_offset(&self.offset)) .single() + .ok() } /// Subtracts given `TimeDelta` from the current date and time. @@ -399,6 +400,7 @@ impl DateTime { .checked_sub_months(rhs)? .and_local_timezone(Tz::from_offset(&self.offset)) .single() + .ok() } /// Add a duration in [`Days`] to the date part of the `DateTime`. @@ -415,6 +417,7 @@ impl DateTime { .checked_add_days(days)? .and_local_timezone(TimeZone::from_offset(&self.offset)) .single() + .ok() } /// Subtract a duration in [`Days`] from the date part of the `DateTime`. @@ -431,6 +434,7 @@ impl DateTime { .checked_sub_days(days)? .and_local_timezone(TimeZone::from_offset(&self.offset)) .single() + .ok() } /// Subtracts another `DateTime` from the current date and time. @@ -732,7 +736,7 @@ where F: FnMut(NaiveDateTime) -> Option, { f(dt.overflowing_naive_local()) - .and_then(|datetime| dt.timezone().from_local_datetime(&datetime).single()) + .and_then(|datetime| dt.timezone().from_local_datetime(&datetime).single().ok()) .filter(|dt| dt >= &DateTime::::MIN_UTC && dt <= &DateTime::::MAX_UTC) } @@ -1645,7 +1649,7 @@ impl TryFrom for DateTime { } } }; - Utc.timestamp(sec, nsec).single().ok_or(OutOfRange::new()) + Utc.timestamp(sec, nsec).single().map_err(|_| OutOfRange::new()) } } diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 42e6f2c800..1c84508bd6 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -80,7 +80,7 @@ impl TimeZone for DstTester { } else if *local >= local_to_summer_transition_start && *local < local_to_summer_transition_end { - LocalResult::None + LocalResult::InGap } else { panic!("Unexpected local time {}", local) } diff --git a/src/error.rs b/src/error.rs index 770e2ef715..1e06e188ee 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,10 +1,18 @@ //! Error type use core::fmt; +use crate::offset::TzLookupError; + /// Error type for date and time operations. #[non_exhaustive] #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Error { + /// There is not enough information to create a date/time. + /// + /// An example is parsing a string with not enough date/time fields, or the result of a + /// time that is ambiguous during a time zone transitions (due to for example DST). + Ambiguous, + /// A date or datetime does not exist. /// /// Examples are: @@ -25,14 +33,19 @@ pub enum Error { /// An example is a date for the year 500.000, which is out of the range supported by chrono's /// types. OutOfRange, + + /// Lookup of a local datetime in a timezone failed. + TzLookupFailure(TzLookupError), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + Error::Ambiguous => write!(f, "not enough information for a concrete date/time"), Error::DoesNotExist => write!(f, "date or datetime does not exist"), Error::InvalidArgument => write!(f, "invalid parameter"), Error::OutOfRange => write!(f, "date outside of the supported range"), + Error::TzLookupFailure(e) => fmt::Display::fmt(&e, f), } } } diff --git a/src/format/parsed.rs b/src/format/parsed.rs index 0070b785b4..935d5c0a88 100644 --- a/src/format/parsed.rs +++ b/src/format/parsed.rs @@ -629,11 +629,7 @@ impl Parsed { let datetime = self.to_naive_datetime_with_offset(offset)?; let offset = FixedOffset::east(offset).ok_or(OUT_OF_RANGE)?; - match offset.from_local_datetime(&datetime) { - LocalResult::None => Err(IMPOSSIBLE), - LocalResult::Single(t) => Ok(t), - LocalResult::Ambiguous(..) => Err(NOT_ENOUGH), - } + Ok(offset.from_local_datetime(&datetime).single().unwrap()) } /// Returns a parsed timezone-aware date and time out of given fields, @@ -670,7 +666,6 @@ impl Parsed { // it will be 0 otherwise, but this is fine as the algorithm ignores offset for that case. let datetime = self.to_naive_datetime_with_offset(guessed_offset)?; match tz.from_local_datetime(&datetime) { - LocalResult::None => Err(IMPOSSIBLE), LocalResult::Single(t) => { if check_offset(&t) { Ok(t) @@ -687,6 +682,7 @@ impl Parsed { (true, true) => Err(NOT_ENOUGH), } } + LocalResult::InGap | LocalResult::Error(_) => Err(IMPOSSIBLE), } } } diff --git a/src/lib.rs b/src/lib.rs index fd8bcd7607..2b383742f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,7 +123,7 @@ //! #![cfg_attr(not(feature = "now"), doc = "```ignore")] #![cfg_attr(feature = "now", doc = "```rust")] -//! use chrono::offset::LocalResult; +//! use chrono::offset::{LocalResult, TzLookupError}; //! use chrono::prelude::*; //! //! # fn doctest() -> Option<()> { @@ -143,8 +143,8 @@ //! // dynamic verification //! assert_eq!(Utc.with_ymd_and_hms(2014, 7, 8, 21, 15, 33), //! LocalResult::Single(NaiveDate::from_ymd(2014, 7, 8).unwrap().and_hms(21, 15, 33).unwrap().and_utc())); -//! assert_eq!(Utc.with_ymd_and_hms(2014, 7, 8, 80, 15, 33), LocalResult::None); -//! assert_eq!(Utc.with_ymd_and_hms(2014, 7, 38, 21, 15, 33), LocalResult::None); +//! assert_eq!(Utc.with_ymd_and_hms(2014, 7, 8, 80, 15, 33), LocalResult::Error(TzLookupError::Other)); +//! assert_eq!(Utc.with_ymd_and_hms(2014, 7, 38, 21, 15, 33), LocalResult::Error(TzLookupError::Other)); //! //! # #[cfg(feature = "clock")] { //! // other time zone objects can be used to construct a local datetime. diff --git a/src/naive/datetime/tests.rs b/src/naive/datetime/tests.rs index 66072c22f8..1090ad1510 100644 --- a/src/naive/datetime/tests.rs +++ b/src/naive/datetime/tests.rs @@ -1,4 +1,5 @@ use super::NaiveDateTime; +use crate::offset::TzLookupError; use crate::{Datelike, FixedOffset, LocalResult, NaiveDate, TimeDelta, Utc}; #[test] @@ -563,13 +564,13 @@ fn test_and_timezone_min_max_dates() { if offset_hour >= 0 { assert_eq!(local_max.unwrap().naive_local(), NaiveDateTime::MAX); } else { - assert_eq!(local_max, LocalResult::None); + assert_eq!(local_max, LocalResult::Error(TzLookupError::OutOfRange)); } 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); + assert_eq!(local_min, LocalResult::Error(TzLookupError::OutOfRange)); } } } diff --git a/src/offset/local/mod.rs b/src/offset/local/mod.rs index 915653ef82..09c106e97d 100644 --- a/src/offset/local/mod.rs +++ b/src/offset/local/mod.rs @@ -235,7 +235,7 @@ fn lookup_with_dst_transitions( } else if dt == wall_latest { LocalResult::Single(t.offset_after) } else { - LocalResult::None + LocalResult::InGap } } }; diff --git a/src/offset/local/tz_info/mod.rs b/src/offset/local/tz_info/mod.rs index 780e15ace9..09f7fe7178 100644 --- a/src/offset/local/tz_info/mod.rs +++ b/src/offset/local/tz_info/mod.rs @@ -10,6 +10,8 @@ use std::{error, fmt, io}; mod timezone; pub(crate) use timezone::TimeZone; +use crate::offset::TzLookupError; + mod parser; mod rule; @@ -100,6 +102,29 @@ impl From for Error { } } +impl From for TzLookupError { + fn from(error: Error) -> TzLookupError { + match error { + Error::DateTime(_) => TzLookupError::InvalidTimeZoneData, + Error::FindLocalTimeType(_) => TzLookupError::InvalidTimeZoneData, + Error::LocalTimeType(_) => TzLookupError::InvalidTimeZoneData, + Error::InvalidSlice(_) => TzLookupError::InvalidTimeZoneData, + Error::InvalidTzString(_) => TzLookupError::InvalidTzString, + Error::InvalidTzFile(_) => TzLookupError::InvalidTimeZoneData, + Error::Io(_) => TzLookupError::InvalidTimeZoneData, + Error::OutOfRange(_) => TzLookupError::InvalidTimeZoneData, + Error::ParseInt(_) => TzLookupError::InvalidTimeZoneData, + Error::ProjectDateTime(_) => TzLookupError::InvalidTimeZoneData, + Error::SystemTime(_) => TzLookupError::InvalidTimeZoneData, + Error::TransitionRule(_) => TzLookupError::InvalidTimeZoneData, + Error::TimeZone(_) => TzLookupError::InvalidTimeZoneData, + Error::UnsupportedTzFile(_) => TzLookupError::InvalidTimeZoneData, + Error::UnsupportedTzString(_) => TzLookupError::InvalidTzString, + Error::Utf8(_) => TzLookupError::InvalidTimeZoneData, + } + } +} + /// Number of hours in one day const HOURS_PER_DAY: i64 = 24; /// Number of seconds in one hour diff --git a/src/offset/local/tz_info/rule.rs b/src/offset/local/tz_info/rule.rs index 0ff4fe7bde..4cf3d4c1c0 100644 --- a/src/offset/local/tz_info/rule.rs +++ b/src/offset/local/tz_info/rule.rs @@ -264,7 +264,7 @@ impl AlternateTime { } else if local_time > dst_start_transition_start && local_time < dst_start_transition_end { - Ok(crate::LocalResult::None) + Ok(crate::LocalResult::InGap) } else if local_time >= dst_start_transition_end && local_time < dst_end_transition_end { @@ -292,7 +292,7 @@ impl AlternateTime { } else if local_time >= dst_start_transition_start && local_time < dst_start_transition_end { - Ok(crate::LocalResult::None) + Ok(crate::LocalResult::InGap) } else { Ok(crate::LocalResult::Single(self.dst)) } @@ -317,7 +317,7 @@ impl AlternateTime { } else if local_time >= dst_end_transition_start && local_time < dst_end_transition_end { - Ok(crate::LocalResult::None) + Ok(crate::LocalResult::InGap) } else { Ok(crate::LocalResult::Single(self.std)) } @@ -329,7 +329,7 @@ impl AlternateTime { } else if local_time > dst_end_transition_start && local_time < dst_end_transition_end { - Ok(crate::LocalResult::None) + Ok(crate::LocalResult::InGap) } else if local_time >= dst_end_transition_end && local_time < dst_start_transition_end { diff --git a/src/offset/local/tz_info/timezone.rs b/src/offset/local/tz_info/timezone.rs index 0965a8a3e3..888236f749 100644 --- a/src/offset/local/tz_info/timezone.rs +++ b/src/offset/local/tz_info/timezone.rs @@ -275,7 +275,7 @@ impl<'a> TimeZoneRef<'a> { if local_leap_time <= transition_start { return Ok(crate::LocalResult::Single(prev)); } else if local_leap_time < transition_end { - return Ok(crate::LocalResult::None); + return Ok(crate::LocalResult::InGap); } else if local_leap_time == transition_end { return Ok(crate::LocalResult::Single(after_ltt)); } diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs index 66aa5f2d39..1301c21f6d 100644 --- a/src/offset/local/unix.rs +++ b/src/offset/local/unix.rs @@ -13,6 +13,7 @@ use std::{cell::RefCell, collections::hash_map, env, fs, hash::Hasher, time::Sys use super::tz_info::TimeZone; use super::{FixedOffset, NaiveDateTime}; use crate::{Datelike, LocalResult}; +use crate::offset::TzLookupError; pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult { offset(utc, false) @@ -66,7 +67,7 @@ impl Source { } struct Cache { - zone: TimeZone, + zone: Result, source: Source, last_checked: SystemTime, } @@ -77,13 +78,13 @@ const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo"; #[cfg(not(any(target_os = "android", target_os = "aix")))] const TZDB_LOCATION: &str = "/usr/share/zoneinfo"; -fn fallback_timezone() -> Option { - let tz_name = iana_time_zone::get_timezone().ok()?; +fn fallback_timezone() -> Result { + let tz_name = iana_time_zone::get_timezone().map_err(|_| TzLookupError::TimeZoneUnknown)?; #[cfg(not(target_os = "android"))] - let bytes = fs::read(format!("{}/{}", TZDB_LOCATION, tz_name)).ok()?; + let bytes = fs::read(format!("{}/{}", TZDB_LOCATION, tz_name)).map_err(|_| TzLookupError::TimeZoneNotFound)?; #[cfg(target_os = "android")] - let bytes = android_tzdata::find_tz_data(&tz_name).ok()?; - TimeZone::from_tz_data(&bytes).ok() + let bytes = android_tzdata::find_tz_data(&tz_name).map_err(|_| TzLookupError::TimeZoneNotFound)?; + TimeZone::from_tz_data(&bytes).map_err(|_| TzLookupError::InvalidTimeZoneData) } impl Default for Cache { @@ -99,8 +100,8 @@ impl Default for Cache { } } -fn current_zone(var: Option<&str>) -> TimeZone { - TimeZone::local(var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc) +fn current_zone(var: Option<&str>) -> Result { + TimeZone::local(var).or_else(|_| fallback_timezone()) } impl Cache { @@ -148,24 +149,28 @@ impl Cache { } } - if !local { - let offset = self - .zone - .find_local_time_type(d.timestamp()) - .expect("unable to select local time type") - .offset(); + let zone = match self.zone.as_ref() { + Ok(zone) => zone, + Err(e) => return LocalResult::Error(*e), + }; + if !local { + let offset = match zone.find_local_time_type(d.timestamp()) { + Ok(ltt) => ltt.offset(), + Err(e) => return LocalResult::Error(e.into()), + }; return match FixedOffset::east(offset) { Some(offset) => LocalResult::Single(offset), - None => LocalResult::None, + None => LocalResult::Error(TzLookupError::OutOfRange), // or invalid? }; } // we pass through the year as the year of a local point in time must either be valid in that locale, or // the entire time was skipped in which case we will return LocalResult::None anyway. - self.zone - .find_local_time_type_from_local(d.timestamp(), d.year()) - .expect("unable to select local time type") - .map(|o| FixedOffset::east(o.offset()).unwrap()) + let ltt = match zone.find_local_time_type_from_local(d.timestamp(), d.year()) { + Ok(ltt) => ltt, + Err(e) => return LocalResult::Error(e.into()), + }; + ltt.map(|o| FixedOffset::east(o.offset()).unwrap()) // FIXME: report out of range offset } } diff --git a/src/offset/local/windows.rs b/src/offset/local/windows.rs index 2842b8bd92..551c04a11e 100644 --- a/src/offset/local/windows.rs +++ b/src/offset/local/windows.rs @@ -16,6 +16,7 @@ use std::ptr; use super::win_bindings::{GetTimeZoneInformationForYear, SYSTEMTIME, TIME_ZONE_INFORMATION}; use crate::offset::local::{lookup_with_dst_transitions, Transition}; +use crate::offset::TzLookupError; use crate::{Datelike, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Weekday}; // We don't use `SystemTimeToTzSpecificLocalTime` because it doesn't support the same range of dates @@ -30,8 +31,8 @@ pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult tz_info, - None => return LocalResult::None, + Ok(tz_info) => tz_info, + Err(e) => return LocalResult::Error(e), }; let offset = match (tz_info.std_transition, tz_info.dst_transition) { (Some(std_transition), Some(dst_transition)) => { @@ -73,8 +74,8 @@ pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult LocalResult { let tz_info = match TzInfo::for_year(local.year()) { - Some(tz_info) => tz_info, - None => return LocalResult::None, + Ok(tz_info) => tz_info, + Err(e) => return LocalResult::Error(e), }; // Create a sorted slice of transitions and use `lookup_with_dst_transitions`. match (tz_info.std_transition, tz_info.dst_transition) { @@ -128,7 +129,7 @@ struct TzInfo { } impl TzInfo { - fn for_year(year: i32) -> Option { + fn for_year(year: i32) -> Result { // The API limits years to 1601..=30827. // Working with timezones and daylight saving time this far into the past or future makes // little sense. But whatever is extrapolated for 1601 or 30827 is what can be extrapolated @@ -137,13 +138,23 @@ impl TzInfo { let tz_info = unsafe { let mut tz_info = MaybeUninit::::uninit(); if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 { - return None; + return Err(TzLookupError::last_os_error()); } tz_info.assume_init() }; - Some(TzInfo { - std_offset: FixedOffset::west((tz_info.Bias + tz_info.StandardBias) * 60)?, - dst_offset: FixedOffset::west((tz_info.Bias + tz_info.DaylightBias) * 60)?, + let std_offset = (tz_info.Bias) + .checked_add(tz_info.StandardBias) + .and_then(|o| o.checked_mul(60)) + .and_then(FixedOffset::west) + .ok_or(TzLookupError::InvalidTimeZoneData)?; + let dst_offset = (tz_info.Bias) + .checked_add(tz_info.DaylightBias) + .and_then(|o| o.checked_mul(60)) + .and_then(FixedOffset::west) + .ok_or(TzLookupError::InvalidTimeZoneData)?; + Ok(TzInfo { + std_offset, + dst_offset, std_transition: system_time_from_naive_date_time(tz_info.StandardDate, year), dst_transition: system_time_from_naive_date_time(tz_info.DaylightDate, year), }) diff --git a/src/offset/mod.rs b/src/offset/mod.rs index 63b6067286..74913235c0 100644 --- a/src/offset/mod.rs +++ b/src/offset/mod.rs @@ -21,7 +21,7 @@ use core::fmt; use crate::naive::{NaiveDate, NaiveDateTime}; -use crate::DateTime; +use crate::{DateTime, Error}; pub(crate) mod fixed; pub use self::fixed::FixedOffset; @@ -35,43 +35,52 @@ pub(crate) mod utc; pub use self::utc::Utc; /// The conversion result from the local time to the timezone-aware datetime types. -#[derive(Clone, PartialEq, Debug, Copy, Eq, Hash)] +#[derive(Clone, PartialEq, Debug, Copy, Eq)] pub enum LocalResult { - /// Given local time representation is invalid. - /// This can occur when, for example, the positive timezone transition. - None, /// Given local time representation has a single unique result. Single(T), + /// Given local time representation has multiple results and thus ambiguous. /// This can occur when, for example, the negative timezone transition. Ambiguous(T /* min */, T /* max */), + + /// Given local time representation is invalid. + /// This can occur when, for example, the positive timezone transition. + InGap, + + /// Error type + Error(TzLookupError), } impl LocalResult { /// Returns `Some` only when the conversion result is unique, or `None` otherwise. #[must_use] - pub fn single(self) -> Option { + pub fn single(self) -> Result { match self { - LocalResult::Single(t) => Some(t), - _ => None, + LocalResult::Single(t) => Ok(t), + LocalResult::Ambiguous(_, _) => Err(Error::Ambiguous), + LocalResult::InGap => Err(Error::DoesNotExist), + LocalResult::Error(e) => Err(e.into()), } } /// Returns `Some` for the earliest possible conversion result, or `None` if none. #[must_use] - pub fn earliest(self) -> Option { + pub fn earliest(self) -> Result { match self { - LocalResult::Single(t) | LocalResult::Ambiguous(t, _) => Some(t), - _ => None, + LocalResult::Single(t) | LocalResult::Ambiguous(t, _) => Ok(t), + LocalResult::InGap => Err(Error::DoesNotExist), + LocalResult::Error(e) => Err(e.into()), } } /// Returns `Some` for the latest possible conversion result, or `None` if none. #[must_use] - pub fn latest(self) -> Option { + pub fn latest(self) -> Result { match self { - LocalResult::Single(t) | LocalResult::Ambiguous(_, t) => Some(t), - _ => None, + LocalResult::Single(t) | LocalResult::Ambiguous(_, t) => Ok(t), + LocalResult::InGap => Err(Error::DoesNotExist), + LocalResult::Error(e) => Err(e.into()), } } @@ -79,9 +88,10 @@ impl LocalResult { #[must_use] pub fn map U>(self, mut f: F) -> LocalResult { match self { - LocalResult::None => LocalResult::None, LocalResult::Single(v) => LocalResult::Single(f(v)), LocalResult::Ambiguous(min, max) => LocalResult::Ambiguous(f(min), f(max)), + LocalResult::InGap => LocalResult::InGap, + LocalResult::Error(e) => LocalResult::Error(e), } } } @@ -92,11 +102,12 @@ impl LocalResult { #[track_caller] pub fn unwrap(self) -> T { match self { - LocalResult::None => panic!("No such local time"), LocalResult::Single(t) => t, LocalResult::Ambiguous(t1, t2) => { panic!("Ambiguous local time, ranging from {:?} to {:?}", t1, t2) } + LocalResult::InGap => panic!("No such local time"), + LocalResult::Error(e) => panic!("{}", e), } } } @@ -132,7 +143,7 @@ pub trait TimeZone: Sized + Clone { ) -> LocalResult> { match NaiveDate::from_ymd(year, month, day).and_then(|d| d.and_hms(hour, min, sec)) { Ok(dt) => self.from_local_datetime(&dt), - Err(_) => LocalResult::None, + Err(_) => LocalResult::Error(TzLookupError::Other), // FIXME: change return type } } @@ -159,7 +170,7 @@ pub trait TimeZone: Sized + Clone { fn timestamp(&self, secs: i64, nsecs: u32) -> LocalResult> { match NaiveDateTime::from_timestamp(secs, nsecs) { Some(dt) => LocalResult::Single(self.from_utc_datetime(&dt)), - None => LocalResult::None, + None => LocalResult::Error(TzLookupError::Other), // FIXME: change return type } } @@ -183,7 +194,7 @@ pub trait TimeZone: Sized + Clone { fn timestamp_millis(&self, millis: i64) -> LocalResult> { match NaiveDateTime::from_timestamp_millis(millis) { Some(dt) => LocalResult::Single(self.from_utc_datetime(&dt)), - None => LocalResult::None, + None => LocalResult::Error(TzLookupError::Other), // FIXME: change return type } } @@ -221,7 +232,7 @@ pub trait TimeZone: Sized + Clone { fn timestamp_micros(&self, micros: i64) -> LocalResult> { match NaiveDateTime::from_timestamp_micros(micros) { Some(dt) => LocalResult::Single(self.from_utc_datetime(&dt)), - None => LocalResult::None, + None => LocalResult::Error(TzLookupError::Other), // FIXME: change return type } } @@ -234,13 +245,11 @@ 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> { - // 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::InGap => LocalResult::InGap, 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, + None => LocalResult::Error(TzLookupError::OutOfRange), }, LocalResult::Ambiguous(o1, o2) => { match (local.checked_sub_offset(o1.fix()), local.checked_sub_offset(o2.fix())) { @@ -248,9 +257,10 @@ pub trait TimeZone: Sized + Clone { DateTime::from_naive_utc_and_offset(d1, o1), DateTime::from_naive_utc_and_offset(d2, o2), ), - _ => LocalResult::None, + _ => LocalResult::Error(TzLookupError::OutOfRange), } } + LocalResult::Error(e) => LocalResult::Error(e), } } @@ -265,6 +275,77 @@ pub trait TimeZone: Sized + Clone { } } +/// Error type for time zone lookups. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum TzLookupError { + /// Unable to determine the local time zone of the os/platform. + TimeZoneUnknown, + + /// Error returned by a platform API. + OsError(i32), + + /// `TZ` environment variable set to an invalid value. + InvalidTzString, + + /// Unable to locate an IANA time zone database. + NoTzdb, + + /// The specified time zone is not found (in the database). + TimeZoneNotFound, + + /// There is an error when reading/validating the time zone data. + InvalidTimeZoneData, + + /// The result would be out of range. + OutOfRange, + + /// FIXME: temporary variant until the return type of some methods is changed to `Result`. + Other, +} + +impl fmt::Display for TzLookupError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TzLookupError::TimeZoneUnknown => write!(f, "unable to determine the local time zone"), + TzLookupError::OsError(code) => write!(f, "TODO"), + TzLookupError::InvalidTzString => { + write!(f, "`TZ` environment variable set to an invalid value") + } + TzLookupError::NoTzdb => write!(f, "unable to locate an IANA time zone database"), + TzLookupError::TimeZoneNotFound => write!(f, "the specified time zone is not found"), + TzLookupError::InvalidTimeZoneData => { + write!(f, "error when reading/validating the time zone data") + } + TzLookupError::OutOfRange => write!(f, "date or offset outside of the supported range"), + TzLookupError::Other => write!(f, "FIXME: temporary error type"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for TzLookupError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + MyError::OsError(code) => Some(std::io::Error::from_raw_os_error(*code)), + _ => None, + } + } +} + +impl From for Error { + fn from(error: TzLookupError) -> Self { + Error::TzLookupFailure(error) + } +} + +impl TzLookupError { + /// TODO + pub fn last_os_error() -> Self { + TzLookupError::OsError(std::io::Error::last_os_error().raw_os_error().unwrap()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -284,13 +365,13 @@ mod tests { if offset_hour >= 0 { assert_eq!(local_max.unwrap().naive_local(), NaiveDateTime::MAX); } else { - assert_eq!(local_max, LocalResult::None); + assert_eq!(local_max, LocalResult::Error(TzLookupError::OutOfRange)); } 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); + assert_eq!(local_min, LocalResult::Error(TzLookupError::OutOfRange)); } } } diff --git a/tests/dateutils.rs b/tests/dateutils.rs index 6ea0d8d097..564e5d13ca 100644 --- a/tests/dateutils.rs +++ b/tests/dateutils.rs @@ -40,9 +40,10 @@ fn verify_against_date_command_local(path: &'static str, dt: NaiveDateTime) { chrono::LocalResult::Single(a) => { assert_eq!(format!("{}\n", a), date_command_str); } - chrono::LocalResult::None => { + chrono::LocalResult::InGap => { assert_eq!("", date_command_str); } + chrono::LocalResult::Error(e) => panic!("{}", e), } }