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