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(×tamp, 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