use crate::{
bounds::{self as b, RangeError},
civil::{self, DateTime},
constants as c,
macros::{rtry, unwrapr},
Timestamp,
};
#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct Offset {
seconds: i32,
}
impl Offset {
pub const MIN: Offset = Offset { seconds: b::OffsetTotalSeconds::MIN };
pub const MAX: Offset = Offset { seconds: b::OffsetTotalSeconds::MAX };
pub const UTC: Offset = Offset { seconds: 0 };
pub const ZERO: Offset = Offset { seconds: 0 };
#[inline]
pub const fn constant(hours: i8) -> Offset {
unwrapr!(Offset::from_hours(hours), "invalid time zone offset hours")
}
#[inline]
pub const fn constant_seconds(seconds: i32) -> Offset {
unwrapr!(
Offset::from_seconds(seconds),
"invalid time zone offset seconds",
)
}
#[inline]
pub const fn from_hours(hours: i8) -> Result<Offset, RangeError> {
Offset::from_seconds(hours as i32 * c::SECS_PER_HOUR_32)
}
#[inline]
pub const fn from_seconds(seconds: i32) -> Result<Offset, RangeError> {
let seconds = rtry!(b::OffsetTotalSeconds::checkc(seconds as i64));
Ok(Offset { seconds })
}
#[inline]
pub const fn seconds(self) -> i32 {
self.seconds
}
#[inline]
pub const fn negate(self) -> Offset {
Offset { seconds: -self.seconds() }
}
#[inline]
pub const fn signum(self) -> i8 {
self.seconds().signum() as i8
}
#[inline]
pub const fn is_positive(self) -> bool {
self.seconds() > 0
}
#[inline]
pub const fn is_negative(self) -> bool {
self.seconds() < 0
}
#[inline]
pub const fn is_zero(self) -> bool {
self.seconds() == 0
}
#[inline]
pub const fn checked_add(
self,
seconds: i32,
) -> Result<Offset, RangeError> {
let seconds =
rtry!(b::OffsetTotalSeconds::checked_add(self.seconds(), seconds));
Ok(Offset { seconds })
}
#[inline]
pub const fn checked_sub(
self,
seconds: i32,
) -> Result<Offset, RangeError> {
let seconds =
rtry!(b::OffsetTotalSeconds::checked_add(self.seconds(), seconds));
Ok(Offset { seconds })
}
#[inline]
pub const fn until(self, other: Offset) -> i32 {
other.seconds() - self.seconds()
}
#[inline]
pub const fn since(self, other: Offset) -> i32 {
self.seconds() - other.seconds()
}
#[inline]
pub const fn to_datetime(self, timestamp: Timestamp) -> civil::DateTime {
let offset = self;
let second = timestamp.as_second();
let mut nanosecond = timestamp.subsec_nanosecond();
const DAY_SHIFT: i32 = 30 * 146097;
const SEC_SHIFT: i64 = (DAY_SHIFT as i64) * 86_400;
let pos_sec = (second + (offset.seconds() as i64) + SEC_SHIFT) as u64;
let mut epoch_day = (pos_sec / 86_400) as i32;
let mut second = (pos_sec % 86_400) as i32;
if nanosecond < 0 {
if second > 0 {
second -= 1;
nanosecond += 1_000_000_000;
} else {
epoch_day -= 1;
second += 86_399;
nanosecond += 1_000_000_000;
}
}
epoch_day -= DAY_SHIFT;
let date = unwrapr!(
civil::UnixEpochDay::new(epoch_day),
"always valid Unix epoch day",
)
.to_date();
let time = unwrapr!(
unwrapr!(
civil::TimeSecond::new(second),
"always valid civil second time"
)
.to_time()
.with_subsec_nanosecond(nanosecond),
"always valid civil subsecond"
);
civil::DateTime::from_parts(date, time)
}
#[inline]
pub const fn to_timestamp(
self,
dt: civil::DateTime,
) -> Result<Timestamp, RangeError> {
let offset = self;
let epoch_day = dt.date().to_unix_epoch_day().day();
let mut second = (epoch_day as i64) * c::SECS_PER_CIVIL_DAY
+ (dt.time().to_second().second() as i64);
let mut nanosecond = dt.time().subsec_nanosecond();
second -= offset.seconds() as i64;
if second < 0 && nanosecond != 0 {
second += 1;
nanosecond -= c::NANOS_PER_SEC_32;
}
let second = rtry!(b::UnixEpochSeconds::checkc(second));
Ok(Timestamp::new_unchecked(second, nanosecond))
}
}
impl Offset {
#[inline]
fn part_hours(self) -> i8 {
(self.seconds() / c::SECS_PER_HOUR_32) as i8
}
#[inline]
fn part_minutes(self) -> i8 {
((self.seconds() / c::SECS_PER_MIN_32) % c::MINS_PER_HOUR_32) as i8
}
#[inline]
fn part_seconds(self) -> i8 {
(self.seconds() % c::SECS_PER_MIN_32) as i8
}
}
impl core::ops::Neg for Offset {
type Output = Offset;
#[inline]
fn neg(self) -> Offset {
self.negate()
}
}
impl core::ops::Add<i32> for Offset {
type Output = Offset;
fn add(self, seconds: i32) -> Offset {
self.checked_add(seconds).unwrap()
}
}
impl core::ops::AddAssign<i32> for Offset {
#[inline]
fn add_assign(&mut self, rhs: i32) {
*self = *self + rhs;
}
}
impl core::ops::Sub<i32> for Offset {
type Output = Offset;
fn sub(self, seconds: i32) -> Offset {
self.checked_sub(seconds).unwrap()
}
}
impl core::ops::SubAssign<i32> for Offset {
#[inline]
fn sub_assign(&mut self, rhs: i32) {
*self = *self - rhs;
}
}
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}")
}
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for Offset {
fn format(&self, f: defmt::Formatter) {
let sign = if self.is_negative() { "-" } else { "" };
defmt::write!(
f,
"{=str}{=u8:02}:{=u8:02}:{=u8:02}",
sign,
self.part_hours().unsigned_abs(),
self.part_minutes().unsigned_abs(),
self.part_seconds().unsigned_abs(),
)
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for Offset {
fn arbitrary(g: &mut quickcheck::Gen) -> Offset {
let secs = b::OffsetTotalSeconds::arbitrary(g);
Offset::from_seconds(secs).unwrap_or(Offset::UTC)
}
fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = Self>> {
let secs = self.seconds();
alloc::boxed::Box::new(secs.shrink().filter_map(|secs| {
let secs = b::OffsetTotalSeconds::check(secs).ok()?;
Offset::from_seconds(secs).ok()
}))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum AmbiguousOffset {
Unambiguous {
offset: Offset,
},
Gap {
before: Offset,
after: Offset,
},
Fold {
before: Offset,
after: Offset,
},
}
impl AmbiguousOffset {
#[inline]
pub(crate) const fn into_ambiguous_timestamp(
self,
dt: DateTime,
) -> AmbiguousTimestamp {
AmbiguousTimestamp { dt, offset: self }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct AmbiguousTimestamp {
dt: DateTime,
offset: AmbiguousOffset,
}
impl AmbiguousTimestamp {
#[inline]
pub const fn datetime(&self) -> DateTime {
self.dt
}
#[inline]
pub const fn offset(&self) -> AmbiguousOffset {
self.offset
}
#[inline]
pub const fn is_ambiguous(&self) -> bool {
!matches!(self.offset(), AmbiguousOffset::Unambiguous { .. })
}
#[inline]
pub const fn compatible(self) -> Result<Timestamp, RangeError> {
let offset = match self.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { before, .. } => before,
AmbiguousOffset::Fold { before, .. } => before,
};
offset.to_timestamp(self.dt)
}
#[inline]
pub const fn earlier(self) -> Result<Timestamp, RangeError> {
let offset = match self.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { after, .. } => after,
AmbiguousOffset::Fold { before, .. } => before,
};
offset.to_timestamp(self.dt)
}
#[inline]
pub const fn later(self) -> Result<Timestamp, RangeError> {
let offset = match self.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { before, .. } => before,
AmbiguousOffset::Fold { after, .. } => after,
};
offset.to_timestamp(self.dt)
}
#[inline]
pub const fn unambiguous(self) -> Result<Timestamp, AmbiguousError> {
let offset = match self.offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { before, after } => {
return Err(AmbiguousError {
kind: AmbiguousErrorKind::BecauseGap { before, after },
});
}
AmbiguousOffset::Fold { before, after } => {
return Err(AmbiguousError {
kind: AmbiguousErrorKind::BecauseFold { before, after },
});
}
};
match offset.to_timestamp(self.dt) {
Ok(timestamp) => Ok(timestamp),
Err(range_error) => Err(AmbiguousError {
kind: AmbiguousErrorKind::Range(range_error),
}),
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct AmbiguousError {
kind: AmbiguousErrorKind,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
enum AmbiguousErrorKind {
Range(RangeError),
BecauseFold { before: Offset, after: Offset },
BecauseGap { before: Offset, after: Offset },
}
impl core::fmt::Display for AmbiguousError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
use self::AmbiguousErrorKind::*;
match self.kind {
Range(ref err) => core::fmt::Display::fmt(err, f),
BecauseFold { before, after } => write!(
f,
"datetime is ambiguous since it falls into a \
fold between offsets {before} and {after}",
),
BecauseGap { before, after } => write!(
f,
"datetime is ambiguous since it falls into a \
gap between offsets {before} and {after}",
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AmbiguousError {}