diff --git a/zoneinfo/src/posix.rs b/zoneinfo/src/posix.rs index ff1f7f467..5be5f634b 100644 --- a/zoneinfo/src/posix.rs +++ b/zoneinfo/src/posix.rs @@ -136,22 +136,80 @@ pub enum PosixDate { } impl PosixDate { - pub(crate) fn from_rule(rule: &Rule) -> Self { + /// Creates a [`PosixDate`] from a provided rule. This method returns both a posix date and an + /// integer, representing the days off the target weekday in seconds. + pub(crate) fn from_rule(rule: &Rule) -> (Self, i64) { match rule.on_date { - DayOfMonth::Day(day) if rule.in_month == Month::Jan || rule.in_month == Month::Feb => { - PosixDate::JulianNoLeap(month_to_day(rule.in_month as u8, 1) as u16 + day as u16) - } - DayOfMonth::Day(day) => { - PosixDate::JulianLeap(month_to_day(rule.in_month as u8, 1) as u16 + day as u16) - } - DayOfMonth::Last(wd) => PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, 5, wd)), + DayOfMonth::Day(day) if rule.in_month == Month::Jan || rule.in_month == Month::Feb => ( + PosixDate::JulianNoLeap(month_to_day(rule.in_month as u8, 1) as u16 + day as u16), + 0, + ), + DayOfMonth::Day(day) => ( + PosixDate::JulianLeap(month_to_day(rule.in_month as u8, 1) as u16 + day as u16), + 0, + ), + DayOfMonth::Last(wd) => ( + PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, 5, wd)), + 0, + ), DayOfMonth::WeekDayGEThanMonthDay(week_day, day_of_month) => { - let week = 1 + (day_of_month - 1) / 7; - PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, week, week_day)) + // Handle week day offset correctly (See America/Santiago; i.e. Sun>=2) + // + // To do this for the GE case, we work with a zero based day of month, + // This ensures that day_of_month being 1 aligns with Sun = 0, for + // Sun>=1 purposes. + // + // The primary purpose for this approach as noted in zic.c is to support + // America/Santiago timestamps beyond 2038. + // + // See the below link for more info. + // + // https://github.com/eggert/tz/commit/07351e0248b5a42151e49e4506bca0363c846f8c + + // Calculate the difference between the day of month and the week day. + let zero_based_day_of_month = day_of_month - 1; + let week_day_from_dom = zero_based_day_of_month % 7; + // N.B., this could be a negative. If we look at Sun>=2, then this becomes + // 0 - 1. + let mut adjusted_week_day = week_day as i8 - week_day_from_dom as i8; + + // Calculate what week we are in. + // + // Since we are operating with a zero based day of month, we add + let week = 1 + zero_based_day_of_month / 7; + + // If we have shifted beyond the month, add 7 to shift back into the first + // week. + if adjusted_week_day < 0 { + adjusted_week_day += 7; + } + let week_day = WeekDay::from_u8(adjusted_week_day as u8); + // N.B. The left of time the target weekday becomes a time overflow added + // to the minutes. + ( + PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, week, week_day)), + week_day_from_dom as i64 * 86_400, + ) } DayOfMonth::WeekDayLEThanMonthDay(week_day, day_of_month) => { + // Handle week day offset correctly + // + // We don't worry about the last day of the month in this scenario, which + // is the upper bound as that is handled by DayOfMonth::Last + let week_day_from_dom = day_of_month as i8 % 7; + let mut adjusted_week_day = week_day as i8 - week_day_from_dom; let week = day_of_month / 7; - PosixDate::MonthWeekDay(MonthWeekDay(rule.in_month, week, week_day)) + if adjusted_week_day < 0 { + adjusted_week_day += 7; + } + ( + PosixDate::MonthWeekDay(MonthWeekDay( + rule.in_month, + week, + WeekDay::from_u8(adjusted_week_day as u8), + )), + week_day_from_dom as i64 * 86_400, + ) } } } @@ -159,18 +217,23 @@ impl PosixDate { #[derive(Debug, PartialEq, Clone, Copy)] pub struct PosixDateTime { + /// The designated [`PosixDate`] pub date: PosixDate, + /// The local time for a [`PosixDateTime`] at which a transition occurs. + /// + /// N.B., this can be in the range of -167..=167 pub time: Time, } impl PosixDateTime { pub(crate) fn from_rule_and_transition_info(rule: &Rule, offset: Time, savings: Time) -> Self { - let date = PosixDate::from_rule(rule); + let (date, time_overflow) = PosixDate::from_rule(rule); let time = match rule.at { QualifiedTime::Local(time) => time, QualifiedTime::Standard(standard_time) => standard_time.add(rule.save), QualifiedTime::Universal(universal_time) => universal_time.add(offset).add(savings), }; + let time = time.add(Time::from_seconds(time_overflow)); Self { date, time } } } diff --git a/zoneinfo/src/types.rs b/zoneinfo/src/types.rs index 8c9080bc7..32ae0d6c8 100644 --- a/zoneinfo/src/types.rs +++ b/zoneinfo/src/types.rs @@ -444,6 +444,21 @@ pub enum WeekDay { Sat, } +impl WeekDay { + pub(crate) fn from_u8(value: u8) -> Self { + match value { + 0 => Self::Sun, + 1 => Self::Mon, + 2 => Self::Tues, + 3 => Self::Wed, + 4 => Self::Thurs, + 5 => Self::Fri, + 6 => Self::Sat, + _ => unreachable!("invalid week day value"), + } + } +} + impl TryFromStr for WeekDay { type Error = ZoneInfoParseError; fn try_from_str(s: &str, ctx: &mut LineParseContext) -> Result { diff --git a/zoneinfo/tests/posix.rs b/zoneinfo/tests/posix.rs index 133060f7e..5e8fe7bf2 100644 --- a/zoneinfo/tests/posix.rs +++ b/zoneinfo/tests/posix.rs @@ -40,4 +40,10 @@ fn posix_string_test() { let moscow_posix = zic.get_posix_time_zone("Europe/Moscow").unwrap(); assert_eq!(moscow_posix.to_string(), Ok("MSK-3".into())); + + let santiago_posix = zic.get_posix_time_zone("America/Santiago").unwrap(); + assert_eq!( + santiago_posix.to_string(), + Ok("<-04>4<-03>,M9.1.6/24,M4.1.6/24".into()) + ) } diff --git a/zoneinfo/tests/zoneinfo b/zoneinfo/tests/zoneinfo index cc3f26c37..922f8eb8c 100644 --- a/zoneinfo/tests/zoneinfo +++ b/zoneinfo/tests/zoneinfo @@ -435,4 +435,61 @@ Zone Europe/Riga 1:36:34 - LMT 1880 2:00 - EET 2001 Jan 2 2:00 EU EE%sT +# America/Santiago test case +# Rule NAME FROM TO - IN ON AT SAVE LETTER/S +Rule Chile 1927 1931 - Sep 1 0:00 1:00 - +Rule Chile 1928 1932 - Apr 1 0:00 0 - +Rule Chile 1968 only - Nov 3 4:00u 1:00 - +Rule Chile 1969 only - Mar 30 3:00u 0 - +Rule Chile 1969 only - Nov 23 4:00u 1:00 - +Rule Chile 1970 only - Mar 29 3:00u 0 - +Rule Chile 1971 only - Mar 14 3:00u 0 - +Rule Chile 1970 1972 - Oct Sun>=9 4:00u 1:00 - +Rule Chile 1972 1986 - Mar Sun>=9 3:00u 0 - +Rule Chile 1973 only - Sep 30 4:00u 1:00 - +Rule Chile 1974 1987 - Oct Sun>=9 4:00u 1:00 - +Rule Chile 1987 only - Apr 12 3:00u 0 - +Rule Chile 1988 1990 - Mar Sun>=9 3:00u 0 - +Rule Chile 1988 1989 - Oct Sun>=9 4:00u 1:00 - +Rule Chile 1990 only - Sep 16 4:00u 1:00 - +Rule Chile 1991 1996 - Mar Sun>=9 3:00u 0 - +Rule Chile 1991 1997 - Oct Sun>=9 4:00u 1:00 - +Rule Chile 1997 only - Mar 30 3:00u 0 - +Rule Chile 1998 only - Mar Sun>=9 3:00u 0 - +Rule Chile 1998 only - Sep 27 4:00u 1:00 - +Rule Chile 1999 only - Apr 4 3:00u 0 - +Rule Chile 1999 2010 - Oct Sun>=9 4:00u 1:00 - +Rule Chile 2000 2007 - Mar Sun>=9 3:00u 0 - +# N.B.: the end of March 29 in Chile is March 30 in Universal time, +# which is used below in specifying the transition. +Rule Chile 2008 only - Mar 30 3:00u 0 - +Rule Chile 2009 only - Mar Sun>=9 3:00u 0 - +Rule Chile 2010 only - Apr Sun>=1 3:00u 0 - +Rule Chile 2011 only - May Sun>=2 3:00u 0 - +Rule Chile 2011 only - Aug Sun>=16 4:00u 1:00 - +Rule Chile 2012 2014 - Apr Sun>=23 3:00u 0 - +Rule Chile 2012 2014 - Sep Sun>=2 4:00u 1:00 - +Rule Chile 2016 2018 - May Sun>=9 3:00u 0 - +Rule Chile 2016 2018 - Aug Sun>=9 4:00u 1:00 - +Rule Chile 2019 max - Apr Sun>=2 3:00u 0 - +Rule Chile 2019 2021 - Sep Sun>=2 4:00u 1:00 - +Rule Chile 2022 only - Sep Sun>=9 4:00u 1:00 - +Rule Chile 2023 max - Sep Sun>=2 4:00u 1:00 - +# IATA SSIM anomalies: (1992-02) says 1992-03-14; +# (1996-09) says 1998-03-08. Ignore these. +# Zone NAME STDOFF RULES FORMAT [UNTIL] +Zone America/Santiago -4:42:45 - LMT 1890 + -4:42:45 - SMT 1910 Jan 10 # Santiago Mean Time + -5:00 - %z 1916 Jul 1 + -4:42:45 - SMT 1918 Sep 10 + -4:00 - %z 1919 Jul 1 + -4:42:45 - SMT 1927 Sep 1 + -5:00 Chile %z 1932 Sep 1 + -4:00 - %z 1942 Jun 1 + -5:00 - %z 1942 Aug 1 + -4:00 - %z 1946 Jul 14 24:00 + -4:00 1:00 %z 1946 Aug 28 24:00 # central CL + -5:00 1:00 %z 1947 Mar 31 24:00 + -5:00 - %z 1947 May 21 23:00 + -4:00 Chile %z