| 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 |  | 
|---|