1use core::cmp::Ordering;
2use core::fmt::{Debug, Display, Error, Formatter, Write};
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: Option<&'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 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
69impl 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)]
76pub 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/// ```
109pub 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/// ```
141pub 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
152impl 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
168impl 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
178impl 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
188impl Offset for TzOffset {
189 fn fix(&self) -> FixedOffset {
190 self.offset.fix()
191 }
192}
193
194impl Display for TzOffset {
195 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
196 Display::fmt(&self.offset, f)
197 }
198}
199
200impl 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.
215struct Span {
216 begin: Option<i64>,
217 end: Option<i64>,
218}
219
220impl 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)]
246pub struct FixedTimespanSet {
247 pub first: FixedTimespan,
248 pub rest: &'static [(i64, FixedTimespan)],
249}
250
251impl 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
309pub trait TimeSpans {
310 fn timespans(&self) -> FixedTimespanSet;
311}
312
313impl 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

Provided by KDAB

Privacy Policy
Learn Rust with the experts
Find out more