chrono_tz/
timezone_impl.rs

1use super::timezones::Tz;
2use binary_search::binary_search;
3use chrono::{Duration, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, Offset, TimeZone};
4use core::cmp::Ordering;
5use core::fmt::{Debug, Display, Error, Formatter};
6
7/// An Offset that applies for a period of time
8///
9/// For example, [`::US::Eastern`] is composed of at least two
10/// `FixedTimespan`s: `EST` and `EDT`, that are variously in effect.
11#[derive(Copy, Clone, PartialEq, Eq)]
12pub struct FixedTimespan {
13    /// The base offset from UTC; this usually doesn't change unless the government changes something
14    pub utc_offset: i32,
15    /// The additional offset from UTC for this timespan; typically for daylight saving time
16    pub dst_offset: i32,
17    /// The name of this timezone, for example the difference between `EDT`/`EST`
18    pub name: &'static str,
19}
20
21impl Offset for FixedTimespan {
22    fn fix(&self) -> FixedOffset {
23        FixedOffset::east(self.utc_offset + self.dst_offset)
24    }
25}
26
27impl Display for FixedTimespan {
28    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
29        write!(f, "{}", self.name)
30    }
31}
32
33impl Debug for FixedTimespan {
34    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
35        write!(f, "{}", self.name)
36    }
37}
38
39#[derive(Copy, Clone, PartialEq, Eq)]
40pub struct TzOffset {
41    tz: Tz,
42    offset: FixedTimespan,
43}
44
45/// Detailed timezone offset components that expose any special conditions currently in effect.
46///
47/// This trait breaks down an offset into the standard UTC offset and any special offset
48/// in effect (such as DST) at a given time.
49///
50/// ```
51/// # extern crate chrono;
52/// # extern crate chrono_tz;
53/// use chrono::{Duration, Offset, TimeZone};
54/// use chrono_tz::Europe::London;
55/// use chrono_tz::OffsetComponents;
56///
57/// # fn main() {
58/// let london_time = London.ymd(2016, 5, 10).and_hms(12, 0, 0);
59///
60/// // London typically has zero offset from UTC, but has a 1h adjustment forward
61/// // when summer time is in effect.
62/// let lon_utc_offset = london_time.offset().base_utc_offset();
63/// let lon_dst_offset = london_time.offset().dst_offset();
64/// let total_offset = lon_utc_offset + lon_dst_offset;
65/// assert_eq!(lon_utc_offset, Duration::hours(0));
66/// assert_eq!(lon_dst_offset, Duration::hours(1));
67///
68/// // As a sanity check, make sure that the total offsets added together are equivalent to the
69/// // total fixed offset.
70/// assert_eq!(total_offset.num_seconds(), london_time.offset().fix().local_minus_utc() as i64);
71/// # }
72/// ```
73pub trait OffsetComponents {
74    /// The base offset from UTC; this usually doesn't change unless the government changes something
75    fn base_utc_offset(&self) -> Duration;
76    /// The additional offset from UTC that is currently in effect; typically for daylight saving time
77    fn dst_offset(&self) -> Duration;
78}
79
80/// Timezone offset name information.
81///
82/// This trait exposes display names that describe an offset in
83/// various situations.
84///
85/// ```
86/// # extern crate chrono;
87/// # extern crate chrono_tz;
88/// use chrono::{Duration, Offset, TimeZone};
89/// use chrono_tz::Europe::London;
90/// use chrono_tz::OffsetName;
91///
92/// # fn main() {
93/// let london_time = London.ymd(2016, 2, 10).and_hms(12, 0, 0);
94/// assert_eq!(london_time.offset().tz_id(), "Europe/London");
95/// // London is normally on GMT
96/// assert_eq!(london_time.offset().abbreviation(), "GMT");
97///
98/// let london_summer_time = London.ymd(2016, 5, 10).and_hms(12, 0, 0);
99/// // The TZ ID remains constant year round
100/// assert_eq!(london_summer_time.offset().tz_id(), "Europe/London");
101/// // During the summer, this becomes British Summer Time
102/// assert_eq!(london_summer_time.offset().abbreviation(), "BST");
103/// # }
104/// ```
105pub trait OffsetName {
106    /// The IANA TZDB identifier (ex: America/New_York)
107    fn tz_id(&self) -> &str;
108    /// The abbreviation to use in a longer timestamp (ex: EST)
109    ///
110    /// This takes into account any special offsets that may be in effect.
111    /// For example, at a given instant, the time zone with ID *America/New_York*
112    /// may be either *EST* or *EDT*.
113    fn abbreviation(&self) -> &str;
114}
115
116impl TzOffset {
117    fn new(tz: Tz, offset: FixedTimespan) -> Self {
118        TzOffset { tz, offset }
119    }
120
121    fn map_localresult(tz: Tz, result: LocalResult<FixedTimespan>) -> LocalResult<Self> {
122        match result {
123            LocalResult::None => LocalResult::None,
124            LocalResult::Single(s) => LocalResult::Single(TzOffset::new(tz, s)),
125            LocalResult::Ambiguous(a, b) => {
126                LocalResult::Ambiguous(TzOffset::new(tz, a), TzOffset::new(tz, b))
127            }
128        }
129    }
130}
131
132impl OffsetComponents for TzOffset {
133    fn base_utc_offset(&self) -> Duration {
134        Duration::seconds(self.offset.utc_offset as i64)
135    }
136
137    fn dst_offset(&self) -> Duration {
138        Duration::seconds(self.offset.dst_offset as i64)
139    }
140}
141
142impl OffsetName for TzOffset {
143    fn tz_id(&self) -> &str {
144        self.tz.name()
145    }
146
147    fn abbreviation(&self) -> &str {
148        self.offset.name
149    }
150}
151
152impl Offset for TzOffset {
153    fn fix(&self) -> FixedOffset {
154        self.offset.fix()
155    }
156}
157
158impl Display for TzOffset {
159    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
160        Display::fmt(&self.offset, f)
161    }
162}
163
164impl Debug for TzOffset {
165    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
166        Debug::fmt(&self.offset, f)
167    }
168}
169
170/// Represents the span of time that a given rule is valid for.
171/// Note that I have made the assumption that all ranges are
172/// left-inclusive and right-exclusive - that is to say,
173/// if the clocks go forward by 1 hour at 1am, the time 1am
174/// does not exist in local time (the clock goes from 00:59:59
175/// to 02:00:00). Likewise, if the clocks go back by one hour
176/// at 2am, the clock goes from 01:59:59 to 01:00:00. This is
177/// an arbitrary choice, and I could not find a source to
178/// confirm whether or not this is correct.
179struct Span {
180    begin: Option<i64>,
181    end: Option<i64>,
182}
183
184impl Span {
185    fn contains(&self, x: i64) -> bool {
186        match (self.begin, self.end) {
187            (Some(a), Some(b)) if a <= x && x < b => true,
188            (Some(a), None) if a <= x => true,
189            (None, Some(b)) if b > x => true,
190            (None, None) => true,
191            _ => false,
192        }
193    }
194
195    fn cmp(&self, x: i64) -> Ordering {
196        match (self.begin, self.end) {
197            (Some(a), Some(b)) if a <= x && x < b => Ordering::Equal,
198            (Some(a), Some(b)) if a <= x && b <= x => Ordering::Less,
199            (Some(_), Some(_)) => Ordering::Greater,
200            (Some(a), None) if a <= x => Ordering::Equal,
201            (Some(_), None) => Ordering::Greater,
202            (None, Some(b)) if b <= x => Ordering::Less,
203            (None, Some(_)) => Ordering::Equal,
204            (None, None) => Ordering::Equal,
205        }
206    }
207}
208
209#[derive(Copy, Clone)]
210pub struct FixedTimespanSet {
211    pub first: FixedTimespan,
212    pub rest: &'static [(i64, FixedTimespan)],
213}
214
215impl FixedTimespanSet {
216    fn len(&self) -> usize {
217        1 + self.rest.len()
218    }
219
220    fn utc_span(&self, index: usize) -> Span {
221        debug_assert!(index < self.len());
222        Span {
223            begin: if index == 0 { None } else { Some(self.rest[index - 1].0) },
224            end: if index == self.rest.len() { None } else { Some(self.rest[index].0) },
225        }
226    }
227
228    fn local_span(&self, index: usize) -> Span {
229        debug_assert!(index < self.len());
230        Span {
231            begin: if index == 0 {
232                None
233            } else {
234                let span = self.rest[index - 1];
235                Some(span.0 + span.1.utc_offset as i64 + span.1.dst_offset as i64)
236            },
237            end: if index == self.rest.len() {
238                None
239            } else if index == 0 {
240                Some(
241                    self.rest[index].0
242                        + self.first.utc_offset as i64
243                        + self.first.dst_offset as i64,
244                )
245            } else {
246                Some(
247                    self.rest[index].0
248                        + self.rest[index - 1].1.utc_offset as i64
249                        + self.rest[index - 1].1.dst_offset as i64,
250                )
251            },
252        }
253    }
254
255    fn get(&self, index: usize) -> FixedTimespan {
256        debug_assert!(index < self.len());
257        if index == 0 {
258            self.first
259        } else {
260            self.rest[index - 1].1
261        }
262    }
263}
264
265pub trait TimeSpans {
266    fn timespans(&self) -> FixedTimespanSet;
267}
268
269impl TimeZone for Tz {
270    type Offset = TzOffset;
271
272    fn from_offset(offset: &Self::Offset) -> Self {
273        offset.tz
274    }
275
276    fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<Self::Offset> {
277        let earliest = self.offset_from_local_datetime(&local.and_hms(0, 0, 0));
278        let latest = self.offset_from_local_datetime(&local.and_hms(23, 59, 59));
279        // From the chrono docs:
280        //
281        // > This type should be considered ambiguous at best, due to the inherent lack of
282        // > precision required for the time zone resolution. There are some guarantees on the usage
283        // > of `Date<Tz>`:
284        // > - If properly constructed via `TimeZone::ymd` and others without an error,
285        // >   the corresponding local date should exist for at least a moment.
286        // >   (It may still have a gap from the offset changes.)
287        //
288        // > - The `TimeZone` is free to assign *any* `Offset` to the local date,
289        // >   as long as that offset did occur in given day.
290        // >   For example, if `2015-03-08T01:59-08:00` is followed by `2015-03-08T03:00-07:00`,
291        // >   it may produce either `2015-03-08-08:00` or `2015-03-08-07:00`
292        // >   but *not* `2015-03-08+00:00` and others.
293        //
294        // > - Once constructed as a full `DateTime`,
295        // >   `DateTime::date` and other associated methods should return those for the original `Date`.
296        // >   For example, if `dt = tz.ymd(y,m,d).hms(h,n,s)` were valid, `dt.date() == tz.ymd(y,m,d)`.
297        //
298        // > - The date is timezone-agnostic up to one day (i.e. practically always),
299        // >   so the local date and UTC date should be equal for most cases
300        // >   even though the raw calculation between `NaiveDate` and `Duration` may not.
301        //
302        // For these reasons we return always a single offset here if we can, rather than being
303        // technically correct and returning Ambiguous(_,_) on days when the clock changes. The
304        // alternative is painful errors when computing unambiguous times such as
305        // `TimeZone.ymd(ambiguous_date).hms(unambiguous_time)`.
306        use chrono::LocalResult::*;
307        match (earliest, latest) {
308            (result @ Single(_), _) => result,
309            (_, result @ Single(_)) => result,
310            (Ambiguous(offset, _), _) => Single(offset),
311            (_, Ambiguous(offset, _)) => Single(offset),
312            (None, None) => None,
313        }
314    }
315
316    // First search for a timespan that the local datetime falls into, then, if it exists,
317    // check the two surrounding timespans (if they exist) to see if there is any ambiguity.
318    fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<Self::Offset> {
319        let timestamp = local.timestamp();
320        let timespans = self.timespans();
321        let index = binary_search(0, timespans.len(), |i| timespans.local_span(i).cmp(timestamp));
322        TzOffset::map_localresult(
323            *self,
324            match index {
325                Ok(0) if timespans.len() == 1 => LocalResult::Single(timespans.get(0)),
326                Ok(0) if timespans.local_span(1).contains(timestamp) => {
327                    LocalResult::Ambiguous(timespans.get(0), timespans.get(1))
328                }
329                Ok(0) => LocalResult::Single(timespans.get(0)),
330                Ok(i) if timespans.local_span(i - 1).contains(timestamp) => {
331                    LocalResult::Ambiguous(timespans.get(i - 1), timespans.get(i))
332                }
333                Ok(i) if i == timespans.len() - 1 => LocalResult::Single(timespans.get(i)),
334                Ok(i) if timespans.local_span(i + 1).contains(timestamp) => {
335                    LocalResult::Ambiguous(timespans.get(i), timespans.get(i + 1))
336                }
337                Ok(i) => LocalResult::Single(timespans.get(i)),
338                Err(_) => LocalResult::None,
339            },
340        )
341    }
342
343    fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
344        // See comment above for why it is OK to just take any arbitrary time in the day
345        self.offset_from_utc_datetime(&utc.and_hms(12, 0, 0))
346    }
347
348    // Binary search for the required timespan. Any i64 is guaranteed to fall within
349    // exactly one timespan, no matter what (so the `unwrap` is safe).
350    fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
351        let timestamp = utc.timestamp();
352        let timespans = self.timespans();
353        let index =
354            binary_search(0, timespans.len(), |i| timespans.utc_span(i).cmp(timestamp)).unwrap();
355        TzOffset::new(*self, timespans.get(index))
356    }
357}