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::ops::{Add, Sub}; |
8 | |
9 | #[cfg (feature = "rkyv" )] |
10 | use rkyv::{Archive, Deserialize, Serialize}; |
11 | |
12 | use super::{LocalResult, Offset, TimeZone}; |
13 | use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime}; |
14 | use crate::oldtime::Duration as OldDuration; |
15 | use crate::DateTime; |
16 | use crate::Timelike; |
17 | |
18 | /// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59. |
19 | /// |
20 | /// Using the [`TimeZone`](./trait.TimeZone.html) methods |
21 | /// on a `FixedOffset` struct is the preferred way to construct |
22 | /// `DateTime<FixedOffset>` instances. See the [`east_opt`](#method.east_opt) and |
23 | /// [`west_opt`](#method.west_opt) methods for examples. |
24 | #[derive (PartialEq, Eq, Hash, Copy, Clone)] |
25 | #[cfg_attr (feature = "rkyv" , derive(Archive, Deserialize, Serialize))] |
26 | pub struct FixedOffset { |
27 | local_minus_utc: i32, |
28 | } |
29 | |
30 | impl FixedOffset { |
31 | /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. |
32 | /// The negative `secs` means the Western Hemisphere. |
33 | /// |
34 | /// Panics on the out-of-bound `secs`. |
35 | #[deprecated (since = "0.4.23" , note = "use `east_opt()` instead" )] |
36 | #[must_use ] |
37 | pub fn east(secs: i32) -> FixedOffset { |
38 | FixedOffset::east_opt(secs).expect("FixedOffset::east out of bounds" ) |
39 | } |
40 | |
41 | /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. |
42 | /// The negative `secs` means the Western Hemisphere. |
43 | /// |
44 | /// Returns `None` on the out-of-bound `secs`. |
45 | /// |
46 | /// # Example |
47 | /// |
48 | #[cfg_attr (not(feature = "std" ), doc = "```ignore" )] |
49 | #[cfg_attr (feature = "std" , doc = "```" )] |
50 | /// use chrono::{FixedOffset, TimeZone}; |
51 | /// let hour = 3600; |
52 | /// let datetime = FixedOffset::east_opt(5 * hour) |
53 | /// .unwrap() |
54 | /// .with_ymd_and_hms(2016, 11, 08, 0, 0, 0) |
55 | /// .unwrap(); |
56 | /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00+05:00" ) |
57 | /// ``` |
58 | #[must_use ] |
59 | pub const fn east_opt(secs: i32) -> Option<FixedOffset> { |
60 | if -86_400 < secs && secs < 86_400 { |
61 | Some(FixedOffset { local_minus_utc: secs }) |
62 | } else { |
63 | None |
64 | } |
65 | } |
66 | |
67 | /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. |
68 | /// The negative `secs` means the Eastern Hemisphere. |
69 | /// |
70 | /// Panics on the out-of-bound `secs`. |
71 | #[deprecated (since = "0.4.23" , note = "use `west_opt()` instead" )] |
72 | #[must_use ] |
73 | pub fn west(secs: i32) -> FixedOffset { |
74 | FixedOffset::west_opt(secs).expect("FixedOffset::west out of bounds" ) |
75 | } |
76 | |
77 | /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. |
78 | /// The negative `secs` means the Eastern Hemisphere. |
79 | /// |
80 | /// Returns `None` on the out-of-bound `secs`. |
81 | /// |
82 | /// # Example |
83 | /// |
84 | #[cfg_attr (not(feature = "std" ), doc = "```ignore" )] |
85 | #[cfg_attr (feature = "std" , doc = "```" )] |
86 | /// use chrono::{FixedOffset, TimeZone}; |
87 | /// let hour = 3600; |
88 | /// let datetime = FixedOffset::west_opt(5 * hour) |
89 | /// .unwrap() |
90 | /// .with_ymd_and_hms(2016, 11, 08, 0, 0, 0) |
91 | /// .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 | impl TimeZone for FixedOffset { |
117 | type Offset = FixedOffset; |
118 | |
119 | fn from_offset(offset: &FixedOffset) -> FixedOffset { |
120 | *offset |
121 | } |
122 | |
123 | fn offset_from_local_date(&self, _local: &NaiveDate) -> LocalResult<FixedOffset> { |
124 | LocalResult::Single(*self) |
125 | } |
126 | fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> LocalResult<FixedOffset> { |
127 | LocalResult::Single(*self) |
128 | } |
129 | |
130 | fn offset_from_utc_date(&self, _utc: &NaiveDate) -> FixedOffset { |
131 | *self |
132 | } |
133 | fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> FixedOffset { |
134 | *self |
135 | } |
136 | } |
137 | |
138 | impl Offset for FixedOffset { |
139 | fn fix(&self) -> FixedOffset { |
140 | *self |
141 | } |
142 | } |
143 | |
144 | impl fmt::Debug for FixedOffset { |
145 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
146 | let offset: i32 = self.local_minus_utc; |
147 | let (sign: char, offset: i32) = if offset < 0 { ('-' , -offset) } else { ('+' , offset) }; |
148 | let sec: i32 = offset.rem_euclid(60); |
149 | let mins: i32 = offset.div_euclid(60); |
150 | let min: i32 = mins.rem_euclid(60); |
151 | let hour: i32 = mins.div_euclid(60); |
152 | if sec == 0 { |
153 | write!(f, " {}{:02}: {:02}" , sign, hour, min) |
154 | } else { |
155 | write!(f, " {}{:02}: {:02}: {:02}" , sign, hour, min, sec) |
156 | } |
157 | } |
158 | } |
159 | |
160 | impl fmt::Display for FixedOffset { |
161 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
162 | fmt::Debug::fmt(self, f) |
163 | } |
164 | } |
165 | |
166 | #[cfg (feature = "arbitrary" )] |
167 | impl arbitrary::Arbitrary<'_> for FixedOffset { |
168 | fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result<FixedOffset> { |
169 | let secs = u.int_in_range(-86_399..=86_399)?; |
170 | let fixed_offset = FixedOffset::east_opt(secs) |
171 | .expect("Could not generate a valid chrono::FixedOffset. It looks like implementation of Arbitrary for FixedOffset is erroneous." ); |
172 | Ok(fixed_offset) |
173 | } |
174 | } |
175 | |
176 | // addition or subtraction of FixedOffset to/from Timelike values is the same as |
177 | // adding or subtracting the offset's local_minus_utc value |
178 | // but keep keeps the leap second information. |
179 | // this should be implemented more efficiently, but for the time being, this is generic right now. |
180 | |
181 | fn add_with_leapsecond<T>(lhs: &T, rhs: i32) -> T |
182 | where |
183 | T: Timelike + Add<OldDuration, Output = T>, |
184 | { |
185 | // extract and temporarily remove the fractional part and later recover it |
186 | let nanos: u32 = lhs.nanosecond(); |
187 | let lhs: T = lhs.with_nanosecond(nano:0).unwrap(); |
188 | (lhs + OldDuration::seconds(i64::from(rhs))).with_nanosecond(nano:nanos).unwrap() |
189 | } |
190 | |
191 | impl Add<FixedOffset> for NaiveTime { |
192 | type Output = NaiveTime; |
193 | |
194 | #[inline ] |
195 | fn add(self, rhs: FixedOffset) -> NaiveTime { |
196 | add_with_leapsecond(&self, rhs:rhs.local_minus_utc) |
197 | } |
198 | } |
199 | |
200 | impl Sub<FixedOffset> for NaiveTime { |
201 | type Output = NaiveTime; |
202 | |
203 | #[inline ] |
204 | fn sub(self, rhs: FixedOffset) -> NaiveTime { |
205 | add_with_leapsecond(&self, -rhs.local_minus_utc) |
206 | } |
207 | } |
208 | |
209 | impl Add<FixedOffset> for NaiveDateTime { |
210 | type Output = NaiveDateTime; |
211 | |
212 | #[inline ] |
213 | fn add(self, rhs: FixedOffset) -> NaiveDateTime { |
214 | add_with_leapsecond(&self, rhs:rhs.local_minus_utc) |
215 | } |
216 | } |
217 | |
218 | impl Sub<FixedOffset> for NaiveDateTime { |
219 | type Output = NaiveDateTime; |
220 | |
221 | #[inline ] |
222 | fn sub(self, rhs: FixedOffset) -> NaiveDateTime { |
223 | add_with_leapsecond(&self, -rhs.local_minus_utc) |
224 | } |
225 | } |
226 | |
227 | impl<Tz: TimeZone> Add<FixedOffset> for DateTime<Tz> { |
228 | type Output = DateTime<Tz>; |
229 | |
230 | #[inline ] |
231 | fn add(self, rhs: FixedOffset) -> DateTime<Tz> { |
232 | add_with_leapsecond(&self, rhs:rhs.local_minus_utc) |
233 | } |
234 | } |
235 | |
236 | impl<Tz: TimeZone> Sub<FixedOffset> for DateTime<Tz> { |
237 | type Output = DateTime<Tz>; |
238 | |
239 | #[inline ] |
240 | fn sub(self, rhs: FixedOffset) -> DateTime<Tz> { |
241 | add_with_leapsecond(&self, -rhs.local_minus_utc) |
242 | } |
243 | } |
244 | |
245 | #[cfg (test)] |
246 | mod tests { |
247 | use super::FixedOffset; |
248 | use crate::offset::TimeZone; |
249 | |
250 | #[test ] |
251 | fn test_date_extreme_offset() { |
252 | // starting from 0.3 we don't have an offset exceeding one day. |
253 | // this makes everything easier! |
254 | assert_eq!( |
255 | format!( |
256 | " {:?}" , |
257 | FixedOffset::east_opt(86399) |
258 | .unwrap() |
259 | .with_ymd_and_hms(2012, 2, 29, 5, 6, 7) |
260 | .unwrap() |
261 | ), |
262 | "2012-02-29T05:06:07+23:59:59" .to_string() |
263 | ); |
264 | assert_eq!( |
265 | format!( |
266 | " {:?}" , |
267 | FixedOffset::east_opt(86399) |
268 | .unwrap() |
269 | .with_ymd_and_hms(2012, 2, 29, 5, 6, 7) |
270 | .unwrap() |
271 | ), |
272 | "2012-02-29T05:06:07+23:59:59" .to_string() |
273 | ); |
274 | assert_eq!( |
275 | format!( |
276 | " {:?}" , |
277 | FixedOffset::west_opt(86399) |
278 | .unwrap() |
279 | .with_ymd_and_hms(2012, 3, 4, 5, 6, 7) |
280 | .unwrap() |
281 | ), |
282 | "2012-03-04T05:06:07-23:59:59" .to_string() |
283 | ); |
284 | assert_eq!( |
285 | format!( |
286 | " {:?}" , |
287 | FixedOffset::west_opt(86399) |
288 | .unwrap() |
289 | .with_ymd_and_hms(2012, 3, 4, 5, 6, 7) |
290 | .unwrap() |
291 | ), |
292 | "2012-03-04T05:06:07-23:59:59" .to_string() |
293 | ); |
294 | } |
295 | } |
296 | |