1//! The [`UtcOffset`] struct and its associated `impl`s.
2
3use core::fmt;
4use core::ops::Neg;
5#[cfg(feature = "formatting")]
6use std::io;
7
8use crate::convert::*;
9use crate::error;
10#[cfg(feature = "formatting")]
11use crate::formatting::Formattable;
12#[cfg(feature = "parsing")]
13use crate::parsing::Parsable;
14#[cfg(feature = "local-offset")]
15use crate::sys::local_offset_at;
16#[cfg(feature = "local-offset")]
17use 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)]
25pub 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
34impl 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")]
285impl 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")]
310impl 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
329impl 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
342impl 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
349impl 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