Skip to content

Commit 5f1c492

Browse files
jhpratthroi
andauthored
Method to get local UTC offset at a given datetime (#217)
This implementation should work on Windows, Mac, Linux, and Solaris. As Redox is *nix, it should work there as well. `OffsetDateTime::now_local()` returns a value with the local offset. `UtcOffset::local_offset_at(OffsetDateTime)` and `UtcOffset::current_local_offset()` have also been implemented. This necessarily requires syscalls, and as such `#![forbid(unsafe_code)]` has been changed to `#![deny(unsafe_code)]`. It must be explicitly `#[allow]`ed in every location it is used, along with a general message describing why the usage is safe. Co-authored-by: Hroi Sigurdsson <[email protected]>
1 parent 73f55ce commit 5f1c492

File tree

5 files changed

+237
-6
lines changed

5 files changed

+237
-6
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ Versioning].
3232
- `NumericalDuration` has been implemented for `f32` and `f64`.
3333
`NumericalStdDuration` and `NumericalStdDurationShort` have been implemented
3434
for `f64` only.
35+
- `UtcOffset::local_offset_at(OffsetDateTime)`, which will obtain the system's
36+
local offset at the provided moment in time.
37+
- `OffsetDateTime::now_local()` is equivalent to calling
38+
`OffsetDateTime::now().to_offset(UtcOffset::local_offset_at(OffsetDateTime::now()))`
39+
(but more efficient).
40+
- `UtcOffset::current_local_offset()` will return the equivalent of
41+
`OffsetDateTime::now_local().offset()`.
3542

3643
### Changed
3744

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ all-features = true
1818
default = ["deprecated", "std"]
1919
deprecated = []
2020
panicking-api = []
21-
std = []
21+
std = ["libc", "winapi"]
2222

2323
# Internal usage. This is used when building for docs.rs and time-rs.github.io.
2424
# This feature should never be used by external users. It will likely be
@@ -33,3 +33,9 @@ time-macros = { version = "0.1", path = "time-macros" }
3333

3434
[workspace]
3535
members = ["time-macros", "time-macros-impl"]
36+
37+
[target.'cfg(unix)'.dependencies]
38+
libc = { version = "0.2", optional = true }
39+
40+
[target.'cfg(windows)'.dependencies]
41+
winapi = { version = "0.3", features = ["minwinbase", "minwindef", "timezoneapi"], optional = true }

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@
141141
142142
#![cfg_attr(feature = "__doc", feature(doc_cfg))]
143143
#![cfg_attr(not(feature = "std"), no_std)]
144-
#![forbid(unsafe_code)]
145144
#![deny(
145+
unsafe_code, // Used when interacting with system APIs
146146
anonymous_parameters,
147147
rust_2018_idioms,
148148
trivial_casts,

src/offset_date_time.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,7 @@ pub struct OffsetDateTime {
4040
}
4141

4242
impl OffsetDateTime {
43-
/// Create a new `OffsetDateTime` with the current date and time.
44-
///
45-
/// This currently returns an offset of UTC, though this behavior will
46-
/// change once a way to obtain the local offset is implemented.
43+
/// Create a new `OffsetDateTime` with the current date and time in UTC.
4744
///
4845
/// ```rust
4946
/// # use time::{OffsetDateTime, offset};
@@ -57,6 +54,21 @@ impl OffsetDateTime {
5754
SystemTime::now().into()
5855
}
5956

57+
/// Create a new `OffsetDateTime` with the current date and time in the
58+
/// local offset.
59+
///
60+
/// ```rust
61+
/// # use time::{OffsetDateTime, offset};
62+
/// assert!(OffsetDateTime::now_local().year() >= 2019);
63+
/// ```
64+
#[inline(always)]
65+
#[cfg(feature = "std")]
66+
#[cfg_attr(feature = "__doc", doc(cfg(feature = "std")))]
67+
pub fn now_local() -> Self {
68+
let t = Self::now();
69+
t.to_offset(UtcOffset::local_offset_at(t))
70+
}
71+
6072
/// Convert the `OffsetDateTime` from the current `UtcOffset` to the
6173
/// provided `UtcOffset`.
6274
///
@@ -960,6 +972,16 @@ mod test {
960972
assert_eq!(OffsetDateTime::now().offset(), offset!(UTC));
961973
}
962974

975+
#[test]
976+
#[cfg(feature = "std")]
977+
fn now_local() {
978+
assert!(OffsetDateTime::now().year() >= 2019);
979+
assert_eq!(
980+
OffsetDateTime::now_local().offset(),
981+
UtcOffset::current_local_offset()
982+
);
983+
}
984+
963985
#[test]
964986
fn to_offset() {
965987
assert_eq!(

src/utc_offset.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#[cfg(not(feature = "std"))]
22
use crate::alloc_prelude::*;
3+
#[cfg(feature = "std")]
4+
use crate::OffsetDateTime;
35
use crate::{
46
format::{parse, ParseError, ParseResult, ParsedItems},
57
DeferredFormat, Duration,
@@ -197,6 +199,35 @@ impl UtcOffset {
197199
pub(crate) const fn as_duration(self) -> Duration {
198200
Duration::seconds(self.seconds as i64)
199201
}
202+
203+
/// Obtain the system's UTC offset at a known moment in time. If the offset
204+
/// cannot be determined, UTC is returned.
205+
///
206+
/// ```rust,no_run
207+
/// # use time::{UtcOffset, OffsetDateTime};
208+
/// let unix_epoch = OffsetDateTime::unix_epoch();
209+
/// let local_offset = UtcOffset::local_offset_at(unix_epoch);
210+
/// println!("{}", local_offset.format("%z"));
211+
/// ```
212+
#[inline(always)]
213+
#[cfg(feature = "std")]
214+
pub fn local_offset_at(datetime: OffsetDateTime) -> Self {
215+
try_local_offset_at(datetime).unwrap_or(Self::UTC)
216+
}
217+
218+
/// Obtain the system's current UTC offset. If the offset cannot be
219+
/// determined, UTC is returned.
220+
///
221+
/// ```rust,no_run
222+
/// # use time::UtcOffset;
223+
/// let local_offset = UtcOffset::current_local_offset();
224+
/// println!("{}", local_offset.format("%z"));
225+
/// ```
226+
#[inline(always)]
227+
#[cfg(feature = "std")]
228+
pub fn current_local_offset() -> Self {
229+
OffsetDateTime::now_local().offset()
230+
}
200231
}
201232

202233
/// Methods that allow parsing and formatting the `UtcOffset`.
@@ -256,6 +287,171 @@ impl Display for UtcOffset {
256287
}
257288
}
258289

290+
/// Attempt to obtain the system's UTC offset. If the offset cannot be
291+
/// determined, `None` is returned.
292+
#[cfg(feature = "std")]
293+
#[allow(clippy::too_many_lines)] //
294+
fn try_local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
295+
use core::{convert::TryInto, mem};
296+
297+
#[cfg(target_family = "unix")]
298+
{
299+
/// Convert the given Unix timestamp to a `libc::tm`. Returns `None` on
300+
/// any error.
301+
fn timestamp_to_tm(timestamp: i64) -> Option<libc::tm> {
302+
extern "C" {
303+
fn tzset();
304+
}
305+
306+
let timestamp = timestamp.try_into().ok()?;
307+
308+
// Safety: Plain old data.
309+
#[allow(unsafe_code)]
310+
let mut tm = unsafe { mem::zeroed() };
311+
312+
// Update timezone information from system. `localtime_r` does not
313+
// do this for us.
314+
//
315+
// Safety: tzset is thread-safe.
316+
#[allow(unsafe_code)]
317+
unsafe {
318+
tzset();
319+
}
320+
321+
// Safety: We are calling a system API, which mutates the `tm`
322+
// variable. If a null pointer is returned, an error occurred.
323+
#[allow(unsafe_code)]
324+
let tm_ptr = unsafe { libc::localtime_r(&timestamp, &mut tm) };
325+
326+
if tm_ptr.is_null() {
327+
None
328+
} else {
329+
Some(tm)
330+
}
331+
}
332+
333+
let tm = timestamp_to_tm(datetime.timestamp())?;
334+
335+
// `tm_gmtoff` extension
336+
#[cfg(not(target_os = "solaris"))]
337+
{
338+
tm.tm_gmtoff.try_into().ok().map(UtcOffset::seconds)
339+
}
340+
341+
// No `tm_gmtoff` extension
342+
#[cfg(target_os = "solaris")]
343+
{
344+
use crate::Date;
345+
use core::convert::TryFrom;
346+
347+
let mut tm = tm;
348+
if tm.tm_sec == 60 {
349+
// Leap seconds are not currently supported.
350+
tm.tm_sec = 59;
351+
}
352+
353+
let local_timestamp =
354+
Date::try_from_yo(1900 + tm.tm_year, u16::try_from(tm.tm_yday).ok()? + 1)
355+
.ok()?
356+
.try_with_hms(
357+
tm.tm_hour.try_into().ok()?,
358+
tm.tm_min.try_into().ok()?,
359+
tm.tm_sec.try_into().ok()?,
360+
)
361+
.ok()?
362+
.assume_utc()
363+
.timestamp();
364+
365+
(local_timestamp - datetime.timestamp())
366+
.try_into()
367+
.ok()
368+
.map(UtcOffset::seconds)
369+
}
370+
}
371+
372+
#[cfg(target_family = "windows")]
373+
{
374+
use crate::offset;
375+
use winapi::{
376+
shared::minwindef::FILETIME,
377+
um::{
378+
minwinbase::SYSTEMTIME,
379+
timezoneapi::{SystemTimeToFileTime, SystemTimeToTzSpecificLocalTime},
380+
},
381+
};
382+
383+
/// Convert a `SYSTEMTIME` to a `FILETIME`. Returns `None` if any error
384+
/// occurred.
385+
fn systemtime_to_filetime(systime: &SYSTEMTIME) -> Option<FILETIME> {
386+
// Safety: We only read `ft` if it is properly initialized.
387+
#[allow(unsafe_code, deprecated)]
388+
let mut ft = unsafe { mem::uninitialized() };
389+
390+
// Safety: `SystemTimeToFileTime` is thread-safe.
391+
#[allow(unsafe_code)]
392+
{
393+
if 0 == unsafe { SystemTimeToFileTime(systime, &mut ft) } {
394+
// failed
395+
None
396+
} else {
397+
Some(ft)
398+
}
399+
}
400+
}
401+
402+
/// Convert a `FILETIME` to an `i64`, representing a number of seconds.
403+
fn filetime_to_secs(filetime: &FILETIME) -> i64 {
404+
/// FILETIME represents 100-nanosecond intervals
405+
const FT_TO_SECS: i64 = 10_000_000;
406+
((filetime.dwHighDateTime as i64) << 32 | filetime.dwLowDateTime as i64) / FT_TO_SECS
407+
}
408+
409+
/// Convert an `OffsetDateTime` to a `SYSTEMTIME`.
410+
fn offset_to_systemtime(datetime: OffsetDateTime) -> SYSTEMTIME {
411+
let (month, day_of_month) = datetime.to_offset(offset!(UTC)).month_day();
412+
SYSTEMTIME {
413+
wYear: datetime.year() as u16,
414+
wMonth: month as u16,
415+
wDay: day_of_month as u16,
416+
wDayOfWeek: 0, // ignored
417+
wHour: datetime.hour() as u16,
418+
wMinute: datetime.minute() as u16,
419+
wSecond: datetime.second() as u16,
420+
wMilliseconds: datetime.millisecond(),
421+
}
422+
}
423+
424+
// This function falls back to UTC if any system call fails.
425+
let systime_utc = offset_to_systemtime(datetime.to_offset(offset!(UTC)));
426+
427+
// Safety: `local_time` is only read if it is properly initialized, and
428+
// `SystemTimeToTzSpecificLocalTime` is thread-safe.
429+
#[allow(unsafe_code)]
430+
let systime_local = unsafe {
431+
#[allow(deprecated)]
432+
let mut local_time = mem::uninitialized();
433+
if 0 == SystemTimeToTzSpecificLocalTime(
434+
core::ptr::null(), // use system's current timezone
435+
&systime_utc,
436+
&mut local_time,
437+
) {
438+
// call failed
439+
return None;
440+
} else {
441+
local_time
442+
}
443+
};
444+
445+
// Convert SYSTEMTIMEs to FILETIMEs so we can perform arithmetic on them.
446+
let ft_system = systemtime_to_filetime(&systime_utc)?;
447+
let ft_local = systemtime_to_filetime(&systime_local)?;
448+
449+
let diff_secs = filetime_to_secs(&ft_local) - filetime_to_secs(&ft_system);
450+
451+
diff_secs.try_into().ok().map(UtcOffset::seconds)
452+
}
453+
}
454+
259455
#[cfg(test)]
260456
mod test {
261457
use super::*;

0 commit comments

Comments
 (0)