chrono_tz/
timezone_impl.rs

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