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