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