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