use crate::{
format::{parse, ParsedItems},
internal_prelude::*,
};
use core::fmt::{self, Display};
#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
serde,
serde(from = "crate::serde::UtcOffset", into = "crate::serde::UtcOffset")
)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct UtcOffset {
pub(crate) seconds: i32,
}
impl UtcOffset {
pub const UTC: Self = Self::seconds(0);
#[inline(always)]
pub const fn east_hours(hours: u8) -> Self {
Self::hours(hours as i8)
}
#[inline(always)]
pub const fn west_hours(hours: u8) -> Self {
Self::hours(-(hours as i8))
}
#[inline(always)]
pub const fn hours(hours: i8) -> Self {
Self::seconds(hours as i32 * 3_600)
}
#[inline(always)]
pub const fn east_minutes(minutes: u16) -> Self {
Self::minutes(minutes as i16)
}
#[inline(always)]
pub const fn west_minutes(minutes: u16) -> Self {
Self::minutes(-(minutes as i16))
}
#[inline(always)]
pub const fn minutes(minutes: i16) -> Self {
Self::seconds(minutes as i32 * 60)
}
#[inline(always)]
pub const fn east_seconds(seconds: u32) -> Self {
Self::seconds(seconds as i32)
}
#[inline(always)]
pub const fn west_seconds(seconds: u32) -> Self {
Self::seconds(-(seconds as i32))
}
#[inline(always)]
pub const fn seconds(seconds: i32) -> Self {
Self { seconds }
}
#[inline(always)]
pub const fn as_seconds(self) -> i32 {
self.seconds
}
#[inline(always)]
pub const fn as_minutes(self) -> i16 {
(self.as_seconds() / 60) as i16
}
#[inline(always)]
pub const fn as_hours(self) -> i8 {
(self.as_seconds() / 3_600) as i8
}
#[inline(always)]
pub(crate) const fn as_duration(self) -> Duration {
Duration::seconds(self.seconds as i64)
}
#[inline(always)]
#[cfg(std)]
pub fn local_offset_at(datetime: OffsetDateTime) -> Self {
try_local_offset_at(datetime).unwrap_or(Self::UTC)
}
#[inline(always)]
#[cfg(std)]
pub fn try_local_offset_at(datetime: OffsetDateTime) -> Result<Self, IndeterminateOffsetError> {
try_local_offset_at(datetime).ok_or_else(IndeterminateOffsetError::new)
}
#[inline(always)]
#[cfg(std)]
pub fn current_local_offset() -> Self {
let now = OffsetDateTime::now_utc();
try_local_offset_at(now).unwrap_or(Self::UTC)
}
#[inline(always)]
#[cfg(std)]
pub fn try_current_local_offset() -> Result<Self, IndeterminateOffsetError> {
let now = OffsetDateTime::now_utc();
try_local_offset_at(now).ok_or_else(IndeterminateOffsetError::new)
}
}
impl UtcOffset {
#[inline(always)]
pub fn format(self, format: impl AsRef<str>) -> String {
self.lazy_format(format).to_string()
}
#[inline(always)]
pub fn lazy_format(self, format: impl AsRef<str>) -> impl Display {
DeferredFormat::new(format.as_ref())
.with_offset(self)
.to_owned()
}
#[inline(always)]
pub fn parse(s: impl AsRef<str>, format: impl AsRef<str>) -> ParseResult<Self> {
Self::try_from_parsed_items(parse(s.as_ref(), &format.into())?)
}
#[inline(always)]
pub(crate) fn try_from_parsed_items(items: ParsedItems) -> ParseResult<Self> {
items.offset.ok_or(ParseError::InsufficientInformation)
}
}
impl Display for UtcOffset {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sign = if self.seconds < 0 { '-' } else { '+' };
let hours = self.as_hours().abs();
let minutes = self.as_minutes().abs() - hours as i16 * 60;
let seconds = self.as_seconds().abs() - hours as i32 * 3_600 - minutes as i32 * 60;
write!(f, "{}{}", sign, hours)?;
if minutes != 0 || seconds != 0 {
write!(f, ":{:02}", minutes)?;
}
if seconds != 0 {
write!(f, ":{:02}", seconds)?;
}
Ok(())
}
}
#[cfg(std)]
#[allow(clippy::too_many_lines)]
fn try_local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
cfg_if::cfg_if! {
if #[cfg(target_family = "unix")] {
use standback::mem::MaybeUninit;
fn timestamp_to_tm(timestamp: i64) -> Option<libc::tm> {
extern "C" {
fn tzset();
}
let timestamp = timestamp.try_into().ok()?;
let mut tm = MaybeUninit::uninit();
#[allow(unsafe_code)]
unsafe {
tzset();
}
#[allow(unsafe_code)]
let tm_ptr = unsafe { libc::localtime_r(×tamp, tm.as_mut_ptr()) };
if tm_ptr.is_null() {
None
} else {
#[allow(unsafe_code)]
{
Some(unsafe { tm.assume_init() })
}
}
}
let tm = timestamp_to_tm(datetime.timestamp())?;
#[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
{
tm.tm_gmtoff.try_into().ok().map(UtcOffset::seconds)
}
#[cfg(any(target_os = "solaris", target_os = "illumos"))]
{
use crate::Date;
let mut tm = tm;
if tm.tm_sec == 60 {
tm.tm_sec = 59;
}
let local_timestamp =
Date::try_from_yo(1900 + tm.tm_year, u16::try_from(tm.tm_yday).ok()? + 1)
.ok()?
.try_with_hms(
tm.tm_hour.try_into().ok()?,
tm.tm_min.try_into().ok()?,
tm.tm_sec.try_into().ok()?,
)
.ok()?
.assume_utc()
.timestamp();
(local_timestamp - datetime.timestamp())
.try_into()
.ok()
.map(UtcOffset::seconds)
}
} else if #[cfg(target_family = "windows")] {
use standback::mem::MaybeUninit;
use winapi::{
shared::minwindef::FILETIME,
um::{
minwinbase::SYSTEMTIME,
timezoneapi::{SystemTimeToFileTime, SystemTimeToTzSpecificLocalTime},
},
};
fn systemtime_to_filetime(systime: &SYSTEMTIME) -> Option<FILETIME> {
let mut ft = MaybeUninit::uninit();
#[allow(unsafe_code)]
{
if 0 == unsafe { SystemTimeToFileTime(systime, ft.as_mut_ptr()) } {
None
} else {
Some(unsafe { ft.assume_init() })
}
}
}
fn filetime_to_secs(filetime: &FILETIME) -> i64 {
const FT_TO_SECS: i64 = 10_000_000;
((filetime.dwHighDateTime as i64) << 32 | filetime.dwLowDateTime as i64) /
FT_TO_SECS
}
fn offset_to_systemtime(datetime: OffsetDateTime) -> SYSTEMTIME {
let (month, day_of_month) = datetime.to_offset(UtcOffset::UTC).month_day();
SYSTEMTIME {
wYear: datetime.year() as u16,
wMonth: month as u16,
wDay: day_of_month as u16,
wDayOfWeek: 0,
wHour: datetime.hour() as u16,
wMinute: datetime.minute() as u16,
wSecond: datetime.second() as u16,
wMilliseconds: datetime.millisecond(),
}
}
let systime_utc = offset_to_systemtime(datetime.to_offset(UtcOffset::UTC));
#[allow(unsafe_code)]
let systime_local = unsafe {
let mut local_time = MaybeUninit::uninit();
if 0 == SystemTimeToTzSpecificLocalTime(
core::ptr::null(),
&systime_utc,
local_time.as_mut_ptr(),
) {
return None;
} else {
local_time.assume_init()
}
};
let ft_system = systemtime_to_filetime(&systime_utc)?;
let ft_local = systemtime_to_filetime(&systime_local)?;
let diff_secs = filetime_to_secs(&ft_local) - filetime_to_secs(&ft_system);
diff_secs.try_into().ok().map(UtcOffset::seconds)
} else if #[cfg(cargo_web)] {
use stdweb::js;
let timestamp_utc = datetime.timestamp();
let low_bits = (timestamp_utc & 0xFF_FF_FF_FF) as i32;
let high_bits = (timestamp_utc >> 32) as i32;
let timezone_offset = js! {
return
new Date(((@{high_bits} << 32) + @{low_bits}) * 1000)
.getTimezoneOffset() * -60;
};
stdweb::unstable::TryInto::try_into(timezone_offset).ok().map(UtcOffset::seconds)
} else {
None
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn hours() {
assert_eq!(UtcOffset::hours(1).as_seconds(), 3_600);
assert_eq!(UtcOffset::hours(-1).as_seconds(), -3_600);
assert_eq!(UtcOffset::hours(23).as_seconds(), 82_800);
assert_eq!(UtcOffset::hours(-23).as_seconds(), -82_800);
}
#[test]
fn directional_hours() {
assert_eq!(UtcOffset::east_hours(1), offset!(+1));
assert_eq!(UtcOffset::west_hours(1), offset!(-1));
}
#[test]
fn minutes() {
assert_eq!(UtcOffset::minutes(1).as_seconds(), 60);
assert_eq!(UtcOffset::minutes(-1).as_seconds(), -60);
assert_eq!(UtcOffset::minutes(1_439).as_seconds(), 86_340);
assert_eq!(UtcOffset::minutes(-1_439).as_seconds(), -86_340);
}
#[test]
fn directional_minutes() {
assert_eq!(UtcOffset::east_minutes(1), offset!(+0:01));
assert_eq!(UtcOffset::west_minutes(1), offset!(-0:01));
}
#[test]
fn seconds() {
assert_eq!(UtcOffset::seconds(1).as_seconds(), 1);
assert_eq!(UtcOffset::seconds(-1).as_seconds(), -1);
assert_eq!(UtcOffset::seconds(86_399).as_seconds(), 86_399);
assert_eq!(UtcOffset::seconds(-86_399).as_seconds(), -86_399);
}
#[test]
fn directional_seconds() {
assert_eq!(UtcOffset::east_seconds(1), offset!(+0:00:01));
assert_eq!(UtcOffset::west_seconds(1), offset!(-0:00:01));
}
#[test]
fn as_hours() {
assert_eq!(offset!(+1).as_hours(), 1);
assert_eq!(offset!(+0:59).as_hours(), 0);
assert_eq!(offset!(-1).as_hours(), -1);
assert_eq!(offset!(-0:59).as_hours(), -0);
}
#[test]
fn as_minutes() {
assert_eq!(offset!(+1).as_minutes(), 60);
assert_eq!(offset!(+0:01).as_minutes(), 1);
assert_eq!(offset!(+0:00:59).as_minutes(), 0);
assert_eq!(offset!(-1).as_minutes(), -60);
assert_eq!(offset!(-0:01).as_minutes(), -1);
assert_eq!(offset!(-0:00:59).as_minutes(), 0);
}
#[test]
fn as_seconds() {
assert_eq!(offset!(+1).as_seconds(), 3_600);
assert_eq!(offset!(+0:01).as_seconds(), 60);
assert_eq!(offset!(+0:00:01).as_seconds(), 1);
assert_eq!(offset!(-1).as_seconds(), -3_600);
assert_eq!(offset!(-0:01).as_seconds(), -60);
assert_eq!(offset!(-0:00:01).as_seconds(), -1);
}
#[test]
fn as_duration() {
assert_eq!(offset!(+1).as_duration(), 1.hours());
assert_eq!(offset!(-1).as_duration(), (-1).hours());
}
#[test]
fn utc_is_zero() {
assert_eq!(UtcOffset::UTC, offset!(+0));
}
#[test]
fn format() {
assert_eq!(offset!(+1).format("%z"), "+0100");
assert_eq!(offset!(-1).format("%z"), "-0100");
assert_eq!(offset!(+0).format("%z"), "+0000");
assert_ne!(offset!(-0).format("%z"), "-0000");
assert_eq!(offset!(+0:01).format("%z"), "+0001");
assert_eq!(offset!(-0:01).format("%z"), "-0001");
assert_eq!(offset!(+0:00:01).format("%z"), "+0000");
assert_eq!(offset!(-0:00:01).format("%z"), "-0000");
}
#[test]
fn parse() {
assert_eq!(UtcOffset::parse("+0100", "%z"), Ok(offset!(+1)));
assert_eq!(UtcOffset::parse("-0100", "%z"), Ok(offset!(-1)));
assert_eq!(UtcOffset::parse("+0000", "%z"), Ok(offset!(+0)));
assert_eq!(UtcOffset::parse("-0000", "%z"), Ok(offset!(+0)));
assert_eq!(UtcOffset::parse("+0001", "%z"), Ok(offset!(+0:01)));
assert_eq!(UtcOffset::parse("-0001", "%z"), Ok(offset!(-0:01)));
}
#[test]
fn display() {
assert_eq!(offset!(UTC).to_string(), "+0");
assert_eq!(offset!(+0:00:01).to_string(), "+0:00:01");
assert_eq!(offset!(-0:00:01).to_string(), "-0:00:01");
assert_eq!(offset!(+1).to_string(), "+1");
assert_eq!(offset!(-1).to_string(), "-1");
assert_eq!(offset!(+23:59).to_string(), "+23:59");
assert_eq!(offset!(-23:59).to_string(), "-23:59");
assert_eq!(offset!(+23:59:59).to_string(), "+23:59:59");
assert_eq!(offset!(-23:59:59).to_string(), "-23:59:59");
}
}