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::{LocalResult, Offset, TimeZone}; 13 use crate::format::{scan, ParseError, OUT_OF_RANGE}; 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] east(secs: i32) -> FixedOffset41 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 #[cfg_attr(not(feature = "std"), doc = "```ignore")] 53 #[cfg_attr(feature = "std", doc = "```")] 54 /// use chrono::{FixedOffset, TimeZone}; 55 /// let hour = 3600; 56 /// let datetime = FixedOffset::east_opt(5 * hour) 57 /// .unwrap() 58 /// .with_ymd_and_hms(2016, 11, 08, 0, 0, 0) 59 /// .unwrap(); 60 /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00+05:00") 61 /// ``` 62 #[must_use] east_opt(secs: i32) -> Option<FixedOffset>63 pub const fn east_opt(secs: i32) -> Option<FixedOffset> { 64 if -86_400 < secs && secs < 86_400 { 65 Some(FixedOffset { local_minus_utc: secs }) 66 } else { 67 None 68 } 69 } 70 71 /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. 72 /// The negative `secs` means the Eastern Hemisphere. 73 /// 74 /// Panics on the out-of-bound `secs`. 75 #[deprecated(since = "0.4.23", note = "use `west_opt()` instead")] 76 #[must_use] west(secs: i32) -> FixedOffset77 pub fn west(secs: i32) -> FixedOffset { 78 FixedOffset::west_opt(secs).expect("FixedOffset::west out of bounds") 79 } 80 81 /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. 82 /// The negative `secs` means the Eastern Hemisphere. 83 /// 84 /// Returns `None` on the out-of-bound `secs`. 85 /// 86 /// # Example 87 /// 88 #[cfg_attr(not(feature = "std"), doc = "```ignore")] 89 #[cfg_attr(feature = "std", doc = "```")] 90 /// use chrono::{FixedOffset, TimeZone}; 91 /// let hour = 3600; 92 /// let datetime = FixedOffset::west_opt(5 * hour) 93 /// .unwrap() 94 /// .with_ymd_and_hms(2016, 11, 08, 0, 0, 0) 95 /// .unwrap(); 96 /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00-05:00") 97 /// ``` 98 #[must_use] west_opt(secs: i32) -> Option<FixedOffset>99 pub const fn west_opt(secs: i32) -> Option<FixedOffset> { 100 if -86_400 < secs && secs < 86_400 { 101 Some(FixedOffset { local_minus_utc: -secs }) 102 } else { 103 None 104 } 105 } 106 107 /// Returns the number of seconds to add to convert from UTC to the local time. 108 #[inline] local_minus_utc(&self) -> i32109 pub const fn local_minus_utc(&self) -> i32 { 110 self.local_minus_utc 111 } 112 113 /// Returns the number of seconds to add to convert from the local time to UTC. 114 #[inline] utc_minus_local(&self) -> i32115 pub const fn utc_minus_local(&self) -> i32 { 116 -self.local_minus_utc 117 } 118 } 119 120 /// Parsing a `str` into a `FixedOffset` uses the format [`%z`](crate::format::strftime). 121 impl FromStr for FixedOffset { 122 type Err = ParseError; from_str(s: &str) -> Result<Self, Self::Err>123 fn from_str(s: &str) -> Result<Self, Self::Err> { 124 let (_, offset) = scan::timezone_offset(s, scan::colon_or_space, false, false, true)?; 125 Self::east_opt(offset).ok_or(OUT_OF_RANGE) 126 } 127 } 128 129 impl TimeZone for FixedOffset { 130 type Offset = FixedOffset; 131 from_offset(offset: &FixedOffset) -> FixedOffset132 fn from_offset(offset: &FixedOffset) -> FixedOffset { 133 *offset 134 } 135 offset_from_local_date(&self, _local: &NaiveDate) -> LocalResult<FixedOffset>136 fn offset_from_local_date(&self, _local: &NaiveDate) -> LocalResult<FixedOffset> { 137 LocalResult::Single(*self) 138 } offset_from_local_datetime(&self, _local: &NaiveDateTime) -> LocalResult<FixedOffset>139 fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> LocalResult<FixedOffset> { 140 LocalResult::Single(*self) 141 } 142 offset_from_utc_date(&self, _utc: &NaiveDate) -> FixedOffset143 fn offset_from_utc_date(&self, _utc: &NaiveDate) -> FixedOffset { 144 *self 145 } offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> FixedOffset146 fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> FixedOffset { 147 *self 148 } 149 } 150 151 impl Offset for FixedOffset { fix(&self) -> FixedOffset152 fn fix(&self) -> FixedOffset { 153 *self 154 } 155 } 156 157 impl fmt::Debug for FixedOffset { fmt(&self, f: &mut fmt::Formatter) -> fmt::Result158 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 159 let offset = self.local_minus_utc; 160 let (sign, offset) = if offset < 0 { ('-', -offset) } else { ('+', offset) }; 161 let sec = offset.rem_euclid(60); 162 let mins = offset.div_euclid(60); 163 let min = mins.rem_euclid(60); 164 let hour = mins.div_euclid(60); 165 if sec == 0 { 166 write!(f, "{}{:02}:{:02}", sign, hour, min) 167 } else { 168 write!(f, "{}{:02}:{:02}:{:02}", sign, hour, min, sec) 169 } 170 } 171 } 172 173 impl fmt::Display for FixedOffset { fmt(&self, f: &mut fmt::Formatter) -> fmt::Result174 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 175 fmt::Debug::fmt(self, f) 176 } 177 } 178 179 #[cfg(all(feature = "arbitrary", feature = "std"))] 180 impl arbitrary::Arbitrary<'_> for FixedOffset { arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<FixedOffset>181 fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<FixedOffset> { 182 let secs = u.int_in_range(-86_399..=86_399)?; 183 let fixed_offset = FixedOffset::east_opt(secs) 184 .expect("Could not generate a valid chrono::FixedOffset. It looks like implementation of Arbitrary for FixedOffset is erroneous."); 185 Ok(fixed_offset) 186 } 187 } 188 189 #[cfg(test)] 190 mod tests { 191 use super::FixedOffset; 192 use crate::offset::TimeZone; 193 use std::str::FromStr; 194 195 #[test] test_date_extreme_offset()196 fn test_date_extreme_offset() { 197 // starting from 0.3 we don't have an offset exceeding one day. 198 // this makes everything easier! 199 let offset = FixedOffset::east_opt(86399).unwrap(); 200 assert_eq!( 201 format!("{:?}", offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()), 202 "2012-02-29T05:06:07+23:59:59" 203 ); 204 let offset = FixedOffset::east_opt(-86399).unwrap(); 205 assert_eq!( 206 format!("{:?}", offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()), 207 "2012-02-29T05:06:07-23:59:59" 208 ); 209 let offset = FixedOffset::west_opt(86399).unwrap(); 210 assert_eq!( 211 format!("{:?}", offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()), 212 "2012-03-04T05:06:07-23:59:59" 213 ); 214 let offset = FixedOffset::west_opt(-86399).unwrap(); 215 assert_eq!( 216 format!("{:?}", offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()), 217 "2012-03-04T05:06:07+23:59:59" 218 ); 219 } 220 221 #[test] test_parse_offset()222 fn test_parse_offset() { 223 let offset = FixedOffset::from_str("-0500").unwrap(); 224 assert_eq!(offset.local_minus_utc, -5 * 3600); 225 let offset = FixedOffset::from_str("-08:00").unwrap(); 226 assert_eq!(offset.local_minus_utc, -8 * 3600); 227 let offset = FixedOffset::from_str("+06:30").unwrap(); 228 assert_eq!(offset.local_minus_utc, (6 * 3600) + 1800); 229 } 230 231 #[test] 232 #[cfg(feature = "rkyv-validation")] test_rkyv_validation()233 fn test_rkyv_validation() { 234 let offset = FixedOffset::from_str("-0500").unwrap(); 235 let bytes = rkyv::to_bytes::<_, 4>(&offset).unwrap(); 236 assert_eq!(rkyv::from_bytes::<FixedOffset>(&bytes).unwrap(), offset); 237 } 238 } 239