aboutsummaryrefslogtreecommitdiff
path: root/src/offset
diff options
context:
space:
mode:
Diffstat (limited to 'src/offset')
-rw-r--r--src/offset/fixed.rs212
-rw-r--r--src/offset/local.rs227
-rw-r--r--src/offset/local/mod.rs537
-rw-r--r--src/offset/local/tz_info/mod.rs116
-rw-r--r--src/offset/local/tz_info/parser.rs333
-rw-r--r--src/offset/local/tz_info/rule.rs1045
-rw-r--r--src/offset/local/tz_info/timezone.rs950
-rw-r--r--src/offset/local/unix.rs171
-rw-r--r--src/offset/local/win_bindings.rs71
-rw-r--r--src/offset/local/win_bindings.txt7
-rw-r--r--src/offset/local/windows.rs262
-rw-r--r--src/offset/mod.rs308
-rw-r--r--src/offset/utc.rs98
13 files changed, 3884 insertions, 453 deletions
diff --git a/src/offset/fixed.rs b/src/offset/fixed.rs
index 83f42a1..8f37558 100644
--- a/src/offset/fixed.rs
+++ b/src/offset/fixed.rs
@@ -4,22 +4,29 @@
//! The time zone which has a fixed offset from UTC.
use core::fmt;
-use core::ops::{Add, Sub};
-use oldtime::Duration as OldDuration;
+use core::str::FromStr;
+
+#[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
+use rkyv::{Archive, Deserialize, Serialize};
use super::{LocalResult, Offset, TimeZone};
-use div::div_mod_floor;
-use naive::{NaiveDate, NaiveDateTime, NaiveTime};
-use DateTime;
-use Timelike;
+use crate::format::{scan, ParseError, OUT_OF_RANGE};
+use crate::naive::{NaiveDate, NaiveDateTime};
/// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59.
///
/// Using the [`TimeZone`](./trait.TimeZone.html) methods
/// on a `FixedOffset` struct is the preferred way to construct
-/// `DateTime<FixedOffset>` instances. See the [`east`](#method.east) and
-/// [`west`](#method.west) methods for examples.
+/// `DateTime<FixedOffset>` instances. See the [`east_opt`](#method.east_opt) and
+/// [`west_opt`](#method.west_opt) methods for examples.
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
+#[cfg_attr(
+ any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
+ derive(Archive, Deserialize, Serialize),
+ archive(compare(PartialEq)),
+ archive_attr(derive(Clone, Copy, PartialEq, Eq, Hash, Debug))
+)]
+#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
pub struct FixedOffset {
local_minus_utc: i32,
}
@@ -29,16 +36,8 @@ impl FixedOffset {
/// The negative `secs` means the Western Hemisphere.
///
/// Panics on the out-of-bound `secs`.
- ///
- /// # Example
- ///
- /// ~~~~
- /// use chrono::{FixedOffset, TimeZone};
- /// let hour = 3600;
- /// let datetime = FixedOffset::east(5 * hour).ymd(2016, 11, 08)
- /// .and_hms(0, 0, 0);
- /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00+05:00")
- /// ~~~~
+ #[deprecated(since = "0.4.23", note = "use `east_opt()` instead")]
+ #[must_use]
pub fn east(secs: i32) -> FixedOffset {
FixedOffset::east_opt(secs).expect("FixedOffset::east out of bounds")
}
@@ -47,7 +46,21 @@ impl FixedOffset {
/// The negative `secs` means the Western Hemisphere.
///
/// Returns `None` on the out-of-bound `secs`.
- pub fn east_opt(secs: i32) -> Option<FixedOffset> {
+ ///
+ /// # Example
+ ///
+ #[cfg_attr(not(feature = "std"), doc = "```ignore")]
+ #[cfg_attr(feature = "std", doc = "```")]
+ /// use chrono::{FixedOffset, TimeZone};
+ /// let hour = 3600;
+ /// let datetime = FixedOffset::east_opt(5 * hour)
+ /// .unwrap()
+ /// .with_ymd_and_hms(2016, 11, 08, 0, 0, 0)
+ /// .unwrap();
+ /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00+05:00")
+ /// ```
+ #[must_use]
+ pub const fn east_opt(secs: i32) -> Option<FixedOffset> {
if -86_400 < secs && secs < 86_400 {
Some(FixedOffset { local_minus_utc: secs })
} else {
@@ -59,16 +72,8 @@ impl FixedOffset {
/// The negative `secs` means the Eastern Hemisphere.
///
/// Panics on the out-of-bound `secs`.
- ///
- /// # Example
- ///
- /// ~~~~
- /// use chrono::{FixedOffset, TimeZone};
- /// let hour = 3600;
- /// let datetime = FixedOffset::west(5 * hour).ymd(2016, 11, 08)
- /// .and_hms(0, 0, 0);
- /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00-05:00")
- /// ~~~~
+ #[deprecated(since = "0.4.23", note = "use `west_opt()` instead")]
+ #[must_use]
pub fn west(secs: i32) -> FixedOffset {
FixedOffset::west_opt(secs).expect("FixedOffset::west out of bounds")
}
@@ -77,7 +82,21 @@ impl FixedOffset {
/// The negative `secs` means the Eastern Hemisphere.
///
/// Returns `None` on the out-of-bound `secs`.
- pub fn west_opt(secs: i32) -> Option<FixedOffset> {
+ ///
+ /// # Example
+ ///
+ #[cfg_attr(not(feature = "std"), doc = "```ignore")]
+ #[cfg_attr(feature = "std", doc = "```")]
+ /// use chrono::{FixedOffset, TimeZone};
+ /// let hour = 3600;
+ /// let datetime = FixedOffset::west_opt(5 * hour)
+ /// .unwrap()
+ /// .with_ymd_and_hms(2016, 11, 08, 0, 0, 0)
+ /// .unwrap();
+ /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00-05:00")
+ /// ```
+ #[must_use]
+ pub const fn west_opt(secs: i32) -> Option<FixedOffset> {
if -86_400 < secs && secs < 86_400 {
Some(FixedOffset { local_minus_utc: -secs })
} else {
@@ -87,17 +106,26 @@ impl FixedOffset {
/// Returns the number of seconds to add to convert from UTC to the local time.
#[inline]
- pub fn local_minus_utc(&self) -> i32 {
+ pub const fn local_minus_utc(&self) -> i32 {
self.local_minus_utc
}
/// Returns the number of seconds to add to convert from the local time to UTC.
#[inline]
- pub fn utc_minus_local(&self) -> i32 {
+ pub const fn utc_minus_local(&self) -> i32 {
-self.local_minus_utc
}
}
+/// Parsing a `str` into a `FixedOffset` uses the format [`%z`](crate::format::strftime).
+impl FromStr for FixedOffset {
+ type Err = ParseError;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let (_, offset) = scan::timezone_offset(s, scan::colon_or_space, false, false, true)?;
+ Self::east_opt(offset).ok_or(OUT_OF_RANGE)
+ }
+}
+
impl TimeZone for FixedOffset {
type Offset = FixedOffset;
@@ -130,8 +158,10 @@ impl fmt::Debug for FixedOffset {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let offset = self.local_minus_utc;
let (sign, offset) = if offset < 0 { ('-', -offset) } else { ('+', offset) };
- let (mins, sec) = div_mod_floor(offset, 60);
- let (hour, min) = div_mod_floor(mins, 60);
+ let sec = offset.rem_euclid(60);
+ let mins = offset.div_euclid(60);
+ let min = mins.rem_euclid(60);
+ let hour = mins.div_euclid(60);
if sec == 0 {
write!(f, "{}{:02}:{:02}", sign, hour, min)
} else {
@@ -146,99 +176,63 @@ impl fmt::Display for FixedOffset {
}
}
-// addition or subtraction of FixedOffset to/from Timelike values is the same as
-// adding or subtracting the offset's local_minus_utc value
-// but keep keeps the leap second information.
-// this should be implemented more efficiently, but for the time being, this is generic right now.
-
-fn add_with_leapsecond<T>(lhs: &T, rhs: i32) -> T
-where
- T: Timelike + Add<OldDuration, Output = T>,
-{
- // extract and temporarily remove the fractional part and later recover it
- let nanos = lhs.nanosecond();
- let lhs = lhs.with_nanosecond(0).unwrap();
- (lhs + OldDuration::seconds(i64::from(rhs))).with_nanosecond(nanos).unwrap()
-}
-
-impl Add<FixedOffset> for NaiveTime {
- type Output = NaiveTime;
-
- #[inline]
- fn add(self, rhs: FixedOffset) -> NaiveTime {
- add_with_leapsecond(&self, rhs.local_minus_utc)
- }
-}
-
-impl Sub<FixedOffset> for NaiveTime {
- type Output = NaiveTime;
-
- #[inline]
- fn sub(self, rhs: FixedOffset) -> NaiveTime {
- add_with_leapsecond(&self, -rhs.local_minus_utc)
- }
-}
-
-impl Add<FixedOffset> for NaiveDateTime {
- type Output = NaiveDateTime;
-
- #[inline]
- fn add(self, rhs: FixedOffset) -> NaiveDateTime {
- add_with_leapsecond(&self, rhs.local_minus_utc)
- }
-}
-
-impl Sub<FixedOffset> for NaiveDateTime {
- type Output = NaiveDateTime;
-
- #[inline]
- fn sub(self, rhs: FixedOffset) -> NaiveDateTime {
- add_with_leapsecond(&self, -rhs.local_minus_utc)
- }
-}
-
-impl<Tz: TimeZone> Add<FixedOffset> for DateTime<Tz> {
- type Output = DateTime<Tz>;
-
- #[inline]
- fn add(self, rhs: FixedOffset) -> DateTime<Tz> {
- add_with_leapsecond(&self, rhs.local_minus_utc)
- }
-}
-
-impl<Tz: TimeZone> Sub<FixedOffset> for DateTime<Tz> {
- type Output = DateTime<Tz>;
-
- #[inline]
- fn sub(self, rhs: FixedOffset) -> DateTime<Tz> {
- add_with_leapsecond(&self, -rhs.local_minus_utc)
+#[cfg(all(feature = "arbitrary", feature = "std"))]
+impl arbitrary::Arbitrary<'_> for FixedOffset {
+ fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<FixedOffset> {
+ let secs = u.int_in_range(-86_399..=86_399)?;
+ let fixed_offset = FixedOffset::east_opt(secs)
+ .expect("Could not generate a valid chrono::FixedOffset. It looks like implementation of Arbitrary for FixedOffset is erroneous.");
+ Ok(fixed_offset)
}
}
#[cfg(test)]
mod tests {
use super::FixedOffset;
- use offset::TimeZone;
+ use crate::offset::TimeZone;
+ use std::str::FromStr;
#[test]
fn test_date_extreme_offset() {
// starting from 0.3 we don't have an offset exceeding one day.
// this makes everything easier!
+ let offset = FixedOffset::east_opt(86399).unwrap();
assert_eq!(
- format!("{:?}", FixedOffset::east(86399).ymd(2012, 2, 29)),
- "2012-02-29+23:59:59".to_string()
+ format!("{:?}", offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()),
+ "2012-02-29T05:06:07+23:59:59"
);
+ let offset = FixedOffset::east_opt(-86399).unwrap();
assert_eq!(
- format!("{:?}", FixedOffset::east(86399).ymd(2012, 2, 29).and_hms(5, 6, 7)),
- "2012-02-29T05:06:07+23:59:59".to_string()
+ format!("{:?}", offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()),
+ "2012-02-29T05:06:07-23:59:59"
);
+ let offset = FixedOffset::west_opt(86399).unwrap();
assert_eq!(
- format!("{:?}", FixedOffset::west(86399).ymd(2012, 3, 4)),
- "2012-03-04-23:59:59".to_string()
+ format!("{:?}", offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()),
+ "2012-03-04T05:06:07-23:59:59"
);
+ let offset = FixedOffset::west_opt(-86399).unwrap();
assert_eq!(
- format!("{:?}", FixedOffset::west(86399).ymd(2012, 3, 4).and_hms(5, 6, 7)),
- "2012-03-04T05:06:07-23:59:59".to_string()
+ format!("{:?}", offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()),
+ "2012-03-04T05:06:07+23:59:59"
);
}
+
+ #[test]
+ fn test_parse_offset() {
+ let offset = FixedOffset::from_str("-0500").unwrap();
+ assert_eq!(offset.local_minus_utc, -5 * 3600);
+ let offset = FixedOffset::from_str("-08:00").unwrap();
+ assert_eq!(offset.local_minus_utc, -8 * 3600);
+ let offset = FixedOffset::from_str("+06:30").unwrap();
+ assert_eq!(offset.local_minus_utc, (6 * 3600) + 1800);
+ }
+
+ #[test]
+ #[cfg(feature = "rkyv-validation")]
+ fn test_rkyv_validation() {
+ let offset = FixedOffset::from_str("-0500").unwrap();
+ let bytes = rkyv::to_bytes::<_, 4>(&offset).unwrap();
+ assert_eq!(rkyv::from_bytes::<FixedOffset>(&bytes).unwrap(), offset);
+ }
}
diff --git a/src/offset/local.rs b/src/offset/local.rs
deleted file mode 100644
index 1abb3a9..0000000
--- a/src/offset/local.rs
+++ /dev/null
@@ -1,227 +0,0 @@
-// This is a part of Chrono.
-// See README.md and LICENSE.txt for details.
-
-//! The local (system) time zone.
-
-#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
-use sys::{self, Timespec};
-
-use super::fixed::FixedOffset;
-use super::{LocalResult, TimeZone};
-#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
-use naive::NaiveTime;
-use naive::{NaiveDate, NaiveDateTime};
-use {Date, DateTime};
-#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
-use {Datelike, Timelike};
-
-/// Converts a `time::Tm` struct into the timezone-aware `DateTime`.
-/// This assumes that `time` is working correctly, i.e. any error is fatal.
-#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
-fn tm_to_datetime(mut tm: sys::Tm) -> DateTime<Local> {
- if tm.tm_sec >= 60 {
- tm.tm_nsec += (tm.tm_sec - 59) * 1_000_000_000;
- tm.tm_sec = 59;
- }
-
- #[cfg(not(windows))]
- fn tm_to_naive_date(tm: &sys::Tm) -> NaiveDate {
- // from_yo is more efficient than from_ymd (since it's the internal representation).
- NaiveDate::from_yo(tm.tm_year + 1900, tm.tm_yday as u32 + 1)
- }
-
- #[cfg(windows)]
- fn tm_to_naive_date(tm: &sys::Tm) -> NaiveDate {
- // ...but tm_yday is broken in Windows (issue #85)
- NaiveDate::from_ymd(tm.tm_year + 1900, tm.tm_mon as u32 + 1, tm.tm_mday as u32)
- }
-
- let date = tm_to_naive_date(&tm);
- let time = NaiveTime::from_hms_nano(
- tm.tm_hour as u32,
- tm.tm_min as u32,
- tm.tm_sec as u32,
- tm.tm_nsec as u32,
- );
- let offset = FixedOffset::east(tm.tm_utcoff);
- DateTime::from_utc(date.and_time(time) - offset, offset)
-}
-
-/// Converts a local `NaiveDateTime` to the `time::Timespec`.
-#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
-fn datetime_to_timespec(d: &NaiveDateTime, local: bool) -> sys::Timespec {
- // well, this exploits an undocumented `Tm::to_timespec` behavior
- // to get the exact function we want (either `timegm` or `mktime`).
- // the number 1 is arbitrary but should be non-zero to trigger `mktime`.
- let tm_utcoff = if local { 1 } else { 0 };
-
- let tm = sys::Tm {
- tm_sec: d.second() as i32,
- tm_min: d.minute() as i32,
- tm_hour: d.hour() as i32,
- tm_mday: d.day() as i32,
- tm_mon: d.month0() as i32, // yes, C is that strange...
- tm_year: d.year() - 1900, // this doesn't underflow, we know that d is `NaiveDateTime`.
- tm_wday: 0, // to_local ignores this
- tm_yday: 0, // and this
- tm_isdst: -1,
- tm_utcoff: tm_utcoff,
- // do not set this, OS APIs are heavily inconsistent in terms of leap second handling
- tm_nsec: 0,
- };
-
- tm.to_timespec()
-}
-
-/// The local timescale. This is implemented via the standard `time` crate.
-///
-/// Using the [`TimeZone`](./trait.TimeZone.html) methods
-/// on the Local struct is the preferred way to construct `DateTime<Local>`
-/// instances.
-///
-/// # Example
-///
-/// ~~~~
-/// use chrono::{Local, DateTime, TimeZone};
-///
-/// let dt: DateTime<Local> = Local::now();
-/// let dt: DateTime<Local> = Local.timestamp(0, 0);
-/// ~~~~
-#[derive(Copy, Clone, Debug)]
-pub struct Local;
-
-impl Local {
- /// Returns a `Date` which corresponds to the current date.
- pub fn today() -> Date<Local> {
- Local::now().date()
- }
-
- /// Returns a `DateTime` which corresponds to the current date.
- #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
- pub fn now() -> DateTime<Local> {
- tm_to_datetime(Timespec::now().local())
- }
-
- /// Returns a `DateTime` which corresponds to the current date.
- #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))]
- pub fn now() -> DateTime<Local> {
- use super::Utc;
- let now: DateTime<Utc> = super::Utc::now();
-
- // Workaround missing timezone logic in `time` crate
- let offset = FixedOffset::west((js_sys::Date::new_0().get_timezone_offset() as i32) * 60);
- DateTime::from_utc(now.naive_utc(), offset)
- }
-}
-
-impl TimeZone for Local {
- type Offset = FixedOffset;
-
- fn from_offset(_offset: &FixedOffset) -> Local {
- Local
- }
-
- // they are easier to define in terms of the finished date and time unlike other offsets
- fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<FixedOffset> {
- self.from_local_date(local).map(|date| *date.offset())
- }
-
- fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<FixedOffset> {
- self.from_local_datetime(local).map(|datetime| *datetime.offset())
- }
-
- fn offset_from_utc_date(&self, utc: &NaiveDate) -> FixedOffset {
- *self.from_utc_date(utc).offset()
- }
-
- fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> FixedOffset {
- *self.from_utc_datetime(utc).offset()
- }
-
- // override them for avoiding redundant works
- fn from_local_date(&self, local: &NaiveDate) -> LocalResult<Date<Local>> {
- // this sounds very strange, but required for keeping `TimeZone::ymd` sane.
- // in the other words, we use the offset at the local midnight
- // but keep the actual date unaltered (much like `FixedOffset`).
- let midnight = self.from_local_datetime(&local.and_hms(0, 0, 0));
- midnight.map(|datetime| Date::from_utc(*local, *datetime.offset()))
- }
-
- #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))]
- fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<DateTime<Local>> {
- let mut local = local.clone();
- // Get the offset from the js runtime
- let offset = FixedOffset::west((js_sys::Date::new_0().get_timezone_offset() as i32) * 60);
- local -= ::Duration::seconds(offset.local_minus_utc() as i64);
- LocalResult::Single(DateTime::from_utc(local, offset))
- }
-
- #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
- fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<DateTime<Local>> {
- let timespec = datetime_to_timespec(local, true);
-
- // datetime_to_timespec completely ignores leap seconds, so we need to adjust for them
- let mut tm = timespec.local();
- assert_eq!(tm.tm_nsec, 0);
- tm.tm_nsec = local.nanosecond() as i32;
-
- LocalResult::Single(tm_to_datetime(tm))
- }
-
- fn from_utc_date(&self, utc: &NaiveDate) -> Date<Local> {
- let midnight = self.from_utc_datetime(&utc.and_hms(0, 0, 0));
- Date::from_utc(*utc, *midnight.offset())
- }
-
- #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))]
- fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime<Local> {
- // Get the offset from the js runtime
- let offset = FixedOffset::west((js_sys::Date::new_0().get_timezone_offset() as i32) * 60);
- DateTime::from_utc(*utc, offset)
- }
-
- #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
- fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime<Local> {
- let timespec = datetime_to_timespec(utc, false);
-
- // datetime_to_timespec completely ignores leap seconds, so we need to adjust for them
- let mut tm = timespec.local();
- assert_eq!(tm.tm_nsec, 0);
- tm.tm_nsec = utc.nanosecond() as i32;
-
- tm_to_datetime(tm)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::Local;
- use offset::TimeZone;
- use Datelike;
-
- #[test]
- fn test_local_date_sanity_check() {
- // issue #27
- assert_eq!(Local.ymd(2999, 12, 28).day(), 28);
- }
-
- #[test]
- fn test_leap_second() {
- // issue #123
- let today = Local::today();
-
- let dt = today.and_hms_milli(1, 2, 59, 1000);
- let timestr = dt.time().to_string();
- // the OS API may or may not support the leap second,
- // but there are only two sensible options.
- assert!(timestr == "01:02:60" || timestr == "01:03:00", "unexpected timestr {:?}", timestr);
-
- let dt = today.and_hms_milli(1, 2, 3, 1234);
- let timestr = dt.time().to_string();
- assert!(
- timestr == "01:02:03.234" || timestr == "01:02:04.234",
- "unexpected timestr {:?}",
- timestr
- );
- }
-}
diff --git a/src/offset/local/mod.rs b/src/offset/local/mod.rs
new file mode 100644
index 0000000..37e7889
--- /dev/null
+++ b/src/offset/local/mod.rs
@@ -0,0 +1,537 @@
+// This is a part of Chrono.
+// See README.md and LICENSE.txt for details.
+
+//! The local (system) time zone.
+
+#[cfg(windows)]
+use std::cmp::Ordering;
+
+#[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
+use rkyv::{Archive, Deserialize, Serialize};
+
+use super::fixed::FixedOffset;
+use super::{LocalResult, TimeZone};
+use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime};
+#[allow(deprecated)]
+use crate::Date;
+use crate::{DateTime, Utc};
+
+#[cfg(unix)]
+#[path = "unix.rs"]
+mod inner;
+
+#[cfg(windows)]
+#[path = "windows.rs"]
+mod inner;
+
+#[cfg(all(windows, feature = "clock"))]
+#[allow(unreachable_pub)]
+mod win_bindings;
+
+#[cfg(all(
+ not(unix),
+ not(windows),
+ not(all(
+ target_arch = "wasm32",
+ feature = "wasmbind",
+ not(any(target_os = "emscripten", target_os = "wasi"))
+ ))
+))]
+mod inner {
+ use crate::{FixedOffset, LocalResult, NaiveDateTime};
+
+ pub(super) fn offset_from_utc_datetime(_utc_time: &NaiveDateTime) -> LocalResult<FixedOffset> {
+ LocalResult::Single(FixedOffset::east_opt(0).unwrap())
+ }
+
+ pub(super) fn offset_from_local_datetime(
+ _local_time: &NaiveDateTime,
+ ) -> LocalResult<FixedOffset> {
+ LocalResult::Single(FixedOffset::east_opt(0).unwrap())
+ }
+}
+
+#[cfg(all(
+ target_arch = "wasm32",
+ feature = "wasmbind",
+ not(any(target_os = "emscripten", target_os = "wasi"))
+))]
+mod inner {
+ use crate::{Datelike, FixedOffset, LocalResult, NaiveDateTime, Timelike};
+
+ pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset> {
+ let offset = js_sys::Date::from(utc.and_utc()).get_timezone_offset();
+ LocalResult::Single(FixedOffset::west_opt((offset as i32) * 60).unwrap())
+ }
+
+ pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset> {
+ let mut year = local.year();
+ if year < 100 {
+ // The API in `js_sys` does not let us create a `Date` with negative years.
+ // And values for years from `0` to `99` map to the years `1900` to `1999`.
+ // Shift the value by a multiple of 400 years until it is `>= 100`.
+ let shift_cycles = (year - 100).div_euclid(400);
+ year -= shift_cycles * 400;
+ }
+ let js_date = js_sys::Date::new_with_year_month_day_hr_min_sec(
+ year as u32,
+ local.month0() as i32,
+ local.day() as i32,
+ local.hour() as i32,
+ local.minute() as i32,
+ local.second() as i32,
+ // ignore milliseconds, our representation of leap seconds may be problematic
+ );
+ let offset = js_date.get_timezone_offset();
+ // We always get a result, even if this time does not exist or is ambiguous.
+ LocalResult::Single(FixedOffset::west_opt((offset as i32) * 60).unwrap())
+ }
+}
+
+#[cfg(unix)]
+mod tz_info;
+
+/// The local timescale.
+///
+/// Using the [`TimeZone`](./trait.TimeZone.html) methods
+/// on the Local struct is the preferred way to construct `DateTime<Local>`
+/// instances.
+///
+/// # Example
+///
+/// ```
+/// use chrono::{Local, DateTime, TimeZone};
+///
+/// let dt1: DateTime<Local> = Local::now();
+/// let dt2: DateTime<Local> = Local.timestamp_opt(0, 0).unwrap();
+/// assert!(dt1 >= dt2);
+/// ```
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(
+ any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
+ derive(Archive, Deserialize, Serialize),
+ archive(compare(PartialEq)),
+ archive_attr(derive(Clone, Copy, Debug))
+)]
+#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+pub struct Local;
+
+impl Local {
+ /// Returns a `Date` which corresponds to the current date.
+ #[deprecated(since = "0.4.23", note = "use `Local::now()` instead")]
+ #[allow(deprecated)]
+ #[must_use]
+ pub fn today() -> Date<Local> {
+ Local::now().date()
+ }
+
+ /// Returns a `DateTime<Local>` which corresponds to the current date, time and offset from
+ /// UTC.
+ ///
+ /// See also the similar [`Utc::now()`] which returns `DateTime<Utc>`, i.e. without the local
+ /// offset.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # #![allow(unused_variables)]
+ /// # use chrono::{DateTime, FixedOffset, Local};
+ /// // Current local time
+ /// let now = Local::now();
+ ///
+ /// // Current local date
+ /// let today = now.date_naive();
+ ///
+ /// // Current local time, converted to `DateTime<FixedOffset>`
+ /// let now_fixed_offset = Local::now().fixed_offset();
+ /// // or
+ /// let now_fixed_offset: DateTime<FixedOffset> = Local::now().into();
+ ///
+ /// // Current time in some timezone (let's use +05:00)
+ /// // Note that it is usually more efficient to use `Utc::now` for this use case.
+ /// let offset = FixedOffset::east_opt(5 * 60 * 60).unwrap();
+ /// let now_with_offset = Local::now().with_timezone(&offset);
+ /// ```
+ pub fn now() -> DateTime<Local> {
+ Utc::now().with_timezone(&Local)
+ }
+}
+
+impl TimeZone for Local {
+ type Offset = FixedOffset;
+
+ fn from_offset(_offset: &FixedOffset) -> Local {
+ Local
+ }
+
+ #[allow(deprecated)]
+ fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<FixedOffset> {
+ // Get the offset at local midnight.
+ self.offset_from_local_datetime(&local.and_time(NaiveTime::MIN))
+ }
+
+ fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<FixedOffset> {
+ inner::offset_from_local_datetime(local)
+ }
+
+ #[allow(deprecated)]
+ fn offset_from_utc_date(&self, utc: &NaiveDate) -> FixedOffset {
+ // Get the offset at midnight.
+ self.offset_from_utc_datetime(&utc.and_time(NaiveTime::MIN))
+ }
+
+ fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> FixedOffset {
+ inner::offset_from_utc_datetime(utc).unwrap()
+ }
+}
+
+#[cfg(windows)]
+#[derive(Copy, Clone, Eq, PartialEq)]
+struct Transition {
+ transition_utc: NaiveDateTime,
+ offset_before: FixedOffset,
+ offset_after: FixedOffset,
+}
+
+#[cfg(windows)]
+impl Transition {
+ fn new(
+ transition_local: NaiveDateTime,
+ offset_before: FixedOffset,
+ offset_after: FixedOffset,
+ ) -> Transition {
+ // It is no problem if the transition time in UTC falls a couple of hours inside the buffer
+ // space around the `NaiveDateTime` range (although it is very theoretical to have a
+ // transition at midnight around `NaiveDate::(MIN|MAX)`.
+ let transition_utc = transition_local.overflowing_sub_offset(offset_before);
+ Transition { transition_utc, offset_before, offset_after }
+ }
+}
+
+#[cfg(windows)]
+impl PartialOrd for Transition {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.transition_utc.cmp(&other.transition_utc))
+ }
+}
+
+#[cfg(windows)]
+impl Ord for Transition {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.transition_utc.cmp(&other.transition_utc)
+ }
+}
+
+// Calculate the time in UTC given a local time and transitions.
+// `transitions` must be sorted.
+#[cfg(windows)]
+fn lookup_with_dst_transitions(
+ transitions: &[Transition],
+ dt: NaiveDateTime,
+) -> LocalResult<FixedOffset> {
+ for t in transitions.iter() {
+ // A transition can result in the wall clock time going forward (creating a gap) or going
+ // backward (creating a fold). We are interested in the earliest and latest wall time of the
+ // transition, as this are the times between which `dt` does may not exist or is ambiguous.
+ //
+ // It is no problem if the transition times falls a couple of hours inside the buffer
+ // space around the `NaiveDateTime` range (although it is very theoretical to have a
+ // transition at midnight around `NaiveDate::(MIN|MAX)`.
+ let (offset_min, offset_max) =
+ match t.offset_after.local_minus_utc() > t.offset_before.local_minus_utc() {
+ true => (t.offset_before, t.offset_after),
+ false => (t.offset_after, t.offset_before),
+ };
+ let wall_earliest = t.transition_utc.overflowing_add_offset(offset_min);
+ let wall_latest = t.transition_utc.overflowing_add_offset(offset_max);
+
+ if dt < wall_earliest {
+ return LocalResult::Single(t.offset_before);
+ } else if dt <= wall_latest {
+ return match t.offset_after.local_minus_utc().cmp(&t.offset_before.local_minus_utc()) {
+ Ordering::Equal => LocalResult::Single(t.offset_before),
+ Ordering::Less => LocalResult::Ambiguous(t.offset_before, t.offset_after),
+ Ordering::Greater => {
+ if dt == wall_earliest {
+ LocalResult::Single(t.offset_before)
+ } else if dt == wall_latest {
+ LocalResult::Single(t.offset_after)
+ } else {
+ LocalResult::None
+ }
+ }
+ };
+ }
+ }
+ LocalResult::Single(transitions.last().unwrap().offset_after)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::Local;
+ #[cfg(windows)]
+ use crate::offset::local::{lookup_with_dst_transitions, Transition};
+ use crate::offset::TimeZone;
+ use crate::{Datelike, TimeDelta, Utc};
+ #[cfg(windows)]
+ use crate::{FixedOffset, LocalResult, NaiveDate, NaiveDateTime};
+
+ #[test]
+ fn verify_correct_offsets() {
+ let now = Local::now();
+ let from_local = Local.from_local_datetime(&now.naive_local()).unwrap();
+ let from_utc = Local.from_utc_datetime(&now.naive_utc());
+
+ assert_eq!(now.offset().local_minus_utc(), from_local.offset().local_minus_utc());
+ assert_eq!(now.offset().local_minus_utc(), from_utc.offset().local_minus_utc());
+
+ assert_eq!(now, from_local);
+ assert_eq!(now, from_utc);
+ }
+
+ #[test]
+ fn verify_correct_offsets_distant_past() {
+ let distant_past = Local::now() - TimeDelta::days(365 * 500);
+ let from_local = Local.from_local_datetime(&distant_past.naive_local()).unwrap();
+ let from_utc = Local.from_utc_datetime(&distant_past.naive_utc());
+
+ assert_eq!(distant_past.offset().local_minus_utc(), from_local.offset().local_minus_utc());
+ assert_eq!(distant_past.offset().local_minus_utc(), from_utc.offset().local_minus_utc());
+
+ assert_eq!(distant_past, from_local);
+ assert_eq!(distant_past, from_utc);
+ }
+
+ #[test]
+ fn verify_correct_offsets_distant_future() {
+ let distant_future = Local::now() + TimeDelta::days(365 * 35000);
+ let from_local = Local.from_local_datetime(&distant_future.naive_local()).unwrap();
+ let from_utc = Local.from_utc_datetime(&distant_future.naive_utc());
+
+ assert_eq!(
+ distant_future.offset().local_minus_utc(),
+ from_local.offset().local_minus_utc()
+ );
+ assert_eq!(distant_future.offset().local_minus_utc(), from_utc.offset().local_minus_utc());
+
+ assert_eq!(distant_future, from_local);
+ assert_eq!(distant_future, from_utc);
+ }
+
+ #[test]
+ fn test_local_date_sanity_check() {
+ // issue #27
+ assert_eq!(Local.with_ymd_and_hms(2999, 12, 28, 0, 0, 0).unwrap().day(), 28);
+ }
+
+ #[test]
+ fn test_leap_second() {
+ // issue #123
+ let today = Utc::now().date_naive();
+
+ if let Some(dt) = today.and_hms_milli_opt(15, 2, 59, 1000) {
+ let timestr = dt.time().to_string();
+ // the OS API may or may not support the leap second,
+ // but there are only two sensible options.
+ assert!(
+ timestr == "15:02:60" || timestr == "15:03:00",
+ "unexpected timestr {:?}",
+ timestr
+ );
+ }
+
+ if let Some(dt) = today.and_hms_milli_opt(15, 2, 3, 1234) {
+ let timestr = dt.time().to_string();
+ assert!(
+ timestr == "15:02:03.234" || timestr == "15:02:04.234",
+ "unexpected timestr {:?}",
+ timestr
+ );
+ }
+ }
+
+ #[test]
+ #[cfg(windows)]
+ fn test_lookup_with_dst_transitions() {
+ let ymdhms = |y, m, d, h, n, s| {
+ NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap()
+ };
+
+ #[track_caller]
+ #[allow(clippy::too_many_arguments)]
+ fn compare_lookup(
+ transitions: &[Transition],
+ y: i32,
+ m: u32,
+ d: u32,
+ h: u32,
+ n: u32,
+ s: u32,
+ result: LocalResult<FixedOffset>,
+ ) {
+ let dt = NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap();
+ assert_eq!(lookup_with_dst_transitions(transitions, dt), result);
+ }
+
+ // dst transition before std transition
+ // dst offset > std offset
+ let std = FixedOffset::east_opt(3 * 60 * 60).unwrap();
+ let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap();
+ let transitions = [
+ Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, dst),
+ Transition::new(ymdhms(2023, 10, 29, 3, 0, 0), dst, std),
+ ];
+ compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, LocalResult::None);
+ compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, LocalResult::Single(dst));
+ compare_lookup(&transitions, 2023, 3, 26, 4, 0, 0, LocalResult::Single(dst));
+
+ compare_lookup(&transitions, 2023, 10, 29, 1, 0, 0, LocalResult::Single(dst));
+ compare_lookup(&transitions, 2023, 10, 29, 2, 0, 0, LocalResult::Ambiguous(dst, std));
+ compare_lookup(&transitions, 2023, 10, 29, 2, 30, 0, LocalResult::Ambiguous(dst, std));
+ compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, LocalResult::Ambiguous(dst, std));
+ compare_lookup(&transitions, 2023, 10, 29, 4, 0, 0, LocalResult::Single(std));
+
+ // std transition before dst transition
+ // dst offset > std offset
+ let std = FixedOffset::east_opt(-5 * 60 * 60).unwrap();
+ let dst = FixedOffset::east_opt(-4 * 60 * 60).unwrap();
+ let transitions = [
+ Transition::new(ymdhms(2023, 3, 24, 3, 0, 0), dst, std),
+ Transition::new(ymdhms(2023, 10, 27, 2, 0, 0), std, dst),
+ ];
+ compare_lookup(&transitions, 2023, 3, 24, 1, 0, 0, LocalResult::Single(dst));
+ compare_lookup(&transitions, 2023, 3, 24, 2, 0, 0, LocalResult::Ambiguous(dst, std));
+ compare_lookup(&transitions, 2023, 3, 24, 2, 30, 0, LocalResult::Ambiguous(dst, std));
+ compare_lookup(&transitions, 2023, 3, 24, 3, 0, 0, LocalResult::Ambiguous(dst, std));
+ compare_lookup(&transitions, 2023, 3, 24, 4, 0, 0, LocalResult::Single(std));
+
+ compare_lookup(&transitions, 2023, 10, 27, 1, 0, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 10, 27, 2, 0, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 10, 27, 2, 30, 0, LocalResult::None);
+ compare_lookup(&transitions, 2023, 10, 27, 3, 0, 0, LocalResult::Single(dst));
+ compare_lookup(&transitions, 2023, 10, 27, 4, 0, 0, LocalResult::Single(dst));
+
+ // dst transition before std transition
+ // dst offset < std offset
+ let std = FixedOffset::east_opt(3 * 60 * 60).unwrap();
+ let dst = FixedOffset::east_opt((2 * 60 + 30) * 60).unwrap();
+ let transitions = [
+ Transition::new(ymdhms(2023, 3, 26, 2, 30, 0), std, dst),
+ Transition::new(ymdhms(2023, 10, 29, 2, 0, 0), dst, std),
+ ];
+ compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Ambiguous(std, dst));
+ compare_lookup(&transitions, 2023, 3, 26, 2, 15, 0, LocalResult::Ambiguous(std, dst));
+ compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, LocalResult::Ambiguous(std, dst));
+ compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, LocalResult::Single(dst));
+
+ compare_lookup(&transitions, 2023, 10, 29, 1, 0, 0, LocalResult::Single(dst));
+ compare_lookup(&transitions, 2023, 10, 29, 2, 0, 0, LocalResult::Single(dst));
+ compare_lookup(&transitions, 2023, 10, 29, 2, 15, 0, LocalResult::None);
+ compare_lookup(&transitions, 2023, 10, 29, 2, 30, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, LocalResult::Single(std));
+
+ // std transition before dst transition
+ // dst offset < std offset
+ let std = FixedOffset::east_opt(-(4 * 60 + 30) * 60).unwrap();
+ let dst = FixedOffset::east_opt(-5 * 60 * 60).unwrap();
+ let transitions = [
+ Transition::new(ymdhms(2023, 3, 24, 2, 0, 0), dst, std),
+ Transition::new(ymdhms(2023, 10, 27, 2, 30, 0), std, dst),
+ ];
+ compare_lookup(&transitions, 2023, 3, 24, 1, 0, 0, LocalResult::Single(dst));
+ compare_lookup(&transitions, 2023, 3, 24, 2, 0, 0, LocalResult::Single(dst));
+ compare_lookup(&transitions, 2023, 3, 24, 2, 15, 0, LocalResult::None);
+ compare_lookup(&transitions, 2023, 3, 24, 2, 30, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 3, 24, 3, 0, 0, LocalResult::Single(std));
+
+ compare_lookup(&transitions, 2023, 10, 27, 1, 0, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 10, 27, 2, 0, 0, LocalResult::Ambiguous(std, dst));
+ compare_lookup(&transitions, 2023, 10, 27, 2, 15, 0, LocalResult::Ambiguous(std, dst));
+ compare_lookup(&transitions, 2023, 10, 27, 2, 30, 0, LocalResult::Ambiguous(std, dst));
+ compare_lookup(&transitions, 2023, 10, 27, 3, 0, 0, LocalResult::Single(dst));
+
+ // offset stays the same
+ let std = FixedOffset::east_opt(3 * 60 * 60).unwrap();
+ let transitions = [
+ Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, std),
+ Transition::new(ymdhms(2023, 10, 29, 3, 0, 0), std, std),
+ ];
+ compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, LocalResult::Single(std));
+
+ // single transition
+ let std = FixedOffset::east_opt(3 * 60 * 60).unwrap();
+ let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap();
+ let transitions = [Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, dst)];
+ compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Single(std));
+ compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, LocalResult::None);
+ compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, LocalResult::Single(dst));
+ compare_lookup(&transitions, 2023, 3, 26, 4, 0, 0, LocalResult::Single(dst));
+ }
+
+ #[test]
+ #[cfg(windows)]
+ fn test_lookup_with_dst_transitions_limits() {
+ // Transition beyond UTC year end doesn't panic in year of `NaiveDate::MAX`
+ let std = FixedOffset::east_opt(3 * 60 * 60).unwrap();
+ let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap();
+ let transitions = [
+ Transition::new(NaiveDateTime::MAX.with_month(7).unwrap(), std, dst),
+ Transition::new(NaiveDateTime::MAX, dst, std),
+ ];
+ assert_eq!(
+ lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX.with_month(3).unwrap()),
+ LocalResult::Single(std)
+ );
+ assert_eq!(
+ lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX.with_month(8).unwrap()),
+ LocalResult::Single(dst)
+ );
+ // Doesn't panic with `NaiveDateTime::MAX` as argument (which would be out of range when
+ // converted to UTC).
+ assert_eq!(
+ lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX),
+ LocalResult::Ambiguous(dst, std)
+ );
+
+ // Transition before UTC year end doesn't panic in year of `NaiveDate::MIN`
+ let std = FixedOffset::west_opt(3 * 60 * 60).unwrap();
+ let dst = FixedOffset::west_opt(4 * 60 * 60).unwrap();
+ let transitions = [
+ Transition::new(NaiveDateTime::MIN, std, dst),
+ Transition::new(NaiveDateTime::MIN.with_month(6).unwrap(), dst, std),
+ ];
+ assert_eq!(
+ lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN.with_month(3).unwrap()),
+ LocalResult::Single(dst)
+ );
+ assert_eq!(
+ lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN.with_month(8).unwrap()),
+ LocalResult::Single(std)
+ );
+ // Doesn't panic with `NaiveDateTime::MIN` as argument (which would be out of range when
+ // converted to UTC).
+ assert_eq!(
+ lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN),
+ LocalResult::Ambiguous(std, dst)
+ );
+ }
+
+ #[test]
+ #[cfg(feature = "rkyv-validation")]
+ fn test_rkyv_validation() {
+ let local = Local;
+ // Local is a ZST and serializes to 0 bytes
+ let bytes = rkyv::to_bytes::<_, 0>(&local).unwrap();
+ assert_eq!(bytes.len(), 0);
+
+ // but is deserialized to an archived variant without a
+ // wrapping object
+ assert_eq!(rkyv::from_bytes::<Local>(&bytes).unwrap(), super::ArchivedLocal);
+ }
+}
diff --git a/src/offset/local/tz_info/mod.rs b/src/offset/local/tz_info/mod.rs
new file mode 100644
index 0000000..780e15a
--- /dev/null
+++ b/src/offset/local/tz_info/mod.rs
@@ -0,0 +1,116 @@
+#![deny(missing_docs)]
+#![allow(dead_code)]
+#![warn(unreachable_pub)]
+
+use std::num::ParseIntError;
+use std::str::Utf8Error;
+use std::time::SystemTimeError;
+use std::{error, fmt, io};
+
+mod timezone;
+pub(crate) use timezone::TimeZone;
+
+mod parser;
+mod rule;
+
+/// Unified error type for everything in the crate
+#[derive(Debug)]
+pub(crate) enum Error {
+ /// Date time error
+ DateTime(&'static str),
+ /// Local time type search error
+ FindLocalTimeType(&'static str),
+ /// Local time type error
+ LocalTimeType(&'static str),
+ /// Invalid slice for integer conversion
+ InvalidSlice(&'static str),
+ /// Invalid Tzif file
+ InvalidTzFile(&'static str),
+ /// Invalid TZ string
+ InvalidTzString(&'static str),
+ /// I/O error
+ Io(io::Error),
+ /// Out of range error
+ OutOfRange(&'static str),
+ /// Integer parsing error
+ ParseInt(ParseIntError),
+ /// Date time projection error
+ ProjectDateTime(&'static str),
+ /// System time error
+ SystemTime(SystemTimeError),
+ /// Time zone error
+ TimeZone(&'static str),
+ /// Transition rule error
+ TransitionRule(&'static str),
+ /// Unsupported Tzif file
+ UnsupportedTzFile(&'static str),
+ /// Unsupported TZ string
+ UnsupportedTzString(&'static str),
+ /// UTF-8 error
+ Utf8(Utf8Error),
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use Error::*;
+ match self {
+ DateTime(error) => write!(f, "invalid date time: {}", error),
+ FindLocalTimeType(error) => error.fmt(f),
+ LocalTimeType(error) => write!(f, "invalid local time type: {}", error),
+ InvalidSlice(error) => error.fmt(f),
+ InvalidTzString(error) => write!(f, "invalid TZ string: {}", error),
+ InvalidTzFile(error) => error.fmt(f),
+ Io(error) => error.fmt(f),
+ OutOfRange(error) => error.fmt(f),
+ ParseInt(error) => error.fmt(f),
+ ProjectDateTime(error) => error.fmt(f),
+ SystemTime(error) => error.fmt(f),
+ TransitionRule(error) => write!(f, "invalid transition rule: {}", error),
+ TimeZone(error) => write!(f, "invalid time zone: {}", error),
+ UnsupportedTzFile(error) => error.fmt(f),
+ UnsupportedTzString(error) => write!(f, "unsupported TZ string: {}", error),
+ Utf8(error) => error.fmt(f),
+ }
+ }
+}
+
+impl error::Error for Error {}
+
+impl From<io::Error> for Error {
+ fn from(error: io::Error) -> Self {
+ Error::Io(error)
+ }
+}
+
+impl From<ParseIntError> for Error {
+ fn from(error: ParseIntError) -> Self {
+ Error::ParseInt(error)
+ }
+}
+
+impl From<SystemTimeError> for Error {
+ fn from(error: SystemTimeError) -> Self {
+ Error::SystemTime(error)
+ }
+}
+
+impl From<Utf8Error> for Error {
+ fn from(error: Utf8Error) -> Self {
+ Error::Utf8(error)
+ }
+}
+
+/// Number of hours in one day
+const HOURS_PER_DAY: i64 = 24;
+/// Number of seconds in one hour
+const SECONDS_PER_HOUR: i64 = 3600;
+/// Number of seconds in one day
+const SECONDS_PER_DAY: i64 = SECONDS_PER_HOUR * HOURS_PER_DAY;
+/// Number of days in one week
+const DAYS_PER_WEEK: i64 = 7;
+
+/// Month days in a normal year
+const DAY_IN_MONTHS_NORMAL_YEAR: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+/// Cumulated month days in a normal year
+const CUMUL_DAY_IN_MONTHS_NORMAL_YEAR: [i64; 12] =
+ [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
diff --git a/src/offset/local/tz_info/parser.rs b/src/offset/local/tz_info/parser.rs
new file mode 100644
index 0000000..47cc037
--- /dev/null
+++ b/src/offset/local/tz_info/parser.rs
@@ -0,0 +1,333 @@
+use std::io::{self, ErrorKind};
+use std::iter;
+use std::num::ParseIntError;
+use std::str::{self, FromStr};
+
+use super::rule::TransitionRule;
+use super::timezone::{LeapSecond, LocalTimeType, TimeZone, Transition};
+use super::Error;
+
+pub(super) fn parse(bytes: &[u8]) -> Result<TimeZone, Error> {
+ let mut cursor = Cursor::new(bytes);
+ let state = State::new(&mut cursor, true)?;
+ let (state, footer) = match state.header.version {
+ Version::V1 => match cursor.is_empty() {
+ true => (state, None),
+ false => {
+ return Err(Error::InvalidTzFile("remaining data after end of TZif v1 data block"))
+ }
+ },
+ Version::V2 | Version::V3 => {
+ let state = State::new(&mut cursor, false)?;
+ (state, Some(cursor.remaining()))
+ }
+ };
+
+ let mut transitions = Vec::with_capacity(state.header.transition_count);
+ for (arr_time, &local_time_type_index) in
+ state.transition_times.chunks_exact(state.time_size).zip(state.transition_types)
+ {
+ let unix_leap_time =
+ state.parse_time(&arr_time[0..state.time_size], state.header.version)?;
+ let local_time_type_index = local_time_type_index as usize;
+ transitions.push(Transition::new(unix_leap_time, local_time_type_index));
+ }
+
+ let mut local_time_types = Vec::with_capacity(state.header.type_count);
+ for arr in state.local_time_types.chunks_exact(6) {
+ let ut_offset = read_be_i32(&arr[..4])?;
+
+ let is_dst = match arr[4] {
+ 0 => false,
+ 1 => true,
+ _ => return Err(Error::InvalidTzFile("invalid DST indicator")),
+ };
+
+ let char_index = arr[5] as usize;
+ if char_index >= state.header.char_count {
+ return Err(Error::InvalidTzFile("invalid time zone name char index"));
+ }
+
+ let position = match state.names[char_index..].iter().position(|&c| c == b'\0') {
+ Some(position) => position,
+ None => return Err(Error::InvalidTzFile("invalid time zone name char index")),
+ };
+
+ let name = &state.names[char_index..char_index + position];
+ let name = if !name.is_empty() { Some(name) } else { None };
+ local_time_types.push(LocalTimeType::new(ut_offset, is_dst, name)?);
+ }
+
+ let mut leap_seconds = Vec::with_capacity(state.header.leap_count);
+ for arr in state.leap_seconds.chunks_exact(state.time_size + 4) {
+ let unix_leap_time = state.parse_time(&arr[0..state.time_size], state.header.version)?;
+ let correction = read_be_i32(&arr[state.time_size..state.time_size + 4])?;
+ leap_seconds.push(LeapSecond::new(unix_leap_time, correction));
+ }
+
+ let std_walls_iter = state.std_walls.iter().copied().chain(iter::repeat(0));
+ let ut_locals_iter = state.ut_locals.iter().copied().chain(iter::repeat(0));
+ if std_walls_iter.zip(ut_locals_iter).take(state.header.type_count).any(|pair| pair == (0, 1)) {
+ return Err(Error::InvalidTzFile(
+ "invalid couple of standard/wall and UT/local indicators",
+ ));
+ }
+
+ let extra_rule = match footer {
+ Some(footer) => {
+ let footer = str::from_utf8(footer)?;
+ if !(footer.starts_with('\n') && footer.ends_with('\n')) {
+ return Err(Error::InvalidTzFile("invalid footer"));
+ }
+
+ let tz_string = footer.trim_matches(|c: char| c.is_ascii_whitespace());
+ if tz_string.starts_with(':') || tz_string.contains('\0') {
+ return Err(Error::InvalidTzFile("invalid footer"));
+ }
+
+ match tz_string.is_empty() {
+ true => None,
+ false => Some(TransitionRule::from_tz_string(
+ tz_string.as_bytes(),
+ state.header.version == Version::V3,
+ )?),
+ }
+ }
+ None => None,
+ };
+
+ TimeZone::new(transitions, local_time_types, leap_seconds, extra_rule)
+}
+
+/// TZif data blocks
+struct State<'a> {
+ header: Header,
+ /// Time size in bytes
+ time_size: usize,
+ /// Transition times data block
+ transition_times: &'a [u8],
+ /// Transition types data block
+ transition_types: &'a [u8],
+ /// Local time types data block
+ local_time_types: &'a [u8],
+ /// Time zone names data block
+ names: &'a [u8],
+ /// Leap seconds data block
+ leap_seconds: &'a [u8],
+ /// UT/local indicators data block
+ std_walls: &'a [u8],
+ /// Standard/wall indicators data block
+ ut_locals: &'a [u8],
+}
+
+impl<'a> State<'a> {
+ /// Read TZif data blocks
+ fn new(cursor: &mut Cursor<'a>, first: bool) -> Result<Self, Error> {
+ let header = Header::new(cursor)?;
+ let time_size = match first {
+ true => 4, // We always parse V1 first
+ false => 8,
+ };
+
+ Ok(Self {
+ time_size,
+ transition_times: cursor.read_exact(header.transition_count * time_size)?,
+ transition_types: cursor.read_exact(header.transition_count)?,
+ local_time_types: cursor.read_exact(header.type_count * 6)?,
+ names: cursor.read_exact(header.char_count)?,
+ leap_seconds: cursor.read_exact(header.leap_count * (time_size + 4))?,
+ std_walls: cursor.read_exact(header.std_wall_count)?,
+ ut_locals: cursor.read_exact(header.ut_local_count)?,
+ header,
+ })
+ }
+
+ /// Parse time values
+ fn parse_time(&self, arr: &[u8], version: Version) -> Result<i64, Error> {
+ match version {
+ Version::V1 => Ok(read_be_i32(&arr[..4])?.into()),
+ Version::V2 | Version::V3 => read_be_i64(arr),
+ }
+ }
+}
+
+/// TZif header
+#[derive(Debug)]
+struct Header {
+ /// TZif version
+ version: Version,
+ /// Number of UT/local indicators
+ ut_local_count: usize,
+ /// Number of standard/wall indicators
+ std_wall_count: usize,
+ /// Number of leap-second records
+ leap_count: usize,
+ /// Number of transition times
+ transition_count: usize,
+ /// Number of local time type records
+ type_count: usize,
+ /// Number of time zone names bytes
+ char_count: usize,
+}
+
+impl Header {
+ fn new(cursor: &mut Cursor) -> Result<Self, Error> {
+ let magic = cursor.read_exact(4)?;
+ if magic != *b"TZif" {
+ return Err(Error::InvalidTzFile("invalid magic number"));
+ }
+
+ let version = match cursor.read_exact(1)? {
+ [0x00] => Version::V1,
+ [0x32] => Version::V2,
+ [0x33] => Version::V3,
+ _ => return Err(Error::UnsupportedTzFile("unsupported TZif version")),
+ };
+
+ cursor.read_exact(15)?;
+ let ut_local_count = cursor.read_be_u32()?;
+ let std_wall_count = cursor.read_be_u32()?;
+ let leap_count = cursor.read_be_u32()?;
+ let transition_count = cursor.read_be_u32()?;
+ let type_count = cursor.read_be_u32()?;
+ let char_count = cursor.read_be_u32()?;
+
+ if !(type_count != 0
+ && char_count != 0
+ && (ut_local_count == 0 || ut_local_count == type_count)
+ && (std_wall_count == 0 || std_wall_count == type_count))
+ {
+ return Err(Error::InvalidTzFile("invalid header"));
+ }
+
+ Ok(Self {
+ version,
+ ut_local_count: ut_local_count as usize,
+ std_wall_count: std_wall_count as usize,
+ leap_count: leap_count as usize,
+ transition_count: transition_count as usize,
+ type_count: type_count as usize,
+ char_count: char_count as usize,
+ })
+ }
+}
+
+/// A `Cursor` contains a slice of a buffer and a read count.
+#[derive(Debug, Eq, PartialEq)]
+pub(crate) struct Cursor<'a> {
+ /// Slice representing the remaining data to be read
+ remaining: &'a [u8],
+ /// Number of already read bytes
+ read_count: usize,
+}
+
+impl<'a> Cursor<'a> {
+ /// Construct a new `Cursor` from remaining data
+ pub(crate) const fn new(remaining: &'a [u8]) -> Self {
+ Self { remaining, read_count: 0 }
+ }
+
+ pub(crate) fn peek(&self) -> Option<&u8> {
+ self.remaining().first()
+ }
+
+ /// Returns remaining data
+ pub(crate) const fn remaining(&self) -> &'a [u8] {
+ self.remaining
+ }
+
+ /// Returns `true` if data is remaining
+ pub(crate) const fn is_empty(&self) -> bool {
+ self.remaining.is_empty()
+ }
+
+ pub(crate) fn read_be_u32(&mut self) -> Result<u32, Error> {
+ let mut buf = [0; 4];
+ buf.copy_from_slice(self.read_exact(4)?);
+ Ok(u32::from_be_bytes(buf))
+ }
+
+ /// Read exactly `count` bytes, reducing remaining data and incrementing read count
+ pub(crate) fn read_exact(&mut self, count: usize) -> Result<&'a [u8], io::Error> {
+ match (self.remaining.get(..count), self.remaining.get(count..)) {
+ (Some(result), Some(remaining)) => {
+ self.remaining = remaining;
+ self.read_count += count;
+ Ok(result)
+ }
+ _ => Err(io::Error::from(ErrorKind::UnexpectedEof)),
+ }
+ }
+
+ /// Read bytes and compare them to the provided tag
+ pub(crate) fn read_tag(&mut self, tag: &[u8]) -> Result<(), io::Error> {
+ if self.read_exact(tag.len())? == tag {
+ Ok(())
+ } else {
+ Err(io::Error::from(ErrorKind::InvalidData))
+ }
+ }
+
+ /// Read bytes if the remaining data is prefixed by the provided tag
+ pub(crate) fn read_optional_tag(&mut self, tag: &[u8]) -> Result<bool, io::Error> {
+ if self.remaining.starts_with(tag) {
+ self.read_exact(tag.len())?;
+ Ok(true)
+ } else {
+ Ok(false)
+ }
+ }
+
+ /// Read bytes as long as the provided predicate is true
+ pub(crate) fn read_while<F: Fn(&u8) -> bool>(&mut self, f: F) -> Result<&'a [u8], io::Error> {
+ match self.remaining.iter().position(|x| !f(x)) {
+ None => self.read_exact(self.remaining.len()),
+ Some(position) => self.read_exact(position),
+ }
+ }
+
+ // Parse an integer out of the ASCII digits
+ pub(crate) fn read_int<T: FromStr<Err = ParseIntError>>(&mut self) -> Result<T, Error> {
+ let bytes = self.read_while(u8::is_ascii_digit)?;
+ Ok(str::from_utf8(bytes)?.parse()?)
+ }
+
+ /// Read bytes until the provided predicate is true
+ pub(crate) fn read_until<F: Fn(&u8) -> bool>(&mut self, f: F) -> Result<&'a [u8], io::Error> {
+ match self.remaining.iter().position(f) {
+ None => self.read_exact(self.remaining.len()),
+ Some(position) => self.read_exact(position),
+ }
+ }
+}
+
+pub(crate) fn read_be_i32(bytes: &[u8]) -> Result<i32, Error> {
+ if bytes.len() != 4 {
+ return Err(Error::InvalidSlice("too short for i32"));
+ }
+
+ let mut buf = [0; 4];
+ buf.copy_from_slice(bytes);
+ Ok(i32::from_be_bytes(buf))
+}
+
+pub(crate) fn read_be_i64(bytes: &[u8]) -> Result<i64, Error> {
+ if bytes.len() != 8 {
+ return Err(Error::InvalidSlice("too short for i64"));
+ }
+
+ let mut buf = [0; 8];
+ buf.copy_from_slice(bytes);
+ Ok(i64::from_be_bytes(buf))
+}
+
+/// TZif version
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+enum Version {
+ /// Version 1
+ V1,
+ /// Version 2
+ V2,
+ /// Version 3
+ V3,
+}
diff --git a/src/offset/local/tz_info/rule.rs b/src/offset/local/tz_info/rule.rs
new file mode 100644
index 0000000..369e317
--- /dev/null
+++ b/src/offset/local/tz_info/rule.rs
@@ -0,0 +1,1045 @@
+use std::cmp::Ordering;
+
+use super::parser::Cursor;
+use super::timezone::{LocalTimeType, SECONDS_PER_WEEK};
+use super::{
+ Error, CUMUL_DAY_IN_MONTHS_NORMAL_YEAR, DAYS_PER_WEEK, DAY_IN_MONTHS_NORMAL_YEAR,
+ SECONDS_PER_DAY,
+};
+
+/// Transition rule
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub(super) enum TransitionRule {
+ /// Fixed local time type
+ Fixed(LocalTimeType),
+ /// Alternate local time types
+ Alternate(AlternateTime),
+}
+
+impl TransitionRule {
+ /// Parse a POSIX TZ string containing a time zone description, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html).
+ ///
+ /// TZ string extensions from [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536#section-3.3.1) may be used.
+ ///
+ pub(super) fn from_tz_string(
+ tz_string: &[u8],
+ use_string_extensions: bool,
+ ) -> Result<Self, Error> {
+ let mut cursor = Cursor::new(tz_string);
+
+ let std_time_zone = Some(parse_name(&mut cursor)?);
+ let std_offset = parse_offset(&mut cursor)?;
+
+ if cursor.is_empty() {
+ return Ok(LocalTimeType::new(-std_offset, false, std_time_zone)?.into());
+ }
+
+ let dst_time_zone = Some(parse_name(&mut cursor)?);
+
+ let dst_offset = match cursor.peek() {
+ Some(&b',') => std_offset - 3600,
+ Some(_) => parse_offset(&mut cursor)?,
+ None => {
+ return Err(Error::UnsupportedTzString("DST start and end rules must be provided"))
+ }
+ };
+
+ if cursor.is_empty() {
+ return Err(Error::UnsupportedTzString("DST start and end rules must be provided"));
+ }
+
+ cursor.read_tag(b",")?;
+ let (dst_start, dst_start_time) = RuleDay::parse(&mut cursor, use_string_extensions)?;
+
+ cursor.read_tag(b",")?;
+ let (dst_end, dst_end_time) = RuleDay::parse(&mut cursor, use_string_extensions)?;
+
+ if !cursor.is_empty() {
+ return Err(Error::InvalidTzString("remaining data after parsing TZ string"));
+ }
+
+ Ok(AlternateTime::new(
+ LocalTimeType::new(-std_offset, false, std_time_zone)?,
+ LocalTimeType::new(-dst_offset, true, dst_time_zone)?,
+ dst_start,
+ dst_start_time,
+ dst_end,
+ dst_end_time,
+ )?
+ .into())
+ }
+
+ /// Find the local time type associated to the transition rule at the specified Unix time in seconds
+ pub(super) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> {
+ match self {
+ TransitionRule::Fixed(local_time_type) => Ok(local_time_type),
+ TransitionRule::Alternate(alternate_time) => {
+ alternate_time.find_local_time_type(unix_time)
+ }
+ }
+ }
+
+ /// Find the local time type associated to the transition rule at the specified Unix time in seconds
+ pub(super) fn find_local_time_type_from_local(
+ &self,
+ local_time: i64,
+ year: i32,
+ ) -> Result<crate::LocalResult<LocalTimeType>, Error> {
+ match self {
+ TransitionRule::Fixed(local_time_type) => {
+ Ok(crate::LocalResult::Single(*local_time_type))
+ }
+ TransitionRule::Alternate(alternate_time) => {
+ alternate_time.find_local_time_type_from_local(local_time, year)
+ }
+ }
+ }
+}
+
+impl From<LocalTimeType> for TransitionRule {
+ fn from(inner: LocalTimeType) -> Self {
+ TransitionRule::Fixed(inner)
+ }
+}
+
+impl From<AlternateTime> for TransitionRule {
+ fn from(inner: AlternateTime) -> Self {
+ TransitionRule::Alternate(inner)
+ }
+}
+
+/// Transition rule representing alternate local time types
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub(super) struct AlternateTime {
+ /// Local time type for standard time
+ pub(super) std: LocalTimeType,
+ /// Local time type for Daylight Saving Time
+ pub(super) dst: LocalTimeType,
+ /// Start day of Daylight Saving Time
+ dst_start: RuleDay,
+ /// Local start day time of Daylight Saving Time, in seconds
+ dst_start_time: i32,
+ /// End day of Daylight Saving Time
+ dst_end: RuleDay,
+ /// Local end day time of Daylight Saving Time, in seconds
+ dst_end_time: i32,
+}
+
+impl AlternateTime {
+ /// Construct a transition rule representing alternate local time types
+ const fn new(
+ std: LocalTimeType,
+ dst: LocalTimeType,
+ dst_start: RuleDay,
+ dst_start_time: i32,
+ dst_end: RuleDay,
+ dst_end_time: i32,
+ ) -> Result<Self, Error> {
+ // Overflow is not possible
+ if !((dst_start_time as i64).abs() < SECONDS_PER_WEEK
+ && (dst_end_time as i64).abs() < SECONDS_PER_WEEK)
+ {
+ return Err(Error::TransitionRule("invalid DST start or end time"));
+ }
+
+ Ok(Self { std, dst, dst_start, dst_start_time, dst_end, dst_end_time })
+ }
+
+ /// Find the local time type associated to the alternate transition rule at the specified Unix time in seconds
+ fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> {
+ // Overflow is not possible
+ let dst_start_time_in_utc = self.dst_start_time as i64 - self.std.ut_offset as i64;
+ let dst_end_time_in_utc = self.dst_end_time as i64 - self.dst.ut_offset as i64;
+
+ let current_year = match UtcDateTime::from_timespec(unix_time) {
+ Ok(dt) => dt.year,
+ Err(error) => return Err(error),
+ };
+
+ // Check if the current year is valid for the following computations
+ if !(i32::min_value() + 2 <= current_year && current_year <= i32::max_value() - 2) {
+ return Err(Error::OutOfRange("out of range date time"));
+ }
+
+ let current_year_dst_start_unix_time =
+ self.dst_start.unix_time(current_year, dst_start_time_in_utc);
+ let current_year_dst_end_unix_time =
+ self.dst_end.unix_time(current_year, dst_end_time_in_utc);
+
+ // Check DST start/end Unix times for previous/current/next years to support for transition day times outside of [0h, 24h] range
+ let is_dst =
+ match Ord::cmp(&current_year_dst_start_unix_time, &current_year_dst_end_unix_time) {
+ Ordering::Less | Ordering::Equal => {
+ if unix_time < current_year_dst_start_unix_time {
+ let previous_year_dst_end_unix_time =
+ self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc);
+ if unix_time < previous_year_dst_end_unix_time {
+ let previous_year_dst_start_unix_time =
+ self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc);
+ previous_year_dst_start_unix_time <= unix_time
+ } else {
+ false
+ }
+ } else if unix_time < current_year_dst_end_unix_time {
+ true
+ } else {
+ let next_year_dst_start_unix_time =
+ self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc);
+ if next_year_dst_start_unix_time <= unix_time {
+ let next_year_dst_end_unix_time =
+ self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc);
+ unix_time < next_year_dst_end_unix_time
+ } else {
+ false
+ }
+ }
+ }
+ Ordering::Greater => {
+ if unix_time < current_year_dst_end_unix_time {
+ let previous_year_dst_start_unix_time =
+ self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc);
+ if unix_time < previous_year_dst_start_unix_time {
+ let previous_year_dst_end_unix_time =
+ self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc);
+ unix_time < previous_year_dst_end_unix_time
+ } else {
+ true
+ }
+ } else if unix_time < current_year_dst_start_unix_time {
+ false
+ } else {
+ let next_year_dst_end_unix_time =
+ self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc);
+ if next_year_dst_end_unix_time <= unix_time {
+ let next_year_dst_start_unix_time =
+ self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc);
+ next_year_dst_start_unix_time <= unix_time
+ } else {
+ true
+ }
+ }
+ }
+ };
+
+ if is_dst {
+ Ok(&self.dst)
+ } else {
+ Ok(&self.std)
+ }
+ }
+
+ fn find_local_time_type_from_local(
+ &self,
+ local_time: i64,
+ current_year: i32,
+ ) -> Result<crate::LocalResult<LocalTimeType>, Error> {
+ // Check if the current year is valid for the following computations
+ if !(i32::min_value() + 2 <= current_year && current_year <= i32::max_value() - 2) {
+ return Err(Error::OutOfRange("out of range date time"));
+ }
+
+ let dst_start_transition_start =
+ self.dst_start.unix_time(current_year, 0) + i64::from(self.dst_start_time);
+ let dst_start_transition_end = self.dst_start.unix_time(current_year, 0)
+ + i64::from(self.dst_start_time)
+ + i64::from(self.dst.ut_offset)
+ - i64::from(self.std.ut_offset);
+
+ let dst_end_transition_start =
+ self.dst_end.unix_time(current_year, 0) + i64::from(self.dst_end_time);
+ let dst_end_transition_end = self.dst_end.unix_time(current_year, 0)
+ + i64::from(self.dst_end_time)
+ + i64::from(self.std.ut_offset)
+ - i64::from(self.dst.ut_offset);
+
+ match self.std.ut_offset.cmp(&self.dst.ut_offset) {
+ Ordering::Equal => Ok(crate::LocalResult::Single(self.std)),
+ Ordering::Less => {
+ if self.dst_start.transition_date(current_year).0
+ < self.dst_end.transition_date(current_year).0
+ {
+ // northern hemisphere
+ // For the DST END transition, the `start` happens at a later timestamp than the `end`.
+ if local_time <= dst_start_transition_start {
+ Ok(crate::LocalResult::Single(self.std))
+ } else if local_time > dst_start_transition_start
+ && local_time < dst_start_transition_end
+ {
+ Ok(crate::LocalResult::None)
+ } else if local_time >= dst_start_transition_end
+ && local_time < dst_end_transition_end
+ {
+ Ok(crate::LocalResult::Single(self.dst))
+ } else if local_time >= dst_end_transition_end
+ && local_time <= dst_end_transition_start
+ {
+ Ok(crate::LocalResult::Ambiguous(self.std, self.dst))
+ } else {
+ Ok(crate::LocalResult::Single(self.std))
+ }
+ } else {
+ // southern hemisphere regular DST
+ // For the DST END transition, the `start` happens at a later timestamp than the `end`.
+ if local_time < dst_end_transition_end {
+ Ok(crate::LocalResult::Single(self.dst))
+ } else if local_time >= dst_end_transition_end
+ && local_time <= dst_end_transition_start
+ {
+ Ok(crate::LocalResult::Ambiguous(self.std, self.dst))
+ } else if local_time > dst_end_transition_end
+ && local_time < dst_start_transition_start
+ {
+ Ok(crate::LocalResult::Single(self.std))
+ } else if local_time >= dst_start_transition_start
+ && local_time < dst_start_transition_end
+ {
+ Ok(crate::LocalResult::None)
+ } else {
+ Ok(crate::LocalResult::Single(self.dst))
+ }
+ }
+ }
+ Ordering::Greater => {
+ if self.dst_start.transition_date(current_year).0
+ < self.dst_end.transition_date(current_year).0
+ {
+ // southern hemisphere reverse DST
+ // For the DST END transition, the `start` happens at a later timestamp than the `end`.
+ if local_time < dst_start_transition_end {
+ Ok(crate::LocalResult::Single(self.std))
+ } else if local_time >= dst_start_transition_end
+ && local_time <= dst_start_transition_start
+ {
+ Ok(crate::LocalResult::Ambiguous(self.dst, self.std))
+ } else if local_time > dst_start_transition_start
+ && local_time < dst_end_transition_start
+ {
+ Ok(crate::LocalResult::Single(self.dst))
+ } else if local_time >= dst_end_transition_start
+ && local_time < dst_end_transition_end
+ {
+ Ok(crate::LocalResult::None)
+ } else {
+ Ok(crate::LocalResult::Single(self.std))
+ }
+ } else {
+ // northern hemisphere reverse DST
+ // For the DST END transition, the `start` happens at a later timestamp than the `end`.
+ if local_time <= dst_end_transition_start {
+ Ok(crate::LocalResult::Single(self.dst))
+ } else if local_time > dst_end_transition_start
+ && local_time < dst_end_transition_end
+ {
+ Ok(crate::LocalResult::None)
+ } else if local_time >= dst_end_transition_end
+ && local_time < dst_start_transition_end
+ {
+ Ok(crate::LocalResult::Single(self.std))
+ } else if local_time >= dst_start_transition_end
+ && local_time <= dst_start_transition_start
+ {
+ Ok(crate::LocalResult::Ambiguous(self.dst, self.std))
+ } else {
+ Ok(crate::LocalResult::Single(self.dst))
+ }
+ }
+ }
+ }
+ }
+}
+
+/// Parse time zone name
+fn parse_name<'a>(cursor: &mut Cursor<'a>) -> Result<&'a [u8], Error> {
+ match cursor.peek() {
+ Some(b'<') => {}
+ _ => return Ok(cursor.read_while(u8::is_ascii_alphabetic)?),
+ }
+
+ cursor.read_exact(1)?;
+ let unquoted = cursor.read_until(|&x| x == b'>')?;
+ cursor.read_exact(1)?;
+ Ok(unquoted)
+}
+
+/// Parse time zone offset
+fn parse_offset(cursor: &mut Cursor) -> Result<i32, Error> {
+ let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?;
+
+ if !(0..=24).contains(&hour) {
+ return Err(Error::InvalidTzString("invalid offset hour"));
+ }
+ if !(0..=59).contains(&minute) {
+ return Err(Error::InvalidTzString("invalid offset minute"));
+ }
+ if !(0..=59).contains(&second) {
+ return Err(Error::InvalidTzString("invalid offset second"));
+ }
+
+ Ok(sign * (hour * 3600 + minute * 60 + second))
+}
+
+/// Parse transition rule time
+fn parse_rule_time(cursor: &mut Cursor) -> Result<i32, Error> {
+ let (hour, minute, second) = parse_hhmmss(cursor)?;
+
+ if !(0..=24).contains(&hour) {
+ return Err(Error::InvalidTzString("invalid day time hour"));
+ }
+ if !(0..=59).contains(&minute) {
+ return Err(Error::InvalidTzString("invalid day time minute"));
+ }
+ if !(0..=59).contains(&second) {
+ return Err(Error::InvalidTzString("invalid day time second"));
+ }
+
+ Ok(hour * 3600 + minute * 60 + second)
+}
+
+/// Parse transition rule time with TZ string extensions
+fn parse_rule_time_extended(cursor: &mut Cursor) -> Result<i32, Error> {
+ let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?;
+
+ if !(-167..=167).contains(&hour) {
+ return Err(Error::InvalidTzString("invalid day time hour"));
+ }
+ if !(0..=59).contains(&minute) {
+ return Err(Error::InvalidTzString("invalid day time minute"));
+ }
+ if !(0..=59).contains(&second) {
+ return Err(Error::InvalidTzString("invalid day time second"));
+ }
+
+ Ok(sign * (hour * 3600 + minute * 60 + second))
+}
+
+/// Parse hours, minutes and seconds
+fn parse_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32), Error> {
+ let hour = cursor.read_int()?;
+
+ let mut minute = 0;
+ let mut second = 0;
+
+ if cursor.read_optional_tag(b":")? {
+ minute = cursor.read_int()?;
+
+ if cursor.read_optional_tag(b":")? {
+ second = cursor.read_int()?;
+ }
+ }
+
+ Ok((hour, minute, second))
+}
+
+/// Parse signed hours, minutes and seconds
+fn parse_signed_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32, i32), Error> {
+ let mut sign = 1;
+ if let Some(&c) = cursor.peek() {
+ if c == b'+' || c == b'-' {
+ cursor.read_exact(1)?;
+ if c == b'-' {
+ sign = -1;
+ }
+ }
+ }
+
+ let (hour, minute, second) = parse_hhmmss(cursor)?;
+ Ok((sign, hour, minute, second))
+}
+
+/// Transition rule day
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+enum RuleDay {
+ /// Julian day in `[1, 365]`, without taking occasional Feb 29 into account, which is not referenceable
+ Julian1WithoutLeap(u16),
+ /// Zero-based Julian day in `[0, 365]`, taking occasional Feb 29 into account
+ Julian0WithLeap(u16),
+ /// Day represented by a month, a month week and a week day
+ MonthWeekday {
+ /// Month in `[1, 12]`
+ month: u8,
+ /// Week of the month in `[1, 5]`, with `5` representing the last week of the month
+ week: u8,
+ /// Day of the week in `[0, 6]` from Sunday
+ week_day: u8,
+ },
+}
+
+impl RuleDay {
+ /// Parse transition rule
+ fn parse(cursor: &mut Cursor, use_string_extensions: bool) -> Result<(Self, i32), Error> {
+ let date = match cursor.peek() {
+ Some(b'M') => {
+ cursor.read_exact(1)?;
+ let month = cursor.read_int()?;
+ cursor.read_tag(b".")?;
+ let week = cursor.read_int()?;
+ cursor.read_tag(b".")?;
+ let week_day = cursor.read_int()?;
+ RuleDay::month_weekday(month, week, week_day)?
+ }
+ Some(b'J') => {
+ cursor.read_exact(1)?;
+ RuleDay::julian_1(cursor.read_int()?)?
+ }
+ _ => RuleDay::julian_0(cursor.read_int()?)?,
+ };
+
+ Ok((
+ date,
+ match (cursor.read_optional_tag(b"/")?, use_string_extensions) {
+ (false, _) => 2 * 3600,
+ (true, true) => parse_rule_time_extended(cursor)?,
+ (true, false) => parse_rule_time(cursor)?,
+ },
+ ))
+ }
+
+ /// Construct a transition rule day represented by a Julian day in `[1, 365]`, without taking occasional Feb 29 into account, which is not referenceable
+ fn julian_1(julian_day_1: u16) -> Result<Self, Error> {
+ if !(1..=365).contains(&julian_day_1) {
+ return Err(Error::TransitionRule("invalid rule day julian day"));
+ }
+
+ Ok(RuleDay::Julian1WithoutLeap(julian_day_1))
+ }
+
+ /// Construct a transition rule day represented by a zero-based Julian day in `[0, 365]`, taking occasional Feb 29 into account
+ const fn julian_0(julian_day_0: u16) -> Result<Self, Error> {
+ if julian_day_0 > 365 {
+ return Err(Error::TransitionRule("invalid rule day julian day"));
+ }
+
+ Ok(RuleDay::Julian0WithLeap(julian_day_0))
+ }
+
+ /// Construct a transition rule day represented by a month, a month week and a week day
+ fn month_weekday(month: u8, week: u8, week_day: u8) -> Result<Self, Error> {
+ if !(1..=12).contains(&month) {
+ return Err(Error::TransitionRule("invalid rule day month"));
+ }
+
+ if !(1..=5).contains(&week) {
+ return Err(Error::TransitionRule("invalid rule day week"));
+ }
+
+ if week_day > 6 {
+ return Err(Error::TransitionRule("invalid rule day week day"));
+ }
+
+ Ok(RuleDay::MonthWeekday { month, week, week_day })
+ }
+
+ /// Get the transition date for the provided year
+ ///
+ /// ## Outputs
+ ///
+ /// * `month`: Month in `[1, 12]`
+ /// * `month_day`: Day of the month in `[1, 31]`
+ fn transition_date(&self, year: i32) -> (usize, i64) {
+ match *self {
+ RuleDay::Julian1WithoutLeap(year_day) => {
+ let year_day = year_day as i64;
+
+ let month = match CUMUL_DAY_IN_MONTHS_NORMAL_YEAR.binary_search(&(year_day - 1)) {
+ Ok(x) => x + 1,
+ Err(x) => x,
+ };
+
+ let month_day = year_day - CUMUL_DAY_IN_MONTHS_NORMAL_YEAR[month - 1];
+
+ (month, month_day)
+ }
+ RuleDay::Julian0WithLeap(year_day) => {
+ let leap = is_leap_year(year) as i64;
+
+ let cumul_day_in_months = [
+ 0,
+ 31,
+ 59 + leap,
+ 90 + leap,
+ 120 + leap,
+ 151 + leap,
+ 181 + leap,
+ 212 + leap,
+ 243 + leap,
+ 273 + leap,
+ 304 + leap,
+ 334 + leap,
+ ];
+
+ let year_day = year_day as i64;
+
+ let month = match cumul_day_in_months.binary_search(&year_day) {
+ Ok(x) => x + 1,
+ Err(x) => x,
+ };
+
+ let month_day = 1 + year_day - cumul_day_in_months[month - 1];
+
+ (month, month_day)
+ }
+ RuleDay::MonthWeekday { month: rule_month, week, week_day } => {
+ let leap = is_leap_year(year) as i64;
+
+ let month = rule_month as usize;
+
+ let mut day_in_month = DAY_IN_MONTHS_NORMAL_YEAR[month - 1];
+ if month == 2 {
+ day_in_month += leap;
+ }
+
+ let week_day_of_first_month_day =
+ (4 + days_since_unix_epoch(year, month, 1)).rem_euclid(DAYS_PER_WEEK);
+ let first_week_day_occurence_in_month =
+ 1 + (week_day as i64 - week_day_of_first_month_day).rem_euclid(DAYS_PER_WEEK);
+
+ let mut month_day =
+ first_week_day_occurence_in_month + (week as i64 - 1) * DAYS_PER_WEEK;
+ if month_day > day_in_month {
+ month_day -= DAYS_PER_WEEK
+ }
+
+ (month, month_day)
+ }
+ }
+ }
+
+ /// Returns the UTC Unix time in seconds associated to the transition date for the provided year
+ fn unix_time(&self, year: i32, day_time_in_utc: i64) -> i64 {
+ let (month, month_day) = self.transition_date(year);
+ days_since_unix_epoch(year, month, month_day) * SECONDS_PER_DAY + day_time_in_utc
+ }
+}
+
+/// UTC date time exprimed in the [proleptic gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar)
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub(crate) struct UtcDateTime {
+ /// Year
+ pub(crate) year: i32,
+ /// Month in `[1, 12]`
+ pub(crate) month: u8,
+ /// Day of the month in `[1, 31]`
+ pub(crate) month_day: u8,
+ /// Hours since midnight in `[0, 23]`
+ pub(crate) hour: u8,
+ /// Minutes in `[0, 59]`
+ pub(crate) minute: u8,
+ /// Seconds in `[0, 60]`, with a possible leap second
+ pub(crate) second: u8,
+}
+
+impl UtcDateTime {
+ /// Construct a UTC date time from a Unix time in seconds and nanoseconds
+ pub(crate) fn from_timespec(unix_time: i64) -> Result<Self, Error> {
+ let seconds = match unix_time.checked_sub(UNIX_OFFSET_SECS) {
+ Some(seconds) => seconds,
+ None => return Err(Error::OutOfRange("out of range operation")),
+ };
+
+ let mut remaining_days = seconds / SECONDS_PER_DAY;
+ let mut remaining_seconds = seconds % SECONDS_PER_DAY;
+ if remaining_seconds < 0 {
+ remaining_seconds += SECONDS_PER_DAY;
+ remaining_days -= 1;
+ }
+
+ let mut cycles_400_years = remaining_days / DAYS_PER_400_YEARS;
+ remaining_days %= DAYS_PER_400_YEARS;
+ if remaining_days < 0 {
+ remaining_days += DAYS_PER_400_YEARS;
+ cycles_400_years -= 1;
+ }
+
+ let cycles_100_years = Ord::min(remaining_days / DAYS_PER_100_YEARS, 3);
+ remaining_days -= cycles_100_years * DAYS_PER_100_YEARS;
+
+ let cycles_4_years = Ord::min(remaining_days / DAYS_PER_4_YEARS, 24);
+ remaining_days -= cycles_4_years * DAYS_PER_4_YEARS;
+
+ let remaining_years = Ord::min(remaining_days / DAYS_PER_NORMAL_YEAR, 3);
+ remaining_days -= remaining_years * DAYS_PER_NORMAL_YEAR;
+
+ let mut year = OFFSET_YEAR
+ + remaining_years
+ + cycles_4_years * 4
+ + cycles_100_years * 100
+ + cycles_400_years * 400;
+
+ let mut month = 0;
+ while month < DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH.len() {
+ let days = DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH[month];
+ if remaining_days < days {
+ break;
+ }
+ remaining_days -= days;
+ month += 1;
+ }
+ month += 2;
+
+ if month >= MONTHS_PER_YEAR as usize {
+ month -= MONTHS_PER_YEAR as usize;
+ year += 1;
+ }
+ month += 1;
+
+ let month_day = 1 + remaining_days;
+
+ let hour = remaining_seconds / SECONDS_PER_HOUR;
+ let minute = (remaining_seconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR;
+ let second = remaining_seconds % SECONDS_PER_MINUTE;
+
+ let year = match year >= i32::min_value() as i64 && year <= i32::max_value() as i64 {
+ true => year as i32,
+ false => return Err(Error::OutOfRange("i64 is out of range for i32")),
+ };
+
+ Ok(Self {
+ year,
+ month: month as u8,
+ month_day: month_day as u8,
+ hour: hour as u8,
+ minute: minute as u8,
+ second: second as u8,
+ })
+ }
+}
+
+/// Number of nanoseconds in one second
+const NANOSECONDS_PER_SECOND: u32 = 1_000_000_000;
+/// Number of seconds in one minute
+const SECONDS_PER_MINUTE: i64 = 60;
+/// Number of seconds in one hour
+const SECONDS_PER_HOUR: i64 = 3600;
+/// Number of minutes in one hour
+const MINUTES_PER_HOUR: i64 = 60;
+/// Number of months in one year
+const MONTHS_PER_YEAR: i64 = 12;
+/// Number of days in a normal year
+const DAYS_PER_NORMAL_YEAR: i64 = 365;
+/// Number of days in 4 years (including 1 leap year)
+const DAYS_PER_4_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 4 + 1;
+/// Number of days in 100 years (including 24 leap years)
+const DAYS_PER_100_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 100 + 24;
+/// Number of days in 400 years (including 97 leap years)
+const DAYS_PER_400_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 400 + 97;
+/// Unix time at `2000-03-01T00:00:00Z` (Wednesday)
+const UNIX_OFFSET_SECS: i64 = 951868800;
+/// Offset year
+const OFFSET_YEAR: i64 = 2000;
+/// Month days in a leap year from March
+const DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH: [i64; 12] =
+ [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
+
+/// Compute the number of days since Unix epoch (`1970-01-01T00:00:00Z`).
+///
+/// ## Inputs
+///
+/// * `year`: Year
+/// * `month`: Month in `[1, 12]`
+/// * `month_day`: Day of the month in `[1, 31]`
+pub(crate) const fn days_since_unix_epoch(year: i32, month: usize, month_day: i64) -> i64 {
+ let is_leap_year = is_leap_year(year);
+
+ let year = year as i64;
+
+ let mut result = (year - 1970) * 365;
+
+ if year >= 1970 {
+ result += (year - 1968) / 4;
+ result -= (year - 1900) / 100;
+ result += (year - 1600) / 400;
+
+ if is_leap_year && month < 3 {
+ result -= 1;
+ }
+ } else {
+ result += (year - 1972) / 4;
+ result -= (year - 2000) / 100;
+ result += (year - 2000) / 400;
+
+ if is_leap_year && month >= 3 {
+ result += 1;
+ }
+ }
+
+ result += CUMUL_DAY_IN_MONTHS_NORMAL_YEAR[month - 1] + month_day - 1;
+
+ result
+}
+
+/// Check if a year is a leap year
+pub(crate) const fn is_leap_year(year: i32) -> bool {
+ year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::super::timezone::Transition;
+ use super::super::{Error, TimeZone};
+ use super::{AlternateTime, LocalTimeType, RuleDay, TransitionRule};
+
+ #[test]
+ fn test_quoted() -> Result<(), Error> {
+ let transition_rule = TransitionRule::from_tz_string(b"<-03>+3<+03>-3,J1,J365", false)?;
+ assert_eq!(
+ transition_rule,
+ AlternateTime::new(
+ LocalTimeType::new(-10800, false, Some(b"-03"))?,
+ LocalTimeType::new(10800, true, Some(b"+03"))?,
+ RuleDay::julian_1(1)?,
+ 7200,
+ RuleDay::julian_1(365)?,
+ 7200,
+ )?
+ .into()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_full() -> Result<(), Error> {
+ let tz_string = b"NZST-12:00:00NZDT-13:00:00,M10.1.0/02:00:00,M3.3.0/02:00:00";
+ let transition_rule = TransitionRule::from_tz_string(tz_string, false)?;
+ assert_eq!(
+ transition_rule,
+ AlternateTime::new(
+ LocalTimeType::new(43200, false, Some(b"NZST"))?,
+ LocalTimeType::new(46800, true, Some(b"NZDT"))?,
+ RuleDay::month_weekday(10, 1, 0)?,
+ 7200,
+ RuleDay::month_weekday(3, 3, 0)?,
+ 7200,
+ )?
+ .into()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_negative_dst() -> Result<(), Error> {
+ let tz_string = b"IST-1GMT0,M10.5.0,M3.5.0/1";
+ let transition_rule = TransitionRule::from_tz_string(tz_string, false)?;
+ assert_eq!(
+ transition_rule,
+ AlternateTime::new(
+ LocalTimeType::new(3600, false, Some(b"IST"))?,
+ LocalTimeType::new(0, true, Some(b"GMT"))?,
+ RuleDay::month_weekday(10, 5, 0)?,
+ 7200,
+ RuleDay::month_weekday(3, 5, 0)?,
+ 3600,
+ )?
+ .into()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_negative_hour() -> Result<(), Error> {
+ let tz_string = b"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1";
+ assert!(TransitionRule::from_tz_string(tz_string, false).is_err());
+
+ assert_eq!(
+ TransitionRule::from_tz_string(tz_string, true)?,
+ AlternateTime::new(
+ LocalTimeType::new(-10800, false, Some(b"-03"))?,
+ LocalTimeType::new(-7200, true, Some(b"-02"))?,
+ RuleDay::month_weekday(3, 5, 0)?,
+ -7200,
+ RuleDay::month_weekday(10, 5, 0)?,
+ -3600,
+ )?
+ .into()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_all_year_dst() -> Result<(), Error> {
+ let tz_string = b"EST5EDT,0/0,J365/25";
+ assert!(TransitionRule::from_tz_string(tz_string, false).is_err());
+
+ assert_eq!(
+ TransitionRule::from_tz_string(tz_string, true)?,
+ AlternateTime::new(
+ LocalTimeType::new(-18000, false, Some(b"EST"))?,
+ LocalTimeType::new(-14400, true, Some(b"EDT"))?,
+ RuleDay::julian_0(0)?,
+ 0,
+ RuleDay::julian_1(365)?,
+ 90000,
+ )?
+ .into()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_v3_file() -> Result<(), Error> {
+ let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\x1c\x20\0\0IST\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x04\0\0\0\0\x7f\xe8\x17\x80\0\0\0\x1c\x20\0\0IST\0\x01\x01\x0aIST-2IDT,M3.4.4/26,M10.5.0\x0a";
+
+ let time_zone = TimeZone::from_tz_data(bytes)?;
+
+ let time_zone_result = TimeZone::new(
+ vec![Transition::new(2145916800, 0)],
+ vec![LocalTimeType::new(7200, false, Some(b"IST"))?],
+ Vec::new(),
+ Some(TransitionRule::from(AlternateTime::new(
+ LocalTimeType::new(7200, false, Some(b"IST"))?,
+ LocalTimeType::new(10800, true, Some(b"IDT"))?,
+ RuleDay::month_weekday(3, 4, 4)?,
+ 93600,
+ RuleDay::month_weekday(10, 5, 0)?,
+ 7200,
+ )?)),
+ )?;
+
+ assert_eq!(time_zone, time_zone_result);
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_rule_day() -> Result<(), Error> {
+ let rule_day_j1 = RuleDay::julian_1(60)?;
+ assert_eq!(rule_day_j1.transition_date(2000), (3, 1));
+ assert_eq!(rule_day_j1.transition_date(2001), (3, 1));
+ assert_eq!(rule_day_j1.unix_time(2000, 43200), 951912000);
+
+ let rule_day_j0 = RuleDay::julian_0(59)?;
+ assert_eq!(rule_day_j0.transition_date(2000), (2, 29));
+ assert_eq!(rule_day_j0.transition_date(2001), (3, 1));
+ assert_eq!(rule_day_j0.unix_time(2000, 43200), 951825600);
+
+ let rule_day_mwd = RuleDay::month_weekday(2, 5, 2)?;
+ assert_eq!(rule_day_mwd.transition_date(2000), (2, 29));
+ assert_eq!(rule_day_mwd.transition_date(2001), (2, 27));
+ assert_eq!(rule_day_mwd.unix_time(2000, 43200), 951825600);
+ assert_eq!(rule_day_mwd.unix_time(2001, 43200), 983275200);
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_transition_rule() -> Result<(), Error> {
+ let transition_rule_fixed = TransitionRule::from(LocalTimeType::new(-36000, false, None)?);
+ assert_eq!(transition_rule_fixed.find_local_time_type(0)?.offset(), -36000);
+
+ let transition_rule_dst = TransitionRule::from(AlternateTime::new(
+ LocalTimeType::new(43200, false, Some(b"NZST"))?,
+ LocalTimeType::new(46800, true, Some(b"NZDT"))?,
+ RuleDay::month_weekday(10, 1, 0)?,
+ 7200,
+ RuleDay::month_weekday(3, 3, 0)?,
+ 7200,
+ )?);
+
+ assert_eq!(transition_rule_dst.find_local_time_type(953384399)?.offset(), 46800);
+ assert_eq!(transition_rule_dst.find_local_time_type(953384400)?.offset(), 43200);
+ assert_eq!(transition_rule_dst.find_local_time_type(970322399)?.offset(), 43200);
+ assert_eq!(transition_rule_dst.find_local_time_type(970322400)?.offset(), 46800);
+
+ let transition_rule_negative_dst = TransitionRule::from(AlternateTime::new(
+ LocalTimeType::new(3600, false, Some(b"IST"))?,
+ LocalTimeType::new(0, true, Some(b"GMT"))?,
+ RuleDay::month_weekday(10, 5, 0)?,
+ 7200,
+ RuleDay::month_weekday(3, 5, 0)?,
+ 3600,
+ )?);
+
+ assert_eq!(transition_rule_negative_dst.find_local_time_type(954032399)?.offset(), 0);
+ assert_eq!(transition_rule_negative_dst.find_local_time_type(954032400)?.offset(), 3600);
+ assert_eq!(transition_rule_negative_dst.find_local_time_type(972781199)?.offset(), 3600);
+ assert_eq!(transition_rule_negative_dst.find_local_time_type(972781200)?.offset(), 0);
+
+ let transition_rule_negative_time_1 = TransitionRule::from(AlternateTime::new(
+ LocalTimeType::new(0, false, None)?,
+ LocalTimeType::new(0, true, None)?,
+ RuleDay::julian_0(100)?,
+ 0,
+ RuleDay::julian_0(101)?,
+ -86500,
+ )?);
+
+ assert!(transition_rule_negative_time_1.find_local_time_type(8639899)?.is_dst());
+ assert!(!transition_rule_negative_time_1.find_local_time_type(8639900)?.is_dst());
+ assert!(!transition_rule_negative_time_1.find_local_time_type(8639999)?.is_dst());
+ assert!(transition_rule_negative_time_1.find_local_time_type(8640000)?.is_dst());
+
+ let transition_rule_negative_time_2 = TransitionRule::from(AlternateTime::new(
+ LocalTimeType::new(-10800, false, Some(b"-03"))?,
+ LocalTimeType::new(-7200, true, Some(b"-02"))?,
+ RuleDay::month_weekday(3, 5, 0)?,
+ -7200,
+ RuleDay::month_weekday(10, 5, 0)?,
+ -3600,
+ )?);
+
+ assert_eq!(
+ transition_rule_negative_time_2.find_local_time_type(954032399)?.offset(),
+ -10800
+ );
+ assert_eq!(
+ transition_rule_negative_time_2.find_local_time_type(954032400)?.offset(),
+ -7200
+ );
+ assert_eq!(
+ transition_rule_negative_time_2.find_local_time_type(972781199)?.offset(),
+ -7200
+ );
+ assert_eq!(
+ transition_rule_negative_time_2.find_local_time_type(972781200)?.offset(),
+ -10800
+ );
+
+ let transition_rule_all_year_dst = TransitionRule::from(AlternateTime::new(
+ LocalTimeType::new(-18000, false, Some(b"EST"))?,
+ LocalTimeType::new(-14400, true, Some(b"EDT"))?,
+ RuleDay::julian_0(0)?,
+ 0,
+ RuleDay::julian_1(365)?,
+ 90000,
+ )?);
+
+ assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702799)?.offset(), -14400);
+ assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702800)?.offset(), -14400);
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_transition_rule_overflow() -> Result<(), Error> {
+ let transition_rule_1 = TransitionRule::from(AlternateTime::new(
+ LocalTimeType::new(-1, false, None)?,
+ LocalTimeType::new(-1, true, None)?,
+ RuleDay::julian_1(365)?,
+ 0,
+ RuleDay::julian_1(1)?,
+ 0,
+ )?);
+
+ let transition_rule_2 = TransitionRule::from(AlternateTime::new(
+ LocalTimeType::new(1, false, None)?,
+ LocalTimeType::new(1, true, None)?,
+ RuleDay::julian_1(365)?,
+ 0,
+ RuleDay::julian_1(1)?,
+ 0,
+ )?);
+
+ let min_unix_time = -67768100567971200;
+ let max_unix_time = 67767976233532799;
+
+ assert!(matches!(
+ transition_rule_1.find_local_time_type(min_unix_time),
+ Err(Error::OutOfRange(_))
+ ));
+ assert!(matches!(
+ transition_rule_2.find_local_time_type(max_unix_time),
+ Err(Error::OutOfRange(_))
+ ));
+
+ Ok(())
+ }
+}
diff --git a/src/offset/local/tz_info/timezone.rs b/src/offset/local/tz_info/timezone.rs
new file mode 100644
index 0000000..02b6f34
--- /dev/null
+++ b/src/offset/local/tz_info/timezone.rs
@@ -0,0 +1,950 @@
+//! Types related to a time zone.
+
+use std::fs::{self, File};
+use std::io::{self, Read};
+use std::path::{Path, PathBuf};
+use std::{cmp::Ordering, fmt, str};
+
+use super::rule::{AlternateTime, TransitionRule};
+use super::{parser, Error, DAYS_PER_WEEK, SECONDS_PER_DAY};
+
+/// Time zone
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub(crate) struct TimeZone {
+ /// List of transitions
+ transitions: Vec<Transition>,
+ /// List of local time types (cannot be empty)
+ local_time_types: Vec<LocalTimeType>,
+ /// List of leap seconds
+ leap_seconds: Vec<LeapSecond>,
+ /// Extra transition rule applicable after the last transition
+ extra_rule: Option<TransitionRule>,
+}
+
+impl TimeZone {
+ /// Returns local time zone.
+ ///
+ /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead.
+ ///
+ pub(crate) fn local(env_tz: Option<&str>) -> Result<Self, Error> {
+ match env_tz {
+ Some(tz) => Self::from_posix_tz(tz),
+ None => Self::from_posix_tz("localtime"),
+ }
+ }
+
+ /// Construct a time zone from a POSIX TZ string, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html).
+ fn from_posix_tz(tz_string: &str) -> Result<Self, Error> {
+ if tz_string.is_empty() {
+ return Err(Error::InvalidTzString("empty TZ string"));
+ }
+
+ if tz_string == "localtime" {
+ return Self::from_tz_data(&fs::read("/etc/localtime")?);
+ }
+
+ // attributes are not allowed on if blocks in Rust 1.38
+ #[cfg(target_os = "android")]
+ {
+ if let Ok(bytes) = android_tzdata::find_tz_data(tz_string) {
+ return Self::from_tz_data(&bytes);
+ }
+ }
+
+ let mut chars = tz_string.chars();
+ if chars.next() == Some(':') {
+ return Self::from_file(&mut find_tz_file(chars.as_str())?);
+ }
+
+ if let Ok(mut file) = find_tz_file(tz_string) {
+ return Self::from_file(&mut file);
+ }
+
+ // TZ string extensions are not allowed
+ let tz_string = tz_string.trim_matches(|c: char| c.is_ascii_whitespace());
+ let rule = TransitionRule::from_tz_string(tz_string.as_bytes(), false)?;
+ Self::new(
+ vec![],
+ match rule {
+ TransitionRule::Fixed(local_time_type) => vec![local_time_type],
+ TransitionRule::Alternate(AlternateTime { std, dst, .. }) => vec![std, dst],
+ },
+ vec![],
+ Some(rule),
+ )
+ }
+
+ /// Construct a time zone
+ pub(super) fn new(
+ transitions: Vec<Transition>,
+ local_time_types: Vec<LocalTimeType>,
+ leap_seconds: Vec<LeapSecond>,
+ extra_rule: Option<TransitionRule>,
+ ) -> Result<Self, Error> {
+ let new = Self { transitions, local_time_types, leap_seconds, extra_rule };
+ new.as_ref().validate()?;
+ Ok(new)
+ }
+
+ /// Construct a time zone from the contents of a time zone file
+ fn from_file(file: &mut File) -> Result<Self, Error> {
+ let mut bytes = Vec::new();
+ file.read_to_end(&mut bytes)?;
+ Self::from_tz_data(&bytes)
+ }
+
+ /// Construct a time zone from the contents of a time zone file
+ ///
+ /// Parse TZif data as described in [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536).
+ pub(crate) fn from_tz_data(bytes: &[u8]) -> Result<Self, Error> {
+ parser::parse(bytes)
+ }
+
+ /// Construct a time zone with the specified UTC offset in seconds
+ fn fixed(ut_offset: i32) -> Result<Self, Error> {
+ Ok(Self {
+ transitions: Vec::new(),
+ local_time_types: vec![LocalTimeType::with_offset(ut_offset)?],
+ leap_seconds: Vec::new(),
+ extra_rule: None,
+ })
+ }
+
+ /// Construct the time zone associated to UTC
+ pub(crate) fn utc() -> Self {
+ Self {
+ transitions: Vec::new(),
+ local_time_types: vec![LocalTimeType::UTC],
+ leap_seconds: Vec::new(),
+ extra_rule: None,
+ }
+ }
+
+ /// Find the local time type associated to the time zone at the specified Unix time in seconds
+ pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> {
+ self.as_ref().find_local_time_type(unix_time)
+ }
+
+ // should we pass NaiveDateTime all the way through to this fn?
+ pub(crate) fn find_local_time_type_from_local(
+ &self,
+ local_time: i64,
+ year: i32,
+ ) -> Result<crate::LocalResult<LocalTimeType>, Error> {
+ self.as_ref().find_local_time_type_from_local(local_time, year)
+ }
+
+ /// Returns a reference to the time zone
+ fn as_ref(&self) -> TimeZoneRef {
+ TimeZoneRef {
+ transitions: &self.transitions,
+ local_time_types: &self.local_time_types,
+ leap_seconds: &self.leap_seconds,
+ extra_rule: &self.extra_rule,
+ }
+ }
+}
+
+/// Reference to a time zone
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub(crate) struct TimeZoneRef<'a> {
+ /// List of transitions
+ transitions: &'a [Transition],
+ /// List of local time types (cannot be empty)
+ local_time_types: &'a [LocalTimeType],
+ /// List of leap seconds
+ leap_seconds: &'a [LeapSecond],
+ /// Extra transition rule applicable after the last transition
+ extra_rule: &'a Option<TransitionRule>,
+}
+
+impl<'a> TimeZoneRef<'a> {
+ /// Find the local time type associated to the time zone at the specified Unix time in seconds
+ pub(crate) fn find_local_time_type(&self, unix_time: i64) -> Result<&'a LocalTimeType, Error> {
+ let extra_rule = match self.transitions.last() {
+ None => match self.extra_rule {
+ Some(extra_rule) => extra_rule,
+ None => return Ok(&self.local_time_types[0]),
+ },
+ Some(last_transition) => {
+ let unix_leap_time = match self.unix_time_to_unix_leap_time(unix_time) {
+ Ok(unix_leap_time) => unix_leap_time,
+ Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)),
+ Err(err) => return Err(err),
+ };
+
+ if unix_leap_time >= last_transition.unix_leap_time {
+ match self.extra_rule {
+ Some(extra_rule) => extra_rule,
+ None => {
+ // RFC 8536 3.2:
+ // "Local time for timestamps on or after the last transition is
+ // specified by the TZ string in the footer (Section 3.3) if present
+ // and nonempty; otherwise, it is unspecified."
+ //
+ // Older versions of macOS (1.12 and before?) have TZif file with a
+ // missing TZ string, and use the offset given by the last transition.
+ return Ok(
+ &self.local_time_types[last_transition.local_time_type_index]
+ );
+ }
+ }
+ } else {
+ let index = match self
+ .transitions
+ .binary_search_by_key(&unix_leap_time, Transition::unix_leap_time)
+ {
+ Ok(x) => x + 1,
+ Err(x) => x,
+ };
+
+ let local_time_type_index = if index > 0 {
+ self.transitions[index - 1].local_time_type_index
+ } else {
+ 0
+ };
+ return Ok(&self.local_time_types[local_time_type_index]);
+ }
+ }
+ };
+
+ match extra_rule.find_local_time_type(unix_time) {
+ Ok(local_time_type) => Ok(local_time_type),
+ Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)),
+ err => err,
+ }
+ }
+
+ pub(crate) fn find_local_time_type_from_local(
+ &self,
+ local_time: i64,
+ year: i32,
+ ) -> Result<crate::LocalResult<LocalTimeType>, Error> {
+ // #TODO: this is wrong as we need 'local_time_to_local_leap_time ?
+ // but ... does the local time even include leap seconds ??
+ // let unix_leap_time = match self.unix_time_to_unix_leap_time(local_time) {
+ // Ok(unix_leap_time) => unix_leap_time,
+ // Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)),
+ // Err(err) => return Err(err),
+ // };
+ let local_leap_time = local_time;
+
+ // if we have at least one transition,
+ // we must check _all_ of them, incase of any Overlapping (LocalResult::Ambiguous) or Skipping (LocalResult::None) transitions
+ let offset_after_last = if !self.transitions.is_empty() {
+ let mut prev = self.local_time_types[0];
+
+ for transition in self.transitions {
+ let after_ltt = self.local_time_types[transition.local_time_type_index];
+
+ // the end and start here refers to where the time starts prior to the transition
+ // and where it ends up after. not the temporal relationship.
+ let transition_end = transition.unix_leap_time + i64::from(after_ltt.ut_offset);
+ let transition_start = transition.unix_leap_time + i64::from(prev.ut_offset);
+
+ match transition_start.cmp(&transition_end) {
+ Ordering::Greater => {
+ // bakwards transition, eg from DST to regular
+ // this means a given local time could have one of two possible offsets
+ if local_leap_time < transition_end {
+ return Ok(crate::LocalResult::Single(prev));
+ } else if local_leap_time >= transition_end
+ && local_leap_time <= transition_start
+ {
+ if prev.ut_offset < after_ltt.ut_offset {
+ return Ok(crate::LocalResult::Ambiguous(prev, after_ltt));
+ } else {
+ return Ok(crate::LocalResult::Ambiguous(after_ltt, prev));
+ }
+ }
+ }
+ Ordering::Equal => {
+ // should this ever happen? presumably we have to handle it anyway.
+ if local_leap_time < transition_start {
+ return Ok(crate::LocalResult::Single(prev));
+ } else if local_leap_time == transition_end {
+ if prev.ut_offset < after_ltt.ut_offset {
+ return Ok(crate::LocalResult::Ambiguous(prev, after_ltt));
+ } else {
+ return Ok(crate::LocalResult::Ambiguous(after_ltt, prev));
+ }
+ }
+ }
+ Ordering::Less => {
+ // forwards transition, eg from regular to DST
+ // this means that times that are skipped are invalid local times
+ if local_leap_time <= transition_start {
+ return Ok(crate::LocalResult::Single(prev));
+ } else if local_leap_time < transition_end {
+ return Ok(crate::LocalResult::None);
+ } else if local_leap_time == transition_end {
+ return Ok(crate::LocalResult::Single(after_ltt));
+ }
+ }
+ }
+
+ // try the next transition, we are fully after this one
+ prev = after_ltt;
+ }
+
+ prev
+ } else {
+ self.local_time_types[0]
+ };
+
+ if let Some(extra_rule) = self.extra_rule {
+ match extra_rule.find_local_time_type_from_local(local_time, year) {
+ Ok(local_time_type) => Ok(local_time_type),
+ Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)),
+ err => err,
+ }
+ } else {
+ Ok(crate::LocalResult::Single(offset_after_last))
+ }
+ }
+
+ /// Check time zone inputs
+ fn validate(&self) -> Result<(), Error> {
+ // Check local time types
+ let local_time_types_size = self.local_time_types.len();
+ if local_time_types_size == 0 {
+ return Err(Error::TimeZone("list of local time types must not be empty"));
+ }
+
+ // Check transitions
+ let mut i_transition = 0;
+ while i_transition < self.transitions.len() {
+ if self.transitions[i_transition].local_time_type_index >= local_time_types_size {
+ return Err(Error::TimeZone("invalid local time type index"));
+ }
+
+ if i_transition + 1 < self.transitions.len()
+ && self.transitions[i_transition].unix_leap_time
+ >= self.transitions[i_transition + 1].unix_leap_time
+ {
+ return Err(Error::TimeZone("invalid transition"));
+ }
+
+ i_transition += 1;
+ }
+
+ // Check leap seconds
+ if !(self.leap_seconds.is_empty()
+ || self.leap_seconds[0].unix_leap_time >= 0
+ && self.leap_seconds[0].correction.saturating_abs() == 1)
+ {
+ return Err(Error::TimeZone("invalid leap second"));
+ }
+
+ let min_interval = SECONDS_PER_28_DAYS - 1;
+
+ let mut i_leap_second = 0;
+ while i_leap_second < self.leap_seconds.len() {
+ if i_leap_second + 1 < self.leap_seconds.len() {
+ let x0 = &self.leap_seconds[i_leap_second];
+ let x1 = &self.leap_seconds[i_leap_second + 1];
+
+ let diff_unix_leap_time = x1.unix_leap_time.saturating_sub(x0.unix_leap_time);
+ let abs_diff_correction =
+ x1.correction.saturating_sub(x0.correction).saturating_abs();
+
+ if !(diff_unix_leap_time >= min_interval && abs_diff_correction == 1) {
+ return Err(Error::TimeZone("invalid leap second"));
+ }
+ }
+ i_leap_second += 1;
+ }
+
+ // Check extra rule
+ let (extra_rule, last_transition) = match (&self.extra_rule, self.transitions.last()) {
+ (Some(rule), Some(trans)) => (rule, trans),
+ _ => return Ok(()),
+ };
+
+ let last_local_time_type = &self.local_time_types[last_transition.local_time_type_index];
+ let unix_time = match self.unix_leap_time_to_unix_time(last_transition.unix_leap_time) {
+ Ok(unix_time) => unix_time,
+ Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)),
+ Err(err) => return Err(err),
+ };
+
+ let rule_local_time_type = match extra_rule.find_local_time_type(unix_time) {
+ Ok(rule_local_time_type) => rule_local_time_type,
+ Err(Error::OutOfRange(error)) => return Err(Error::TimeZone(error)),
+ Err(err) => return Err(err),
+ };
+
+ let check = last_local_time_type.ut_offset == rule_local_time_type.ut_offset
+ && last_local_time_type.is_dst == rule_local_time_type.is_dst
+ && match (&last_local_time_type.name, &rule_local_time_type.name) {
+ (Some(x), Some(y)) => x.equal(y),
+ (None, None) => true,
+ _ => false,
+ };
+
+ if !check {
+ return Err(Error::TimeZone(
+ "extra transition rule is inconsistent with the last transition",
+ ));
+ }
+
+ Ok(())
+ }
+
+ /// Convert Unix time to Unix leap time, from the list of leap seconds in a time zone
+ const fn unix_time_to_unix_leap_time(&self, unix_time: i64) -> Result<i64, Error> {
+ let mut unix_leap_time = unix_time;
+
+ let mut i = 0;
+ while i < self.leap_seconds.len() {
+ let leap_second = &self.leap_seconds[i];
+
+ if unix_leap_time < leap_second.unix_leap_time {
+ break;
+ }
+
+ unix_leap_time = match unix_time.checked_add(leap_second.correction as i64) {
+ Some(unix_leap_time) => unix_leap_time,
+ None => return Err(Error::OutOfRange("out of range operation")),
+ };
+
+ i += 1;
+ }
+
+ Ok(unix_leap_time)
+ }
+
+ /// Convert Unix leap time to Unix time, from the list of leap seconds in a time zone
+ fn unix_leap_time_to_unix_time(&self, unix_leap_time: i64) -> Result<i64, Error> {
+ if unix_leap_time == i64::min_value() {
+ return Err(Error::OutOfRange("out of range operation"));
+ }
+
+ let index = match self
+ .leap_seconds
+ .binary_search_by_key(&(unix_leap_time - 1), LeapSecond::unix_leap_time)
+ {
+ Ok(x) => x + 1,
+ Err(x) => x,
+ };
+
+ let correction = if index > 0 { self.leap_seconds[index - 1].correction } else { 0 };
+
+ match unix_leap_time.checked_sub(correction as i64) {
+ Some(unix_time) => Ok(unix_time),
+ None => Err(Error::OutOfRange("out of range operation")),
+ }
+ }
+
+ /// The UTC time zone
+ const UTC: TimeZoneRef<'static> = TimeZoneRef {
+ transitions: &[],
+ local_time_types: &[LocalTimeType::UTC],
+ leap_seconds: &[],
+ extra_rule: &None,
+ };
+}
+
+/// Transition of a TZif file
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub(super) struct Transition {
+ /// Unix leap time
+ unix_leap_time: i64,
+ /// Index specifying the local time type of the transition
+ local_time_type_index: usize,
+}
+
+impl Transition {
+ /// Construct a TZif file transition
+ pub(super) const fn new(unix_leap_time: i64, local_time_type_index: usize) -> Self {
+ Self { unix_leap_time, local_time_type_index }
+ }
+
+ /// Returns Unix leap time
+ const fn unix_leap_time(&self) -> i64 {
+ self.unix_leap_time
+ }
+}
+
+/// Leap second of a TZif file
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub(super) struct LeapSecond {
+ /// Unix leap time
+ unix_leap_time: i64,
+ /// Leap second correction
+ correction: i32,
+}
+
+impl LeapSecond {
+ /// Construct a TZif file leap second
+ pub(super) const fn new(unix_leap_time: i64, correction: i32) -> Self {
+ Self { unix_leap_time, correction }
+ }
+
+ /// Returns Unix leap time
+ const fn unix_leap_time(&self) -> i64 {
+ self.unix_leap_time
+ }
+}
+
+/// ASCII-encoded fixed-capacity string, used for storing time zone names
+#[derive(Copy, Clone, Eq, PartialEq)]
+struct TimeZoneName {
+ /// Length-prefixed string buffer
+ bytes: [u8; 8],
+}
+
+impl TimeZoneName {
+ /// Construct a time zone name
+ ///
+ /// man tzfile(5):
+ /// Time zone designations should consist of at least three (3) and no more than six (6) ASCII
+ /// characters from the set of alphanumerics, “-”, and “+”. This is for compatibility with
+ /// POSIX requirements for time zone abbreviations.
+ fn new(input: &[u8]) -> Result<Self, Error> {
+ let len = input.len();
+
+ if !(3..=7).contains(&len) {
+ return Err(Error::LocalTimeType(
+ "time zone name must have between 3 and 7 characters",
+ ));
+ }
+
+ let mut bytes = [0; 8];
+ bytes[0] = input.len() as u8;
+
+ let mut i = 0;
+ while i < len {
+ let b = input[i];
+ match b {
+ b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'-' => {}
+ _ => return Err(Error::LocalTimeType("invalid characters in time zone name")),
+ }
+
+ bytes[i + 1] = b;
+ i += 1;
+ }
+
+ Ok(Self { bytes })
+ }
+
+ /// Returns time zone name as a byte slice
+ fn as_bytes(&self) -> &[u8] {
+ match self.bytes[0] {
+ 3 => &self.bytes[1..4],
+ 4 => &self.bytes[1..5],
+ 5 => &self.bytes[1..6],
+ 6 => &self.bytes[1..7],
+ 7 => &self.bytes[1..8],
+ _ => unreachable!(),
+ }
+ }
+
+ /// Check if two time zone names are equal
+ fn equal(&self, other: &Self) -> bool {
+ self.bytes == other.bytes
+ }
+}
+
+impl AsRef<str> for TimeZoneName {
+ fn as_ref(&self) -> &str {
+ // SAFETY: ASCII is valid UTF-8
+ unsafe { str::from_utf8_unchecked(self.as_bytes()) }
+ }
+}
+
+impl fmt::Debug for TimeZoneName {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.as_ref().fmt(f)
+ }
+}
+
+/// Local time type associated to a time zone
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub(crate) struct LocalTimeType {
+ /// Offset from UTC in seconds
+ pub(super) ut_offset: i32,
+ /// Daylight Saving Time indicator
+ is_dst: bool,
+ /// Time zone name
+ name: Option<TimeZoneName>,
+}
+
+impl LocalTimeType {
+ /// Construct a local time type
+ pub(super) fn new(ut_offset: i32, is_dst: bool, name: Option<&[u8]>) -> Result<Self, Error> {
+ if ut_offset == i32::min_value() {
+ return Err(Error::LocalTimeType("invalid UTC offset"));
+ }
+
+ let name = match name {
+ Some(name) => TimeZoneName::new(name)?,
+ None => return Ok(Self { ut_offset, is_dst, name: None }),
+ };
+
+ Ok(Self { ut_offset, is_dst, name: Some(name) })
+ }
+
+ /// Construct a local time type with the specified UTC offset in seconds
+ pub(super) const fn with_offset(ut_offset: i32) -> Result<Self, Error> {
+ if ut_offset == i32::min_value() {
+ return Err(Error::LocalTimeType("invalid UTC offset"));
+ }
+
+ Ok(Self { ut_offset, is_dst: false, name: None })
+ }
+
+ /// Returns offset from UTC in seconds
+ pub(crate) const fn offset(&self) -> i32 {
+ self.ut_offset
+ }
+
+ /// Returns daylight saving time indicator
+ pub(super) const fn is_dst(&self) -> bool {
+ self.is_dst
+ }
+
+ pub(super) const UTC: LocalTimeType = Self { ut_offset: 0, is_dst: false, name: None };
+}
+
+/// Open the TZif file corresponding to a TZ string
+fn find_tz_file(path: impl AsRef<Path>) -> Result<File, Error> {
+ // Don't check system timezone directories on non-UNIX platforms
+ #[cfg(not(unix))]
+ return Ok(File::open(path)?);
+
+ #[cfg(unix)]
+ {
+ let path = path.as_ref();
+ if path.is_absolute() {
+ return Ok(File::open(path)?);
+ }
+
+ for folder in &ZONE_INFO_DIRECTORIES {
+ if let Ok(file) = File::open(PathBuf::from(folder).join(path)) {
+ return Ok(file);
+ }
+ }
+
+ Err(Error::Io(io::ErrorKind::NotFound.into()))
+ }
+}
+
+// Possible system timezone directories
+#[cfg(unix)]
+const ZONE_INFO_DIRECTORIES: [&str; 4] =
+ ["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo", "/usr/share/lib/zoneinfo"];
+
+/// Number of seconds in one week
+pub(crate) const SECONDS_PER_WEEK: i64 = SECONDS_PER_DAY * DAYS_PER_WEEK;
+/// Number of seconds in 28 days
+const SECONDS_PER_28_DAYS: i64 = SECONDS_PER_DAY * 28;
+
+#[cfg(test)]
+mod tests {
+ use super::super::Error;
+ use super::{LeapSecond, LocalTimeType, TimeZone, TimeZoneName, Transition, TransitionRule};
+
+ #[test]
+ fn test_no_dst() -> Result<(), Error> {
+ let tz_string = b"HST10";
+ let transition_rule = TransitionRule::from_tz_string(tz_string, false)?;
+ assert_eq!(transition_rule, LocalTimeType::new(-36000, false, Some(b"HST"))?.into());
+ Ok(())
+ }
+
+ #[test]
+ fn test_error() -> Result<(), Error> {
+ assert!(matches!(
+ TransitionRule::from_tz_string(b"IST-1GMT0", false),
+ Err(Error::UnsupportedTzString(_))
+ ));
+ assert!(matches!(
+ TransitionRule::from_tz_string(b"EET-2EEST", false),
+ Err(Error::UnsupportedTzString(_))
+ ));
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_v1_file_with_leap_seconds() -> Result<(), Error> {
+ let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x1b\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\0\0\0\0UTC\0\x04\xb2\x58\0\0\0\0\x01\x05\xa4\xec\x01\0\0\0\x02\x07\x86\x1f\x82\0\0\0\x03\x09\x67\x53\x03\0\0\0\x04\x0b\x48\x86\x84\0\0\0\x05\x0d\x2b\x0b\x85\0\0\0\x06\x0f\x0c\x3f\x06\0\0\0\x07\x10\xed\x72\x87\0\0\0\x08\x12\xce\xa6\x08\0\0\0\x09\x15\x9f\xca\x89\0\0\0\x0a\x17\x80\xfe\x0a\0\0\0\x0b\x19\x62\x31\x8b\0\0\0\x0c\x1d\x25\xea\x0c\0\0\0\x0d\x21\xda\xe5\x0d\0\0\0\x0e\x25\x9e\x9d\x8e\0\0\0\x0f\x27\x7f\xd1\x0f\0\0\0\x10\x2a\x50\xf5\x90\0\0\0\x11\x2c\x32\x29\x11\0\0\0\x12\x2e\x13\x5c\x92\0\0\0\x13\x30\xe7\x24\x13\0\0\0\x14\x33\xb8\x48\x94\0\0\0\x15\x36\x8c\x10\x15\0\0\0\x16\x43\xb7\x1b\x96\0\0\0\x17\x49\x5c\x07\x97\0\0\0\x18\x4f\xef\x93\x18\0\0\0\x19\x55\x93\x2d\x99\0\0\0\x1a\x58\x68\x46\x9a\0\0\0\x1b\0\0";
+
+ let time_zone = TimeZone::from_tz_data(bytes)?;
+
+ let time_zone_result = TimeZone::new(
+ Vec::new(),
+ vec![LocalTimeType::new(0, false, Some(b"UTC"))?],
+ vec![
+ LeapSecond::new(78796800, 1),
+ LeapSecond::new(94694401, 2),
+ LeapSecond::new(126230402, 3),
+ LeapSecond::new(157766403, 4),
+ LeapSecond::new(189302404, 5),
+ LeapSecond::new(220924805, 6),
+ LeapSecond::new(252460806, 7),
+ LeapSecond::new(283996807, 8),
+ LeapSecond::new(315532808, 9),
+ LeapSecond::new(362793609, 10),
+ LeapSecond::new(394329610, 11),
+ LeapSecond::new(425865611, 12),
+ LeapSecond::new(489024012, 13),
+ LeapSecond::new(567993613, 14),
+ LeapSecond::new(631152014, 15),
+ LeapSecond::new(662688015, 16),
+ LeapSecond::new(709948816, 17),
+ LeapSecond::new(741484817, 18),
+ LeapSecond::new(773020818, 19),
+ LeapSecond::new(820454419, 20),
+ LeapSecond::new(867715220, 21),
+ LeapSecond::new(915148821, 22),
+ LeapSecond::new(1136073622, 23),
+ LeapSecond::new(1230768023, 24),
+ LeapSecond::new(1341100824, 25),
+ LeapSecond::new(1435708825, 26),
+ LeapSecond::new(1483228826, 27),
+ ],
+ None,
+ )?;
+
+ assert_eq!(time_zone, time_zone_result);
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_v2_file() -> Result<(), Error> {
+ let bytes = b"TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\x80\0\0\0\xbb\x05\x43\x48\xbb\x21\x71\x58\xcb\x89\x3d\xc8\xd2\x23\xf4\x70\xd2\x61\x49\x38\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT\0HST\0HDT\0HWT\0HPT\0\0\0\0\0\x01\0\0\0\0\0\x01\0TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\xff\xff\xff\xff\x74\xe0\x70\xbe\xff\xff\xff\xff\xbb\x05\x43\x48\xff\xff\xff\xff\xbb\x21\x71\x58\xff\xff\xff\xff\xcb\x89\x3d\xc8\xff\xff\xff\xff\xd2\x23\xf4\x70\xff\xff\xff\xff\xd2\x61\x49\x38\xff\xff\xff\xff\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT\0HST\0HDT\0HWT\0HPT\0\0\0\0\0\x01\0\0\0\0\0\x01\0\x0aHST10\x0a";
+
+ let time_zone = TimeZone::from_tz_data(bytes)?;
+
+ let time_zone_result = TimeZone::new(
+ vec![
+ Transition::new(-2334101314, 1),
+ Transition::new(-1157283000, 2),
+ Transition::new(-1155436200, 1),
+ Transition::new(-880198200, 3),
+ Transition::new(-769395600, 4),
+ Transition::new(-765376200, 1),
+ Transition::new(-712150200, 5),
+ ],
+ vec![
+ LocalTimeType::new(-37886, false, Some(b"LMT"))?,
+ LocalTimeType::new(-37800, false, Some(b"HST"))?,
+ LocalTimeType::new(-34200, true, Some(b"HDT"))?,
+ LocalTimeType::new(-34200, true, Some(b"HWT"))?,
+ LocalTimeType::new(-34200, true, Some(b"HPT"))?,
+ LocalTimeType::new(-36000, false, Some(b"HST"))?,
+ ],
+ Vec::new(),
+ Some(TransitionRule::from(LocalTimeType::new(-36000, false, Some(b"HST"))?)),
+ )?;
+
+ assert_eq!(time_zone, time_zone_result);
+
+ assert_eq!(
+ *time_zone.find_local_time_type(-1156939200)?,
+ LocalTimeType::new(-34200, true, Some(b"HDT"))?
+ );
+ assert_eq!(
+ *time_zone.find_local_time_type(1546300800)?,
+ LocalTimeType::new(-36000, false, Some(b"HST"))?
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_no_tz_string() -> Result<(), Error> {
+ // Guayaquil from macOS 10.11
+ let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02\0\0\0\x02\0\0\0\0\0\0\0\x01\0\0\0\x02\0\0\0\x08\xb6\xa4B\x18\x01\xff\xff\xb6h\0\0\xff\xff\xb9\xb0\0\x04QMT\0ECT\0\0\0\0\0";
+
+ let time_zone = TimeZone::from_tz_data(bytes)?;
+ dbg!(&time_zone);
+
+ let time_zone_result = TimeZone::new(
+ vec![Transition::new(-1230749160, 1)],
+ vec![
+ LocalTimeType::new(-18840, false, Some(b"QMT"))?,
+ LocalTimeType::new(-18000, false, Some(b"ECT"))?,
+ ],
+ Vec::new(),
+ None,
+ )?;
+
+ assert_eq!(time_zone, time_zone_result);
+
+ assert_eq!(
+ *time_zone.find_local_time_type(-1500000000)?,
+ LocalTimeType::new(-18840, false, Some(b"QMT"))?
+ );
+ assert_eq!(
+ *time_zone.find_local_time_type(0)?,
+ LocalTimeType::new(-18000, false, Some(b"ECT"))?
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_tz_ascii_str() -> Result<(), Error> {
+ assert!(matches!(TimeZoneName::new(b""), Err(Error::LocalTimeType(_))));
+ assert!(matches!(TimeZoneName::new(b"A"), Err(Error::LocalTimeType(_))));
+ assert!(matches!(TimeZoneName::new(b"AB"), Err(Error::LocalTimeType(_))));
+ assert_eq!(TimeZoneName::new(b"CET")?.as_bytes(), b"CET");
+ assert_eq!(TimeZoneName::new(b"CHADT")?.as_bytes(), b"CHADT");
+ assert_eq!(TimeZoneName::new(b"abcdefg")?.as_bytes(), b"abcdefg");
+ assert_eq!(TimeZoneName::new(b"UTC+02")?.as_bytes(), b"UTC+02");
+ assert_eq!(TimeZoneName::new(b"-1230")?.as_bytes(), b"-1230");
+ assert!(matches!(TimeZoneName::new("−0330".as_bytes()), Err(Error::LocalTimeType(_)))); // MINUS SIGN (U+2212)
+ assert!(matches!(TimeZoneName::new(b"\x00123"), Err(Error::LocalTimeType(_))));
+ assert!(matches!(TimeZoneName::new(b"12345678"), Err(Error::LocalTimeType(_))));
+ assert!(matches!(TimeZoneName::new(b"GMT\0\0\0"), Err(Error::LocalTimeType(_))));
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_time_zone() -> Result<(), Error> {
+ let utc = LocalTimeType::UTC;
+ let cet = LocalTimeType::with_offset(3600)?;
+
+ let utc_local_time_types = vec![utc];
+ let fixed_extra_rule = TransitionRule::from(cet);
+
+ let time_zone_1 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], None)?;
+ let time_zone_2 =
+ TimeZone::new(vec![], utc_local_time_types.clone(), vec![], Some(fixed_extra_rule))?;
+ let time_zone_3 =
+ TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types.clone(), vec![], None)?;
+ let time_zone_4 = TimeZone::new(
+ vec![Transition::new(i32::min_value().into(), 0), Transition::new(0, 1)],
+ vec![utc, cet],
+ Vec::new(),
+ Some(fixed_extra_rule),
+ )?;
+
+ assert_eq!(*time_zone_1.find_local_time_type(0)?, utc);
+ assert_eq!(*time_zone_2.find_local_time_type(0)?, cet);
+
+ assert_eq!(*time_zone_3.find_local_time_type(-1)?, utc);
+ assert_eq!(*time_zone_3.find_local_time_type(0)?, utc);
+
+ assert_eq!(*time_zone_4.find_local_time_type(-1)?, utc);
+ assert_eq!(*time_zone_4.find_local_time_type(0)?, cet);
+
+ let time_zone_err = TimeZone::new(
+ vec![Transition::new(0, 0)],
+ utc_local_time_types,
+ vec![],
+ Some(fixed_extra_rule),
+ );
+ assert!(time_zone_err.is_err());
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_time_zone_from_posix_tz() -> Result<(), Error> {
+ #[cfg(unix)]
+ {
+ // if the TZ var is set, this essentially _overrides_ the
+ // time set by the localtime symlink
+ // so just ensure that ::local() acts as expected
+ // in this case
+ if let Ok(tz) = std::env::var("TZ") {
+ let time_zone_local = TimeZone::local(Some(tz.as_str()))?;
+ let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?;
+ assert_eq!(time_zone_local, time_zone_local_1);
+ }
+
+ // `TimeZone::from_posix_tz("UTC")` will return `Error` if the environment does not have
+ // a time zone database, like for example some docker containers.
+ // In that case skip the test.
+ if let Ok(time_zone_utc) = TimeZone::from_posix_tz("UTC") {
+ assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0);
+ }
+ }
+
+ assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25").is_err());
+ assert!(TimeZone::from_posix_tz("").is_err());
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_leap_seconds() -> Result<(), Error> {
+ let time_zone = TimeZone::new(
+ Vec::new(),
+ vec![LocalTimeType::new(0, false, Some(b"UTC"))?],
+ vec![
+ LeapSecond::new(78796800, 1),
+ LeapSecond::new(94694401, 2),
+ LeapSecond::new(126230402, 3),
+ LeapSecond::new(157766403, 4),
+ LeapSecond::new(189302404, 5),
+ LeapSecond::new(220924805, 6),
+ LeapSecond::new(252460806, 7),
+ LeapSecond::new(283996807, 8),
+ LeapSecond::new(315532808, 9),
+ LeapSecond::new(362793609, 10),
+ LeapSecond::new(394329610, 11),
+ LeapSecond::new(425865611, 12),
+ LeapSecond::new(489024012, 13),
+ LeapSecond::new(567993613, 14),
+ LeapSecond::new(631152014, 15),
+ LeapSecond::new(662688015, 16),
+ LeapSecond::new(709948816, 17),
+ LeapSecond::new(741484817, 18),
+ LeapSecond::new(773020818, 19),
+ LeapSecond::new(820454419, 20),
+ LeapSecond::new(867715220, 21),
+ LeapSecond::new(915148821, 22),
+ LeapSecond::new(1136073622, 23),
+ LeapSecond::new(1230768023, 24),
+ LeapSecond::new(1341100824, 25),
+ LeapSecond::new(1435708825, 26),
+ LeapSecond::new(1483228826, 27),
+ ],
+ None,
+ )?;
+
+ let time_zone_ref = time_zone.as_ref();
+
+ assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073621), Ok(1136073599)));
+ assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073622), Ok(1136073600)));
+ assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073623), Ok(1136073600)));
+ assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073624), Ok(1136073601)));
+
+ assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073599), Ok(1136073621)));
+ assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073600), Ok(1136073623)));
+ assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073601), Ok(1136073624)));
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_leap_seconds_overflow() -> Result<(), Error> {
+ let time_zone_err = TimeZone::new(
+ vec![Transition::new(i64::min_value(), 0)],
+ vec![LocalTimeType::UTC],
+ vec![LeapSecond::new(0, 1)],
+ Some(TransitionRule::from(LocalTimeType::UTC)),
+ );
+ assert!(time_zone_err.is_err());
+
+ let time_zone = TimeZone::new(
+ vec![Transition::new(i64::max_value(), 0)],
+ vec![LocalTimeType::UTC],
+ vec![LeapSecond::new(0, 1)],
+ None,
+ )?;
+ assert!(matches!(
+ time_zone.find_local_time_type(i64::max_value()),
+ Err(Error::FindLocalTimeType(_))
+ ));
+
+ Ok(())
+ }
+}
diff --git a/src/offset/local/unix.rs b/src/offset/local/unix.rs
new file mode 100644
index 0000000..ce96a6e
--- /dev/null
+++ b/src/offset/local/unix.rs
@@ -0,0 +1,171 @@
+// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+use std::{cell::RefCell, collections::hash_map, env, fs, hash::Hasher, time::SystemTime};
+
+use super::tz_info::TimeZone;
+use super::{FixedOffset, NaiveDateTime};
+use crate::{Datelike, LocalResult};
+
+pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset> {
+ offset(utc, false)
+}
+
+pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset> {
+ offset(local, true)
+}
+
+fn offset(d: &NaiveDateTime, local: bool) -> LocalResult<FixedOffset> {
+ TZ_INFO.with(|maybe_cache| {
+ maybe_cache.borrow_mut().get_or_insert_with(Cache::default).offset(*d, local)
+ })
+}
+
+// we have to store the `Cache` in an option as it can't
+// be initalized in a static context.
+thread_local! {
+ static TZ_INFO: RefCell<Option<Cache>> = Default::default();
+}
+
+enum Source {
+ LocalTime { mtime: SystemTime },
+ Environment { hash: u64 },
+}
+
+impl Source {
+ fn new(env_tz: Option<&str>) -> Source {
+ match env_tz {
+ Some(tz) => {
+ let mut hasher = hash_map::DefaultHasher::new();
+ hasher.write(tz.as_bytes());
+ let hash = hasher.finish();
+ Source::Environment { hash }
+ }
+ None => match fs::symlink_metadata("/etc/localtime") {
+ Ok(data) => Source::LocalTime {
+ // we have to pick a sensible default when the mtime fails
+ // by picking SystemTime::now() we raise the probability of
+ // the cache being invalidated if/when the mtime starts working
+ mtime: data.modified().unwrap_or_else(|_| SystemTime::now()),
+ },
+ Err(_) => {
+ // as above, now() should be a better default than some constant
+ // TODO: see if we can improve caching in the case where the fallback is a valid timezone
+ Source::LocalTime { mtime: SystemTime::now() }
+ }
+ },
+ }
+ }
+}
+
+struct Cache {
+ zone: TimeZone,
+ source: Source,
+ last_checked: SystemTime,
+}
+
+#[cfg(target_os = "aix")]
+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<TimeZone> {
+ let tz_name = iana_time_zone::get_timezone().ok()?;
+ #[cfg(not(target_os = "android"))]
+ let bytes = fs::read(format!("{}/{}", TZDB_LOCATION, tz_name)).ok()?;
+ #[cfg(target_os = "android")]
+ let bytes = android_tzdata::find_tz_data(&tz_name).ok()?;
+ TimeZone::from_tz_data(&bytes).ok()
+}
+
+impl Default for Cache {
+ fn default() -> Cache {
+ // default to UTC if no local timezone can be found
+ let env_tz = env::var("TZ").ok();
+ let env_ref = env_tz.as_deref();
+ Cache {
+ last_checked: SystemTime::now(),
+ source: Source::new(env_ref),
+ zone: current_zone(env_ref),
+ }
+ }
+}
+
+fn current_zone(var: Option<&str>) -> TimeZone {
+ TimeZone::local(var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc)
+}
+
+impl Cache {
+ fn offset(&mut self, d: NaiveDateTime, local: bool) -> LocalResult<FixedOffset> {
+ let now = SystemTime::now();
+
+ match now.duration_since(self.last_checked) {
+ // If the cache has been around for less than a second then we reuse it
+ // unconditionally. This is a reasonable tradeoff because the timezone
+ // generally won't be changing _that_ often, but if the time zone does
+ // change, it will reflect sufficiently quickly from an application
+ // user's perspective.
+ Ok(d) if d.as_secs() < 1 => (),
+ Ok(_) | Err(_) => {
+ let env_tz = env::var("TZ").ok();
+ let env_ref = env_tz.as_deref();
+ let new_source = Source::new(env_ref);
+
+ let out_of_date = match (&self.source, &new_source) {
+ // change from env to file or file to env, must recreate the zone
+ (Source::Environment { .. }, Source::LocalTime { .. })
+ | (Source::LocalTime { .. }, Source::Environment { .. }) => true,
+ // stay as file, but mtime has changed
+ (Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime })
+ if old_mtime != mtime =>
+ {
+ true
+ }
+ // stay as env, but hash of variable has changed
+ (Source::Environment { hash: old_hash }, Source::Environment { hash })
+ if old_hash != hash =>
+ {
+ true
+ }
+ // cache can be reused
+ _ => false,
+ };
+
+ if out_of_date {
+ self.zone = current_zone(env_ref);
+ }
+
+ self.last_checked = now;
+ self.source = new_source;
+ }
+ }
+
+ if !local {
+ let offset = self
+ .zone
+ .find_local_time_type(d.timestamp())
+ .expect("unable to select local time type")
+ .offset();
+
+ return match FixedOffset::east_opt(offset) {
+ Some(offset) => LocalResult::Single(offset),
+ None => LocalResult::None,
+ };
+ }
+
+ // 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_opt(o.offset()).unwrap())
+ }
+}
diff --git a/src/offset/local/win_bindings.rs b/src/offset/local/win_bindings.rs
new file mode 100644
index 0000000..7574fb3
--- /dev/null
+++ b/src/offset/local/win_bindings.rs
@@ -0,0 +1,71 @@
+// Bindings generated by `windows-bindgen` 0.52.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);
+pub type BOOL = i32;
+pub type BOOLEAN = u8;
+#[repr(C)]
+pub struct DYNAMIC_TIME_ZONE_INFORMATION {
+ pub Bias: i32,
+ pub StandardName: [u16; 32],
+ pub StandardDate: SYSTEMTIME,
+ pub StandardBias: i32,
+ pub DaylightName: [u16; 32],
+ pub DaylightDate: SYSTEMTIME,
+ pub DaylightBias: i32,
+ 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 {
+ fn clone(&self) -> Self {
+ *self
+ }
+}
+#[repr(C)]
+pub struct FILETIME {
+ pub dwLowDateTime: u32,
+ pub dwHighDateTime: u32,
+}
+impl ::core::marker::Copy for FILETIME {}
+impl ::core::clone::Clone for FILETIME {
+ fn clone(&self) -> Self {
+ *self
+ }
+}
+#[repr(C)]
+pub struct SYSTEMTIME {
+ pub wYear: u16,
+ pub wMonth: u16,
+ pub wDayOfWeek: u16,
+ pub wDay: u16,
+ pub wHour: u16,
+ pub wMinute: u16,
+ pub wSecond: u16,
+ pub wMilliseconds: u16,
+}
+impl ::core::marker::Copy for SYSTEMTIME {}
+impl ::core::clone::Clone for SYSTEMTIME {
+ fn clone(&self) -> Self {
+ *self
+ }
+}
+#[repr(C)]
+pub struct TIME_ZONE_INFORMATION {
+ pub Bias: i32,
+ pub StandardName: [u16; 32],
+ pub StandardDate: SYSTEMTIME,
+ pub StandardBias: i32,
+ pub DaylightName: [u16; 32],
+ pub DaylightDate: SYSTEMTIME,
+ pub DaylightBias: i32,
+}
+impl ::core::marker::Copy for TIME_ZONE_INFORMATION {}
+impl ::core::clone::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
new file mode 100644
index 0000000..7fb3e2f
--- /dev/null
+++ b/src/offset/local/win_bindings.txt
@@ -0,0 +1,7 @@
+--out src/offset/local/win_bindings.rs
+--config flatten sys
+--filter
+ Windows.Win32.System.Time.GetTimeZoneInformationForYear
+ Windows.Win32.System.Time.SystemTimeToFileTime
+ Windows.Win32.System.Time.SystemTimeToTzSpecificLocalTime
+ Windows.Win32.System.Time.TzSpecificLocalTimeToSystemTime
diff --git a/src/offset/local/windows.rs b/src/offset/local/windows.rs
new file mode 100644
index 0000000..cee09ec
--- /dev/null
+++ b/src/offset/local/windows.rs
@@ -0,0 +1,262 @@
+// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+use std::cmp::Ordering;
+use std::convert::TryFrom;
+use std::mem::MaybeUninit;
+use std::ptr;
+
+use super::win_bindings::{GetTimeZoneInformationForYear, SYSTEMTIME, TIME_ZONE_INFORMATION};
+
+use crate::offset::local::{lookup_with_dst_transitions, Transition};
+use crate::{Datelike, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Weekday};
+
+// We don't use `SystemTimeToTzSpecificLocalTime` because it doesn't support the same range of dates
+// as Chrono. Also it really isn't that difficult to work out the correct offset from the provided
+// DST rules.
+//
+// This method uses `overflowing_sub_offset` because it is no problem if the transition time in UTC
+// falls a couple of hours inside the buffer space around the `NaiveDateTime` range (although it is
+// very theoretical to have a transition at midnight around `NaiveDate::(MIN|MAX)`.
+pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset> {
+ // Using a `TzInfo` based on the year of an UTC datetime is technically wrong, we should be
+ // using the rules for the year of the corresponding local time. But this matches what
+ // `SystemTimeToTzSpecificLocalTime` is documented to do.
+ let tz_info = match TzInfo::for_year(utc.year()) {
+ Some(tz_info) => tz_info,
+ None => return LocalResult::None,
+ };
+ let offset = match (tz_info.std_transition, tz_info.dst_transition) {
+ (Some(std_transition), Some(dst_transition)) => {
+ let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset);
+ let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset);
+ if dst_transition_utc < std_transition_utc {
+ match utc >= &dst_transition_utc && utc < &std_transition_utc {
+ true => tz_info.dst_offset,
+ false => tz_info.std_offset,
+ }
+ } else {
+ match utc >= &std_transition_utc && utc < &dst_transition_utc {
+ true => tz_info.std_offset,
+ false => tz_info.dst_offset,
+ }
+ }
+ }
+ (Some(std_transition), None) => {
+ let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset);
+ match utc < &std_transition_utc {
+ true => tz_info.dst_offset,
+ false => tz_info.std_offset,
+ }
+ }
+ (None, Some(dst_transition)) => {
+ let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset);
+ match utc < &dst_transition_utc {
+ true => tz_info.std_offset,
+ false => tz_info.dst_offset,
+ }
+ }
+ (None, None) => tz_info.std_offset,
+ };
+ LocalResult::Single(offset)
+}
+
+// We don't use `TzSpecificLocalTimeToSystemTime` because it doesn't let us choose how to handle
+// ambiguous cases (during a DST transition). Instead we get the timezone information for the
+// current year and compute it ourselves, like we do on Unix.
+pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset> {
+ let tz_info = match TzInfo::for_year(local.year()) {
+ Some(tz_info) => tz_info,
+ None => return LocalResult::None,
+ };
+ // Create a sorted slice of transitions and use `lookup_with_dst_transitions`.
+ match (tz_info.std_transition, tz_info.dst_transition) {
+ (Some(std_transition), Some(dst_transition)) => {
+ let std_transition =
+ Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset);
+ let dst_transition =
+ Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset);
+ let transitions = match std_transition.cmp(&dst_transition) {
+ Ordering::Less => [std_transition, dst_transition],
+ Ordering::Greater => [dst_transition, std_transition],
+ Ordering::Equal => {
+ // This doesn't make sense. Let's just return the standard offset.
+ return LocalResult::Single(tz_info.std_offset);
+ }
+ };
+ lookup_with_dst_transitions(&transitions, *local)
+ }
+ (Some(std_transition), None) => {
+ let transitions =
+ [Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset)];
+ lookup_with_dst_transitions(&transitions, *local)
+ }
+ (None, Some(dst_transition)) => {
+ let transitions =
+ [Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset)];
+ lookup_with_dst_transitions(&transitions, *local)
+ }
+ (None, None) => return LocalResult::Single(tz_info.std_offset),
+ }
+}
+
+// The basis for Windows timezone and DST support has been in place since Windows 2000. It does not
+// allow for complex rules like the IANA timezone database:
+// - A timezone has the same base offset the whole year.
+// - There seem to be either zero or two DST transitions (but we support having just one).
+// - As of Vista(?) only years from 2004 until a few years into the future are supported.
+// - All other years get the base settings, which seem to be that of the current year.
+//
+// These details don't matter much, we just work with the offsets and transition dates Windows
+// returns through `GetTimeZoneInformationForYear` for a particular year.
+struct TzInfo {
+ // Offset from UTC during standard time.
+ std_offset: FixedOffset,
+ // Offset from UTC during daylight saving time.
+ dst_offset: FixedOffset,
+ // Transition from standard time to daylight saving time, given in local standard time.
+ std_transition: Option<NaiveDateTime>,
+ // Transition from daylight saving time to standard time, given in local daylight saving time.
+ dst_transition: Option<NaiveDateTime>,
+}
+
+impl TzInfo {
+ fn for_year(year: i32) -> Option<TzInfo> {
+ // 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
+ // for years beyond.
+ let ref_year = year.clamp(1601, 30827) as u16;
+ let tz_info = unsafe {
+ let mut tz_info = MaybeUninit::<TIME_ZONE_INFORMATION>::uninit();
+ if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 {
+ return None;
+ }
+ tz_info.assume_init()
+ };
+ Some(TzInfo {
+ std_offset: FixedOffset::west_opt((tz_info.Bias + tz_info.StandardBias) * 60)?,
+ dst_offset: FixedOffset::west_opt((tz_info.Bias + tz_info.DaylightBias) * 60)?,
+ std_transition: system_time_from_naive_date_time(tz_info.StandardDate, year),
+ dst_transition: system_time_from_naive_date_time(tz_info.DaylightDate, year),
+ })
+ }
+}
+
+fn system_time_from_naive_date_time(st: SYSTEMTIME, year: i32) -> Option<NaiveDateTime> {
+ if st.wYear == 0 && st.wMonth == 0 {
+ return None; // No DST transitions for this year in this timezone.
+ }
+ let time = NaiveTime::from_hms_milli_opt(
+ st.wHour as u32,
+ st.wMinute as u32,
+ st.wSecond as u32,
+ st.wMilliseconds as u32,
+ )?;
+ // 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();
+ if st.wYear != 0 {
+ return NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32)
+ .map(|d| d.and_time(time));
+ }
+ let date = if let Some(date) =
+ NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, day_of_week, st.wDay as u8)
+ {
+ date
+ } else if st.wDay == 5 {
+ NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, day_of_week, 4)?
+ } else {
+ return None;
+ };
+ Some(date.and_time(time))
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::offset::local::win_bindings::{
+ SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime, FILETIME, SYSTEMTIME,
+ };
+ use crate::{DateTime, Duration, FixedOffset, Local, NaiveDate, NaiveDateTime};
+ use crate::{Datelike, TimeZone, Timelike};
+ use std::mem::MaybeUninit;
+ use std::ptr;
+
+ #[test]
+ fn verify_against_tz_specific_local_time_to_system_time() {
+ // The implementation in Windows itself is the source of truth on how to work with the OS
+ // timezone information. This test compares for every hour over a period of 125 years our
+ // implementation to `TzSpecificLocalTimeToSystemTime`.
+ //
+ // This uses parts of a previous Windows `Local` implementation in chrono.
+ fn from_local_time(dt: &NaiveDateTime) -> DateTime<Local> {
+ let st = system_time_from_naive_date_time(dt);
+ let utc_time = local_to_utc_time(&st);
+ let utc_secs = system_time_as_unix_seconds(&utc_time);
+ let local_secs = system_time_as_unix_seconds(&st);
+ let offset = (local_secs - utc_secs) as i32;
+ let offset = FixedOffset::east_opt(offset).unwrap();
+ DateTime::from_naive_utc_and_offset(*dt - offset, offset)
+ }
+ fn system_time_from_naive_date_time(dt: &NaiveDateTime) -> SYSTEMTIME {
+ SYSTEMTIME {
+ // Valid values: 1601-30827
+ wYear: dt.year() as u16,
+ // Valid values:1-12
+ wMonth: dt.month() as u16,
+ // Valid values: 0-6, starting Sunday.
+ // NOTE: enum returns 1-7, starting Monday, so we are
+ // off here, but this is not currently used in local.
+ wDayOfWeek: dt.weekday() as u16,
+ // Valid values: 1-31
+ wDay: dt.day() as u16,
+ // Valid values: 0-23
+ wHour: dt.hour() as u16,
+ // Valid values: 0-59
+ wMinute: dt.minute() as u16,
+ // Valid values: 0-59
+ wSecond: dt.second() as u16,
+ // Valid values: 0-999
+ wMilliseconds: 0,
+ }
+ }
+ fn local_to_utc_time(local: &SYSTEMTIME) -> SYSTEMTIME {
+ let mut sys_time = MaybeUninit::<SYSTEMTIME>::uninit();
+ unsafe { TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()) };
+ // SAFETY: TzSpecificLocalTimeToSystemTime must have succeeded at this point, so we can
+ // assume the value is initialized.
+ unsafe { sys_time.assume_init() }
+ }
+ const HECTONANOSECS_IN_SEC: i64 = 10_000_000;
+ const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC;
+ fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> i64 {
+ let mut init = MaybeUninit::<FILETIME>::uninit();
+ unsafe {
+ SystemTimeToFileTime(st, init.as_mut_ptr());
+ }
+ // SystemTimeToFileTime must have succeeded at this point, so we can assume the value is
+ // initalized.
+ let filetime = unsafe { init.assume_init() };
+ let bit_shift =
+ ((filetime.dwHighDateTime as u64) << 32) | (filetime.dwLowDateTime as u64);
+ (bit_shift as i64 - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC
+ }
+
+ let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 30, 0).unwrap();
+
+ while date.year() < 2078 {
+ // Windows doesn't handle non-existing dates, it just treats it as valid.
+ if let Some(our_result) = Local.from_local_datetime(&date).earliest() {
+ assert_eq!(from_local_time(&date), our_result);
+ }
+ date += Duration::hours(1);
+ }
+ }
+}
diff --git a/src/offset/mod.rs b/src/offset/mod.rs
index 0da6bfb..11928b4 100644
--- a/src/offset/mod.rs
+++ b/src/offset/mod.rs
@@ -20,10 +20,22 @@
use core::fmt;
-use format::{parse, ParseResult, Parsed, StrftimeItems};
-use naive::{NaiveDate, NaiveDateTime, NaiveTime};
-use Weekday;
-use {Date, DateTime};
+use crate::format::{parse, ParseResult, Parsed, StrftimeItems};
+use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime};
+use crate::Weekday;
+#[allow(deprecated)]
+use crate::{Date, DateTime};
+
+pub(crate) mod fixed;
+pub use self::fixed::FixedOffset;
+
+#[cfg(feature = "clock")]
+pub(crate) mod local;
+#[cfg(feature = "clock")]
+pub use self::local::Local;
+
+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)]
@@ -40,6 +52,7 @@ pub enum LocalResult<T> {
impl<T> LocalResult<T> {
/// Returns `Some` only when the conversion result is unique, or `None` otherwise.
+ #[must_use]
pub fn single(self) -> Option<T> {
match self {
LocalResult::Single(t) => Some(t),
@@ -48,6 +61,7 @@ impl<T> LocalResult<T> {
}
/// Returns `Some` for the earliest possible conversion result, or `None` if none.
+ #[must_use]
pub fn earliest(self) -> Option<T> {
match self {
LocalResult::Single(t) | LocalResult::Ambiguous(t, _) => Some(t),
@@ -56,6 +70,7 @@ impl<T> LocalResult<T> {
}
/// Returns `Some` for the latest possible conversion result, or `None` if none.
+ #[must_use]
pub fn latest(self) -> Option<T> {
match self {
LocalResult::Single(t) | LocalResult::Ambiguous(_, t) => Some(t),
@@ -64,6 +79,7 @@ impl<T> LocalResult<T> {
}
/// Maps a `LocalResult<T>` into `LocalResult<U>` with given function.
+ #[must_use]
pub fn map<U, F: FnMut(T) -> U>(self, mut f: F) -> LocalResult<U> {
match self {
LocalResult::None => LocalResult::None,
@@ -73,12 +89,14 @@ impl<T> LocalResult<T> {
}
}
+#[allow(deprecated)]
impl<Tz: TimeZone> LocalResult<Date<Tz>> {
/// Makes a new `DateTime` from the current date and given `NaiveTime`.
/// The offset in the current date is preserved.
///
/// Propagates any error. Ambiguous result would be discarded.
#[inline]
+ #[must_use]
pub fn and_time(self, time: NaiveTime) -> LocalResult<DateTime<Tz>> {
match self {
LocalResult::Single(d) => {
@@ -93,6 +111,7 @@ impl<Tz: TimeZone> LocalResult<Date<Tz>> {
///
/// Propagates any error. Ambiguous result would be discarded.
#[inline]
+ #[must_use]
pub fn and_hms_opt(self, hour: u32, min: u32, sec: u32) -> LocalResult<DateTime<Tz>> {
match self {
LocalResult::Single(d) => {
@@ -108,6 +127,7 @@ impl<Tz: TimeZone> LocalResult<Date<Tz>> {
///
/// Propagates any error. Ambiguous result would be discarded.
#[inline]
+ #[must_use]
pub fn and_hms_milli_opt(
self,
hour: u32,
@@ -129,6 +149,7 @@ impl<Tz: TimeZone> LocalResult<Date<Tz>> {
///
/// Propagates any error. Ambiguous result would be discarded.
#[inline]
+ #[must_use]
pub fn and_hms_micro_opt(
self,
hour: u32,
@@ -150,6 +171,7 @@ impl<Tz: TimeZone> LocalResult<Date<Tz>> {
///
/// Propagates any error. Ambiguous result would be discarded.
#[inline]
+ #[must_use]
pub fn and_hms_nano_opt(
self,
hour: u32,
@@ -168,6 +190,8 @@ impl<Tz: TimeZone> LocalResult<Date<Tz>> {
impl<T: fmt::Debug> LocalResult<T> {
/// Returns the single unique conversion result, or panics accordingly.
+ #[must_use]
+ #[track_caller]
pub fn unwrap(self) -> T {
match self {
LocalResult::None => panic!("No such local time"),
@@ -187,14 +211,34 @@ pub trait Offset: Sized + Clone + fmt::Debug {
/// The time zone.
///
-/// The methods here are the primarily constructors for [`Date`](../struct.Date.html) and
-/// [`DateTime`](../struct.DateTime.html) types.
+/// The methods here are the primary constructors for the [`DateTime`] type.
pub trait TimeZone: Sized + Clone {
/// An associated offset type.
/// This type is used to store the actual offset in date and time types.
/// The original `TimeZone` value can be recovered via `TimeZone::from_offset`.
type Offset: Offset;
+ /// Make a new `DateTime` from year, month, day, time components and current time zone.
+ ///
+ /// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE.
+ ///
+ /// Returns `LocalResult::None` on invalid input data.
+ fn with_ymd_and_hms(
+ &self,
+ year: i32,
+ month: u32,
+ day: u32,
+ hour: u32,
+ min: u32,
+ sec: u32,
+ ) -> LocalResult<DateTime<Self>> {
+ match NaiveDate::from_ymd_opt(year, month, day).and_then(|d| d.and_hms_opt(hour, min, sec))
+ {
+ Some(dt) => self.from_local_datetime(&dt),
+ None => LocalResult::None,
+ }
+ }
+
/// Makes a new `Date` from year, month, day and the current time zone.
/// This assumes the proleptic Gregorian calendar, with the year 0 being 1 BCE.
///
@@ -202,14 +246,8 @@ pub trait TimeZone: Sized + Clone {
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Panics on the out-of-range date, invalid month and/or day.
- ///
- /// # Example
- ///
- /// ~~~~
- /// use chrono::{Utc, TimeZone};
- ///
- /// assert_eq!(Utc.ymd(2015, 5, 15).to_string(), "2015-05-15UTC");
- /// ~~~~
+ #[deprecated(since = "0.4.23", note = "use `with_ymd_and_hms()` instead")]
+ #[allow(deprecated)]
fn ymd(&self, year: i32, month: u32, day: u32) -> Date<Self> {
self.ymd_opt(year, month, day).unwrap()
}
@@ -221,15 +259,8 @@ pub trait TimeZone: Sized + Clone {
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Returns `None` on the out-of-range date, invalid month and/or day.
- ///
- /// # Example
- ///
- /// ~~~~
- /// use chrono::{Utc, LocalResult, TimeZone};
- ///
- /// assert_eq!(Utc.ymd_opt(2015, 5, 15).unwrap().to_string(), "2015-05-15UTC");
- /// assert_eq!(Utc.ymd_opt(2000, 0, 0), LocalResult::None);
- /// ~~~~
+ #[deprecated(since = "0.4.23", note = "use `with_ymd_and_hms()` instead")]
+ #[allow(deprecated)]
fn ymd_opt(&self, year: i32, month: u32, day: u32) -> LocalResult<Date<Self>> {
match NaiveDate::from_ymd_opt(year, month, day) {
Some(d) => self.from_local_date(&d),
@@ -244,14 +275,11 @@ pub trait TimeZone: Sized + Clone {
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Panics on the out-of-range date and/or invalid DOY.
- ///
- /// # Example
- ///
- /// ~~~~
- /// use chrono::{Utc, TimeZone};
- ///
- /// assert_eq!(Utc.yo(2015, 135).to_string(), "2015-05-15UTC");
- /// ~~~~
+ #[deprecated(
+ since = "0.4.23",
+ note = "use `from_local_datetime()` with a `NaiveDateTime` instead"
+ )]
+ #[allow(deprecated)]
fn yo(&self, year: i32, ordinal: u32) -> Date<Self> {
self.yo_opt(year, ordinal).unwrap()
}
@@ -263,6 +291,11 @@ pub trait TimeZone: Sized + Clone {
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Returns `None` on the out-of-range date and/or invalid DOY.
+ #[deprecated(
+ since = "0.4.23",
+ note = "use `from_local_datetime()` with a `NaiveDateTime` instead"
+ )]
+ #[allow(deprecated)]
fn yo_opt(&self, year: i32, ordinal: u32) -> LocalResult<Date<Self>> {
match NaiveDate::from_yo_opt(year, ordinal) {
Some(d) => self.from_local_date(&d),
@@ -279,14 +312,11 @@ pub trait TimeZone: Sized + Clone {
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Panics on the out-of-range date and/or invalid week number.
- ///
- /// # Example
- ///
- /// ~~~~
- /// use chrono::{Utc, Weekday, TimeZone};
- ///
- /// assert_eq!(Utc.isoywd(2015, 20, Weekday::Fri).to_string(), "2015-05-15UTC");
- /// ~~~~
+ #[deprecated(
+ since = "0.4.23",
+ note = "use `from_local_datetime()` with a `NaiveDateTime` instead"
+ )]
+ #[allow(deprecated)]
fn isoywd(&self, year: i32, week: u32, weekday: Weekday) -> Date<Self> {
self.isoywd_opt(year, week, weekday).unwrap()
}
@@ -300,6 +330,11 @@ pub trait TimeZone: Sized + Clone {
/// but it will propagate to the `DateTime` values constructed via this date.
///
/// Returns `None` on the out-of-range date and/or invalid week number.
+ #[deprecated(
+ since = "0.4.23",
+ note = "use `from_local_datetime()` with a `NaiveDateTime` instead"
+ )]
+ #[allow(deprecated)]
fn isoywd_opt(&self, year: i32, week: u32, weekday: Weekday) -> LocalResult<Date<Self>> {
match NaiveDate::from_isoywd_opt(year, week, weekday) {
Some(d) => self.from_local_date(&d),
@@ -311,16 +346,15 @@ pub trait TimeZone: Sized + Clone {
/// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp")
/// and the number of nanoseconds since the last whole non-leap second.
///
- /// Panics on the out-of-range number of seconds and/or invalid nanosecond,
- /// for a non-panicking version see [`timestamp_opt`](#method.timestamp_opt).
- ///
- /// # Example
+ /// The nanosecond part can exceed 1,000,000,000 in order to represent a
+ /// [leap second](crate::NaiveTime#leap-second-handling), but only when `secs % 60 == 59`.
+ /// (The true "UNIX timestamp" cannot represent a leap second unambiguously.)
///
- /// ~~~~
- /// use chrono::{Utc, TimeZone};
+ /// # Panics
///
- /// assert_eq!(Utc.timestamp(1431648000, 0).to_string(), "2015-05-15 00:00:00 UTC");
- /// ~~~~
+ /// Panics on the out-of-range number of seconds and/or invalid nanosecond,
+ /// for a non-panicking version see [`timestamp_opt`](#method.timestamp_opt).
+ #[deprecated(since = "0.4.23", note = "use `timestamp_opt()` instead")]
fn timestamp(&self, secs: i64, nsecs: u32) -> DateTime<Self> {
self.timestamp_opt(secs, nsecs).unwrap()
}
@@ -329,8 +363,22 @@ pub trait TimeZone: Sized + Clone {
/// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp")
/// and the number of nanoseconds since the last whole non-leap second.
///
+ /// The nanosecond part can exceed 1,000,000,000 in order to represent a
+ /// [leap second](crate::NaiveTime#leap-second-handling), but only when `secs % 60 == 59`.
+ /// (The true "UNIX timestamp" cannot represent a leap second unambiguously.)
+ ///
+ /// # Errors
+ ///
/// Returns `LocalResult::None` on out-of-range number of seconds and/or
/// invalid nanosecond, otherwise always returns `LocalResult::Single`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use chrono::{Utc, TimeZone};
+ ///
+ /// assert_eq!(Utc.timestamp_opt(1431648000, 0).unwrap().to_string(), "2015-05-15 00:00:00 UTC");
+ /// ```
fn timestamp_opt(&self, secs: i64, nsecs: u32) -> LocalResult<DateTime<Self>> {
match NaiveDateTime::from_timestamp_opt(secs, nsecs) {
Some(dt) => LocalResult::Single(self.from_utc_datetime(&dt)),
@@ -343,14 +391,7 @@ pub trait TimeZone: Sized + Clone {
///
/// Panics on out-of-range number of milliseconds for a non-panicking
/// version see [`timestamp_millis_opt`](#method.timestamp_millis_opt).
- ///
- /// # Example
- ///
- /// ~~~~
- /// use chrono::{Utc, TimeZone};
- ///
- /// assert_eq!(Utc.timestamp_millis(1431648000).timestamp(), 1431648);
- /// ~~~~
+ #[deprecated(since = "0.4.23", note = "use `timestamp_millis_opt()` instead")]
fn timestamp_millis(&self, millis: i64) -> DateTime<Self> {
self.timestamp_millis_opt(millis).unwrap()
}
@@ -365,35 +406,32 @@ pub trait TimeZone: Sized + Clone {
///
/// # Example
///
- /// ~~~~
+ /// ```
/// use chrono::{Utc, TimeZone, LocalResult};
/// match Utc.timestamp_millis_opt(1431648000) {
/// LocalResult::Single(dt) => assert_eq!(dt.timestamp(), 1431648),
/// _ => panic!("Incorrect timestamp_millis"),
/// };
- /// ~~~~
+ /// ```
fn timestamp_millis_opt(&self, millis: i64) -> LocalResult<DateTime<Self>> {
- let (mut secs, mut millis) = (millis / 1000, millis % 1000);
- if millis < 0 {
- secs -= 1;
- millis += 1000;
+ match NaiveDateTime::from_timestamp_millis(millis) {
+ Some(dt) => LocalResult::Single(self.from_utc_datetime(&dt)),
+ None => LocalResult::None,
}
- self.timestamp_opt(secs, millis as u32 * 1_000_000)
}
/// Makes a new `DateTime` from the number of non-leap nanoseconds
/// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp").
///
- /// Unlike [`timestamp_millis`](#method.timestamp_millis), this never
- /// panics.
+ /// Unlike [`timestamp_millis_opt`](#method.timestamp_millis_opt), this never fails.
///
/// # Example
///
- /// ~~~~
+ /// ```
/// use chrono::{Utc, TimeZone};
///
/// assert_eq!(Utc.timestamp_nanos(1431648000000000).timestamp(), 1431648);
- /// ~~~~
+ /// ```
fn timestamp_nanos(&self, nanos: i64) -> DateTime<Self> {
let (mut secs, mut nanos) = (nanos / 1_000_000_000, nanos % 1_000_000_000);
if nanos < 0 {
@@ -403,16 +441,42 @@ pub trait TimeZone: Sized + Clone {
self.timestamp_opt(secs, nanos as u32).unwrap()
}
- /// Parses a string with the specified format string and
- /// returns a `DateTime` with the current offset.
- /// See the [`format::strftime` module](../format/strftime/index.html)
- /// on the supported escape sequences.
+ /// Makes a new `DateTime` from the number of non-leap microseconds
+ /// since January 1, 1970 0:00:00 UTC (aka "UNIX timestamp").
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use chrono::{Utc, TimeZone};
+ ///
+ /// assert_eq!(Utc.timestamp_micros(1431648000000).unwrap().timestamp(), 1431648);
+ /// ```
+ fn timestamp_micros(&self, micros: i64) -> LocalResult<DateTime<Self>> {
+ match NaiveDateTime::from_timestamp_micros(micros) {
+ Some(dt) => LocalResult::Single(self.from_utc_datetime(&dt)),
+ None => LocalResult::None,
+ }
+ }
+
+ /// Parses a string with the specified format string and returns a
+ /// `DateTime` with the current offset.
+ ///
+ /// See the [`crate::format::strftime`] module on the
+ /// supported escape sequences.
+ ///
+ /// If the to-be-parsed string includes an offset, it *must* match the
+ /// offset of the TimeZone, otherwise an error will be returned.
///
- /// If the format does not include offsets, the current offset is assumed;
- /// otherwise the input should have a matching UTC offset.
+ /// See also [`DateTime::parse_from_str`] which gives a [`DateTime`] with
+ /// parsed [`FixedOffset`].
///
- /// See also `DateTime::parse_from_str` which gives a local `DateTime`
- /// with parsed `FixedOffset`.
+ /// See also [`NaiveDateTime::parse_from_str`] which gives a [`NaiveDateTime`] without
+ /// an offset, but can be converted to a [`DateTime`] with [`NaiveDateTime::and_utc`] or
+ /// [`NaiveDateTime::and_local_timezone`].
+ #[deprecated(
+ since = "0.4.29",
+ note = "use `DateTime::parse_from_str` or `NaiveDateTime::parse_from_str` with `and_utc()` or `and_local_timezone()` instead"
+ )]
fn datetime_from_str(&self, s: &str, fmt: &str) -> ParseResult<DateTime<Self>> {
let mut parsed = Parsed::new();
parse(&mut parsed, s, StrftimeItems::new(fmt))?;
@@ -429,6 +493,9 @@ pub trait TimeZone: Sized + Clone {
fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<Self::Offset>;
/// Converts the local `NaiveDate` to the timezone-aware `Date` if possible.
+ #[allow(clippy::wrong_self_convention)]
+ #[deprecated(since = "0.4.23", note = "use `from_local_datetime()` instead")]
+ #[allow(deprecated)]
fn from_local_date(&self, local: &NaiveDate) -> LocalResult<Date<Self>> {
self.offset_from_local_date(local).map(|offset| {
// since FixedOffset is within +/- 1 day, the date is never affected
@@ -437,9 +504,26 @@ 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_utc(*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.
@@ -450,48 +534,68 @@ pub trait TimeZone: Sized + Clone {
/// Converts the UTC `NaiveDate` to the local time.
/// The UTC is continuous and thus this cannot fail (but can give the duplicate local time).
+ #[allow(clippy::wrong_self_convention)]
+ #[deprecated(since = "0.4.23", note = "use `from_utc_datetime()` instead")]
+ #[allow(deprecated)]
fn from_utc_date(&self, utc: &NaiveDate) -> Date<Self> {
Date::from_utc(*utc, self.offset_from_utc_date(utc))
}
/// Converts the UTC `NaiveDateTime` to the local time.
/// The UTC is continuous and thus this cannot fail (but can give the duplicate local time).
+ #[allow(clippy::wrong_self_convention)]
fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime<Self> {
- DateTime::from_utc(*utc, self.offset_from_utc_datetime(utc))
+ DateTime::from_naive_utc_and_offset(*utc, self.offset_from_utc_datetime(utc))
}
}
-mod fixed;
-#[cfg(feature = "clock")]
-mod local;
-mod utc;
-
-pub use self::fixed::FixedOffset;
-#[cfg(feature = "clock")]
-pub use self::local::Local;
-pub use self::utc::Utc;
-
#[cfg(test)]
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(-1000);
+ let dt = Utc.timestamp_millis_opt(-1000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:59 UTC");
- let dt = Utc.timestamp_millis(-7000);
+ let dt = Utc.timestamp_millis_opt(-7000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:53 UTC");
- let dt = Utc.timestamp_millis(-7001);
+ let dt = Utc.timestamp_millis_opt(-7001).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:52.999 UTC");
- let dt = Utc.timestamp_millis(-7003);
+ let dt = Utc.timestamp_millis_opt(-7003).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:52.997 UTC");
- let dt = Utc.timestamp_millis(-999);
+ let dt = Utc.timestamp_millis_opt(-999).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:59.001 UTC");
- let dt = Utc.timestamp_millis(-1);
+ let dt = Utc.timestamp_millis_opt(-1).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:59.999 UTC");
- let dt = Utc.timestamp_millis(-60000);
+ let dt = Utc.timestamp_millis_opt(-60000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:59:00 UTC");
- let dt = Utc.timestamp_millis(-3600000);
+ let dt = Utc.timestamp_millis_opt(-3600000).unwrap();
assert_eq!(dt.to_string(), "1969-12-31 23:00:00 UTC");
for (millis, expected) in &[
@@ -528,4 +632,18 @@ mod tests {
Utc.timestamp_nanos(i64::default());
Utc.timestamp_nanos(i64::min_value());
}
+
+ #[test]
+ fn test_negative_micros() {
+ let dt = Utc.timestamp_micros(-1_000_000).unwrap();
+ assert_eq!(dt.to_string(), "1969-12-31 23:59:59 UTC");
+ let dt = Utc.timestamp_micros(-999_999).unwrap();
+ assert_eq!(dt.to_string(), "1969-12-31 23:59:59.000001 UTC");
+ let dt = Utc.timestamp_micros(-1).unwrap();
+ assert_eq!(dt.to_string(), "1969-12-31 23:59:59.999999 UTC");
+ let dt = Utc.timestamp_micros(-60_000_000).unwrap();
+ assert_eq!(dt.to_string(), "1969-12-31 23:59:00 UTC");
+ let dt = Utc.timestamp_micros(-3_600_000_000).unwrap();
+ assert_eq!(dt.to_string(), "1969-12-31 23:00:00 UTC");
+ }
}
diff --git a/src/offset/utc.rs b/src/offset/utc.rs
index aec6667..29b832a 100644
--- a/src/offset/utc.rs
+++ b/src/offset/utc.rs
@@ -4,16 +4,24 @@
//! The UTC (Coordinated Universal Time) time zone.
use core::fmt;
-
-use super::{FixedOffset, LocalResult, Offset, TimeZone};
-use naive::{NaiveDate, NaiveDateTime};
#[cfg(all(
- feature = "clock",
- not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))
+ feature = "now",
+ not(all(
+ target_arch = "wasm32",
+ feature = "wasmbind",
+ not(any(target_os = "emscripten", target_os = "wasi"))
+ ))
))]
use std::time::{SystemTime, UNIX_EPOCH};
-#[cfg(feature = "clock")]
-use {Date, DateTime};
+
+#[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))]
+use rkyv::{Archive, Deserialize, Serialize};
+
+use super::{FixedOffset, LocalResult, Offset, TimeZone};
+use crate::naive::{NaiveDate, NaiveDateTime};
+#[cfg(feature = "now")]
+#[allow(deprecated)]
+use crate::{Date, DateTime};
/// The UTC time zone. This is the most efficient time zone when you don't need the local time.
/// It is also used as an offset (which is also a dummy type).
@@ -24,35 +32,81 @@ use {Date, DateTime};
///
/// # Example
///
-/// ~~~~
-/// use chrono::{DateTime, TimeZone, NaiveDateTime, Utc};
+/// ```
+/// use chrono::{TimeZone, NaiveDateTime, Utc};
///
-/// let dt = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(61, 0), Utc);
+/// let dt = Utc.from_utc_datetime(&NaiveDateTime::from_timestamp_opt(61, 0).unwrap());
///
-/// assert_eq!(Utc.timestamp(61, 0), dt);
-/// assert_eq!(Utc.ymd(1970, 1, 1).and_hms(0, 1, 1), dt);
-/// ~~~~
-#[derive(Copy, Clone, PartialEq, Eq)]
+/// assert_eq!(Utc.timestamp_opt(61, 0).unwrap(), dt);
+/// assert_eq!(Utc.with_ymd_and_hms(1970, 1, 1, 0, 1, 1).unwrap(), dt);
+/// ```
+#[derive(Copy, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(
+ any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"),
+ derive(Archive, Deserialize, Serialize),
+ archive(compare(PartialEq)),
+ archive_attr(derive(Clone, Copy, PartialEq, Eq, Debug, Hash))
+)]
+#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
+#[cfg_attr(all(feature = "arbitrary", feature = "std"), derive(arbitrary::Arbitrary))]
pub struct Utc;
-#[cfg(feature = "clock")]
+#[cfg(feature = "now")]
impl Utc {
/// Returns a `Date` which corresponds to the current date.
+ #[deprecated(
+ since = "0.4.23",
+ note = "use `Utc::now()` instead, potentially with `.date_naive()`"
+ )]
+ #[allow(deprecated)]
+ #[must_use]
pub fn today() -> Date<Utc> {
Utc::now().date()
}
- /// Returns a `DateTime` which corresponds to the current date.
- #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
+ /// Returns a `DateTime<Utc>` which corresponds to the current date and time in UTC.
+ ///
+ /// See also the similar [`Local::now()`] which returns `DateTime<Local>`, i.e. the local date
+ /// and time including offset from UTC.
+ ///
+ /// [`Local::now()`]: crate::Local::now
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # #![allow(unused_variables)]
+ /// # use chrono::{FixedOffset, Utc};
+ /// // Current time in UTC
+ /// let now_utc = Utc::now();
+ ///
+ /// // Current date in UTC
+ /// let today_utc = now_utc.date_naive();
+ ///
+ /// // Current time in some timezone (let's use +05:00)
+ /// let offset = FixedOffset::east_opt(5 * 60 * 60).unwrap();
+ /// let now_with_offset = Utc::now().with_timezone(&offset);
+ /// ```
+ #[cfg(not(all(
+ target_arch = "wasm32",
+ feature = "wasmbind",
+ not(any(target_os = "emscripten", target_os = "wasi"))
+ )))]
+ #[must_use]
pub fn now() -> DateTime<Utc> {
let now =
SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before Unix epoch");
- let naive = NaiveDateTime::from_timestamp(now.as_secs() as i64, now.subsec_nanos() as u32);
- DateTime::from_utc(naive, Utc)
+ let naive =
+ NaiveDateTime::from_timestamp_opt(now.as_secs() as i64, now.subsec_nanos()).unwrap();
+ Utc.from_utc_datetime(&naive)
}
- /// Returns a `DateTime` which corresponds to the current date.
- #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))]
+ /// Returns a `DateTime` which corresponds to the current date and time.
+ #[cfg(all(
+ target_arch = "wasm32",
+ feature = "wasmbind",
+ not(any(target_os = "emscripten", target_os = "wasi"))
+ ))]
+ #[must_use]
pub fn now() -> DateTime<Utc> {
let now = js_sys::Date::new_0();
DateTime::<Utc>::from(now)
@@ -83,7 +137,7 @@ impl TimeZone for Utc {
impl Offset for Utc {
fn fix(&self) -> FixedOffset {
- FixedOffset::east(0)
+ FixedOffset::east_opt(0).unwrap()
}
}