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