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