use core::{
ops::{Add, AddAssign, Neg, Sub, SubAssign},
time::Duration as UnsignedDuration,
};
use crate::{
civil,
duration::{Duration, SDuration},
error::{err, Error, ErrorContext},
span::Span,
timestamp::Timestamp,
tz::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned, TimeZone},
util::{
rangeint::{RFrom, RInto, TryRFrom},
t::{self, C},
},
SignedDuration,
};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum Dst {
No,
Yes,
}
impl Dst {
pub fn is_dst(self) -> bool {
matches!(self, Dst::Yes)
}
pub fn is_std(self) -> bool {
matches!(self, Dst::No)
}
}
impl From<bool> for Dst {
fn from(is_dst: bool) -> Dst {
if is_dst {
Dst::Yes
} else {
Dst::No
}
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct Offset {
span: t::SpanZoneOffset,
}
impl Offset {
pub const MIN: Offset = Offset { span: t::SpanZoneOffset::MIN_SELF };
pub const MAX: Offset = Offset { span: t::SpanZoneOffset::MAX_SELF };
pub const UTC: Offset = Offset::ZERO;
pub const ZERO: Offset = Offset::constant(0);
#[inline]
pub const fn constant(hours: i8) -> Offset {
if !t::SpanZoneOffsetHours::contains(hours) {
panic!("invalid time zone offset hours")
}
Offset::constant_seconds((hours as i32) * 60 * 60)
}
#[inline]
const fn constant_seconds(seconds: i32) -> Offset {
if !t::SpanZoneOffset::contains(seconds) {
panic!("invalid time zone offset seconds")
}
Offset { span: t::SpanZoneOffset::new_unchecked(seconds) }
}
#[inline]
pub fn from_hours(hours: i8) -> Result<Offset, Error> {
let hours = t::SpanZoneOffsetHours::try_new("offset-hours", hours)?;
Ok(Offset::from_hours_ranged(hours))
}
#[inline]
pub fn from_seconds(seconds: i32) -> Result<Offset, Error> {
let seconds = t::SpanZoneOffset::try_new("offset-seconds", seconds)?;
Ok(Offset::from_seconds_ranged(seconds))
}
#[inline]
pub fn seconds(self) -> i32 {
self.seconds_ranged().get()
}
pub fn negate(self) -> Offset {
Offset { span: -self.span }
}
pub fn is_negative(self) -> bool {
self.seconds_ranged() < 0
}
pub fn to_time_zone(self) -> TimeZone {
TimeZone::fixed(self)
}
#[inline]
pub fn to_datetime(self, timestamp: Timestamp) -> civil::DateTime {
timestamp_to_datetime_zulu(timestamp, self)
}
#[inline]
pub fn to_timestamp(
self,
dt: civil::DateTime,
) -> Result<Timestamp, Error> {
datetime_zulu_to_timestamp(dt, self)
}
#[inline]
pub fn checked_add<A: Into<OffsetArithmetic>>(
self,
duration: A,
) -> Result<Offset, Error> {
let duration: OffsetArithmetic = duration.into();
duration.checked_add(self)
}
#[inline]
fn checked_add_span(self, span: Span) -> Result<Offset, Error> {
if let Some(err) = span.smallest_non_time_non_zero_unit_error() {
return Err(err);
}
let span_seconds = t::SpanZoneOffset::try_rfrom(
"span-seconds",
span.to_invariant_nanoseconds().div_ceil(t::NANOS_PER_SECOND),
)?;
let offset_seconds = self.seconds_ranged();
let seconds =
offset_seconds.try_checked_add("offset-seconds", span_seconds)?;
Ok(Offset::from_seconds_ranged(seconds))
}
#[inline]
fn checked_add_duration(
self,
duration: SignedDuration,
) -> Result<Offset, Error> {
let duration =
t::SpanZoneOffset::try_new("duration-seconds", duration.as_secs())
.with_context(|| {
err!(
"adding signed duration {duration:?} \
to offset {self} overflowed maximum offset seconds"
)
})?;
let offset_seconds = self.seconds_ranged();
let seconds = offset_seconds
.try_checked_add("offset-seconds", duration)
.with_context(|| {
err!(
"adding signed duration {duration:?} \
to offset {self} overflowed"
)
})?;
Ok(Offset::from_seconds_ranged(seconds))
}
#[inline]
pub fn checked_sub<A: Into<OffsetArithmetic>>(
self,
duration: A,
) -> Result<Offset, Error> {
let duration: OffsetArithmetic = duration.into();
duration.checked_neg().and_then(|oa| oa.checked_add(self))
}
#[inline]
pub fn saturating_add<A: Into<OffsetArithmetic>>(
self,
duration: A,
) -> Offset {
let duration: OffsetArithmetic = duration.into();
self.checked_add(duration).unwrap_or_else(|_| {
if duration.is_negative() {
Offset::MIN
} else {
Offset::MAX
}
})
}
#[inline]
pub fn saturating_sub<A: Into<OffsetArithmetic>>(
self,
duration: A,
) -> Offset {
let duration: OffsetArithmetic = duration.into();
let Ok(duration) = duration.checked_neg() else { return Offset::MIN };
self.saturating_add(duration)
}
#[inline]
pub fn until(self, other: Offset) -> Span {
Span::new()
.seconds_ranged(other.seconds_ranged() - self.seconds_ranged())
}
#[inline]
pub fn since(self, other: Offset) -> Span {
self.until(other).negate()
}
#[inline]
pub fn duration_until(self, other: Offset) -> SignedDuration {
SignedDuration::offset_until(self, other)
}
#[inline]
pub fn duration_since(self, other: Offset) -> SignedDuration {
SignedDuration::offset_until(other, self)
}
#[inline]
pub(crate) fn to_duration(self) -> SignedDuration {
SignedDuration::from_secs(i64::from(self.seconds()))
}
}
impl Offset {
#[cfg(test)]
#[inline]
pub(crate) const fn hms(hours: i8, minutes: i8, seconds: i8) -> Offset {
let total = (hours as i32 * 60 * 60)
+ (minutes as i32 * 60)
+ (seconds as i32);
Offset { span: t::SpanZoneOffset::new_unchecked(total) }
}
#[inline]
pub(crate) fn from_hours_ranged(
hours: impl RInto<t::SpanZoneOffsetHours>,
) -> Offset {
let hours: t::SpanZoneOffset = hours.rinto().rinto();
Offset::from_seconds_ranged(hours * t::SECONDS_PER_HOUR)
}
#[inline]
pub(crate) fn from_seconds_ranged(
seconds: impl RInto<t::SpanZoneOffset>,
) -> Offset {
Offset { span: seconds.rinto() }
}
#[inline]
pub(crate) fn seconds_ranged(self) -> t::SpanZoneOffset {
self.span
}
#[inline]
pub(crate) fn part_hours_ranged(self) -> t::SpanZoneOffsetHours {
self.span.div_ceil(t::SECONDS_PER_HOUR).rinto()
}
#[inline]
pub(crate) fn part_minutes_ranged(self) -> t::SpanZoneOffsetMinutes {
self.span
.div_ceil(t::SECONDS_PER_MINUTE)
.rem_ceil(t::MINUTES_PER_HOUR)
.rinto()
}
#[inline]
pub(crate) fn part_seconds_ranged(self) -> t::SpanZoneOffsetSeconds {
self.span.rem_ceil(t::SECONDS_PER_MINUTE).rinto()
}
}
impl core::fmt::Debug for Offset {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let sign = if self.seconds_ranged() < 0 { "-" } else { "" };
write!(
f,
"{sign}{:02}:{:02}:{:02}",
self.part_hours_ranged().abs(),
self.part_minutes_ranged().abs(),
self.part_seconds_ranged().abs(),
)
}
}
impl core::fmt::Display for Offset {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let sign = if self.span < 0 { "-" } else { "+" };
let hours = self.part_hours_ranged().abs().get();
let minutes = self.part_minutes_ranged().abs().get();
let seconds = self.part_seconds_ranged().abs().get();
if hours == 0 && minutes == 0 && seconds == 0 {
write!(f, "+00")
} else if hours != 0 && minutes == 0 && seconds == 0 {
write!(f, "{sign}{hours:02}")
} else if minutes != 0 && seconds == 0 {
write!(f, "{sign}{hours:02}:{minutes:02}")
} else {
write!(f, "{sign}{hours:02}:{minutes:02}:{seconds:02}")
}
}
}
impl Add<Span> for Offset {
type Output = Offset;
#[inline]
fn add(self, rhs: Span) -> Offset {
self.checked_add(rhs)
.expect("adding span to offset should not overflow")
}
}
impl AddAssign<Span> for Offset {
#[inline]
fn add_assign(&mut self, rhs: Span) {
*self = self.add(rhs);
}
}
impl Sub<Span> for Offset {
type Output = Offset;
#[inline]
fn sub(self, rhs: Span) -> Offset {
self.checked_sub(rhs)
.expect("subtracting span from offsetsshould not overflow")
}
}
impl SubAssign<Span> for Offset {
#[inline]
fn sub_assign(&mut self, rhs: Span) {
*self = self.sub(rhs);
}
}
impl Sub for Offset {
type Output = Span;
#[inline]
fn sub(self, rhs: Offset) -> Span {
self.since(rhs)
}
}
impl Add<SignedDuration> for Offset {
type Output = Offset;
#[inline]
fn add(self, rhs: SignedDuration) -> Offset {
self.checked_add(rhs)
.expect("adding signed duration to offset should not overflow")
}
}
impl AddAssign<SignedDuration> for Offset {
#[inline]
fn add_assign(&mut self, rhs: SignedDuration) {
*self = self.add(rhs);
}
}
impl Sub<SignedDuration> for Offset {
type Output = Offset;
#[inline]
fn sub(self, rhs: SignedDuration) -> Offset {
self.checked_sub(rhs).expect(
"subtracting signed duration from offsetsshould not overflow",
)
}
}
impl SubAssign<SignedDuration> for Offset {
#[inline]
fn sub_assign(&mut self, rhs: SignedDuration) {
*self = self.sub(rhs);
}
}
impl Add<UnsignedDuration> for Offset {
type Output = Offset;
#[inline]
fn add(self, rhs: UnsignedDuration) -> Offset {
self.checked_add(rhs)
.expect("adding unsigned duration to offset should not overflow")
}
}
impl AddAssign<UnsignedDuration> for Offset {
#[inline]
fn add_assign(&mut self, rhs: UnsignedDuration) {
*self = self.add(rhs);
}
}
impl Sub<UnsignedDuration> for Offset {
type Output = Offset;
#[inline]
fn sub(self, rhs: UnsignedDuration) -> Offset {
self.checked_sub(rhs).expect(
"subtracting unsigned duration from offsetsshould not overflow",
)
}
}
impl SubAssign<UnsignedDuration> for Offset {
#[inline]
fn sub_assign(&mut self, rhs: UnsignedDuration) {
*self = self.sub(rhs);
}
}
impl Neg for Offset {
type Output = Offset;
#[inline]
fn neg(self) -> Offset {
self.negate()
}
}
#[derive(Clone, Copy, Debug)]
pub struct OffsetArithmetic {
duration: Duration,
}
impl OffsetArithmetic {
#[inline]
fn checked_add(self, offset: Offset) -> Result<Offset, Error> {
match self.duration.to_signed()? {
SDuration::Span(span) => offset.checked_add_span(span),
SDuration::Absolute(sdur) => offset.checked_add_duration(sdur),
}
}
#[inline]
fn checked_neg(self) -> Result<OffsetArithmetic, Error> {
let duration = self.duration.checked_neg()?;
Ok(OffsetArithmetic { duration })
}
#[inline]
fn is_negative(&self) -> bool {
self.duration.is_negative()
}
}
impl From<Span> for OffsetArithmetic {
fn from(span: Span) -> OffsetArithmetic {
let duration = Duration::from(span);
OffsetArithmetic { duration }
}
}
impl From<SignedDuration> for OffsetArithmetic {
fn from(sdur: SignedDuration) -> OffsetArithmetic {
let duration = Duration::from(sdur);
OffsetArithmetic { duration }
}
}
impl From<UnsignedDuration> for OffsetArithmetic {
fn from(udur: UnsignedDuration) -> OffsetArithmetic {
let duration = Duration::from(udur);
OffsetArithmetic { duration }
}
}
impl<'a> From<&'a Span> for OffsetArithmetic {
fn from(span: &'a Span) -> OffsetArithmetic {
OffsetArithmetic::from(*span)
}
}
impl<'a> From<&'a SignedDuration> for OffsetArithmetic {
fn from(sdur: &'a SignedDuration) -> OffsetArithmetic {
OffsetArithmetic::from(*sdur)
}
}
impl<'a> From<&'a UnsignedDuration> for OffsetArithmetic {
fn from(udur: &'a UnsignedDuration) -> OffsetArithmetic {
OffsetArithmetic::from(*udur)
}
}
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub enum OffsetConflict {
AlwaysOffset,
AlwaysTimeZone,
PreferOffset,
#[default]
Reject,
}
impl OffsetConflict {
pub fn resolve(
self,
dt: civil::DateTime,
offset: Offset,
tz: TimeZone,
) -> Result<AmbiguousZoned, Error> {
match self {
OffsetConflict::AlwaysOffset => {
let kind = AmbiguousOffset::Unambiguous { offset };
Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
}
OffsetConflict::AlwaysTimeZone => Ok(tz.into_ambiguous_zoned(dt)),
OffsetConflict::PreferOffset => {
Ok(OffsetConflict::resolve_via_prefer(dt, offset, tz))
}
OffsetConflict::Reject => {
OffsetConflict::resolve_via_reject(dt, offset, tz)
}
}
}
fn resolve_via_prefer(
dt: civil::DateTime,
given: Offset,
tz: TimeZone,
) -> AmbiguousZoned {
use crate::tz::AmbiguousOffset::*;
let amb = tz.to_ambiguous_timestamp(dt);
match amb.offset() {
Gap { before, after } | Fold { before, after }
if given == before || given == after =>
{
let kind = Unambiguous { offset: given };
AmbiguousTimestamp::new(dt, kind)
}
_ => amb,
}
.into_ambiguous_zoned(tz)
}
fn resolve_via_reject(
dt: civil::DateTime,
given: Offset,
tz: TimeZone,
) -> Result<AmbiguousZoned, Error> {
use crate::tz::AmbiguousOffset::*;
let amb = tz.to_ambiguous_timestamp(dt);
match amb.offset() {
Unambiguous { offset } if given != offset => Err(err!(
"datetime {dt} could not resolve to a timestamp since \
'reject' conflict resolution was chosen, and because \
datetime has offset {given}, but the time zone {tzname} for \
the given datetime unambiguously has offset {offset}",
tzname = tz.diagnostic_name(),
)),
Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)),
Gap { before, after } if given != before && given != after => {
Err(err!(
"datetime {dt} could not resolve to timestamp \
since 'reject' conflict resolution was chosen, and \
because datetime has offset {given}, but the time \
zone {tzname} for the given datetime falls in a gap \
between offsets {before} and {after}, neither of which \
match the offset",
tzname = tz.diagnostic_name(),
))
}
Fold { before, after } if given != before && given != after => {
Err(err!(
"datetime {dt} could not resolve to timestamp \
since 'reject' conflict resolution was chosen, and \
because datetime has offset {given}, but the time \
zone {tzname} for the given datetime falls in a fold \
between offsets {before} and {after}, neither of which \
match the offset",
tzname = tz.diagnostic_name(),
))
}
Gap { .. } | Fold { .. } => {
let kind = Unambiguous { offset: given };
Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
}
}
}
}
fn timestamp_to_datetime_zulu(
timestamp: Timestamp,
offset: Offset,
) -> civil::DateTime {
let mut second = timestamp.as_second_ranged();
let nanosecond = timestamp.subsec_nanosecond_ranged();
second += offset.seconds_ranged();
let day = second.without_bounds().div_floor(t::SECONDS_PER_CIVIL_DAY);
let second: t::NoUnits =
second.without_bounds().rem_floor(t::SECONDS_PER_CIVIL_DAY);
let nanosecond = nanosecond.without_bounds();
let [delta_day, delta_second, delta_nanosecond] = t::NoUnits::vary_many(
[day, second, nanosecond],
|[_, second, nanosecond]| {
if nanosecond >= 0 {
[C(0), C(0), C(0)]
} else if second > 0 {
[C(0), C(-1), t::NANOS_PER_SECOND.rinto()]
} else {
[
C(-1),
t::SECONDS_PER_CIVIL_DAY - C(1),
t::NANOS_PER_SECOND.rinto(),
]
}
},
);
let day = t::UnixEpochDays::rfrom(day)
.try_checked_add("day", delta_day)
.unwrap();
let second = (second + delta_second) * t::NANOS_PER_SECOND;
let nanosecond = nanosecond + delta_nanosecond;
let civil_day_nanosecond = second + nanosecond;
let date = civil::Date::from_unix_epoch_days(day);
let time = civil::Time::from_nanosecond(civil_day_nanosecond);
civil::DateTime::from_parts(date, time)
}
fn datetime_zulu_to_timestamp(
dt: civil::DateTime,
offset: Offset,
) -> Result<Timestamp, Error> {
let (date, time) = (dt.date(), dt.time());
let day = date.to_unix_epoch_days().without_bounds();
let civil_day_nanosecond = time.to_nanosecond().without_bounds();
let second = civil_day_nanosecond.div_floor(t::NANOS_PER_SECOND);
let nanosecond = civil_day_nanosecond.rem_floor(t::NANOS_PER_SECOND);
let [delta_day, delta_second, delta_nanosecond] = t::NoUnits::vary_many(
[day, second, nanosecond],
|[day, _second, nanosecond]| {
if day >= 0 || nanosecond == 0 {
[C(0), C(0), C(0)]
} else {
[
C(1),
-(t::SECONDS_PER_CIVIL_DAY - C(1)),
(-t::NANOS_PER_SECOND).rinto(),
]
}
},
);
let day = day + delta_day;
let second = day * t::SECONDS_PER_CIVIL_DAY + second + delta_second;
let second = t::UnixSeconds::rfrom(second)
.try_checked_sub("offset-second", offset.seconds_ranged())
.with_context(|| {
err!(
"converting {dt} with offset {offset} to timestamp overflowed \
(second={second}, nanosecond={nanosecond})",
)
})?;
let nanosecond = nanosecond + delta_nanosecond;
Timestamp::new_ranged(second, nanosecond).with_context(|| {
err!(
"converting {dt} with offset {offset} to timestamp overflowed \
(second={second}, nanosecond={nanosecond})",
)
})
}