| 1 | // This is a part of Chrono. |
| 2 | // See README.md and LICENSE.txt for details. |
| 3 | |
| 4 | //! The time zone which has a fixed offset from UTC. |
| 5 | |
| 6 | use core::fmt; |
| 7 | use core::str::FromStr; |
| 8 | |
| 9 | #[cfg (any(feature = "rkyv" , feature = "rkyv-16" , feature = "rkyv-32" , feature = "rkyv-64" ))] |
| 10 | use rkyv::{Archive, Deserialize, Serialize}; |
| 11 | |
| 12 | use super::{MappedLocalTime, Offset, TimeZone}; |
| 13 | use crate::format::{OUT_OF_RANGE, ParseError, scan}; |
| 14 | use crate::naive::{NaiveDate, NaiveDateTime}; |
| 15 | |
| 16 | /// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59. |
| 17 | /// |
| 18 | /// Using the [`TimeZone`](./trait.TimeZone.html) methods |
| 19 | /// on a `FixedOffset` struct is the preferred way to construct |
| 20 | /// `DateTime<FixedOffset>` instances. See the [`east_opt`](#method.east_opt) and |
| 21 | /// [`west_opt`](#method.west_opt) methods for examples. |
| 22 | #[derive (PartialEq, Eq, Hash, Copy, Clone)] |
| 23 | #[cfg_attr ( |
| 24 | any(feature = "rkyv" , feature = "rkyv-16" , feature = "rkyv-32" , feature = "rkyv-64" ), |
| 25 | derive(Archive, Deserialize, Serialize), |
| 26 | archive(compare(PartialEq)), |
| 27 | archive_attr(derive(Clone, Copy, PartialEq, Eq, Hash, Debug)) |
| 28 | )] |
| 29 | #[cfg_attr (feature = "rkyv-validation" , archive(check_bytes))] |
| 30 | pub struct FixedOffset { |
| 31 | local_minus_utc: i32, |
| 32 | } |
| 33 | |
| 34 | impl FixedOffset { |
| 35 | /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. |
| 36 | /// The negative `secs` means the Western Hemisphere. |
| 37 | /// |
| 38 | /// Panics on the out-of-bound `secs`. |
| 39 | #[deprecated (since = "0.4.23" , note = "use `east_opt()` instead" )] |
| 40 | #[must_use ] |
| 41 | pub fn east(secs: i32) -> FixedOffset { |
| 42 | FixedOffset::east_opt(secs).expect("FixedOffset::east out of bounds" ) |
| 43 | } |
| 44 | |
| 45 | /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. |
| 46 | /// The negative `secs` means the Western Hemisphere. |
| 47 | /// |
| 48 | /// Returns `None` on the out-of-bound `secs`. |
| 49 | /// |
| 50 | /// # Example |
| 51 | /// |
| 52 | /// ``` |
| 53 | /// # #[cfg (feature = "alloc" )] { |
| 54 | /// use chrono::{FixedOffset, TimeZone}; |
| 55 | /// let hour = 3600; |
| 56 | /// let datetime = |
| 57 | /// FixedOffset::east_opt(5 * hour).unwrap().with_ymd_and_hms(2016, 11, 08, 0, 0, 0).unwrap(); |
| 58 | /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00+05:00" ) |
| 59 | /// # } |
| 60 | /// ``` |
| 61 | #[must_use ] |
| 62 | pub const fn east_opt(secs: i32) -> Option<FixedOffset> { |
| 63 | if -86_400 < secs && secs < 86_400 { |
| 64 | Some(FixedOffset { local_minus_utc: secs }) |
| 65 | } else { |
| 66 | None |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. |
| 71 | /// The negative `secs` means the Eastern Hemisphere. |
| 72 | /// |
| 73 | /// Panics on the out-of-bound `secs`. |
| 74 | #[deprecated (since = "0.4.23" , note = "use `west_opt()` instead" )] |
| 75 | #[must_use ] |
| 76 | pub fn west(secs: i32) -> FixedOffset { |
| 77 | FixedOffset::west_opt(secs).expect("FixedOffset::west out of bounds" ) |
| 78 | } |
| 79 | |
| 80 | /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. |
| 81 | /// The negative `secs` means the Eastern Hemisphere. |
| 82 | /// |
| 83 | /// Returns `None` on the out-of-bound `secs`. |
| 84 | /// |
| 85 | /// # Example |
| 86 | /// |
| 87 | /// ``` |
| 88 | /// # #[cfg (feature = "alloc" )] { |
| 89 | /// use chrono::{FixedOffset, TimeZone}; |
| 90 | /// let hour = 3600; |
| 91 | /// let datetime = |
| 92 | /// FixedOffset::west_opt(5 * hour).unwrap().with_ymd_and_hms(2016, 11, 08, 0, 0, 0).unwrap(); |
| 93 | /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00-05:00" ) |
| 94 | /// # } |
| 95 | /// ``` |
| 96 | #[must_use ] |
| 97 | pub const fn west_opt(secs: i32) -> Option<FixedOffset> { |
| 98 | if -86_400 < secs && secs < 86_400 { |
| 99 | Some(FixedOffset { local_minus_utc: -secs }) |
| 100 | } else { |
| 101 | None |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | /// Returns the number of seconds to add to convert from UTC to the local time. |
| 106 | #[inline ] |
| 107 | pub const fn local_minus_utc(&self) -> i32 { |
| 108 | self.local_minus_utc |
| 109 | } |
| 110 | |
| 111 | /// Returns the number of seconds to add to convert from the local time to UTC. |
| 112 | #[inline ] |
| 113 | pub const fn utc_minus_local(&self) -> i32 { |
| 114 | -self.local_minus_utc |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | /// Parsing a `str` into a `FixedOffset` uses the format [`%z`](crate::format::strftime). |
| 119 | impl FromStr for FixedOffset { |
| 120 | type Err = ParseError; |
| 121 | fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 122 | let (_, offset: i32) = scan::timezone_offset(s, consume_colon:scan::colon_or_space, allow_zulu:false, allow_missing_minutes:false, allow_tz_minus_sign:true)?; |
| 123 | Self::east_opt(offset).ok_or(OUT_OF_RANGE) |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | impl TimeZone for FixedOffset { |
| 128 | type Offset = FixedOffset; |
| 129 | |
| 130 | fn from_offset(offset: &FixedOffset) -> FixedOffset { |
| 131 | *offset |
| 132 | } |
| 133 | |
| 134 | fn offset_from_local_date(&self, _local: &NaiveDate) -> MappedLocalTime<FixedOffset> { |
| 135 | MappedLocalTime::Single(*self) |
| 136 | } |
| 137 | fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> MappedLocalTime<FixedOffset> { |
| 138 | MappedLocalTime::Single(*self) |
| 139 | } |
| 140 | |
| 141 | fn offset_from_utc_date(&self, _utc: &NaiveDate) -> FixedOffset { |
| 142 | *self |
| 143 | } |
| 144 | fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> FixedOffset { |
| 145 | *self |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | impl Offset for FixedOffset { |
| 150 | fn fix(&self) -> FixedOffset { |
| 151 | *self |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | impl fmt::Debug for FixedOffset { |
| 156 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 157 | let offset: i32 = self.local_minus_utc; |
| 158 | let (sign: char, offset: i32) = if offset < 0 { ('-' , -offset) } else { ('+' , offset) }; |
| 159 | let sec: i32 = offset.rem_euclid(60); |
| 160 | let mins: i32 = offset.div_euclid(60); |
| 161 | let min: i32 = mins.rem_euclid(60); |
| 162 | let hour: i32 = mins.div_euclid(60); |
| 163 | if sec == 0 { |
| 164 | write!(f, " {}{:02}: {:02}" , sign, hour, min) |
| 165 | } else { |
| 166 | write!(f, " {}{:02}: {:02}: {:02}" , sign, hour, min, sec) |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | impl fmt::Display for FixedOffset { |
| 172 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 173 | fmt::Debug::fmt(self, f) |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | #[cfg (all(feature = "arbitrary" , feature = "std" ))] |
| 178 | impl arbitrary::Arbitrary<'_> for FixedOffset { |
| 179 | fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<FixedOffset> { |
| 180 | let secs = u.int_in_range(-86_399..=86_399)?; |
| 181 | let fixed_offset = FixedOffset::east_opt(secs) |
| 182 | .expect("Could not generate a valid chrono::FixedOffset. It looks like implementation of Arbitrary for FixedOffset is erroneous." ); |
| 183 | Ok(fixed_offset) |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | #[cfg (test)] |
| 188 | mod tests { |
| 189 | use super::FixedOffset; |
| 190 | use crate::offset::TimeZone; |
| 191 | use std::str::FromStr; |
| 192 | |
| 193 | #[test ] |
| 194 | fn test_date_extreme_offset() { |
| 195 | // starting from 0.3 we don't have an offset exceeding one day. |
| 196 | // this makes everything easier! |
| 197 | let offset = FixedOffset::east_opt(86399).unwrap(); |
| 198 | assert_eq!( |
| 199 | format!("{:?}" , offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()), |
| 200 | "2012-02-29T05:06:07+23:59:59" |
| 201 | ); |
| 202 | let offset = FixedOffset::east_opt(-86399).unwrap(); |
| 203 | assert_eq!( |
| 204 | format!("{:?}" , offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()), |
| 205 | "2012-02-29T05:06:07-23:59:59" |
| 206 | ); |
| 207 | let offset = FixedOffset::west_opt(86399).unwrap(); |
| 208 | assert_eq!( |
| 209 | format!("{:?}" , offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()), |
| 210 | "2012-03-04T05:06:07-23:59:59" |
| 211 | ); |
| 212 | let offset = FixedOffset::west_opt(-86399).unwrap(); |
| 213 | assert_eq!( |
| 214 | format!("{:?}" , offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()), |
| 215 | "2012-03-04T05:06:07+23:59:59" |
| 216 | ); |
| 217 | } |
| 218 | |
| 219 | #[test ] |
| 220 | fn test_parse_offset() { |
| 221 | let offset = FixedOffset::from_str("-0500" ).unwrap(); |
| 222 | assert_eq!(offset.local_minus_utc, -5 * 3600); |
| 223 | let offset = FixedOffset::from_str("-08:00" ).unwrap(); |
| 224 | assert_eq!(offset.local_minus_utc, -8 * 3600); |
| 225 | let offset = FixedOffset::from_str("+06:30" ).unwrap(); |
| 226 | assert_eq!(offset.local_minus_utc, (6 * 3600) + 1800); |
| 227 | } |
| 228 | |
| 229 | #[test ] |
| 230 | #[cfg (feature = "rkyv-validation" )] |
| 231 | fn test_rkyv_validation() { |
| 232 | let offset = FixedOffset::from_str("-0500" ).unwrap(); |
| 233 | let bytes = rkyv::to_bytes::<_, 4>(&offset).unwrap(); |
| 234 | assert_eq!(rkyv::from_bytes::<FixedOffset>(&bytes).unwrap(), offset); |
| 235 | } |
| 236 | } |
| 237 | |