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}