use core::{
ops::{Add, AddAssign, Neg, Sub, SubAssign},
time::Duration as UnsignedDuration,
};
use crate::{
civil,
duration::{Duration, SDuration},
error::{tz::offset::Error as E, Error, ErrorContext},
shared::util::itime::IOffset,
span::Span,
timestamp::Timestamp,
tz::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned, TimeZone},
util::{array_str::ArrayStr, b, constant, round::Increment},
RoundMode, SignedDuration, Unit,
};
#[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: i32,
}
impl Offset {
pub const MIN: Offset = Offset { span: b::OffsetTotalSeconds::MIN };
pub const MAX: Offset = Offset { span: b::OffsetTotalSeconds::MAX };
pub const UTC: Offset = Offset::ZERO;
pub const ZERO: Offset = Offset::constant(0);
#[inline]
pub const fn constant(hours: i8) -> Offset {
let hours = constant::unwrapr!(
b::OffsetHours::checkc(hours as i64),
"invalid time zone offset hours",
);
Offset::constant_seconds((hours as i32) * 60 * 60)
}
#[inline]
pub(crate) const fn constant_seconds(seconds: i32) -> Offset {
let span = constant::unwrapr!(
b::OffsetTotalSeconds::checkc(seconds as i64),
"invalid time zone offset seconds",
);
Offset { span }
}
#[inline]
pub fn from_hours(hours: i8) -> Result<Offset, Error> {
Offset::from_seconds(i32::from(hours) * b::SECS_PER_HOUR_32)
}
#[inline]
pub fn from_seconds(seconds: i32) -> Result<Offset, Error> {
let span = b::OffsetTotalSeconds::check(seconds)?;
Ok(Offset::from_seconds_unchecked(span))
}
#[inline]
pub const fn seconds(self) -> i32 {
self.span
}
pub fn negate(self) -> Offset {
Offset { span: -self.span }
}
#[inline]
pub fn signum(self) -> i8 {
b::Sign::from(self.seconds()).as_i8()
}
pub fn is_positive(self) -> bool {
self.seconds() > 0
}
pub fn is_negative(self) -> bool {
self.seconds() < 0
}
pub fn is_zero(self) -> bool {
self.seconds() == 0
}
pub fn to_time_zone(self) -> TimeZone {
TimeZone::fixed(self)
}
#[inline]
pub fn to_datetime(self, timestamp: Timestamp) -> civil::DateTime {
civil::DateTime::from_idatetime_const(
timestamp
.to_itimestamp_const()
.to_datetime(IOffset { second: self.seconds() }),
)
}
#[inline]
pub fn to_timestamp(
self,
dt: civil::DateTime,
) -> Result<Timestamp, Error> {
let its =
dt.to_idatetime_const().to_timestamp(self.to_ioffset_const());
Timestamp::new(its.second, its.nanosecond)
.context(E::ConvertDateTimeToTimestamp { offset: 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 = b::OffsetTotalSeconds::check(
span.to_invariant_duration().as_secs(),
)?;
Offset::from_seconds(span + self.seconds())
}
#[inline]
fn checked_add_duration(
self,
duration: SignedDuration,
) -> Result<Offset, Error> {
let duration = b::OffsetTotalSeconds::check(duration.as_secs())
.context(E::OverflowAddSignedDuration)?;
Offset::from_seconds(duration + self.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 {
let diff = other.seconds() - self.seconds();
Span::new().seconds(diff)
}
#[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 fn round<R: Into<OffsetRound>>(
self,
options: R,
) -> Result<Offset, Error> {
let options: OffsetRound = options.into();
options.round(self)
}
}
impl Offset {
#[cfg(test)]
#[inline]
pub(crate) const fn hms(hours: i8, minutes: i8, seconds: i8) -> Offset {
let hours = constant::unwrapr!(
b::OffsetHours::checkc(hours as i64),
"invalid time zone offset hours",
);
let minutes = constant::unwrapr!(
b::OffsetMinutes::checkc(minutes as i64),
"invalid time zone offset minutes",
);
let seconds = constant::unwrapr!(
b::OffsetSeconds::checkc(seconds as i64),
"invalid time zone offset seconds",
);
let span = (hours as i32 * b::SECS_PER_HOUR_32)
+ (minutes as i32 * b::SECS_PER_MIN_32)
+ (seconds as i32);
Offset { span }
}
#[inline]
pub(crate) fn part_hours(self) -> i8 {
(self.seconds() / b::SECS_PER_HOUR_32) as i8
}
#[inline]
pub(crate) fn part_minutes(self) -> i8 {
((self.seconds() / b::SECS_PER_MIN_32) % b::MINS_PER_HOUR_32) as i8
}
#[inline]
pub(crate) fn part_seconds(self) -> i8 {
(self.seconds() % b::SECS_PER_MIN_32) as i8
}
#[inline]
const fn to_ioffset_const(self) -> IOffset {
IOffset { second: self.span }
}
#[inline]
pub(crate) const fn from_ioffset_const(ioff: IOffset) -> Offset {
Offset::from_seconds_unchecked(ioff.second)
}
#[inline]
pub(crate) const fn from_seconds_unchecked(second: i32) -> Offset {
Offset { span: second }
}
#[inline]
pub(crate) fn to_array_str(&self) -> ArrayStr<9> {
use core::fmt::Write;
let mut dst = ArrayStr::new("").unwrap();
write!(&mut dst, "{}", self).unwrap();
dst
}
#[inline]
pub(crate) fn round_to_nearest_minute(self) -> (u8, u8) {
#[inline(never)]
#[cold]
fn round(mut hours: u8, mut minutes: u8) -> (u8, u8) {
const MAX_HOURS: u8 = b::OffsetHours::MAX.unsigned_abs();
const MAX_MINS: u8 = b::OffsetMinutes::MAX.unsigned_abs();
if minutes == 59 {
hours += 1;
minutes = 0;
if hours > MAX_HOURS {
hours = MAX_HOURS;
minutes = MAX_MINS;
}
} else {
minutes += 1;
}
(hours, minutes)
}
let total_seconds = self.seconds().unsigned_abs();
let hours = (total_seconds / (60 * 60)) as u8;
let minutes = ((total_seconds / 60) % 60) as u8;
let seconds = (total_seconds % 60) as u8;
if seconds >= 30 {
return round(hours, minutes);
}
(hours, minutes)
}
}
impl core::fmt::Debug for Offset {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let sign = if self.is_negative() { "-" } else { "" };
write!(
f,
"{sign}{:02}:{:02}:{:02}",
self.part_hours().unsigned_abs(),
self.part_minutes().unsigned_abs(),
self.part_seconds().unsigned_abs(),
)
}
}
impl core::fmt::Display for Offset {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let sign = if self.is_negative() { "-" } else { "+" };
let hours = self.part_hours().unsigned_abs();
let minutes = self.part_minutes().unsigned_abs();
let seconds = self.part_seconds().unsigned_abs();
if hours == 0 && minutes == 0 && seconds == 0 {
f.write_str("+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()
}
}
impl TryFrom<SignedDuration> for Offset {
type Error = Error;
fn try_from(sdur: SignedDuration) -> Result<Offset, Error> {
let mut seconds = sdur.as_secs();
let subsec = sdur.subsec_nanos();
if subsec >= 500_000_000 {
seconds = seconds.saturating_add(1);
} else if subsec <= -500_000_000 {
seconds = seconds.saturating_sub(1);
}
let seconds =
i32::try_from(seconds).map_err(|_| E::OverflowSignedDuration)?;
Offset::from_seconds(seconds)
.map_err(|_| Error::from(E::OverflowSignedDuration))
}
}
#[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)]
pub struct OffsetRound {
smallest: Unit,
mode: RoundMode,
increment: i64,
}
impl OffsetRound {
#[inline]
pub fn new() -> OffsetRound {
OffsetRound {
smallest: Unit::Second,
mode: RoundMode::HalfExpand,
increment: 1,
}
}
#[inline]
pub fn smallest(self, unit: Unit) -> OffsetRound {
OffsetRound { smallest: unit, ..self }
}
#[inline]
pub fn mode(self, mode: RoundMode) -> OffsetRound {
OffsetRound { mode, ..self }
}
#[inline]
pub fn increment(self, increment: i64) -> OffsetRound {
OffsetRound { increment, ..self }
}
fn round(&self, offset: Offset) -> Result<Offset, Error> {
let increment = Increment::for_offset(self.smallest, self.increment)?;
let rounded = increment
.round(self.mode, SignedDuration::from(offset))
.context(E::RoundOverflow)?;
Offset::try_from(rounded)
.map_err(|_| b::OffsetTotalSeconds::error())
.context(E::RoundOverflow)
}
}
impl Default for OffsetRound {
fn default() -> OffsetRound {
OffsetRound::new()
}
}
impl From<Unit> for OffsetRound {
fn from(unit: Unit) -> OffsetRound {
OffsetRound::default().smallest(unit)
}
}
impl From<(Unit, i64)> for OffsetRound {
fn from((unit, increment): (Unit, i64)) -> OffsetRound {
OffsetRound::default().smallest(unit).increment(increment)
}
}
#[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> {
self.resolve_with(dt, offset, tz, |off1, off2| off1 == off2)
}
pub fn resolve_with<F>(
self,
dt: civil::DateTime,
offset: Offset,
tz: TimeZone,
is_equal: F,
) -> Result<AmbiguousZoned, Error>
where
F: FnMut(Offset, Offset) -> bool,
{
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, is_equal),
),
OffsetConflict::Reject => {
OffsetConflict::resolve_via_reject(dt, offset, tz, is_equal)
}
}
}
fn resolve_via_prefer(
dt: civil::DateTime,
given: Offset,
tz: TimeZone,
mut is_equal: impl FnMut(Offset, Offset) -> bool,
) -> AmbiguousZoned {
use crate::tz::AmbiguousOffset::*;
let amb = tz.to_ambiguous_timestamp(dt);
match amb.offset() {
Fold { before, after }
if is_equal(given, before) || is_equal(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,
mut is_equal: impl FnMut(Offset, Offset) -> bool,
) -> Result<AmbiguousZoned, Error> {
use crate::tz::AmbiguousOffset::*;
let amb = tz.to_ambiguous_timestamp(dt);
match amb.offset() {
Unambiguous { offset } if !is_equal(given, offset) => {
Err(Error::from(E::ResolveRejectUnambiguous {
given,
offset,
tz,
}))
}
Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)),
Gap { before, after } => {
Err(Error::from(E::ResolveRejectGap {
given,
before,
after,
tz,
}))
}
Fold { before, after }
if !is_equal(given, before) && !is_equal(given, after) =>
{
Err(Error::from(E::ResolveRejectFold {
given,
before,
after,
tz,
}))
}
Fold { .. } => {
let kind = Unambiguous { offset: given };
Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
}
}
}
}