1 | //! The [`UtcOffset`] struct and its associated `impl`s. |
2 | |
3 | use core::fmt; |
4 | use core::ops::Neg; |
5 | #[cfg (feature = "formatting" )] |
6 | use std::io; |
7 | |
8 | use crate::convert::*; |
9 | use crate::error; |
10 | #[cfg (feature = "formatting" )] |
11 | use crate::formatting::Formattable; |
12 | #[cfg (feature = "parsing" )] |
13 | use crate::parsing::Parsable; |
14 | #[cfg (feature = "local-offset" )] |
15 | use crate::sys::local_offset_at; |
16 | #[cfg (feature = "local-offset" )] |
17 | use crate::OffsetDateTime; |
18 | |
19 | /// An offset from UTC. |
20 | /// |
21 | /// This struct can store values up to ±23:59:59. If you need support outside this range, please |
22 | /// file an issue with your use case. |
23 | // All three components _must_ have the same sign. |
24 | #[derive (Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] |
25 | pub struct UtcOffset { |
26 | #[allow (clippy::missing_docs_in_private_items)] |
27 | hours: i8, |
28 | #[allow (clippy::missing_docs_in_private_items)] |
29 | minutes: i8, |
30 | #[allow (clippy::missing_docs_in_private_items)] |
31 | seconds: i8, |
32 | } |
33 | |
34 | impl UtcOffset { |
35 | /// A `UtcOffset` that is UTC. |
36 | /// |
37 | /// ```rust |
38 | /// # use time::UtcOffset; |
39 | /// # use time_macros::offset; |
40 | /// assert_eq!(UtcOffset::UTC, offset!(UTC)); |
41 | /// ``` |
42 | pub const UTC: Self = Self::__from_hms_unchecked(0, 0, 0); |
43 | |
44 | // region: constructors |
45 | /// Create a `UtcOffset` representing an offset of the hours, minutes, and seconds provided, the |
46 | /// validity of which must be guaranteed by the caller. All three parameters must have the same |
47 | /// sign. |
48 | #[doc (hidden)] |
49 | pub const fn __from_hms_unchecked(hours: i8, minutes: i8, seconds: i8) -> Self { |
50 | if hours < 0 { |
51 | debug_assert!(minutes <= 0); |
52 | debug_assert!(seconds <= 0); |
53 | } else if hours > 0 { |
54 | debug_assert!(minutes >= 0); |
55 | debug_assert!(seconds >= 0); |
56 | } |
57 | if minutes < 0 { |
58 | debug_assert!(seconds <= 0); |
59 | } else if minutes > 0 { |
60 | debug_assert!(seconds >= 0); |
61 | } |
62 | debug_assert!(hours.unsigned_abs() < 24); |
63 | debug_assert!(minutes.unsigned_abs() < Minute.per(Hour)); |
64 | debug_assert!(seconds.unsigned_abs() < Second.per(Minute)); |
65 | |
66 | Self { |
67 | hours, |
68 | minutes, |
69 | seconds, |
70 | } |
71 | } |
72 | |
73 | /// Create a `UtcOffset` representing an offset by the number of hours, minutes, and seconds |
74 | /// provided. |
75 | /// |
76 | /// The sign of all three components should match. If they do not, all smaller components will |
77 | /// have their signs flipped. |
78 | /// |
79 | /// ```rust |
80 | /// # use time::UtcOffset; |
81 | /// assert_eq!(UtcOffset::from_hms(1, 2, 3)?.as_hms(), (1, 2, 3)); |
82 | /// assert_eq!(UtcOffset::from_hms(1, -2, -3)?.as_hms(), (1, 2, 3)); |
83 | /// # Ok::<_, time::Error>(()) |
84 | /// ``` |
85 | pub const fn from_hms( |
86 | hours: i8, |
87 | mut minutes: i8, |
88 | mut seconds: i8, |
89 | ) -> Result<Self, error::ComponentRange> { |
90 | ensure_value_in_range!(hours in -23 => 23); |
91 | ensure_value_in_range!( |
92 | minutes in -(Minute.per(Hour) as i8 - 1) => Minute.per(Hour) as i8 - 1 |
93 | ); |
94 | ensure_value_in_range!( |
95 | seconds in -(Second.per(Minute) as i8 - 1) => Second.per(Minute) as i8 - 1 |
96 | ); |
97 | |
98 | if (hours > 0 && minutes < 0) || (hours < 0 && minutes > 0) { |
99 | minutes *= -1; |
100 | } |
101 | if (hours > 0 && seconds < 0) |
102 | || (hours < 0 && seconds > 0) |
103 | || (minutes > 0 && seconds < 0) |
104 | || (minutes < 0 && seconds > 0) |
105 | { |
106 | seconds *= -1; |
107 | } |
108 | |
109 | Ok(Self::__from_hms_unchecked(hours, minutes, seconds)) |
110 | } |
111 | |
112 | /// Create a `UtcOffset` representing an offset by the number of seconds provided. |
113 | /// |
114 | /// ```rust |
115 | /// # use time::UtcOffset; |
116 | /// assert_eq!(UtcOffset::from_whole_seconds(3_723)?.as_hms(), (1, 2, 3)); |
117 | /// # Ok::<_, time::Error>(()) |
118 | /// ``` |
119 | pub const fn from_whole_seconds(seconds: i32) -> Result<Self, error::ComponentRange> { |
120 | ensure_value_in_range!( |
121 | seconds in -24 * Second.per(Hour) as i32 - 1 => 24 * Second.per(Hour) as i32 - 1 |
122 | ); |
123 | |
124 | Ok(Self::__from_hms_unchecked( |
125 | (seconds / Second.per(Hour) as i32) as _, |
126 | ((seconds % Second.per(Hour) as i32) / Minute.per(Hour) as i32) as _, |
127 | (seconds % Second.per(Minute) as i32) as _, |
128 | )) |
129 | } |
130 | // endregion constructors |
131 | |
132 | // region: getters |
133 | /// Obtain the UTC offset as its hours, minutes, and seconds. The sign of all three components |
134 | /// will always match. A positive value indicates an offset to the east; a negative to the west. |
135 | /// |
136 | /// ```rust |
137 | /// # use time_macros::offset; |
138 | /// assert_eq!(offset!(+1:02:03).as_hms(), (1, 2, 3)); |
139 | /// assert_eq!(offset!(-1:02:03).as_hms(), (-1, -2, -3)); |
140 | /// ``` |
141 | pub const fn as_hms(self) -> (i8, i8, i8) { |
142 | (self.hours, self.minutes, self.seconds) |
143 | } |
144 | |
145 | /// Obtain the number of whole hours the offset is from UTC. A positive value indicates an |
146 | /// offset to the east; a negative to the west. |
147 | /// |
148 | /// ```rust |
149 | /// # use time_macros::offset; |
150 | /// assert_eq!(offset!(+1:02:03).whole_hours(), 1); |
151 | /// assert_eq!(offset!(-1:02:03).whole_hours(), -1); |
152 | /// ``` |
153 | pub const fn whole_hours(self) -> i8 { |
154 | self.hours |
155 | } |
156 | |
157 | /// Obtain the number of whole minutes the offset is from UTC. A positive value indicates an |
158 | /// offset to the east; a negative to the west. |
159 | /// |
160 | /// ```rust |
161 | /// # use time_macros::offset; |
162 | /// assert_eq!(offset!(+1:02:03).whole_minutes(), 62); |
163 | /// assert_eq!(offset!(-1:02:03).whole_minutes(), -62); |
164 | /// ``` |
165 | pub const fn whole_minutes(self) -> i16 { |
166 | self.hours as i16 * Minute.per(Hour) as i16 + self.minutes as i16 |
167 | } |
168 | |
169 | /// Obtain the number of minutes past the hour the offset is from UTC. A positive value |
170 | /// indicates an offset to the east; a negative to the west. |
171 | /// |
172 | /// ```rust |
173 | /// # use time_macros::offset; |
174 | /// assert_eq!(offset!(+1:02:03).minutes_past_hour(), 2); |
175 | /// assert_eq!(offset!(-1:02:03).minutes_past_hour(), -2); |
176 | /// ``` |
177 | pub const fn minutes_past_hour(self) -> i8 { |
178 | self.minutes |
179 | } |
180 | |
181 | /// Obtain the number of whole seconds the offset is from UTC. A positive value indicates an |
182 | /// offset to the east; a negative to the west. |
183 | /// |
184 | /// ```rust |
185 | /// # use time_macros::offset; |
186 | /// assert_eq!(offset!(+1:02:03).whole_seconds(), 3723); |
187 | /// assert_eq!(offset!(-1:02:03).whole_seconds(), -3723); |
188 | /// ``` |
189 | // This may be useful for anyone manually implementing arithmetic, as it |
190 | // would let them construct a `Duration` directly. |
191 | pub const fn whole_seconds(self) -> i32 { |
192 | self.hours as i32 * Second.per(Hour) as i32 |
193 | + self.minutes as i32 * Second.per(Minute) as i32 |
194 | + self.seconds as i32 |
195 | } |
196 | |
197 | /// Obtain the number of seconds past the minute the offset is from UTC. A positive value |
198 | /// indicates an offset to the east; a negative to the west. |
199 | /// |
200 | /// ```rust |
201 | /// # use time_macros::offset; |
202 | /// assert_eq!(offset!(+1:02:03).seconds_past_minute(), 3); |
203 | /// assert_eq!(offset!(-1:02:03).seconds_past_minute(), -3); |
204 | /// ``` |
205 | pub const fn seconds_past_minute(self) -> i8 { |
206 | self.seconds |
207 | } |
208 | // endregion getters |
209 | |
210 | // region: is_{sign} |
211 | /// Check if the offset is exactly UTC. |
212 | /// |
213 | /// |
214 | /// ```rust |
215 | /// # use time_macros::offset; |
216 | /// assert!(!offset!(+1:02:03).is_utc()); |
217 | /// assert!(!offset!(-1:02:03).is_utc()); |
218 | /// assert!(offset!(UTC).is_utc()); |
219 | /// ``` |
220 | pub const fn is_utc(self) -> bool { |
221 | self.hours == 0 && self.minutes == 0 && self.seconds == 0 |
222 | } |
223 | |
224 | /// Check if the offset is positive, or east of UTC. |
225 | /// |
226 | /// ```rust |
227 | /// # use time_macros::offset; |
228 | /// assert!(offset!(+1:02:03).is_positive()); |
229 | /// assert!(!offset!(-1:02:03).is_positive()); |
230 | /// assert!(!offset!(UTC).is_positive()); |
231 | /// ``` |
232 | pub const fn is_positive(self) -> bool { |
233 | self.hours > 0 || self.minutes > 0 || self.seconds > 0 |
234 | } |
235 | |
236 | /// Check if the offset is negative, or west of UTC. |
237 | /// |
238 | /// ```rust |
239 | /// # use time_macros::offset; |
240 | /// assert!(!offset!(+1:02:03).is_negative()); |
241 | /// assert!(offset!(-1:02:03).is_negative()); |
242 | /// assert!(!offset!(UTC).is_negative()); |
243 | /// ``` |
244 | pub const fn is_negative(self) -> bool { |
245 | self.hours < 0 || self.minutes < 0 || self.seconds < 0 |
246 | } |
247 | // endregion is_{sign} |
248 | |
249 | // region: local offset |
250 | /// Attempt to obtain the system's UTC offset at a known moment in time. If the offset cannot be |
251 | /// determined, an error is returned. |
252 | /// |
253 | /// ```rust |
254 | /// # use time::{UtcOffset, OffsetDateTime}; |
255 | /// let local_offset = UtcOffset::local_offset_at(OffsetDateTime::UNIX_EPOCH); |
256 | /// # if false { |
257 | /// assert!(local_offset.is_ok()); |
258 | /// # } |
259 | /// ``` |
260 | #[cfg (feature = "local-offset" )] |
261 | pub fn local_offset_at(datetime: OffsetDateTime) -> Result<Self, error::IndeterminateOffset> { |
262 | local_offset_at(datetime).ok_or(error::IndeterminateOffset) |
263 | } |
264 | |
265 | /// Attempt to obtain the system's current UTC offset. If the offset cannot be determined, an |
266 | /// error is returned. |
267 | /// |
268 | /// ```rust |
269 | /// # use time::UtcOffset; |
270 | /// let local_offset = UtcOffset::current_local_offset(); |
271 | /// # if false { |
272 | /// assert!(local_offset.is_ok()); |
273 | /// # } |
274 | /// ``` |
275 | #[cfg (feature = "local-offset" )] |
276 | pub fn current_local_offset() -> Result<Self, error::IndeterminateOffset> { |
277 | let now = OffsetDateTime::now_utc(); |
278 | local_offset_at(now).ok_or(error::IndeterminateOffset) |
279 | } |
280 | // endregion: local offset |
281 | } |
282 | |
283 | // region: formatting & parsing |
284 | #[cfg (feature = "formatting" )] |
285 | impl UtcOffset { |
286 | /// Format the `UtcOffset` using the provided [format description](crate::format_description). |
287 | pub fn format_into( |
288 | self, |
289 | output: &mut impl io::Write, |
290 | format: &(impl Formattable + ?Sized), |
291 | ) -> Result<usize, error::Format> { |
292 | format.format_into(output, date:None, time:None, offset:Some(self)) |
293 | } |
294 | |
295 | /// Format the `UtcOffset` using the provided [format description](crate::format_description). |
296 | /// |
297 | /// ```rust |
298 | /// # use time::format_description; |
299 | /// # use time_macros::offset; |
300 | /// let format = format_description::parse("[offset_hour sign:mandatory]:[offset_minute]" )?; |
301 | /// assert_eq!(offset!(+1).format(&format)?, "+01:00" ); |
302 | /// # Ok::<_, time::Error>(()) |
303 | /// ``` |
304 | pub fn format(self, format: &(impl Formattable + ?Sized)) -> Result<String, error::Format> { |
305 | format.format(date:None, time:None, offset:Some(self)) |
306 | } |
307 | } |
308 | |
309 | #[cfg (feature = "parsing" )] |
310 | impl UtcOffset { |
311 | /// Parse a `UtcOffset` from the input using the provided [format |
312 | /// description](crate::format_description). |
313 | /// |
314 | /// ```rust |
315 | /// # use time::UtcOffset; |
316 | /// # use time_macros::{offset, format_description}; |
317 | /// let format = format_description!("[offset_hour]:[offset_minute]"); |
318 | /// assert_eq!(UtcOffset::parse("-03:42", &format)?, offset!(-3:42)); |
319 | /// # Ok::<_, time::Error>(()) |
320 | /// ``` |
321 | pub fn parse( |
322 | input: &str, |
323 | description: &(impl Parsable + ?Sized), |
324 | ) -> Result<Self, error::Parse> { |
325 | description.parse_offset(input.as_bytes()) |
326 | } |
327 | } |
328 | |
329 | impl fmt::Display for UtcOffset { |
330 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
331 | write!( |
332 | f, |
333 | " {}{:02}: {:02}: {:02}" , |
334 | if self.is_negative() { '-' } else { '+' }, |
335 | self.hours.abs(), |
336 | self.minutes.abs(), |
337 | self.seconds.abs() |
338 | ) |
339 | } |
340 | } |
341 | |
342 | impl fmt::Debug for UtcOffset { |
343 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
344 | fmt::Display::fmt(self, f) |
345 | } |
346 | } |
347 | // endregion formatting & parsing |
348 | |
349 | impl Neg for UtcOffset { |
350 | type Output = Self; |
351 | |
352 | fn neg(self) -> Self::Output { |
353 | Self::__from_hms_unchecked(-self.hours, -self.minutes, -self.seconds) |
354 | } |
355 | } |
356 | |