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