chrono/offset/
fixed.rs

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