• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
4 //
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
10 
11 use std::cmp::Ordering;
12 use std::mem::MaybeUninit;
13 use std::ptr;
14 
15 use super::win_bindings::{GetTimeZoneInformationForYear, SYSTEMTIME, TIME_ZONE_INFORMATION};
16 
17 use crate::offset::local::{lookup_with_dst_transitions, Transition};
18 use crate::{Datelike, FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime, NaiveTime, Weekday};
19 
20 // We don't use `SystemTimeToTzSpecificLocalTime` because it doesn't support the same range of dates
21 // as Chrono. Also it really isn't that difficult to work out the correct offset from the provided
22 // DST rules.
23 //
24 // This method uses `overflowing_sub_offset` because it is no problem if the transition time in UTC
25 // falls a couple of hours inside the buffer space around the `NaiveDateTime` range (although it is
26 // very theoretical to have a transition at midnight around `NaiveDate::(MIN|MAX)`.
offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime<FixedOffset>27 pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
28     // Using a `TzInfo` based on the year of an UTC datetime is technically wrong, we should be
29     // using the rules for the year of the corresponding local time. But this matches what
30     // `SystemTimeToTzSpecificLocalTime` is documented to do.
31     let tz_info = match TzInfo::for_year(utc.year()) {
32         Some(tz_info) => tz_info,
33         None => return MappedLocalTime::None,
34     };
35     let offset = match (tz_info.std_transition, tz_info.dst_transition) {
36         (Some(std_transition), Some(dst_transition)) => {
37             let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset);
38             let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset);
39             if dst_transition_utc < std_transition_utc {
40                 match utc >= &dst_transition_utc && utc < &std_transition_utc {
41                     true => tz_info.dst_offset,
42                     false => tz_info.std_offset,
43                 }
44             } else {
45                 match utc >= &std_transition_utc && utc < &dst_transition_utc {
46                     true => tz_info.std_offset,
47                     false => tz_info.dst_offset,
48                 }
49             }
50         }
51         (Some(std_transition), None) => {
52             let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset);
53             match utc < &std_transition_utc {
54                 true => tz_info.dst_offset,
55                 false => tz_info.std_offset,
56             }
57         }
58         (None, Some(dst_transition)) => {
59             let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset);
60             match utc < &dst_transition_utc {
61                 true => tz_info.std_offset,
62                 false => tz_info.dst_offset,
63             }
64         }
65         (None, None) => tz_info.std_offset,
66     };
67     MappedLocalTime::Single(offset)
68 }
69 
70 // We don't use `TzSpecificLocalTimeToSystemTime` because it doesn't let us choose how to handle
71 // ambiguous cases (during a DST transition). Instead we get the timezone information for the
72 // current year and compute it ourselves, like we do on Unix.
offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime<FixedOffset>73 pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
74     let tz_info = match TzInfo::for_year(local.year()) {
75         Some(tz_info) => tz_info,
76         None => return MappedLocalTime::None,
77     };
78     // Create a sorted slice of transitions and use `lookup_with_dst_transitions`.
79     match (tz_info.std_transition, tz_info.dst_transition) {
80         (Some(std_transition), Some(dst_transition)) => {
81             let std_transition =
82                 Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset);
83             let dst_transition =
84                 Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset);
85             let transitions = match std_transition.cmp(&dst_transition) {
86                 Ordering::Less => [std_transition, dst_transition],
87                 Ordering::Greater => [dst_transition, std_transition],
88                 Ordering::Equal => {
89                     // This doesn't make sense. Let's just return the standard offset.
90                     return MappedLocalTime::Single(tz_info.std_offset);
91                 }
92             };
93             lookup_with_dst_transitions(&transitions, *local)
94         }
95         (Some(std_transition), None) => {
96             let transitions =
97                 [Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset)];
98             lookup_with_dst_transitions(&transitions, *local)
99         }
100         (None, Some(dst_transition)) => {
101             let transitions =
102                 [Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset)];
103             lookup_with_dst_transitions(&transitions, *local)
104         }
105         (None, None) => MappedLocalTime::Single(tz_info.std_offset),
106     }
107 }
108 
109 // The basis for Windows timezone and DST support has been in place since Windows 2000. It does not
110 // allow for complex rules like the IANA timezone database:
111 // - A timezone has the same base offset the whole year.
112 // - There seem to be either zero or two DST transitions (but we support having just one).
113 // - As of Vista(?) only years from 2004 until a few years into the future are supported.
114 // - All other years get the base settings, which seem to be that of the current year.
115 //
116 // These details don't matter much, we just work with the offsets and transition dates Windows
117 // returns through `GetTimeZoneInformationForYear` for a particular year.
118 struct TzInfo {
119     // Offset from UTC during standard time.
120     std_offset: FixedOffset,
121     // Offset from UTC during daylight saving time.
122     dst_offset: FixedOffset,
123     // Transition from standard time to daylight saving time, given in local standard time.
124     std_transition: Option<NaiveDateTime>,
125     // Transition from daylight saving time to standard time, given in local daylight saving time.
126     dst_transition: Option<NaiveDateTime>,
127 }
128 
129 impl TzInfo {
for_year(year: i32) -> Option<TzInfo>130     fn for_year(year: i32) -> Option<TzInfo> {
131         // The API limits years to 1601..=30827.
132         // Working with timezones and daylight saving time this far into the past or future makes
133         // little sense. But whatever is extrapolated for 1601 or 30827 is what can be extrapolated
134         // for years beyond.
135         let ref_year = year.clamp(1601, 30827) as u16;
136         let tz_info = unsafe {
137             let mut tz_info = MaybeUninit::<TIME_ZONE_INFORMATION>::uninit();
138             if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 {
139                 return None;
140             }
141             tz_info.assume_init()
142         };
143         let std_offset = (tz_info.Bias)
144             .checked_add(tz_info.StandardBias)
145             .and_then(|o| o.checked_mul(60))
146             .and_then(FixedOffset::west_opt)?;
147         let dst_offset = (tz_info.Bias)
148             .checked_add(tz_info.DaylightBias)
149             .and_then(|o| o.checked_mul(60))
150             .and_then(FixedOffset::west_opt)?;
151         Some(TzInfo {
152             std_offset,
153             dst_offset,
154             std_transition: naive_date_time_from_system_time(tz_info.StandardDate, year).ok()?,
155             dst_transition: naive_date_time_from_system_time(tz_info.DaylightDate, year).ok()?,
156         })
157     }
158 }
159 
160 /// Resolve a `SYSTEMTIME` object to an `Option<NaiveDateTime>`.
161 ///
162 /// A `SYSTEMTIME` within a `TIME_ZONE_INFORMATION` struct can be zero to indicate there is no
163 /// transition.
164 /// If it has year, month and day values it is a concrete date.
165 /// If the year is missing the `SYSTEMTIME` is a rule, which this method resolves for the provided
166 /// year. A rule has a month, weekday, and nth weekday of the month as components.
167 ///
168 /// Returns `Err` if any of the values is invalid, which should never happen.
naive_date_time_from_system_time( st: SYSTEMTIME, year: i32, ) -> Result<Option<NaiveDateTime>, ()>169 fn naive_date_time_from_system_time(
170     st: SYSTEMTIME,
171     year: i32,
172 ) -> Result<Option<NaiveDateTime>, ()> {
173     if st.wYear == 0 && st.wMonth == 0 {
174         return Ok(None);
175     }
176     let time = NaiveTime::from_hms_milli_opt(
177         st.wHour as u32,
178         st.wMinute as u32,
179         st.wSecond as u32,
180         st.wMilliseconds as u32,
181     )
182     .ok_or(())?;
183 
184     if st.wYear != 0 {
185         // We have a concrete date.
186         let date =
187             NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32).ok_or(())?;
188         return Ok(Some(date.and_time(time)));
189     }
190 
191     // Resolve a rule with month, weekday, and nth weekday of the month to a date in the current
192     // year.
193     let weekday = match st.wDayOfWeek {
194         0 => Weekday::Sun,
195         1 => Weekday::Mon,
196         2 => Weekday::Tue,
197         3 => Weekday::Wed,
198         4 => Weekday::Thu,
199         5 => Weekday::Fri,
200         6 => Weekday::Sat,
201         _ => return Err(()),
202     };
203     let nth_day = match st.wDay {
204         1..=5 => st.wDay as u8,
205         _ => return Err(()),
206     };
207     let date = NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, weekday, nth_day)
208         .or_else(|| NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, weekday, 4))
209         .ok_or(())?; // `st.wMonth` must be invalid
210     Ok(Some(date.and_time(time)))
211 }
212 
213 #[cfg(test)]
214 mod tests {
215     use crate::offset::local::win_bindings::{
216         SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime, FILETIME, SYSTEMTIME,
217     };
218     use crate::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeDelta};
219     use crate::{Datelike, TimeZone, Timelike};
220     use std::mem::MaybeUninit;
221     use std::ptr;
222 
223     #[test]
verify_against_tz_specific_local_time_to_system_time()224     fn verify_against_tz_specific_local_time_to_system_time() {
225         // The implementation in Windows itself is the source of truth on how to work with the OS
226         // timezone information. This test compares for every hour over a period of 125 years our
227         // implementation to `TzSpecificLocalTimeToSystemTime`.
228         //
229         // This uses parts of a previous Windows `Local` implementation in chrono.
230         fn from_local_time(dt: &NaiveDateTime) -> DateTime<Local> {
231             let st = system_time_from_naive_date_time(dt);
232             let utc_time = local_to_utc_time(&st);
233             let utc_secs = system_time_as_unix_seconds(&utc_time);
234             let local_secs = system_time_as_unix_seconds(&st);
235             let offset = (local_secs - utc_secs) as i32;
236             let offset = FixedOffset::east_opt(offset).unwrap();
237             DateTime::from_naive_utc_and_offset(*dt - offset, offset)
238         }
239         fn system_time_from_naive_date_time(dt: &NaiveDateTime) -> SYSTEMTIME {
240             SYSTEMTIME {
241                 // Valid values: 1601-30827
242                 wYear: dt.year() as u16,
243                 // Valid values:1-12
244                 wMonth: dt.month() as u16,
245                 // Valid values: 0-6, starting Sunday.
246                 // NOTE: enum returns 1-7, starting Monday, so we are
247                 // off here, but this is not currently used in local.
248                 wDayOfWeek: dt.weekday() as u16,
249                 // Valid values: 1-31
250                 wDay: dt.day() as u16,
251                 // Valid values: 0-23
252                 wHour: dt.hour() as u16,
253                 // Valid values: 0-59
254                 wMinute: dt.minute() as u16,
255                 // Valid values: 0-59
256                 wSecond: dt.second() as u16,
257                 // Valid values: 0-999
258                 wMilliseconds: 0,
259             }
260         }
261         fn local_to_utc_time(local: &SYSTEMTIME) -> SYSTEMTIME {
262             let mut sys_time = MaybeUninit::<SYSTEMTIME>::uninit();
263             unsafe { TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()) };
264             // SAFETY: TzSpecificLocalTimeToSystemTime must have succeeded at this point, so we can
265             // assume the value is initialized.
266             unsafe { sys_time.assume_init() }
267         }
268         const HECTONANOSECS_IN_SEC: i64 = 10_000_000;
269         const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC;
270         fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> i64 {
271             let mut init = MaybeUninit::<FILETIME>::uninit();
272             unsafe {
273                 SystemTimeToFileTime(st, init.as_mut_ptr());
274             }
275             // SystemTimeToFileTime must have succeeded at this point, so we can assume the value is
276             // initialized.
277             let filetime = unsafe { init.assume_init() };
278             let bit_shift =
279                 ((filetime.dwHighDateTime as u64) << 32) | (filetime.dwLowDateTime as u64);
280             (bit_shift as i64 - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC
281         }
282 
283         let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 30, 0).unwrap();
284 
285         while date.year() < 2078 {
286             // Windows doesn't handle non-existing dates, it just treats it as valid.
287             if let Some(our_result) = Local.from_local_datetime(&date).earliest() {
288                 assert_eq!(from_local_time(&date), our_result);
289             }
290             date += TimeDelta::try_hours(1).unwrap();
291         }
292     }
293 }
294