use core::fmt;
use super::{NaiveDateTime, DateTime, UnixTimestamp, Month, DayOfTheWeek};
use num::{div_floor, positive_rem};
pub trait TimeZone {
fn from_timestamp(&self, t: UnixTimestamp) -> NaiveDateTime;
fn to_timestamp(&self, d: &NaiveDateTime) -> Result<UnixTimestamp, LocalTimeConversionError>;
}
#[derive(Eq, PartialEq)]
pub struct LocalTimeConversionError {
_private: (),
}
impl fmt::Debug for LocalTimeConversionError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "LocalTimeConversionError")
}
}
pub trait UnambiguousTimeZone: TimeZone {
fn to_unambiguous_timestamp(&self, d: &NaiveDateTime) -> UnixTimestamp {
self.to_timestamp(d).unwrap()
}
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)]
pub struct Utc;
impl UnambiguousTimeZone for Utc {}
impl TimeZone for Utc {
fn from_timestamp(&self, u: UnixTimestamp) -> NaiveDateTime {
let days_since_unix = div_floor(u.0, SECONDS_PER_DAY) as i32;
let days = days_since_unix + days_since_d0(1970);
let year = div_floor(days * 400, DAYS_PER_400YEARS) as i32;
let day_of_the_year = days - days_since_d0(year);
let (month, day) = Month::from_day_of_the_year(day_of_the_year, year.into());
let hour = positive_rem(div_floor(u.0, SECONDS_PER_HOUR), 24) as u8;
let minute = positive_rem(div_floor(u.0, SECONDS_PER_MINUTE), 60) as u8;
let second = positive_rem(u.0, 60) as u8;
NaiveDateTime::new(year, month, day, hour, minute, second)
}
fn to_timestamp(&self, d: &NaiveDateTime) -> Result<UnixTimestamp, LocalTimeConversionError> {
Ok(UnixTimestamp(
i64::from(days_since_unix(d)) * SECONDS_PER_DAY
+ i64::from(d.hour) * SECONDS_PER_HOUR
+ i64::from(d.minute) * SECONDS_PER_MINUTE
+ i64::from(d.second)
))
}
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)]
pub struct FixedOffsetFromUtc {
seconds_ahead_of_utc: i32,
}
impl FixedOffsetFromUtc {
pub fn from_hours_and_minutes(hours: i32, minutes: i32) -> Self {
FixedOffsetFromUtc {
seconds_ahead_of_utc: (hours * 60 + minutes) * 60,
}
}
}
impl UnambiguousTimeZone for FixedOffsetFromUtc {}
impl TimeZone for FixedOffsetFromUtc {
fn from_timestamp(&self, u: UnixTimestamp) -> NaiveDateTime {
let seconds = u.0 + i64::from(self.seconds_ahead_of_utc);
Utc.from_timestamp(UnixTimestamp(seconds))
}
fn to_timestamp(&self, d: &NaiveDateTime) -> Result<UnixTimestamp, LocalTimeConversionError> {
let seconds = Utc.to_unambiguous_timestamp(d).0;
Ok(UnixTimestamp(seconds - i64::from(self.seconds_ahead_of_utc)))
}
}
pub trait DaylightSaving {
fn offset_outside_dst(&self) -> FixedOffsetFromUtc;
fn offset_during_dst(&self) -> FixedOffsetFromUtc;
fn is_in_dst(&self, t: UnixTimestamp) -> bool;
}
impl<Tz: DaylightSaving> TimeZone for Tz {
fn from_timestamp(&self, u: UnixTimestamp) -> NaiveDateTime {
let offset = if self.is_in_dst(u) {
self.offset_during_dst()
} else {
self.offset_outside_dst()
};
offset.from_timestamp(u)
}
fn to_timestamp(&self, d: &NaiveDateTime) -> Result<UnixTimestamp, LocalTimeConversionError> {
let assuming_outside = self.offset_outside_dst().to_unambiguous_timestamp(d);
let assuming_during = self.offset_during_dst().to_unambiguous_timestamp(d);
match (self.is_in_dst(assuming_outside), self.is_in_dst(assuming_during)) {
(true, true) => Ok(assuming_during),
(false, false) => Ok(assuming_outside),
_ => Err(LocalTimeConversionError { _private: () }),
}
}
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)]
pub struct CentralEurope;
impl DaylightSaving for CentralEurope {
fn offset_outside_dst(&self) -> FixedOffsetFromUtc {
FixedOffsetFromUtc::from_hours_and_minutes(1, 0)
}
fn offset_during_dst(&self) -> FixedOffsetFromUtc {
FixedOffsetFromUtc::from_hours_and_minutes(2, 0)
}
fn is_in_dst(&self, t: UnixTimestamp) -> bool {
use Month::*;
let d = DateTime::from_timestamp(t, Utc);
if d.month() < March || d.month() > October {
false
} else if d.month() > March && d.month() < October {
true
} else if d.month() == March {
!before_last_sunday_1_am(&d)
} else if d.month() == October {
before_last_sunday_1_am(&d)
} else {
unreachable!()
}
}
}
fn before_last_sunday_1_am(d: &DateTime<Utc>) -> bool {
let last_sunday = last_of_the_month(d, DayOfTheWeek::Sunday);
d.day() < last_sunday || (
d.day() == last_sunday &&
(d.hour(), d.minute(), d.second()) < (1, 0, 0)
)
}
fn last_of_the_month(d: &DateTime<Utc>, requested_dow: DayOfTheWeek) -> u8 {
let last_day = d.month().length(d.year().into());
let last_dow = NaiveDateTime::new(d.year(), d.month(), last_day, 0, 0, 0).day_of_the_week();
let difference = i32::from(last_dow.to_iso_number()) - i32::from(requested_dow.to_iso_number());
last_day - (positive_rem(difference, 7) as u8)
}
pub fn days_since_unix(d: &NaiveDateTime) -> i32 {
(d.year - 1970) * DAYS_PER_COMMON_YEAR
+ leap_days_since_y0(d.year) - leap_days_since_y0(1970)
+ d.month.days_since_january_1st(d.year.into())
+ i32::from(d.day - 1)
}
pub fn leap_days_since_y0(year: i32) -> i32 {
if year > 0 {
let year = year - 1; ((year / 4) - (year / 100) + (year / 400)) + 1
} else {
let year = -year;
-((year / 4) - (year / 100) + (year / 400))
}
}
fn days_since_d0(year: i32) -> i32 {
year * DAYS_PER_COMMON_YEAR + leap_days_since_y0(year)
}
const SECONDS_PER_MINUTE: i64 = 60;
const SECONDS_PER_HOUR: i64 = SECONDS_PER_MINUTE * 60;
const SECONDS_PER_DAY: i64 = SECONDS_PER_HOUR * 24;
const LEAP_DAYS_PER_400YEARS: i32 = 100 - 4 + 1;
const DAYS_PER_COMMON_YEAR: i32 = 365;
const DAYS_PER_400YEARS: i32 = DAYS_PER_COMMON_YEAR * 400 + LEAP_DAYS_PER_400YEARS;