Skip to content

Commit 22b1ce8

Browse files
authored
Fix bug in POSIX time zone calculations (#658)
This is going to be a first in a string of PRs to implement a zero-copy compiled data zoneinfo provider. Fixes a bug in the POSIX time zone calculations where we weren't correctly handling the time overflow on some edge cases.
1 parent 5714994 commit 22b1ce8

4 files changed

Lines changed: 153 additions & 12 deletions

File tree

zoneinfo/src/posix.rs

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -136,41 +136,104 @@ pub enum PosixDate {
136136
}
137137

138138
impl PosixDate {
139-
pub(crate) fn from_rule(rule: &Rule) -> Self {
139+
/// Creates a [`PosixDate`] from a provided rule. This method returns both a posix date and an
140+
/// integer, representing the days off the target weekday in seconds.
141+
pub(crate) fn from_rule(rule: &Rule) -> (Self, i64) {
140142
match rule.on_date {
141-
DayOfMonth::Day(day) if rule.in_month == Month::Jan || rule.in_month == Month::Feb => {
142-
PosixDate::JulianNoLeap(month_to_day(rule.in_month as u8, 1) as u16 + day as u16)
143-
}
144-
DayOfMonth::Day(day) => {
145-
PosixDate::JulianLeap(month_to_day(rule.in_month as u8, 1) as u16 + day as u16)
146-
}
147-
DayOfMonth::Last(wd) => PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, 5, wd)),
143+
DayOfMonth::Day(day) if rule.in_month == Month::Jan || rule.in_month == Month::Feb => (
144+
PosixDate::JulianNoLeap(month_to_day(rule.in_month as u8, 1) as u16 + day as u16),
145+
0,
146+
),
147+
DayOfMonth::Day(day) => (
148+
PosixDate::JulianLeap(month_to_day(rule.in_month as u8, 1) as u16 + day as u16),
149+
0,
150+
),
151+
DayOfMonth::Last(wd) => (
152+
PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, 5, wd)),
153+
0,
154+
),
148155
DayOfMonth::WeekDayGEThanMonthDay(week_day, day_of_month) => {
149-
let week = 1 + (day_of_month - 1) / 7;
150-
PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, week, week_day))
156+
// Handle week day offset correctly (See America/Santiago; i.e. Sun>=2)
157+
//
158+
// To do this for the GE case, we work with a zero based day of month,
159+
// This ensures that day_of_month being 1 aligns with Sun = 0, for
160+
// Sun>=1 purposes.
161+
//
162+
// The primary purpose for this approach as noted in zic.c is to support
163+
// America/Santiago timestamps beyond 2038.
164+
//
165+
// See the below link for more info.
166+
//
167+
// https://github.com/eggert/tz/commit/07351e0248b5a42151e49e4506bca0363c846f8c
168+
169+
// Calculate the difference between the day of month and the week day.
170+
let zero_based_day_of_month = day_of_month - 1;
171+
let week_day_from_dom = zero_based_day_of_month % 7;
172+
// N.B., this could be a negative. If we look at Sun>=2, then this becomes
173+
// 0 - 1.
174+
let mut adjusted_week_day = week_day as i8 - week_day_from_dom as i8;
175+
176+
// Calculate what week we are in.
177+
//
178+
// Since we are operating with a zero based day of month, we add
179+
let week = 1 + zero_based_day_of_month / 7;
180+
181+
// If we have shifted beyond the month, add 7 to shift back into the first
182+
// week.
183+
if adjusted_week_day < 0 {
184+
adjusted_week_day += 7;
185+
}
186+
let week_day = WeekDay::from_u8(adjusted_week_day as u8);
187+
// N.B. The left of time the target weekday becomes a time overflow added
188+
// to the minutes.
189+
(
190+
PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, week, week_day)),
191+
week_day_from_dom as i64 * 86_400,
192+
)
151193
}
152194
DayOfMonth::WeekDayLEThanMonthDay(week_day, day_of_month) => {
195+
// Handle week day offset correctly
196+
//
197+
// We don't worry about the last day of the month in this scenario, which
198+
// is the upper bound as that is handled by DayOfMonth::Last
199+
let week_day_from_dom = day_of_month as i8 % 7;
200+
let mut adjusted_week_day = week_day as i8 - week_day_from_dom;
153201
let week = day_of_month / 7;
154-
PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, week, week_day))
202+
if adjusted_week_day < 0 {
203+
adjusted_week_day += 7;
204+
}
205+
(
206+
PosixDate::MonthWeekDay(MonthWeekDay(
207+
rule.in_month,
208+
week,
209+
WeekDay::from_u8(adjusted_week_day as u8),
210+
)),
211+
week_day_from_dom as i64 * 86_400,
212+
)
155213
}
156214
}
157215
}
158216
}
159217

160218
#[derive(Debug, PartialEq, Clone, Copy)]
161219
pub struct PosixDateTime {
220+
/// The designated [`PosixDate`]
162221
pub date: PosixDate,
222+
/// The local time for a [`PosixDateTime`] at which a transition occurs.
223+
///
224+
/// N.B., this can be in the range of -167..=167
163225
pub time: Time,
164226
}
165227

166228
impl PosixDateTime {
167229
pub(crate) fn from_rule_and_transition_info(rule: &Rule, offset: Time, savings: Time) -> Self {
168-
let date = PosixDate::from_rule(rule);
230+
let (date, time_overflow) = PosixDate::from_rule(rule);
169231
let time = match rule.at {
170232
QualifiedTime::Local(time) => time,
171233
QualifiedTime::Standard(standard_time) => standard_time.add(rule.save),
172234
QualifiedTime::Universal(universal_time) => universal_time.add(offset).add(savings),
173235
};
236+
let time = time.add(Time::from_seconds(time_overflow));
174237
Self { date, time }
175238
}
176239
}

zoneinfo/src/types.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,21 @@ pub enum WeekDay {
444444
Sat,
445445
}
446446

447+
impl WeekDay {
448+
pub(crate) fn from_u8(value: u8) -> Self {
449+
match value {
450+
0 => Self::Sun,
451+
1 => Self::Mon,
452+
2 => Self::Tues,
453+
3 => Self::Wed,
454+
4 => Self::Thurs,
455+
5 => Self::Fri,
456+
6 => Self::Sat,
457+
_ => unreachable!("invalid week day value"),
458+
}
459+
}
460+
}
461+
447462
impl TryFromStr<LineParseContext> for WeekDay {
448463
type Error = ZoneInfoParseError;
449464
fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result<Self, Self::Error> {

zoneinfo/tests/posix.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,10 @@ fn posix_string_test() {
4040

4141
let moscow_posix = zic.get_posix_time_zone("Europe/Moscow").unwrap();
4242
assert_eq!(moscow_posix.to_string(), Ok("MSK-3".into()));
43+
44+
let santiago_posix = zic.get_posix_time_zone("America/Santiago").unwrap();
45+
assert_eq!(
46+
santiago_posix.to_string(),
47+
Ok("<-04>4<-03>,M9.1.6/24,M4.1.6/24".into())
48+
)
4349
}

zoneinfo/tests/zoneinfo

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,4 +435,61 @@ Zone Europe/Riga 1:36:34 - LMT 1880
435435
2:00 - EET 2001 Jan 2
436436
2:00 EU EE%sT
437437

438+
# America/Santiago test case
438439

440+
# Rule NAME FROM TO - IN ON AT SAVE LETTER/S
441+
Rule Chile 1927 1931 - Sep 1 0:00 1:00 -
442+
Rule Chile 1928 1932 - Apr 1 0:00 0 -
443+
Rule Chile 1968 only - Nov 3 4:00u 1:00 -
444+
Rule Chile 1969 only - Mar 30 3:00u 0 -
445+
Rule Chile 1969 only - Nov 23 4:00u 1:00 -
446+
Rule Chile 1970 only - Mar 29 3:00u 0 -
447+
Rule Chile 1971 only - Mar 14 3:00u 0 -
448+
Rule Chile 1970 1972 - Oct Sun>=9 4:00u 1:00 -
449+
Rule Chile 1972 1986 - Mar Sun>=9 3:00u 0 -
450+
Rule Chile 1973 only - Sep 30 4:00u 1:00 -
451+
Rule Chile 1974 1987 - Oct Sun>=9 4:00u 1:00 -
452+
Rule Chile 1987 only - Apr 12 3:00u 0 -
453+
Rule Chile 1988 1990 - Mar Sun>=9 3:00u 0 -
454+
Rule Chile 1988 1989 - Oct Sun>=9 4:00u 1:00 -
455+
Rule Chile 1990 only - Sep 16 4:00u 1:00 -
456+
Rule Chile 1991 1996 - Mar Sun>=9 3:00u 0 -
457+
Rule Chile 1991 1997 - Oct Sun>=9 4:00u 1:00 -
458+
Rule Chile 1997 only - Mar 30 3:00u 0 -
459+
Rule Chile 1998 only - Mar Sun>=9 3:00u 0 -
460+
Rule Chile 1998 only - Sep 27 4:00u 1:00 -
461+
Rule Chile 1999 only - Apr 4 3:00u 0 -
462+
Rule Chile 1999 2010 - Oct Sun>=9 4:00u 1:00 -
463+
Rule Chile 2000 2007 - Mar Sun>=9 3:00u 0 -
464+
# N.B.: the end of March 29 in Chile is March 30 in Universal time,
465+
# which is used below in specifying the transition.
466+
Rule Chile 2008 only - Mar 30 3:00u 0 -
467+
Rule Chile 2009 only - Mar Sun>=9 3:00u 0 -
468+
Rule Chile 2010 only - Apr Sun>=1 3:00u 0 -
469+
Rule Chile 2011 only - May Sun>=2 3:00u 0 -
470+
Rule Chile 2011 only - Aug Sun>=16 4:00u 1:00 -
471+
Rule Chile 2012 2014 - Apr Sun>=23 3:00u 0 -
472+
Rule Chile 2012 2014 - Sep Sun>=2 4:00u 1:00 -
473+
Rule Chile 2016 2018 - May Sun>=9 3:00u 0 -
474+
Rule Chile 2016 2018 - Aug Sun>=9 4:00u 1:00 -
475+
Rule Chile 2019 max - Apr Sun>=2 3:00u 0 -
476+
Rule Chile 2019 2021 - Sep Sun>=2 4:00u 1:00 -
477+
Rule Chile 2022 only - Sep Sun>=9 4:00u 1:00 -
478+
Rule Chile 2023 max - Sep Sun>=2 4:00u 1:00 -
479+
# IATA SSIM anomalies: (1992-02) says 1992-03-14;
480+
# (1996-09) says 1998-03-08. Ignore these.
481+
# Zone NAME STDOFF RULES FORMAT [UNTIL]
482+
Zone America/Santiago -4:42:45 - LMT 1890
483+
-4:42:45 - SMT 1910 Jan 10 # Santiago Mean Time
484+
-5:00 - %z 1916 Jul 1
485+
-4:42:45 - SMT 1918 Sep 10
486+
-4:00 - %z 1919 Jul 1
487+
-4:42:45 - SMT 1927 Sep 1
488+
-5:00 Chile %z 1932 Sep 1
489+
-4:00 - %z 1942 Jun 1
490+
-5:00 - %z 1942 Aug 1
491+
-4:00 - %z 1946 Jul 14 24:00
492+
-4:00 1:00 %z 1946 Aug 28 24:00 # central CL
493+
-5:00 1:00 %z 1947 Mar 31 24:00
494+
-5:00 - %z 1947 May 21 23:00
495+
-4:00 Chile %z

0 commit comments

Comments
 (0)