1 #[cfg(feature = "std")]
2 use crate::OffsetDateTime;
3 use crate::{
4     error,
5     format::{parse, ParsedItems},
6     DeferredFormat, Duration, ParseResult,
7 };
8 #[cfg(not(feature = "std"))]
9 use alloc::string::{String, ToString};
10 use core::fmt::{self, Display};
11 
12 /// An offset from UTC.
13 ///
14 /// Guaranteed to store values up to ±23:59:59. Any values outside this range
15 /// may have incidental support that can change at any time without notice. If
16 /// you need support outside this range, please file an issue with your use
17 /// case.
18 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19 #[cfg_attr(
20     feature = "serde",
21     serde(from = "crate::serde::UtcOffset", into = "crate::serde::UtcOffset")
22 )]
23 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
24 pub struct UtcOffset {
25     /// The number of seconds offset from UTC. Positive is east, negative is
26     /// west.
27     pub(crate) seconds: i32,
28 }
29 
30 impl UtcOffset {
31     /// A `UtcOffset` that is UTC.
32     ///
33     /// ```rust
34     /// # use time::{UtcOffset, offset};
35     /// assert_eq!(UtcOffset::UTC, offset!(UTC));
36     /// ```
37     pub const UTC: Self = Self::seconds(0);
38 
39     /// Create a `UtcOffset` representing an easterly offset by the number of
40     /// hours provided.
41     ///
42     /// ```rust
43     /// # use time::UtcOffset;
44     /// assert_eq!(UtcOffset::east_hours(1).as_hours(), 1);
45     /// assert_eq!(UtcOffset::east_hours(2).as_minutes(), 120);
46     /// ```
east_hours(hours: u8) -> Self47     pub const fn east_hours(hours: u8) -> Self {
48         Self::hours(hours as i8)
49     }
50 
51     /// Create a `UtcOffset` representing a westerly offset by the number of
52     /// hours provided.
53     ///
54     /// ```rust
55     /// # use time::UtcOffset;
56     /// assert_eq!(UtcOffset::west_hours(1).as_hours(), -1);
57     /// assert_eq!(UtcOffset::west_hours(2).as_minutes(), -120);
58     /// ```
west_hours(hours: u8) -> Self59     pub const fn west_hours(hours: u8) -> Self {
60         Self::hours(-(hours as i8))
61     }
62 
63     /// Create a `UtcOffset` representing an offset by the number of hours
64     /// provided.
65     ///
66     /// ```rust
67     /// # use time::UtcOffset;
68     /// assert_eq!(UtcOffset::hours(2).as_minutes(), 120);
69     /// assert_eq!(UtcOffset::hours(-2).as_minutes(), -120);
70     /// ```
hours(hours: i8) -> Self71     pub const fn hours(hours: i8) -> Self {
72         Self::seconds(hours as i32 * 3_600)
73     }
74 
75     /// Create a `UtcOffset` representing an easterly offset by the number of
76     /// minutes provided.
77     ///
78     /// ```rust
79     /// # use time::UtcOffset;
80     /// assert_eq!(UtcOffset::east_minutes(60).as_hours(), 1);
81     /// ```
east_minutes(minutes: u16) -> Self82     pub const fn east_minutes(minutes: u16) -> Self {
83         Self::minutes(minutes as i16)
84     }
85 
86     /// Create a `UtcOffset` representing a westerly offset by the number of
87     /// minutes provided.
88     ///
89     /// ```rust
90     /// # use time::UtcOffset;
91     /// assert_eq!(UtcOffset::west_minutes(60).as_hours(), -1);
92     /// ```
west_minutes(minutes: u16) -> Self93     pub const fn west_minutes(minutes: u16) -> Self {
94         Self::minutes(-(minutes as i16))
95     }
96 
97     /// Create a `UtcOffset` representing a offset by the number of minutes
98     /// provided.
99     ///
100     /// ```rust
101     /// # use time::UtcOffset;
102     /// assert_eq!(UtcOffset::minutes(60).as_hours(), 1);
103     /// assert_eq!(UtcOffset::minutes(-60).as_hours(), -1);
104     /// ```
minutes(minutes: i16) -> Self105     pub const fn minutes(minutes: i16) -> Self {
106         Self::seconds(minutes as i32 * 60)
107     }
108 
109     /// Create a `UtcOffset` representing an easterly offset by the number of
110     /// seconds provided.
111     ///
112     /// ```rust
113     /// # use time::UtcOffset;
114     /// assert_eq!(UtcOffset::east_seconds(3_600).as_hours(), 1);
115     /// assert_eq!(UtcOffset::east_seconds(1_800).as_minutes(), 30);
116     /// ```
east_seconds(seconds: u32) -> Self117     pub const fn east_seconds(seconds: u32) -> Self {
118         Self::seconds(seconds as i32)
119     }
120 
121     /// Create a `UtcOffset` representing a westerly offset by the number of
122     /// seconds provided.
123     ///
124     /// ```rust
125     /// # use time::UtcOffset;
126     /// assert_eq!(UtcOffset::west_seconds(3_600).as_hours(), -1);
127     /// assert_eq!(UtcOffset::west_seconds(1_800).as_minutes(), -30);
128     /// ```
west_seconds(seconds: u32) -> Self129     pub const fn west_seconds(seconds: u32) -> Self {
130         Self::seconds(-(seconds as i32))
131     }
132 
133     /// Create a `UtcOffset` representing an offset by the number of seconds
134     /// provided.
135     ///
136     /// ```rust
137     /// # use time::UtcOffset;
138     /// assert_eq!(UtcOffset::seconds(3_600).as_hours(), 1);
139     /// assert_eq!(UtcOffset::seconds(-3_600).as_hours(), -1);
140     /// ```
seconds(seconds: i32) -> Self141     pub const fn seconds(seconds: i32) -> Self {
142         Self { seconds }
143     }
144 
145     /// Get the number of seconds from UTC the value is. Positive is east,
146     /// negative is west.
147     ///
148     /// ```rust
149     /// # use time::UtcOffset;
150     /// assert_eq!(UtcOffset::UTC.as_seconds(), 0);
151     /// assert_eq!(UtcOffset::hours(12).as_seconds(), 43_200);
152     /// assert_eq!(UtcOffset::hours(-12).as_seconds(), -43_200);
153     /// ```
as_seconds(self) -> i32154     pub const fn as_seconds(self) -> i32 {
155         self.seconds
156     }
157 
158     /// Get the number of minutes from UTC the value is. Positive is east,
159     /// negative is west.
160     ///
161     /// ```rust
162     /// # use time::UtcOffset;
163     /// assert_eq!(UtcOffset::UTC.as_minutes(), 0);
164     /// assert_eq!(UtcOffset::hours(12).as_minutes(), 720);
165     /// assert_eq!(UtcOffset::hours(-12).as_minutes(), -720);
166     /// ```
as_minutes(self) -> i16167     pub const fn as_minutes(self) -> i16 {
168         (self.as_seconds() / 60) as i16
169     }
170 
171     /// Get the number of hours from UTC the value is. Positive is east,
172     /// negative is west.
173     ///
174     /// ```rust
175     /// # use time::UtcOffset;
176     /// assert_eq!(UtcOffset::UTC.as_hours(), 0);
177     /// assert_eq!(UtcOffset::hours(12).as_hours(), 12);
178     /// assert_eq!(UtcOffset::hours(-12).as_hours(), -12);
179     /// ```
as_hours(self) -> i8180     pub const fn as_hours(self) -> i8 {
181         (self.as_seconds() / 3_600) as i8
182     }
183 
184     /// Convert a `UtcOffset` to ` Duration`. Useful for implementing operators.
as_duration(self) -> Duration185     pub(crate) const fn as_duration(self) -> Duration {
186         Duration::seconds(self.seconds as i64)
187     }
188 
189     /// Obtain the system's UTC offset at a known moment in time. If the offset
190     /// cannot be determined, UTC is returned.
191     ///
192     /// ```rust
193     /// # #![allow(deprecated)]
194     /// # use time::{UtcOffset, OffsetDateTime};
195     /// let unix_epoch = OffsetDateTime::unix_epoch();
196     /// let local_offset = UtcOffset::local_offset_at(unix_epoch);
197     /// println!("{}", local_offset.format("%z"));
198     /// ```
199     #[cfg(feature = "std")]
200     #[cfg_attr(__time_02_docs, doc(cfg(feature = "std")))]
201     #[deprecated(
202         since = "0.2.23",
203         note = "UTC is returned if the local offset cannot be determined"
204     )]
local_offset_at(datetime: OffsetDateTime) -> Self205     pub fn local_offset_at(datetime: OffsetDateTime) -> Self {
206         try_local_offset_at(datetime).unwrap_or(Self::UTC)
207     }
208 
209     /// Attempt to obtain the system's UTC offset at a known moment in time. If
210     /// the offset cannot be determined, an error is returned.
211     ///
212     /// ```rust
213     /// # use time::{UtcOffset, OffsetDateTime};
214     /// let unix_epoch = OffsetDateTime::unix_epoch();
215     /// let local_offset = UtcOffset::try_local_offset_at(unix_epoch);
216     /// # if false {
217     /// assert!(local_offset.is_ok());
218     /// # }
219     /// ```
220     #[cfg(feature = "std")]
221     #[cfg_attr(__time_02_docs, doc(cfg(feature = "std")))]
try_local_offset_at( datetime: OffsetDateTime, ) -> Result<Self, error::IndeterminateOffset>222     pub fn try_local_offset_at(
223         datetime: OffsetDateTime,
224     ) -> Result<Self, error::IndeterminateOffset> {
225         try_local_offset_at(datetime).ok_or(error::IndeterminateOffset)
226     }
227 
228     /// Obtain the system's current UTC offset. If the offset cannot be
229     /// determined, UTC is returned.
230     ///
231     /// ```rust
232     /// # #![allow(deprecated)]
233     /// # use time::UtcOffset;
234     /// let local_offset = UtcOffset::current_local_offset();
235     /// println!("{}", local_offset.format("%z"));
236     /// ```
237     #[cfg(feature = "std")]
238     #[cfg_attr(__time_02_docs, doc(cfg(feature = "std")))]
239     #[deprecated(
240         since = "0.2.23",
241         note = "UTC is returned if the local offset cannot be determined"
242     )]
current_local_offset() -> Self243     pub fn current_local_offset() -> Self {
244         let now = OffsetDateTime::now_utc();
245         try_local_offset_at(now).unwrap_or(Self::UTC)
246     }
247 
248     /// Attempt to obtain the system's current UTC offset. If the offset cannot
249     /// be determined, an error is returned.
250     ///
251     /// ```rust
252     /// # use time::UtcOffset;
253     /// let local_offset = UtcOffset::try_current_local_offset();
254     /// # if false {
255     /// assert!(local_offset.is_ok());
256     /// # }
257     /// ```
258     #[cfg(feature = "std")]
259     #[cfg_attr(__time_02_docs, doc(cfg(feature = "std")))]
try_current_local_offset() -> Result<Self, error::IndeterminateOffset>260     pub fn try_current_local_offset() -> Result<Self, error::IndeterminateOffset> {
261         let now = OffsetDateTime::now_utc();
262         try_local_offset_at(now).ok_or(error::IndeterminateOffset)
263     }
264 }
265 
266 /// Methods that allow parsing and formatting the `UtcOffset`.
267 impl UtcOffset {
268     /// Format the `UtcOffset` using the provided string.
269     ///
270     /// ```rust
271     /// # use time::UtcOffset;
272     /// assert_eq!(UtcOffset::hours(2).format("%z"), "+0200");
273     /// assert_eq!(UtcOffset::hours(-2).format("%z"), "-0200");
274     /// ```
format(self, format: impl AsRef<str>) -> String275     pub fn format(self, format: impl AsRef<str>) -> String {
276         self.lazy_format(format).to_string()
277     }
278 
279     /// Format the `UtcOffset` using the provided string.
280     ///
281     /// ```rust
282     /// # use time::UtcOffset;
283     /// assert_eq!(UtcOffset::hours(2).lazy_format("%z").to_string(), "+0200");
284     /// assert_eq!(UtcOffset::hours(-2).lazy_format("%z").to_string(), "-0200");
285     /// ```
lazy_format(self, format: impl AsRef<str>) -> impl Display286     pub fn lazy_format(self, format: impl AsRef<str>) -> impl Display {
287         DeferredFormat::new(format.as_ref())
288             .with_offset(self)
289             .clone()
290     }
291 
292     /// Attempt to parse the `UtcOffset` using the provided string.
293     ///
294     /// ```rust
295     /// # use time::UtcOffset;
296     /// assert_eq!(UtcOffset::parse("+0200", "%z"), Ok(UtcOffset::hours(2)));
297     /// assert_eq!(UtcOffset::parse("-0200", "%z"), Ok(UtcOffset::hours(-2)));
298     /// ```
parse(s: impl AsRef<str>, format: impl AsRef<str>) -> ParseResult<Self>299     pub fn parse(s: impl AsRef<str>, format: impl AsRef<str>) -> ParseResult<Self> {
300         Self::try_from_parsed_items(parse(s.as_ref(), &format.into())?)
301     }
302 
303     /// Given the items already parsed, attempt to create a `UtcOffset`.
try_from_parsed_items(items: ParsedItems) -> ParseResult<Self>304     pub(crate) fn try_from_parsed_items(items: ParsedItems) -> ParseResult<Self> {
305         items.offset.ok_or(error::Parse::InsufficientInformation)
306     }
307 }
308 
309 impl Display for UtcOffset {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result310     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311         let sign = if self.seconds < 0 { '-' } else { '+' };
312         let hours = self.as_hours().abs();
313         let minutes = self.as_minutes().abs() - hours as i16 * 60;
314         let seconds = self.as_seconds().abs() - hours as i32 * 3_600 - minutes as i32 * 60;
315 
316         write!(f, "{}{}", sign, hours)?;
317 
318         if minutes != 0 || seconds != 0 {
319             write!(f, ":{:02}", minutes)?;
320         }
321 
322         if seconds != 0 {
323             write!(f, ":{:02}", seconds)?;
324         }
325 
326         Ok(())
327     }
328 }
329 
330 /// Attempt to obtain the system's UTC offset. If the offset cannot be
331 /// determined, `None` is returned.
332 #[cfg(feature = "std")]
333 #[allow(clippy::too_many_lines, clippy::missing_const_for_fn)]
try_local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset>334 fn try_local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
335     #[cfg(target_family = "unix")]
336     {
337         // See #293 for details.
338         let _ = datetime;
339         None
340         // use standback::{convert::TryInto, mem::MaybeUninit};
341         //
342         // /// Convert the given Unix timestamp to a `libc::tm`. Returns `None` on
343         // /// any error.
344         // fn timestamp_to_tm(timestamp: i64) -> Option<libc::tm> {
345         //     extern "C" {
346         //         #[cfg_attr(target_os = "netbsd", link_name = "__tzset50")]
347         //         fn tzset();
348         //     }
349         //
350         //     // The exact type of `timestamp` beforehand can vary, so this
351         //     // conversion is necessary.
352         //     #[allow(clippy::useless_conversion)]
353         //     let timestamp = timestamp.try_into().ok()?;
354         //
355         //     let mut tm = MaybeUninit::uninit();
356         //
357         //     // Update timezone information from system. `localtime_r` does not
358         //     // do this for us.
359         //     //
360         //     // Safety: tzset is thread-safe.
361         //     #[allow(unsafe_code)]
362         //     unsafe {
363         //         tzset();
364         //     }
365         //
366         //     // Safety: We are calling a system API, which mutates the `tm`
367         //     // variable. If a null pointer is returned, an error occurred.
368         //     #[allow(unsafe_code)]
369         //     let tm_ptr = unsafe { libc::localtime_r(&timestamp, tm.as_mut_ptr()) };
370         //
371         //     if tm_ptr.is_null() {
372         //         None
373         //     } else {
374         //         // Safety: The value was initialized, as we no longer have a
375         //         // null pointer.
376         //         #[allow(unsafe_code)]
377         //         {
378         //             Some(unsafe { tm.assume_init() })
379         //         }
380         //     }
381         // }
382         //
383         // let tm = timestamp_to_tm(datetime.unix_timestamp())?;
384         //
385         // // `tm_gmtoff` extension
386         // #[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
387         // {
388         //     tm.tm_gmtoff.try_into().ok().map(UtcOffset::seconds)
389         // }
390         //
391         // // No `tm_gmtoff` extension
392         // #[cfg(any(target_os = "solaris", target_os = "illumos"))]
393         // {
394         //     use crate::Date;
395         //     use standback::convert::TryFrom;
396         //
397         //     let mut tm = tm;
398         //     if tm.tm_sec == 60 {
399         //         // Leap seconds are not currently supported.
400         //         tm.tm_sec = 59;
401         //     }
402         //
403         //     let local_timestamp =
404         //         Date::try_from_yo(1900 + tm.tm_year, u16::try_from(tm.tm_yday).ok()? + 1)
405         //             .ok()?
406         //             .try_with_hms(
407         //                 tm.tm_hour.try_into().ok()?,
408         //                 tm.tm_min.try_into().ok()?,
409         //                 tm.tm_sec.try_into().ok()?,
410         //             )
411         //             .ok()?
412         //             .assume_utc()
413         //             .unix_timestamp();
414         //
415         //     (local_timestamp - datetime.unix_timestamp())
416         //         .try_into()
417         //         .ok()
418         //         .map(UtcOffset::seconds)
419         // }
420     }
421     #[cfg(target_family = "windows")]
422     {
423         use standback::{convert::TryInto, mem::MaybeUninit};
424         use winapi::{
425             shared::minwindef::FILETIME,
426             um::{
427                 minwinbase::SYSTEMTIME,
428                 timezoneapi::{SystemTimeToFileTime, SystemTimeToTzSpecificLocalTime},
429             },
430         };
431 
432         /// Convert a `SYSTEMTIME` to a `FILETIME`. Returns `None` if any error
433         /// occurred.
434         fn systemtime_to_filetime(systime: &SYSTEMTIME) -> Option<FILETIME> {
435             let mut ft = MaybeUninit::uninit();
436 
437             // Safety: `SystemTimeToFileTime` is thread-safe. We are only
438             // assuming initialization if the call succeeded.
439             #[allow(unsafe_code)]
440             {
441                 if 0 == unsafe { SystemTimeToFileTime(systime, ft.as_mut_ptr()) } {
442                     // failed
443                     None
444                 } else {
445                     Some(unsafe { ft.assume_init() })
446                 }
447             }
448         }
449 
450         /// Convert a `FILETIME` to an `i64`, representing a number of seconds.
451         fn filetime_to_secs(filetime: &FILETIME) -> i64 {
452             /// FILETIME represents 100-nanosecond intervals
453             const FT_TO_SECS: i64 = 10_000_000;
454             ((filetime.dwHighDateTime as i64) << 32 | filetime.dwLowDateTime as i64) / FT_TO_SECS
455         }
456 
457         /// Convert an `OffsetDateTime` to a `SYSTEMTIME`.
458         fn offset_to_systemtime(datetime: OffsetDateTime) -> SYSTEMTIME {
459             let (month, day_of_month) = datetime.to_offset(UtcOffset::UTC).month_day();
460             SYSTEMTIME {
461                 wYear: datetime.year() as u16,
462                 wMonth: month as u16,
463                 wDay: day_of_month as u16,
464                 wDayOfWeek: 0, // ignored
465                 wHour: datetime.hour() as u16,
466                 wMinute: datetime.minute() as u16,
467                 wSecond: datetime.second() as u16,
468                 wMilliseconds: datetime.millisecond(),
469             }
470         }
471 
472         // This function falls back to UTC if any system call fails.
473         let systime_utc = offset_to_systemtime(datetime.to_offset(UtcOffset::UTC));
474 
475         // Safety: `local_time` is only read if it is properly initialized, and
476         // `SystemTimeToTzSpecificLocalTime` is thread-safe.
477         #[allow(unsafe_code)]
478         let systime_local = unsafe {
479             let mut local_time = MaybeUninit::uninit();
480 
481             if 0 == SystemTimeToTzSpecificLocalTime(
482                 core::ptr::null(), // use system's current timezone
483                 &systime_utc,
484                 local_time.as_mut_ptr(),
485             ) {
486                 // call failed
487                 return None;
488             } else {
489                 local_time.assume_init()
490             }
491         };
492 
493         // Convert SYSTEMTIMEs to FILETIMEs so we can perform arithmetic on
494         // them.
495         let ft_system = systemtime_to_filetime(&systime_utc)?;
496         let ft_local = systemtime_to_filetime(&systime_local)?;
497 
498         let diff_secs = filetime_to_secs(&ft_local) - filetime_to_secs(&ft_system);
499 
500         diff_secs.try_into().ok().map(UtcOffset::seconds)
501     }
502     #[cfg(__time_02_cargo_web)]
503     {
504         use stdweb::{js, unstable::TryInto};
505 
506         let timestamp_utc = datetime.unix_timestamp();
507         let low_bits = (timestamp_utc & 0xFF_FF_FF_FF) as i32;
508         let high_bits = (timestamp_utc >> 32) as i32;
509 
510         let timezone_offset = js! {
511             return
512                 new Date(((@{high_bits} << 32) + @{low_bits}) * 1000)
513                     .getTimezoneOffset() * -60;
514         };
515 
516         timezone_offset.try_into().ok().map(UtcOffset::seconds)
517     }
518     #[cfg(not(any(target_family = "unix", target_family = "windows", __time_02_cargo_web)))]
519     {
520         // Silence the unused variable warning when appropriate.
521         let _ = datetime;
522         None
523     }
524 }
525 
526 #[cfg(test)]
527 mod test {
528     use super::*;
529     use crate::ext::NumericalDuration;
530 
531     #[test]
as_duration()532     fn as_duration() {
533         assert_eq!(UtcOffset::hours(1).as_duration(), 1.hours());
534         assert_eq!(UtcOffset::hours(-1).as_duration(), (-1).hours());
535     }
536 }
537