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