1use core::fmt;
2use super::{NaiveDateTime, DateTime, UnixTimestamp, Month, DayOfTheWeek};
3use num::{div_floor, positive_rem};
4
5pub trait TimeZone {
6    fn from_timestamp(&self, t: UnixTimestamp) -> NaiveDateTime;
7    fn to_timestamp(&self, d: &NaiveDateTime) -> Result<UnixTimestamp, LocalTimeConversionError>;
8}
9
10#[derive(Eq, PartialEq)]
22pub struct LocalTimeConversionError {
23    _private: (),
25}
26
27impl fmt::Debug for LocalTimeConversionError {
28    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
29        write!(formatter, "LocalTimeConversionError")
30    }
31}
32
33pub trait UnambiguousTimeZone: TimeZone {
39    fn to_unambiguous_timestamp(&self, d: &NaiveDateTime) -> UnixTimestamp {
40        self.to_timestamp(d).unwrap()
41    }
42}
43
44#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)]
46pub struct Utc;
47
48impl UnambiguousTimeZone for Utc {}
49
50impl TimeZone for Utc {
51    fn from_timestamp(&self, u: UnixTimestamp) -> NaiveDateTime {
52        let days_since_unix = div_floor(u.0, SECONDS_PER_DAY) as i32;
53        let days = days_since_unix + days_since_d0(1970);
54        let year = div_floor(days * 400, DAYS_PER_400YEARS) as i32;
55        let day_of_the_year = days - days_since_d0(year);
56        let (month, day) = Month::from_day_of_the_year(day_of_the_year, year.into());
57        let hour = positive_rem(div_floor(u.0, SECONDS_PER_HOUR), 24) as u8;
58        let minute = positive_rem(div_floor(u.0, SECONDS_PER_MINUTE), 60) as u8;
59        let second = positive_rem(u.0, 60) as u8;
60        NaiveDateTime::new(year, month, day, hour, minute, second)
61    }
62
63    fn to_timestamp(&self, d: &NaiveDateTime) -> Result<UnixTimestamp, LocalTimeConversionError> {
64        Ok(UnixTimestamp(
65            i64::from(days_since_unix(d)) * SECONDS_PER_DAY
66            + i64::from(d.hour) * SECONDS_PER_HOUR
67            + i64::from(d.minute) * SECONDS_PER_MINUTE
68            + i64::from(d.second)
69        ))
70    }
71}
72
73#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)]
82pub struct FixedOffsetFromUtc {
83    seconds_ahead_of_utc: i32,
84}
85
86impl FixedOffsetFromUtc {
87    pub fn from_hours_and_minutes(hours: i32, minutes: i32) -> Self {
88        FixedOffsetFromUtc {
89            seconds_ahead_of_utc: (hours * 60 + minutes) * 60,
90        }
91    }
92}
93
94impl UnambiguousTimeZone for FixedOffsetFromUtc {}
95
96impl TimeZone for FixedOffsetFromUtc {
97    fn from_timestamp(&self, u: UnixTimestamp) -> NaiveDateTime {
98        let seconds = u.0 + i64::from(self.seconds_ahead_of_utc);
105
106        Utc.from_timestamp(UnixTimestamp(seconds))
109    }
110
111    fn to_timestamp(&self, d: &NaiveDateTime) -> Result<UnixTimestamp, LocalTimeConversionError> {
112        let seconds = Utc.to_unambiguous_timestamp(d).0;
114
115        Ok(UnixTimestamp(seconds - i64::from(self.seconds_ahead_of_utc)))
118    }
119}
120
121pub trait DaylightSaving {
122    fn offset_outside_dst(&self) -> FixedOffsetFromUtc;
123    fn offset_during_dst(&self) -> FixedOffsetFromUtc;
124    fn is_in_dst(&self, t: UnixTimestamp) -> bool;
125}
126
127impl<Tz: DaylightSaving> TimeZone for Tz {
128    fn from_timestamp(&self, u: UnixTimestamp) -> NaiveDateTime {
129        let offset = if self.is_in_dst(u) {
130            self.offset_during_dst()
131        } else {
132            self.offset_outside_dst()
133        };
134        offset.from_timestamp(u)
135    }
136
137    fn to_timestamp(&self, d: &NaiveDateTime) -> Result<UnixTimestamp, LocalTimeConversionError> {
138        let assuming_outside = self.offset_outside_dst().to_unambiguous_timestamp(d);
140        let assuming_during = self.offset_during_dst().to_unambiguous_timestamp(d);
141
142        match (self.is_in_dst(assuming_outside), self.is_in_dst(assuming_during)) {
162            (true, true) => Ok(assuming_during),
163            (false, false) => Ok(assuming_outside),
164            _ => Err(LocalTimeConversionError { _private: () }),
165        }
166    }
167}
168
169#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)]
171pub struct CentralEurope;
172
173impl DaylightSaving for CentralEurope {
174    fn offset_outside_dst(&self) -> FixedOffsetFromUtc {
175        FixedOffsetFromUtc::from_hours_and_minutes(1, 0)
176    }
177
178    fn offset_during_dst(&self) -> FixedOffsetFromUtc {
179        FixedOffsetFromUtc::from_hours_and_minutes(2, 0)
180    }
181
182    fn is_in_dst(&self, t: UnixTimestamp) -> bool {
183        use Month::*;
184
185        let d = DateTime::from_timestamp(t, Utc);
186
187        if d.month() < March || d.month() > October {
207            false
208        } else if d.month() > March && d.month() < October {
209            true
210        } else if d.month() == March {
211            !before_last_sunday_1_am(&d)
212        } else if d.month() == October {
213            before_last_sunday_1_am(&d)
214        } else {
215            unreachable!()
216        }
217    }
218}
219
220fn before_last_sunday_1_am(d: &DateTime<Utc>) -> bool {
221    let last_sunday = last_of_the_month(d, DayOfTheWeek::Sunday);
222    d.day() < last_sunday || (
223        d.day() == last_sunday &&
224        (d.hour(), d.minute(), d.second()) < (1, 0, 0)
225    )
226}
227
228fn last_of_the_month(d: &DateTime<Utc>, requested_dow: DayOfTheWeek) -> u8 {
229    let last_day = d.month().length(d.year().into());
230    let last_dow = NaiveDateTime::new(d.year(), d.month(), last_day, 0, 0, 0).day_of_the_week();
231    let difference = i32::from(last_dow.to_iso_number()) - i32::from(requested_dow.to_iso_number());
232    last_day - (positive_rem(difference, 7) as u8)
233}
234
235pub fn days_since_unix(d: &NaiveDateTime) -> i32 {
236    (d.year - 1970) * DAYS_PER_COMMON_YEAR
237    + leap_days_since_y0(d.year) - leap_days_since_y0(1970)
238    + d.month.days_since_january_1st(d.year.into())
239    + i32::from(d.day - 1)
240}
241
242pub fn leap_days_since_y0(year: i32) -> i32 {
245    if year > 0 {
246        let year = year - 1;  ((year / 4) - (year / 100) + (year / 400)) + 1
249    } else {
250        let year = -year;
251        -((year / 4) - (year / 100) + (year / 400))
252    }
253}
254
255fn days_since_d0(year: i32) -> i32 {
257    year * DAYS_PER_COMMON_YEAR + leap_days_since_y0(year)
258}
259
260const SECONDS_PER_MINUTE: i64 = 60;
261const SECONDS_PER_HOUR: i64 = SECONDS_PER_MINUTE * 60;
262const SECONDS_PER_DAY: i64 = SECONDS_PER_HOUR * 24;
263
264const LEAP_DAYS_PER_400YEARS: i32 = 100 - 4 + 1;
271
272const DAYS_PER_COMMON_YEAR: i32 = 365;
273const DAYS_PER_400YEARS: i32 = DAYS_PER_COMMON_YEAR * 400 + LEAP_DAYS_PER_400YEARS;