mirror of
https://github.com/BurntSushi/jiff.git
synced 2025-12-23 08:47:45 +00:00
posix: fix handling of "permanent DST" POSIX time zone strings
See the comment in the code for explanation, but basically, this comes from S 3.3.1 of RFC 9636: > 3.3.1. All-Year Daylight Saving Time > > DST is considered to be in effect all year if its UT offset is less > than (i.e., west of) that of standard time, and it starts January 1 > at 00:00 and ends December 31 at 24:00 minus the difference between > standard and daylight saving time, leaving no room for standard time > in the calendar. [POSIX] implies but does not explicitly state this, > so it is spelled out here for clarity. > > Example: XXX3EDT4,0/0,J365/23 > This represents a time zone that is perpetually 4 hours west of UT > and is abbreviated "EDT". The "XXX" is ignored. These odd time zones can occur when compiling TZif files using `zic` with rearguard semantics. In particular, this applies for times at or after 2087 in `Africa/Casablanca`. Rearguard semantics are used by Jiff's `jiff-tzdb` crate, but aren't always otherwise used in standard `/usr/share/zoneinfo` installations. As a result, this bug's effects cannot always be consistently observed. Fixes #386
This commit is contained in:
parent
06564abdf4
commit
292f18640d
3 changed files with 154 additions and 2 deletions
|
|
@ -32,6 +32,10 @@ Allow parsing just a `%s` into a `Zoned` via the `Etc/Unknown` time zone.
|
|||
|
||||
Bug fixes:
|
||||
|
||||
* [#386](https://github.com/BurntSushi/jiff/issues/386):
|
||||
Fix a bug where `2087-12-31T23:00:00Z` in the `Africa/Casablanca` time zone
|
||||
could not be round-tripped (because its offset was calculated incorrectly as
|
||||
a result of not handling "permanent DST" POSIX time zones).
|
||||
* [#407](https://github.com/BurntSushi/jiff/issues/407):
|
||||
Fix a panic that occurred when parsing an empty string as a POSIX time zone.
|
||||
* [#410](https://github.com/BurntSushi/jiff/issues/410):
|
||||
|
|
|
|||
|
|
@ -242,7 +242,69 @@ impl<ABBREV: AsRef<str> + Debug> PosixTimeZone<ABBREV> {
|
|||
dst.rule.start.to_datetime(year, self.std_offset.to_ioffset());
|
||||
// DST time ends with respect to DST time, so offset it by the DST
|
||||
// offset.
|
||||
let end = dst.rule.end.to_datetime(year, dst.offset.to_ioffset());
|
||||
let mut end = dst.rule.end.to_datetime(year, dst.offset.to_ioffset());
|
||||
// This is a whacky special case when DST is permanent, but the math
|
||||
// using to calculate the start/end datetimes ends up leaving a gap
|
||||
// for standard time to appear. In which case, it's possible for a
|
||||
// timestamp at the end of a calendar year to get standard time when
|
||||
// it really should be DST.
|
||||
//
|
||||
// We detect this case by re-interpreting the end of the boundary using
|
||||
// the standard offset. If we get a datetime that is in a different
|
||||
// year, then it follows that standard time is actually impossible to
|
||||
// occur.
|
||||
//
|
||||
// These weird POSIX time zones can occur as the TZ strings in
|
||||
// a TZif file compiled using rearguard semantics. For example,
|
||||
// `Africa/Casablanca` has:
|
||||
//
|
||||
// XXX-2<+01>-1,0/0,J365/23
|
||||
//
|
||||
// Notice here that DST is actually one hour *behind* (it is usually
|
||||
// one hour *ahead*) _and_ it ends at 23:00:00 on the last day of the
|
||||
// year. But if it ends at 23:00, then jumping to standard time moves
|
||||
// the clocks *forward*. Which would bring us to 00:00:00 on the first
|
||||
// of the next year... but that is when DST begins! Hence, DST is
|
||||
// permanent.
|
||||
//
|
||||
// Ideally, this could just be handled by our math automatically. But
|
||||
// I couldn't figure out how to make it work. In particular, in the
|
||||
// above example for year 2087, we get
|
||||
//
|
||||
// start == 2087-01-01T00:00:00Z
|
||||
// end == 2087-12-31T22:00:00Z
|
||||
//
|
||||
// Which leaves a two hour gap for a timestamp to get erroneously
|
||||
// categorized as standard time.
|
||||
//
|
||||
// ... so we special case this. We could pre-compute whether a POSIX
|
||||
// time zone is in permanent DST at construction time, but it's not
|
||||
// obvious to me that it's worth it. Especially since this is an
|
||||
// exceptionally rare case.
|
||||
//
|
||||
// Note that I did try to consult tzcode's (incredibly inscrutable)
|
||||
// `localtime` implementation to figure out how they deal with it. At
|
||||
// first, it looks like they don't have any special handling for this
|
||||
// case. But looking more closely, they skip any time zone transitions
|
||||
// generated by POSIX time zones whose rule spans more than 1 year:
|
||||
//
|
||||
// https://github.com/eggert/tz/blob/8d65db9786753f3b263087e31c59d191561d63e3/localtime.c#L1717-L1735
|
||||
//
|
||||
// By just ignoring them, I think it achieves the desired effect of
|
||||
// permanent DST. But I'm not 100% confident in my understanding of
|
||||
// the code.
|
||||
if start.date.month == 1
|
||||
&& start.date.day == 1
|
||||
&& start.time == ITime::MIN
|
||||
// NOTE: This should come last because it is potentially expensive.
|
||||
&& year
|
||||
!= end.saturating_add_seconds(self.std_offset.second).date.year
|
||||
{
|
||||
end = IDateTime {
|
||||
date: IDate { year, month: 12, day: 31 },
|
||||
time: ITime::MAX,
|
||||
};
|
||||
}
|
||||
Some(DstInfo { dst, start, end })
|
||||
}
|
||||
|
||||
|
|
@ -2536,6 +2598,18 @@ mod tests {
|
|||
assert_eq!(tz.dst_info_utc(2024), Some(dst_info));
|
||||
}
|
||||
|
||||
// See: https://github.com/BurntSushi/jiff/issues/386
|
||||
#[test]
|
||||
fn regression_permanent_dst() {
|
||||
let tz = posix_time_zone("XXX-2<+01>-1,0/0,J365/23");
|
||||
let dst_info = DstInfo {
|
||||
dst: tz.dst.as_ref().unwrap(),
|
||||
start: date(2087, 1, 1).at(0, 0, 0, 0),
|
||||
end: date(2087, 12, 31).at(23, 59, 59, 999_999_999),
|
||||
};
|
||||
assert_eq!(tz.dst_info_utc(2087), Some(dst_info));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasonable() {
|
||||
assert!(PosixTimeZone::parse(b"EST5").is_ok());
|
||||
|
|
|
|||
|
|
@ -253,7 +253,69 @@ impl<ABBREV: AsRef<str> + Debug> PosixTimeZone<ABBREV> {
|
|||
dst.rule.start.to_datetime(year, self.std_offset.to_ioffset());
|
||||
// DST time ends with respect to DST time, so offset it by the DST
|
||||
// offset.
|
||||
let end = dst.rule.end.to_datetime(year, dst.offset.to_ioffset());
|
||||
let mut end = dst.rule.end.to_datetime(year, dst.offset.to_ioffset());
|
||||
// This is a whacky special case when DST is permanent, but the math
|
||||
// using to calculate the start/end datetimes ends up leaving a gap
|
||||
// for standard time to appear. In which case, it's possible for a
|
||||
// timestamp at the end of a calendar year to get standard time when
|
||||
// it really should be DST.
|
||||
//
|
||||
// We detect this case by re-interpreting the end of the boundary using
|
||||
// the standard offset. If we get a datetime that is in a different
|
||||
// year, then it follows that standard time is actually impossible to
|
||||
// occur.
|
||||
//
|
||||
// These weird POSIX time zones can occur as the TZ strings in
|
||||
// a TZif file compiled using rearguard semantics. For example,
|
||||
// `Africa/Casablanca` has:
|
||||
//
|
||||
// XXX-2<+01>-1,0/0,J365/23
|
||||
//
|
||||
// Notice here that DST is actually one hour *behind* (it is usually
|
||||
// one hour *ahead*) _and_ it ends at 23:00:00 on the last day of the
|
||||
// year. But if it ends at 23:00, then jumping to standard time moves
|
||||
// the clocks *forward*. Which would bring us to 00:00:00 on the first
|
||||
// of the next year... but that is when DST begins! Hence, DST is
|
||||
// permanent.
|
||||
//
|
||||
// Ideally, this could just be handled by our math automatically. But
|
||||
// I couldn't figure out how to make it work. In particular, in the
|
||||
// above example for year 2087, we get
|
||||
//
|
||||
// start == 2087-01-01T00:00:00Z
|
||||
// end == 2087-12-31T22:00:00Z
|
||||
//
|
||||
// Which leaves a two hour gap for a timestamp to get erroneously
|
||||
// categorized as standard time.
|
||||
//
|
||||
// ... so we special case this. We could pre-compute whether a POSIX
|
||||
// time zone is in permanent DST at construction time, but it's not
|
||||
// obvious to me that it's worth it. Especially since this is an
|
||||
// exceptionally rare case.
|
||||
//
|
||||
// Note that I did try to consult tzcode's (incredibly inscrutable)
|
||||
// `localtime` implementation to figure out how they deal with it. At
|
||||
// first, it looks like they don't have any special handling for this
|
||||
// case. But looking more closely, they skip any time zone transitions
|
||||
// generated by POSIX time zones whose rule spans more than 1 year:
|
||||
//
|
||||
// https://github.com/eggert/tz/blob/8d65db9786753f3b263087e31c59d191561d63e3/localtime.c#L1717-L1735
|
||||
//
|
||||
// By just ignoring them, I think it achieves the desired effect of
|
||||
// permanent DST. But I'm not 100% confident in my understanding of
|
||||
// the code.
|
||||
if start.date.month == 1
|
||||
&& start.date.day == 1
|
||||
&& start.time == ITime::MIN
|
||||
// NOTE: This should come last because it is potentially expensive.
|
||||
&& year
|
||||
!= end.saturating_add_seconds(self.std_offset.second).date.year
|
||||
{
|
||||
end = IDateTime {
|
||||
date: IDate { year, month: 12, day: 31 },
|
||||
time: ITime::MAX,
|
||||
};
|
||||
}
|
||||
Some(DstInfo { dst, start, end })
|
||||
}
|
||||
|
||||
|
|
@ -2548,6 +2610,18 @@ mod tests {
|
|||
assert_eq!(tz.dst_info_utc(2024), Some(dst_info));
|
||||
}
|
||||
|
||||
// See: https://github.com/BurntSushi/jiff/issues/386
|
||||
#[test]
|
||||
fn regression_permanent_dst() {
|
||||
let tz = posix_time_zone("XXX-2<+01>-1,0/0,J365/23");
|
||||
let dst_info = DstInfo {
|
||||
dst: tz.dst.as_ref().unwrap(),
|
||||
start: date(2087, 1, 1).at(0, 0, 0, 0),
|
||||
end: date(2087, 12, 31).at(23, 59, 59, 999_999_999),
|
||||
};
|
||||
assert_eq!(tz.dst_info_utc(2087), Some(dst_info));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasonable() {
|
||||
assert!(PosixTimeZone::parse(b"EST5").is_ok());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue