// 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 or the MIT license // , 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 { // 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 { 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, // Transition from daylight saving time to standard time, given in local daylight saving time. dst_transition: Option, } impl TzInfo { fn for_year(year: i32) -> Option { // 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::::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 { 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 { 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::::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::::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); } } }