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 =
485 /// format_description::parse_borrowed::<3>("[offset_hour sign:mandatory]:[offset_minute]")?;
486 /// assert_eq!(offset!(+1).format(&format)?, "+01:00");
487 /// # Ok::<_, time::Error>(())
488 /// ```
489 #[inline]
490 pub fn format(self, format: &(impl Formattable + ?Sized)) -> Result<String, error::Format> {
491 format.format(&self, &mut Default::default())
492 }
493}
494
495#[cfg(feature = "parsing")]
496impl UtcOffset {
497 /// Parse a `UtcOffset` from the input using the provided [format
498 /// description](crate::format_description).
499 ///
500 /// ```rust
501 /// # use time::UtcOffset;
502 /// # use time_macros::{offset, format_description};
503 /// let format = format_description!("[offset_hour]:[offset_minute]");
504 /// assert_eq!(UtcOffset::parse("-03:42", &format)?, offset!(-3:42));
505 /// # Ok::<_, time::Error>(())
506 /// ```
507 #[inline]
508 pub fn parse(
509 input: &str,
510 description: &(impl Parsable + ?Sized),
511 ) -> Result<Self, error::Parse> {
512 description.parse_offset(input.as_bytes())
513 }
514}
515
516mod private {
517 /// Metadata for `UtcOffset`.
518 #[non_exhaustive]
519 #[derive(Debug, Clone, Copy)]
520 pub struct UtcOffsetMetadata;
521}
522use private::UtcOffsetMetadata;
523
524// This no longer needs special handling, as the format is fixed and doesn't require anything
525// advanced. Trait impls can't be deprecated and the info is still useful for other types
526// implementing `SmartDisplay`, so leave it as-is for now.
527impl SmartDisplay for UtcOffset {
528 type Metadata = UtcOffsetMetadata;
529
530 #[inline]
531 fn metadata(&self, _: FormatterOptions) -> Metadata<'_, Self> {
532 Metadata::new(9, self, UtcOffsetMetadata)
533 }
534
535 #[inline]
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 fmt::Display::fmt(self, f)
538 }
539}
540
541impl UtcOffset {
542 /// The maximum number of bytes that the `fmt_into_buffer` method will write, which is also used
543 /// for the `Display` implementation.
544 pub(crate) const DISPLAY_BUFFER_SIZE: usize = 9;
545
546 /// Format the `UtcOffset` into the provided buffer, returning the number of bytes written.
547 #[inline]
548 pub(crate) const fn fmt_into_buffer(
549 self,
550 buf: &mut [MaybeUninit<u8>; Self::DISPLAY_BUFFER_SIZE],
551 ) -> usize {
552 let hours = self.hours.get().unsigned_abs();
553 let minutes = self.minutes.get().unsigned_abs();
554 let seconds = self.seconds.get().unsigned_abs();
555
556 let sign = if self.is_negative() { b'-' } else { b'+' };
557 buf[0] = MaybeUninit::new(sign);
558 buf[3] = MaybeUninit::new(b':');
559 buf[6] = MaybeUninit::new(b':');
560
561 // Safety: `hours`, `minutes` and `seconds` are all less than 100. Both the source and
562 // destination are valid for two bytes, aligned, and do not overlap.
563 unsafe {
564 two_digits_zero_padded(ru8::new_unchecked(hours))
565 .as_ptr()
566 .copy_to_nonoverlapping(buf.as_mut_ptr().add(1).cast(), 2);
567 two_digits_zero_padded(ru8::new_unchecked(minutes))
568 .as_ptr()
569 .copy_to_nonoverlapping(buf.as_mut_ptr().add(4).cast(), 2);
570 two_digits_zero_padded(ru8::new_unchecked(seconds))
571 .as_ptr()
572 .copy_to_nonoverlapping(buf.as_mut_ptr().add(7).cast(), 2);
573 }
574
575 // The number of bytes written does not vary; it is always 9.
576 9
577 }
578}
579
580impl fmt::Display for UtcOffset {
581 #[inline]
582 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583 let mut buf = [MaybeUninit::uninit(); Self::DISPLAY_BUFFER_SIZE];
584 let len = self.fmt_into_buffer(&mut buf);
585 // Safety: All bytes up to `len` have been initialized with ASCII characters.
586 let s = unsafe { str_from_raw_parts(buf.as_ptr().cast(), len) };
587 f.pad(s)
588 }
589}
590
591impl fmt::Debug for UtcOffset {
592 #[inline]
593 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594 fmt::Display::fmt(self, f)
595 }
596}
597
598impl Neg for UtcOffset {
599 type Output = Self;
600
601 #[inline]
602 fn neg(self) -> Self::Output {
603 Self::from_hms_ranged(self.hours.neg(), self.minutes.neg(), self.seconds.neg())
604 }
605}