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