time/utc_offset.rs
1//! The [`UtcOffset`] struct and its associated `impl`s.
2
3#[cfg(feature = "formatting")]
4use alloc::string::String;
5use core::fmt;
6use core::ops::Neg;
7#[cfg(feature = "formatting")]
8use std::io;
9
10use deranged::{RangedI32, RangedI8};
11use powerfmt::ext::FormatterExt;
12use powerfmt::smart_display::{self, FormatterOptions, Metadata, SmartDisplay};
13
14use crate::convert::*;
15use crate::error;
16#[cfg(feature = "formatting")]
17use crate::formatting::Formattable;
18use crate::internal_macros::ensure_ranged;
19#[cfg(feature = "parsing")]
20use crate::parsing::Parsable;
21#[cfg(feature = "local-offset")]
22use crate::sys::local_offset_at;
23#[cfg(feature = "local-offset")]
24use crate::OffsetDateTime;
25
26/// The type of the `hours` field of `UtcOffset`.
27type Hours = RangedI8<-25, 25>;
28/// The type of the `minutes` field of `UtcOffset`.
29type Minutes = RangedI8<{ -(Minute::per(Hour) as i8 - 1) }, { Minute::per(Hour) as i8 - 1 }>;
30/// The type of the `seconds` field of `UtcOffset`.
31type Seconds = RangedI8<{ -(Second::per(Minute) as i8 - 1) }, { Second::per(Minute) as i8 - 1 }>;
32/// The type capable of storing the range of whole seconds that a `UtcOffset` can encompass.
33type WholeSeconds = RangedI32<
34 {
35 Hours::MIN.get() as i32 * Second::per(Hour) as i32
36 + Minutes::MIN.get() as i32 * Second::per(Minute) as i32
37 + Seconds::MIN.get() as i32
38 },
39 {
40 Hours::MAX.get() as i32 * Second::per(Hour) as i32
41 + Minutes::MAX.get() as i32 * Second::per(Minute) as i32
42 + Seconds::MAX.get() as i32
43 },
44>;
45
46/// An offset from UTC.
47///
48/// This struct can store values up to ±25:59:59. If you need support outside this range, please
49/// file an issue with your use case.
50// All three components _must_ have the same sign.
51#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
52pub struct UtcOffset {
53 hours: Hours,
54 minutes: Minutes,
55 seconds: Seconds,
56}
57
58impl UtcOffset {
59 /// A `UtcOffset` that is UTC.
60 ///
61 /// ```rust
62 /// # use time::UtcOffset;
63 /// # use time_macros::offset;
64 /// assert_eq!(UtcOffset::UTC, offset!(UTC));
65 /// ```
66 pub const UTC: Self = Self::from_whole_seconds_ranged(WholeSeconds::new_static::<0>());
67
68 /// Create a `UtcOffset` representing an offset of the hours, minutes, and seconds provided, the
69 /// validity of which must be guaranteed by the caller. All three parameters must have the same
70 /// sign.
71 ///
72 /// # Safety
73 ///
74 /// - Hours must be in the range `-25..=25`.
75 /// - Minutes must be in the range `-59..=59`.
76 /// - Seconds must be in the range `-59..=59`.
77 ///
78 /// While the signs of the parameters are required to match to avoid bugs, this is not a safety
79 /// invariant.
80 #[doc(hidden)]
81 pub const unsafe fn __from_hms_unchecked(hours: i8, minutes: i8, seconds: i8) -> Self {
82 // Safety: The caller must uphold the safety invariants.
83 unsafe {
84 Self::from_hms_ranged_unchecked(
85 Hours::new_unchecked(hours),
86 Minutes::new_unchecked(minutes),
87 Seconds::new_unchecked(seconds),
88 )
89 }
90 }
91
92 /// Create a `UtcOffset` representing an offset by the number of hours, minutes, and seconds
93 /// provided.
94 ///
95 /// The sign of all three components should match. If they do not, all smaller components will
96 /// have their signs flipped.
97 ///
98 /// ```rust
99 /// # use time::UtcOffset;
100 /// assert_eq!(UtcOffset::from_hms(1, 2, 3)?.as_hms(), (1, 2, 3));
101 /// assert_eq!(UtcOffset::from_hms(1, -2, -3)?.as_hms(), (1, 2, 3));
102 /// # Ok::<_, time::Error>(())
103 /// ```
104 pub const fn from_hms(
105 hours: i8,
106 minutes: i8,
107 seconds: i8,
108 ) -> Result<Self, error::ComponentRange> {
109 Ok(Self::from_hms_ranged(
110 ensure_ranged!(Hours: hours),
111 ensure_ranged!(Minutes: minutes),
112 ensure_ranged!(Seconds: seconds),
113 ))
114 }
115
116 /// Create a `UtcOffset` representing an offset of the hours, minutes, and seconds provided. All
117 /// three parameters must have the same sign.
118 ///
119 /// While the signs of the parameters are required to match, this is not a safety invariant.
120 pub(crate) const fn from_hms_ranged_unchecked(
121 hours: Hours,
122 minutes: Minutes,
123 seconds: Seconds,
124 ) -> Self {
125 if hours.get() < 0 {
126 debug_assert!(minutes.get() <= 0);
127 debug_assert!(seconds.get() <= 0);
128 } else if hours.get() > 0 {
129 debug_assert!(minutes.get() >= 0);
130 debug_assert!(seconds.get() >= 0);
131 }
132 if minutes.get() < 0 {
133 debug_assert!(seconds.get() <= 0);
134 } else if minutes.get() > 0 {
135 debug_assert!(seconds.get() >= 0);
136 }
137
138 Self {
139 hours,
140 minutes,
141 seconds,
142 }
143 }
144
145 /// Create a `UtcOffset` representing an offset by the number of hours, minutes, and seconds
146 /// provided.
147 ///
148 /// The sign of all three components should match. If they do not, all smaller components will
149 /// have their signs flipped.
150 pub(crate) const fn from_hms_ranged(
151 hours: Hours,
152 mut minutes: Minutes,
153 mut seconds: Seconds,
154 ) -> Self {
155 if (hours.get() > 0 && minutes.get() < 0) || (hours.get() < 0 && minutes.get() > 0) {
156 minutes = minutes.neg();
157 }
158 if (hours.get() > 0 && seconds.get() < 0)
159 || (hours.get() < 0 && seconds.get() > 0)
160 || (minutes.get() > 0 && seconds.get() < 0)
161 || (minutes.get() < 0 && seconds.get() > 0)
162 {
163 seconds = seconds.neg();
164 }
165
166 Self {
167 hours,
168 minutes,
169 seconds,
170 }
171 }
172
173 /// Create a `UtcOffset` representing an offset by the number of seconds provided.
174 ///
175 /// ```rust
176 /// # use time::UtcOffset;
177 /// assert_eq!(UtcOffset::from_whole_seconds(3_723)?.as_hms(), (1, 2, 3));
178 /// # Ok::<_, time::Error>(())
179 /// ```
180 pub const fn from_whole_seconds(seconds: i32) -> Result<Self, error::ComponentRange> {
181 Ok(Self::from_whole_seconds_ranged(
182 ensure_ranged!(WholeSeconds: seconds),
183 ))
184 }
185
186 /// Create a `UtcOffset` representing an offset by the number of seconds provided.
187 // ignore because the function is crate-private
188 /// ```rust,ignore
189 /// # use time::UtcOffset;
190 /// # use deranged::RangedI32;
191 /// assert_eq!(
192 /// UtcOffset::from_whole_seconds_ranged(RangedI32::new_static::<3_723>()).as_hms(),
193 /// (1, 2, 3)
194 /// );
195 /// # Ok::<_, time::Error>(())
196 /// ```
197 pub(crate) const fn from_whole_seconds_ranged(seconds: WholeSeconds) -> Self {
198 // Safety: The type of `seconds` guarantees that all values are in range.
199 unsafe {
200 Self::__from_hms_unchecked(
201 (seconds.get() / Second::per(Hour) as i32) as i8,
202 ((seconds.get() % Second::per(Hour) as i32) / Minute::per(Hour) as i32) as i8,
203 (seconds.get() % Second::per(Minute) as i32) as i8,
204 )
205 }
206 }
207
208 /// Obtain the UTC offset as its hours, minutes, and seconds. The sign of all three components
209 /// will always match. A positive value indicates an offset to the east; a negative to the west.
210 ///
211 /// ```rust
212 /// # use time_macros::offset;
213 /// assert_eq!(offset!(+1:02:03).as_hms(), (1, 2, 3));
214 /// assert_eq!(offset!(-1:02:03).as_hms(), (-1, -2, -3));
215 /// ```
216 pub const fn as_hms(self) -> (i8, i8, i8) {
217 (self.hours.get(), self.minutes.get(), self.seconds.get())
218 }
219
220 /// Obtain the UTC offset as its hours, minutes, and seconds. The sign of all three components
221 /// will always match. A positive value indicates an offset to the east; a negative to the west.
222 #[cfg(feature = "quickcheck")]
223 pub(crate) const fn as_hms_ranged(self) -> (Hours, Minutes, Seconds) {
224 (self.hours, self.minutes, self.seconds)
225 }
226
227 /// Obtain the number of whole hours the offset is from UTC. A positive value indicates an
228 /// offset to the east; a negative to the west.
229 ///
230 /// ```rust
231 /// # use time_macros::offset;
232 /// assert_eq!(offset!(+1:02:03).whole_hours(), 1);
233 /// assert_eq!(offset!(-1:02:03).whole_hours(), -1);
234 /// ```
235 pub const fn whole_hours(self) -> i8 {
236 self.hours.get()
237 }
238
239 /// Obtain the number of whole minutes the offset is from UTC. A positive value indicates an
240 /// offset to the east; a negative to the west.
241 ///
242 /// ```rust
243 /// # use time_macros::offset;
244 /// assert_eq!(offset!(+1:02:03).whole_minutes(), 62);
245 /// assert_eq!(offset!(-1:02:03).whole_minutes(), -62);
246 /// ```
247 pub const fn whole_minutes(self) -> i16 {
248 self.hours.get() as i16 * Minute::per(Hour) as i16 + self.minutes.get() as i16
249 }
250
251 /// Obtain the number of minutes past the hour the offset is from UTC. A positive value
252 /// indicates an offset to the east; a negative to the west.
253 ///
254 /// ```rust
255 /// # use time_macros::offset;
256 /// assert_eq!(offset!(+1:02:03).minutes_past_hour(), 2);
257 /// assert_eq!(offset!(-1:02:03).minutes_past_hour(), -2);
258 /// ```
259 pub const fn minutes_past_hour(self) -> i8 {
260 self.minutes.get()
261 }
262
263 /// Obtain the number of whole seconds the offset is from UTC. A positive value indicates an
264 /// offset to the east; a negative to the west.
265 ///
266 /// ```rust
267 /// # use time_macros::offset;
268 /// assert_eq!(offset!(+1:02:03).whole_seconds(), 3723);
269 /// assert_eq!(offset!(-1:02:03).whole_seconds(), -3723);
270 /// ```
271 // This may be useful for anyone manually implementing arithmetic, as it
272 // would let them construct a `Duration` directly.
273 pub const fn whole_seconds(self) -> i32 {
274 self.hours.get() as i32 * Second::per(Hour) as i32
275 + self.minutes.get() as i32 * Second::per(Minute) as i32
276 + self.seconds.get() as i32
277 }
278
279 /// Obtain the number of seconds past the minute the offset is from UTC. A positive value
280 /// indicates an offset to the east; a negative to the west.
281 ///
282 /// ```rust
283 /// # use time_macros::offset;
284 /// assert_eq!(offset!(+1:02:03).seconds_past_minute(), 3);
285 /// assert_eq!(offset!(-1:02:03).seconds_past_minute(), -3);
286 /// ```
287 pub const fn seconds_past_minute(self) -> i8 {
288 self.seconds.get()
289 }
290
291 /// Check if the offset is exactly UTC.
292 ///
293 ///
294 /// ```rust
295 /// # use time_macros::offset;
296 /// assert!(!offset!(+1:02:03).is_utc());
297 /// assert!(!offset!(-1:02:03).is_utc());
298 /// assert!(offset!(UTC).is_utc());
299 /// ```
300 pub const fn is_utc(self) -> bool {
301 self.hours.get() == 0 && self.minutes.get() == 0 && self.seconds.get() == 0
302 }
303
304 /// Check if the offset is positive, or east of UTC.
305 ///
306 /// ```rust
307 /// # use time_macros::offset;
308 /// assert!(offset!(+1:02:03).is_positive());
309 /// assert!(!offset!(-1:02:03).is_positive());
310 /// assert!(!offset!(UTC).is_positive());
311 /// ```
312 pub const fn is_positive(self) -> bool {
313 self.hours.get() > 0 || self.minutes.get() > 0 || self.seconds.get() > 0
314 }
315
316 /// Check if the offset is negative, or west of UTC.
317 ///
318 /// ```rust
319 /// # use time_macros::offset;
320 /// assert!(!offset!(+1:02:03).is_negative());
321 /// assert!(offset!(-1:02:03).is_negative());
322 /// assert!(!offset!(UTC).is_negative());
323 /// ```
324 pub const fn is_negative(self) -> bool {
325 self.hours.get() < 0 || self.minutes.get() < 0 || self.seconds.get() < 0
326 }
327
328 /// Attempt to obtain the system's UTC offset at a known moment in time. If the offset cannot be
329 /// determined, an error is returned.
330 ///
331 /// ```rust
332 /// # use time::{UtcOffset, OffsetDateTime};
333 /// let local_offset = UtcOffset::local_offset_at(OffsetDateTime::UNIX_EPOCH);
334 /// # if false {
335 /// assert!(local_offset.is_ok());
336 /// # }
337 /// ```
338 #[cfg(feature = "local-offset")]
339 pub fn local_offset_at(datetime: OffsetDateTime) -> Result<Self, error::IndeterminateOffset> {
340 local_offset_at(datetime).ok_or(error::IndeterminateOffset)
341 }
342
343 /// Attempt to obtain the system's current UTC offset. If the offset cannot be determined, an
344 /// error is returned.
345 ///
346 /// ```rust
347 /// # use time::UtcOffset;
348 /// let local_offset = UtcOffset::current_local_offset();
349 /// # if false {
350 /// assert!(local_offset.is_ok());
351 /// # }
352 /// ```
353 #[cfg(feature = "local-offset")]
354 pub fn current_local_offset() -> Result<Self, error::IndeterminateOffset> {
355 let now = OffsetDateTime::now_utc();
356 local_offset_at(now).ok_or(error::IndeterminateOffset)
357 }
358}
359
360#[cfg(feature = "formatting")]
361impl UtcOffset {
362 /// Format the `UtcOffset` using the provided [format description](crate::format_description).
363 pub fn format_into(
364 self,
365 output: &mut (impl io::Write + ?Sized),
366 format: &(impl Formattable + ?Sized),
367 ) -> Result<usize, error::Format> {
368 format.format_into(output, None, None, Some(self))
369 }
370
371 /// Format the `UtcOffset` using the provided [format description](crate::format_description).
372 ///
373 /// ```rust
374 /// # use time::format_description;
375 /// # use time_macros::offset;
376 /// let format = format_description::parse("[offset_hour sign:mandatory]:[offset_minute]")?;
377 /// assert_eq!(offset!(+1).format(&format)?, "+01:00");
378 /// # Ok::<_, time::Error>(())
379 /// ```
380 pub fn format(self, format: &(impl Formattable + ?Sized)) -> Result<String, error::Format> {
381 format.format(None, None, Some(self))
382 }
383}
384
385#[cfg(feature = "parsing")]
386impl UtcOffset {
387 /// Parse a `UtcOffset` from the input using the provided [format
388 /// description](crate::format_description).
389 ///
390 /// ```rust
391 /// # use time::UtcOffset;
392 /// # use time_macros::{offset, format_description};
393 /// let format = format_description!("[offset_hour]:[offset_minute]");
394 /// assert_eq!(UtcOffset::parse("-03:42", &format)?, offset!(-3:42));
395 /// # Ok::<_, time::Error>(())
396 /// ```
397 pub fn parse(
398 input: &str,
399 description: &(impl Parsable + ?Sized),
400 ) -> Result<Self, error::Parse> {
401 description.parse_offset(input.as_bytes())
402 }
403}
404
405mod private {
406 #[non_exhaustive]
407 #[derive(Debug, Clone, Copy)]
408 pub struct UtcOffsetMetadata;
409}
410use private::UtcOffsetMetadata;
411
412impl SmartDisplay for UtcOffset {
413 type Metadata = UtcOffsetMetadata;
414
415 fn metadata(&self, _: FormatterOptions) -> Metadata<Self> {
416 let sign = if self.is_negative() { '-' } else { '+' };
417 let width = smart_display::padded_width_of!(
418 sign,
419 self.hours.abs() => width(2),
420 ":",
421 self.minutes.abs() => width(2),
422 ":",
423 self.seconds.abs() => width(2),
424 );
425 Metadata::new(width, self, UtcOffsetMetadata)
426 }
427
428 fn fmt_with_metadata(
429 &self,
430 f: &mut fmt::Formatter<'_>,
431 metadata: Metadata<Self>,
432 ) -> fmt::Result {
433 f.pad_with_width(
434 metadata.unpadded_width(),
435 format_args!(
436 "{}{:02}:{:02}:{:02}",
437 if self.is_negative() { '-' } else { '+' },
438 self.hours.abs(),
439 self.minutes.abs(),
440 self.seconds.abs(),
441 ),
442 )
443 }
444}
445
446impl fmt::Display for UtcOffset {
447 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448 SmartDisplay::fmt(self, f)
449 }
450}
451
452impl fmt::Debug for UtcOffset {
453 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454 fmt::Display::fmt(self, f)
455 }
456}
457
458impl Neg for UtcOffset {
459 type Output = Self;
460
461 fn neg(self) -> Self::Output {
462 Self::from_hms_ranged(self.hours.neg(), self.minutes.neg(), self.seconds.neg())
463 }
464}