Skip to content

Commit 50eb7e3

Browse files
esheppadjc
authored andcommitted
handle localtime ambiguity
1 parent 2caab34 commit 50eb7e3

6 files changed

Lines changed: 305 additions & 33 deletions

File tree

src/offset/local/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ impl TimeZone for Local {
112112

113113
#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
114114
fn from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<DateTime<Local>> {
115-
LocalResult::Single(inner::naive_to_local(local, true))
115+
inner::naive_to_local(local, true)
116116
}
117117

118118
fn from_utc_date(&self, utc: &NaiveDate) -> Date<Local> {
@@ -129,7 +129,9 @@ impl TimeZone for Local {
129129

130130
#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
131131
fn from_utc_datetime(&self, utc: &NaiveDateTime) -> DateTime<Local> {
132-
inner::naive_to_local(utc, false)
132+
// this is OK to unwrap as getting local time from a UTC
133+
// timestamp is never ambiguous
134+
inner::naive_to_local(utc, false).unwrap()
133135
}
134136
}
135137

src/offset/local/stub.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
use std::time::{SystemTime, UNIX_EPOCH};
1212

1313
use super::{FixedOffset, Local};
14-
use crate::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
14+
use crate::{DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
1515

1616
pub(super) fn now() -> DateTime<Local> {
1717
tm_to_datetime(Timespec::now().local())
1818
}
1919

2020
/// Converts a local `NaiveDateTime` to the `time::Timespec`.
2121
#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind")))]
22-
pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> DateTime<Local> {
22+
pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult<DateTime<Local>> {
2323
let tm = Tm {
2424
tm_sec: d.second() as i32,
2525
tm_min: d.minute() as i32,
@@ -49,7 +49,7 @@ pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> DateTime<Local>
4949
assert_eq!(tm.tm_nsec, 0);
5050
tm.tm_nsec = d.nanosecond() as i32;
5151

52-
tm_to_datetime(tm)
52+
LocalResult::Single(tm_to_datetime(tm))
5353
}
5454

5555
/// Converts a `time::Tm` struct into the timezone-aware `DateTime`.
@@ -116,7 +116,7 @@ impl Timespec {
116116
/// day, and so on), also called a broken-down time value.
117117
// FIXME: use c_int instead of i32?
118118
#[repr(C)]
119-
struct Tm {
119+
pub(super) struct Tm {
120120
/// Seconds after the minute - [0, 60]
121121
tm_sec: i32,
122122

src/offset/local/tz_info/rule.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,22 @@ impl TransitionRule {
7878
}
7979
}
8080
}
81+
82+
/// Find the local time type associated to the transition rule at the specified Unix time in seconds
83+
pub(super) fn find_local_time_type_from_local(
84+
&self,
85+
local_time: i64,
86+
year: i32,
87+
) -> Result<crate::LocalResult<LocalTimeType>, Error> {
88+
match self {
89+
TransitionRule::Fixed(local_time_type) => {
90+
Ok(crate::LocalResult::Single(*local_time_type))
91+
}
92+
TransitionRule::Alternate(alternate_time) => {
93+
alternate_time.find_local_time_type_from_local(local_time, year)
94+
}
95+
}
96+
}
8197
}
8298

8399
impl From<LocalTimeType> for TransitionRule {
@@ -211,6 +227,125 @@ impl AlternateTime {
211227
Ok(&self.std)
212228
}
213229
}
230+
231+
fn find_local_time_type_from_local(
232+
&self,
233+
local_time: i64,
234+
current_year: i32,
235+
) -> Result<crate::LocalResult<LocalTimeType>, Error> {
236+
// Check if the current year is valid for the following computations
237+
if !(i32::min_value() + 2 <= current_year && current_year <= i32::max_value() - 2) {
238+
return Err(Error::OutOfRange("out of range date time"));
239+
}
240+
241+
let dst_start_transition_start =
242+
self.dst_start.unix_time(current_year, 0) + i64::from(self.dst_start_time);
243+
let dst_start_transition_end = self.dst_start.unix_time(current_year, 0)
244+
+ i64::from(self.dst_start_time)
245+
+ i64::from(self.dst.ut_offset)
246+
- i64::from(self.std.ut_offset);
247+
248+
let dst_end_transition_start =
249+
self.dst_end.unix_time(current_year, 0) + i64::from(self.dst_end_time);
250+
let dst_end_transition_end = self.dst_end.unix_time(current_year, 0)
251+
+ i64::from(self.dst_end_time)
252+
+ i64::from(self.std.ut_offset)
253+
- i64::from(self.dst.ut_offset);
254+
255+
match self.std.ut_offset.cmp(&self.dst.ut_offset) {
256+
Ordering::Equal => Ok(crate::LocalResult::Single(self.std)),
257+
Ordering::Less => {
258+
if self.dst_start.transition_date(current_year).0
259+
< self.dst_end.transition_date(current_year).0
260+
{
261+
// northern hemisphere
262+
// For the DST END transition, the `start` happens at a later timestamp than the `end`.
263+
if local_time <= dst_start_transition_start {
264+
Ok(crate::LocalResult::Single(self.std))
265+
} else if local_time > dst_start_transition_start
266+
&& local_time < dst_start_transition_end
267+
{
268+
Ok(crate::LocalResult::None)
269+
} else if local_time >= dst_start_transition_end
270+
&& local_time < dst_end_transition_end
271+
{
272+
Ok(crate::LocalResult::Single(self.dst))
273+
} else if local_time >= dst_end_transition_end
274+
&& local_time <= dst_end_transition_start
275+
{
276+
Ok(crate::LocalResult::Ambiguous(self.std, self.dst))
277+
} else {
278+
Ok(crate::LocalResult::Single(self.std))
279+
}
280+
} else {
281+
// southern hemisphere regular DST
282+
// For the DST END transition, the `start` happens at a later timestamp than the `end`.
283+
if local_time < dst_end_transition_end {
284+
Ok(crate::LocalResult::Single(self.dst))
285+
} else if local_time >= dst_end_transition_end
286+
&& local_time <= dst_end_transition_start
287+
{
288+
Ok(crate::LocalResult::Ambiguous(self.std, self.dst))
289+
} else if local_time > dst_end_transition_end
290+
&& local_time < dst_start_transition_start
291+
{
292+
Ok(crate::LocalResult::Single(self.std))
293+
} else if local_time >= dst_start_transition_start
294+
&& local_time < dst_start_transition_end
295+
{
296+
Ok(crate::LocalResult::None)
297+
} else {
298+
Ok(crate::LocalResult::Single(self.dst))
299+
}
300+
}
301+
}
302+
Ordering::Greater => {
303+
if self.dst_start.transition_date(current_year).0
304+
< self.dst_end.transition_date(current_year).0
305+
{
306+
// southern hemisphere reverse DST
307+
// For the DST END transition, the `start` happens at a later timestamp than the `end`.
308+
if local_time < dst_start_transition_end {
309+
Ok(crate::LocalResult::Single(self.std))
310+
} else if local_time >= dst_start_transition_end
311+
&& local_time <= dst_start_transition_start
312+
{
313+
Ok(crate::LocalResult::Ambiguous(self.dst, self.std))
314+
} else if local_time > dst_start_transition_start
315+
&& local_time < dst_end_transition_start
316+
{
317+
Ok(crate::LocalResult::Single(self.dst))
318+
} else if local_time >= dst_end_transition_start
319+
&& local_time < dst_end_transition_end
320+
{
321+
Ok(crate::LocalResult::None)
322+
} else {
323+
Ok(crate::LocalResult::Single(self.std))
324+
}
325+
} else {
326+
// northern hemisphere reverse DST
327+
// For the DST END transition, the `start` happens at a later timestamp than the `end`.
328+
if local_time <= dst_end_transition_start {
329+
Ok(crate::LocalResult::Single(self.dst))
330+
} else if local_time > dst_end_transition_start
331+
&& local_time < dst_end_transition_end
332+
{
333+
Ok(crate::LocalResult::None)
334+
} else if local_time >= dst_end_transition_end
335+
&& local_time < dst_start_transition_end
336+
{
337+
Ok(crate::LocalResult::Single(self.std))
338+
} else if local_time >= dst_start_transition_end
339+
&& local_time <= dst_start_transition_start
340+
{
341+
Ok(crate::LocalResult::Ambiguous(self.dst, self.std))
342+
} else {
343+
Ok(crate::LocalResult::Single(self.dst))
344+
}
345+
}
346+
}
347+
}
348+
}
214349
}
215350

216351
/// Parse time zone name

src/offset/local/tz_info/timezone.rs

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::fs::{self, File};
44
use std::io::{self, Read};
55
use std::path::{Path, PathBuf};
6-
use std::{fmt, str};
6+
use std::{cmp::Ordering, fmt, str};
77

88
use super::rule::{AlternateTime, TransitionRule};
99
use super::{parser, Error, DAYS_PER_WEEK, SECONDS_PER_DAY};
@@ -27,7 +27,11 @@ impl TimeZone {
2727
/// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead.
2828
///
2929
pub(crate) fn local() -> Result<Self, Error> {
30-
Self::from_posix_tz("localtime")
30+
if let Ok(tz) = std::env::var("TZ") {
31+
Self::from_posix_tz(&tz)
32+
} else {
33+
Self::from_posix_tz("localtime")
34+
}
3135
}
3236

3337
/// 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).
@@ -114,6 +118,15 @@ impl TimeZone {
114118
self.as_ref().find_local_time_type(unix_time)
115119
}
116120

121+
// should we pass NaiveDateTime all the way through to this fn?
122+
pub(crate) fn find_local_time_type_from_local(
123+
&self,
124+
local_time: i64,
125+
year: i32,
126+
) -> Result<crate::LocalResult<LocalTimeType>, Error> {
127+
self.as_ref().find_local_time_type_from_local(local_time, year)
128+
}
129+
117130
/// Returns a reference to the time zone
118131
fn as_ref(&self) -> TimeZoneRef {
119132
TimeZoneRef {
@@ -188,6 +201,91 @@ impl<'a> TimeZoneRef<'a> {
188201
}
189202
}
190203

204+
pub(crate) fn find_local_time_type_from_local(
205+
&self,
206+
local_time: i64,
207+
year: i32,
208+
) -> Result<crate::LocalResult<LocalTimeType>, Error> {
209+
// #TODO: this is wrong as we need 'local_time_to_local_leap_time ?
210+
// but ... does the local time even include leap seconds ??
211+
// let unix_leap_time = match self.unix_time_to_unix_leap_time(local_time) {
212+
// Ok(unix_leap_time) => unix_leap_time,
213+
// Err(Error::OutOfRange(error)) => return Err(Error::FindLocalTimeType(error)),
214+
// Err(err) => return Err(err),
215+
// };
216+
let local_leap_time = local_time;
217+
218+
// if we have at least one transition,
219+
// we must check _all_ of them, incase of any Overlapping (LocalResult::Ambiguous) or Skipping (LocalResult::None) transitions
220+
if !self.transitions.is_empty() {
221+
let mut prev = Some(self.local_time_types[0]);
222+
223+
for transition in self.transitions {
224+
let after_ltt = self.local_time_types[transition.local_time_type_index];
225+
226+
// the end and start here refers to where the time starts prior to the transition
227+
// and where it ends up after. not the temporal relationship.
228+
let transition_end = transition.unix_leap_time + i64::from(after_ltt.ut_offset);
229+
let transition_start =
230+
transition.unix_leap_time + i64::from(prev.unwrap().ut_offset);
231+
232+
match transition_start.cmp(&transition_end) {
233+
Ordering::Greater => {
234+
// bakwards transition, eg from DST to regular
235+
// this means a given local time could have one of two possible offsets
236+
if local_leap_time < transition_end {
237+
return Ok(crate::LocalResult::Single(prev.unwrap()));
238+
} else if local_leap_time >= transition_end
239+
&& local_leap_time <= transition_start
240+
{
241+
if prev.unwrap().ut_offset < after_ltt.ut_offset {
242+
return Ok(crate::LocalResult::Ambiguous(prev.unwrap(), after_ltt));
243+
} else {
244+
return Ok(crate::LocalResult::Ambiguous(after_ltt, prev.unwrap()));
245+
}
246+
}
247+
}
248+
Ordering::Equal => {
249+
// should this ever happen? presumably we have to handle it anyway.
250+
if local_leap_time < transition_start {
251+
return Ok(crate::LocalResult::Single(prev.unwrap()));
252+
} else if local_leap_time == transition_end {
253+
if prev.unwrap().ut_offset < after_ltt.ut_offset {
254+
return Ok(crate::LocalResult::Ambiguous(prev.unwrap(), after_ltt));
255+
} else {
256+
return Ok(crate::LocalResult::Ambiguous(after_ltt, prev.unwrap()));
257+
}
258+
}
259+
}
260+
Ordering::Less => {
261+
// forwards transition, eg from regular to DST
262+
// this means that times that are skipped are invalid local times
263+
if local_leap_time <= transition_start {
264+
return Ok(crate::LocalResult::Single(prev.unwrap()));
265+
} else if local_leap_time < transition_end {
266+
return Ok(crate::LocalResult::None);
267+
} else if local_leap_time == transition_end {
268+
return Ok(crate::LocalResult::Single(after_ltt));
269+
}
270+
}
271+
}
272+
273+
// try the next transition, we are fully after this one
274+
prev = Some(after_ltt);
275+
}
276+
};
277+
278+
if let Some(extra_rule) = self.extra_rule {
279+
match extra_rule.find_local_time_type_from_local(local_time, year) {
280+
Ok(local_time_type) => Ok(local_time_type),
281+
Err(Error::OutOfRange(error)) => Err(Error::FindLocalTimeType(error)),
282+
err => err,
283+
}
284+
} else {
285+
Ok(crate::LocalResult::Single(self.local_time_types[0]))
286+
}
287+
}
288+
191289
/// Check time zone inputs
192290
fn validate(&self) -> Result<(), Error> {
193291
// Check local time types
@@ -710,14 +808,24 @@ mod tests {
710808
fn test_time_zone_from_posix_tz() -> Result<(), Error> {
711809
#[cfg(unix)]
712810
{
713-
let time_zone_local = TimeZone::local()?;
714-
let time_zone_local_1 = TimeZone::from_posix_tz("localtime")?;
715-
let time_zone_local_2 = TimeZone::from_posix_tz("/etc/localtime")?;
716-
let time_zone_local_3 = TimeZone::from_posix_tz(":/etc/localtime")?;
717-
718-
assert_eq!(time_zone_local, time_zone_local_1);
719-
assert_eq!(time_zone_local, time_zone_local_2);
720-
assert_eq!(time_zone_local, time_zone_local_3);
811+
// if the TZ var is set, this essentially _overrides_ the
812+
// time set by the localtime symlink
813+
// so just ensure that ::local() acts as expected
814+
// in this case
815+
if let Ok(tz) = std::env::var("TZ") {
816+
let time_zone_local = TimeZone::local()?;
817+
let time_zone_local_1 = TimeZone::from_posix_tz(&tz)?;
818+
assert_eq!(time_zone_local, time_zone_local_1);
819+
} else {
820+
let time_zone_local = TimeZone::local()?;
821+
let time_zone_local_1 = TimeZone::from_posix_tz("localtime")?;
822+
let time_zone_local_2 = TimeZone::from_posix_tz("/etc/localtime")?;
823+
let time_zone_local_3 = TimeZone::from_posix_tz(":/etc/localtime")?;
824+
825+
assert_eq!(time_zone_local, time_zone_local_1);
826+
assert_eq!(time_zone_local, time_zone_local_2);
827+
assert_eq!(time_zone_local, time_zone_local_3);
828+
}
721829

722830
let time_zone_utc = TimeZone::from_posix_tz("UTC")?;
723831
assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0);

0 commit comments

Comments
 (0)