chrono_tz/
timezone_impl.rs

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