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