#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Daylight {
#[default]
Never,
UnitedStates,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UsFederal {
NoDaylight,
FirstSundayInAprilToLastSundayInOctober,
SecondSundayInMarchToFirstSundayInNovember,
}
impl UsFederal {
fn in_force(year: i64) -> UsFederal {
if year < 1918 {
UsFederal::NoDaylight
} else if year < 2007 {
UsFederal::FirstSundayInAprilToLastSundayInOctober
} else {
UsFederal::SecondSundayInMarchToFirstSundayInNovember
}
}
}
const LOS_ANGELES_LOCAL_MEAN_TIME_SECS: i32 = -(7 * 3600 + 52 * 60 + 58);
const LOS_ANGELES_STANDARD_TIME_ADOPTED: i64 = -2_717_640_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Zone {
pub standard_offset_secs: i32,
pub daylight: Daylight,
}
impl Zone {
pub const fn fixed(offset_secs: i32) -> Zone {
Zone {
standard_offset_secs: offset_secs,
daylight: Daylight::Never,
}
}
pub const LOS_ANGELES: Zone = Zone {
standard_offset_secs: -8 * 3600,
daylight: Daylight::UnitedStates,
};
pub const UTC: Zone = Zone::fixed(0);
pub fn offset_secs_at(self, unix_time_seconds: i64) -> i32 {
match self.daylight {
Daylight::Never => self.standard_offset_secs,
Daylight::UnitedStates => {
if unix_time_seconds < LOS_ANGELES_STANDARD_TIME_ADOPTED {
LOS_ANGELES_LOCAL_MEAN_TIME_SECS
} else if self.is_daylight(unix_time_seconds) {
self.standard_offset_secs + 3600
} else {
self.standard_offset_secs
}
}
}
}
fn is_daylight(self, unix_time_seconds: i64) -> bool {
const TWO_AM: i64 = 2 * 3600;
let local = unix_time_seconds + i64::from(self.standard_offset_secs);
let (year, month, day, seconds_into_day) = civil_from_unix(local);
let rule = UsFederal::in_force(year);
let (start, end) = match rule {
UsFederal::NoDaylight => return false,
UsFederal::FirstSundayInAprilToLastSundayInOctober => {
((4, nth_sunday(year, 4, 1)), (10, last_sunday(year, 10)))
}
UsFederal::SecondSundayInMarchToFirstSundayInNovember => {
((3, nth_sunday(year, 3, 2)), (11, nth_sunday(year, 11, 1)))
}
};
let after_start = (month, day, seconds_into_day) >= (start.0, start.1, TWO_AM);
let before_end = (month, day, seconds_into_day) < (end.0, end.1, TWO_AM);
after_start && before_end
}
}
fn nth_sunday(year: i64, month: u32, n: u32) -> u32 {
let first_weekday = weekday_of(year, month, 1);
let first_sunday = 1 + (7 - first_weekday) % 7;
first_sunday + (n - 1) * 7
}
fn last_sunday(year: i64, month: u32) -> u32 {
let last = days_in_month(year, month);
last - weekday_of(year, month, last)
}
fn weekday_of(year: i64, month: u32, day: u32) -> u32 {
let days = days_from_civil(year, month, day);
u32::try_from((days + 4).rem_euclid(7)).unwrap_or(0)
}
fn is_leap(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
fn days_in_month(year: i64, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
_ if is_leap(year) => 29,
_ => 28,
}
}
fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
let month = i64::from(month);
let day = i64::from(day);
let year = year - i64::from(month <= 2);
let era = if year >= 0 { year } else { year - 399 } / 400;
let year_of_era = year - era * 400;
let day_of_year = (153 * (month + if month > 2 { -3 } else { 9 }) + 2) / 5 + day - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era - 719_468
}
fn civil_from_unix(seconds: i64) -> (i64, u32, u32, i64) {
let days = seconds.div_euclid(86_400);
let seconds_into_day = seconds.rem_euclid(86_400);
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let day_of_era = z - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let mp = (5 * day_of_year + 2) / 153;
let day = u32::try_from(day_of_year - (153 * mp + 2) / 5 + 1).unwrap_or(1);
let month = u32::try_from(if mp < 10 { mp + 3 } else { mp - 9 }).unwrap_or(1);
(year + i64::from(month <= 2), month, day, seconds_into_day)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_civil_calendar_round_trips() {
for &(year, month, day) in &[
(1849, 12, 31),
(1900, 7, 4),
(1970, 1, 1),
(2014, 7, 4),
(2525, 12, 31),
(0, 3, 1),
] {
let days = days_from_civil(year, month, day);
assert_eq!(
civil_from_unix(days * 86_400),
(year, month, day, 0),
"{year}-{month}-{day}"
);
}
}
#[test]
fn weekdays_are_the_calendars_own() {
assert_eq!(weekday_of(1970, 1, 1), 4);
assert_eq!(weekday_of(2014, 7, 4), 5);
assert_eq!(weekday_of(2525, 12, 31), 1);
}
#[test]
fn the_transition_days_are_the_federal_rules() {
assert_eq!(nth_sunday(2006, 4, 1), 2);
assert_eq!(last_sunday(2006, 10), 29);
assert_eq!(nth_sunday(2014, 3, 2), 9);
assert_eq!(nth_sunday(2014, 11, 1), 2);
}
#[test]
fn los_angeles_is_standard_time_in_winter_and_in_1900() {
let winter = [
(
days_from_civil(2525, 12, 31) * 86_400 + 8 * 3600,
"2525-12-31",
),
(
days_from_civil(1900, 7, 4) * 86_400 + 15 * 3600 + 59 * 60 + 58 + 8 * 3600,
"1900-07-04",
),
(
days_from_civil(2015, 12, 9) * 86_400 + 8 * 3600,
"2015-12-09",
),
(
days_from_civil(2014, 3, 2) * 86_400 + 8 * 3600,
"2014-03-02",
),
(
days_from_civil(2013, 12, 30) * 86_400 + 8 * 3600,
"2013-12-30",
),
];
for (instant, label) in winter {
assert_eq!(
Zone::LOS_ANGELES.offset_secs_at(instant),
-8 * 3600,
"{label} is standard time"
);
}
}
#[test]
fn los_angeles_is_daylight_time_in_summer() {
for (instant, label) in [
(
days_from_civil(2014, 7, 4) * 86_400 + 7 * 3600,
"2014-07-04",
),
(super::super::GOLDEN_CLOCK_SECS.cast_signed(), "the seed"),
(
days_from_civil(2015, 9, 4) * 86_400 + 7 * 3600,
"2015-09-04",
),
] {
assert_eq!(
Zone::LOS_ANGELES.offset_secs_at(instant),
-7 * 3600,
"{label} is daylight time"
);
}
}
#[test]
fn los_angeles_kept_local_mean_time_before_the_railroads() {
let instant = days_from_civil(1850, 1, 1) * 86_400 + 7 * 3600 + 52 * 60 + 58;
assert_eq!(
Zone::LOS_ANGELES.offset_secs_at(instant),
LOS_ANGELES_LOCAL_MEAN_TIME_SECS
);
let nineteen_hundred = days_from_civil(1900, 7, 4) * 86_400 + 8 * 3600;
assert_eq!(
Zone::LOS_ANGELES.offset_secs_at(nineteen_hundred),
-8 * 3600
);
}
#[test]
fn a_fixed_zone_never_shifts() {
let zone = Zone::fixed(3600);
assert_eq!(zone.offset_secs_at(0), 3600);
assert_eq!(
zone.offset_secs_at(days_from_civil(2014, 7, 4) * 86_400),
3600
);
}
}