use crate::{
bounds::{self as b, RangeError},
civil::DateTime,
constants as c,
macros::{rbail, rtry, unwrapr},
tz::Offset,
};
#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Timestamp {
second: i64,
nanosecond: i32,
}
impl Timestamp {
pub const MIN: Timestamp = Timestamp {
second: b::UnixEpochSeconds::MIN,
nanosecond: b::SubsecNanosecond::MIN,
};
pub const MAX: Timestamp = Timestamp {
second: b::UnixEpochSeconds::MAX,
nanosecond: b::SubsecNanosecond::MAX,
};
pub const UNIX_EPOCH: Timestamp = Timestamp { second: 0, nanosecond: 0 };
#[inline]
pub const fn new(secs: i64, nanos: i32) -> Result<Timestamp, RangeError> {
let mut secs = rtry!(b::UnixEpochSeconds::checkc(secs));
let mut nanos = rtry!(b::SignedSubsecNanosecond::checkc(nanos as i64));
if secs == b::UnixEpochSeconds::MIN && nanos < 0 {
rbail!(b::UnixEpochSeconds::error());
}
if nanos == 0 || secs == 0 || secs.signum() == (nanos.signum() as i64)
{
return Ok(Timestamp::new_unchecked(secs, nanos));
}
if secs < 0 {
debug_assert!(nanos > 0);
secs += 1;
nanos -= c::NANOS_PER_SEC_32;
} else {
debug_assert!(secs > 0);
debug_assert!(nanos < 0);
secs -= 1;
nanos += c::NANOS_PER_SEC_32;
}
Ok(Timestamp::new_unchecked(secs, nanos))
}
#[inline]
pub const fn constant(second: i64, nanosecond: i32) -> Timestamp {
unwrapr!(Timestamp::new(second, nanosecond), "invalid timestamp")
}
#[inline]
pub(crate) const fn new_unchecked(secs: i64, nanos: i32) -> Timestamp {
debug_assert!(b::UnixEpochSeconds::checkc(secs).is_ok());
debug_assert!(b::SignedSubsecNanosecond::checkc(nanos as i64).is_ok());
debug_assert!(secs != b::UnixEpochSeconds::MIN || nanos >= 0);
debug_assert!(
nanos == 0
|| secs == 0
|| secs.signum() == (nanos.signum() as i64)
);
Timestamp { second: secs, nanosecond: nanos }
}
#[inline]
pub const fn from_second(second: i64) -> Result<Timestamp, RangeError> {
let second = rtry!(b::UnixEpochSeconds::checkc(second));
Ok(Timestamp::new_unchecked(second, 0))
}
#[inline]
pub const fn from_millisecond(
millisecond: i64,
) -> Result<Timestamp, RangeError> {
let millisecond = rtry!(b::UnixEpochMilliseconds::checkc(millisecond));
let secs = millisecond / c::MILLIS_PER_SEC;
let nanos =
(millisecond % c::MILLIS_PER_SEC) as i32 * c::NANOS_PER_MILLI_32;
Ok(Timestamp::new_unchecked(secs, nanos))
}
#[inline]
pub const fn from_microsecond(
microsecond: i64,
) -> Result<Timestamp, RangeError> {
let microsecond = rtry!(b::UnixEpochMicroseconds::checkc(microsecond));
let secs = microsecond / c::MICROS_PER_SEC;
let nanos =
(microsecond % c::MICROS_PER_SEC) as i32 * c::NANOS_PER_MICRO_32;
Ok(Timestamp::new_unchecked(secs, nanos))
}
#[inline]
pub const fn from_nanosecond(
nanosecond: i128,
) -> Result<Timestamp, RangeError> {
const NANOS_PER_SEC: i128 = c::NANOS_PER_SEC as i128;
let secs = nanosecond / NANOS_PER_SEC;
if !(i64::MIN as i128 <= secs && secs <= i64::MAX as i128) {
rbail!(b::SpecialBoundsError::UnixEpochNanoseconds);
}
let secs64 = secs as i64;
let nanosecond = (nanosecond % NANOS_PER_SEC) as i32;
Ok(Timestamp::new_unchecked(secs64, nanosecond))
}
#[inline]
pub const fn as_second(self) -> i64 {
self.second
}
#[inline]
pub const fn as_millisecond(self) -> i64 {
let millis = self.as_second() * c::MILLIS_PER_SEC;
millis + (self.subsec_millisecond() as i64)
}
#[inline]
pub const fn as_microsecond(self) -> i64 {
let micros = self.as_second() * c::MICROS_PER_SEC;
micros + (self.subsec_microsecond() as i64)
}
#[inline]
pub const fn as_nanosecond(self) -> i128 {
let nanos = (self.second as i128) * (c::NANOS_PER_SEC as i128);
nanos + (self.nanosecond as i128)
}
#[inline]
pub const fn subsec_millisecond(&self) -> i32 {
self.nanosecond / c::NANOS_PER_MILLI_32
}
#[inline]
pub const fn subsec_microsecond(&self) -> i32 {
self.nanosecond / c::NANOS_PER_MICRO_32
}
#[inline]
pub const fn subsec_nanosecond(&self) -> i32 {
self.nanosecond
}
#[inline]
pub const fn signum(self) -> i8 {
if self.is_zero() {
0
} else if self.is_positive() {
1
} else {
debug_assert!(self.is_negative());
-1
}
}
#[inline]
pub const fn is_zero(self) -> bool {
self.second == 0 && self.nanosecond == 0
}
#[inline]
pub const fn is_positive(&self) -> bool {
self.second.is_positive() || self.nanosecond.is_positive()
}
#[inline]
pub const fn is_negative(&self) -> bool {
self.second.is_negative() || self.nanosecond.is_negative()
}
#[inline]
pub const fn to_datetime(&self, offset: Offset) -> DateTime {
offset.to_datetime(*self)
}
#[inline]
pub const fn checked_add(
self,
mut seconds: i64,
mut nanos: i32,
) -> Result<Timestamp, RangeError> {
if !(-c::NANOS_PER_SEC_32 < nanos && nanos < c::NANOS_PER_SEC_32) {
let addsecs = nanos / c::NANOS_PER_SEC_32;
seconds = match seconds.checked_add(addsecs as i64) {
Some(secs) => secs,
None => panic!(
"nanoseconds overflowed seconds in SignedDuration::new"
),
};
nanos = nanos % c::NANOS_PER_SEC_32;
}
self.checked_add_sensible(seconds, nanos)
}
#[inline]
pub const fn checked_sub(
self,
seconds: i64,
mut nanos: i32,
) -> Result<Timestamp, RangeError> {
let Some(mut seconds) = seconds.checked_neg() else {
rbail!(b::UnixEpochSeconds::error())
};
if !(-c::NANOS_PER_SEC_32 < nanos && nanos < c::NANOS_PER_SEC_32) {
let addsecs = nanos / c::NANOS_PER_SEC_32;
seconds = match seconds.checked_sub(addsecs as i64) {
Some(secs) => secs,
None => panic!(
"nanoseconds overflowed seconds in SignedDuration::new"
),
};
nanos = nanos % c::NANOS_PER_SEC_32;
}
self.checked_add(seconds, -nanos)
}
#[inline]
const fn checked_add_sensible(
self,
seconds: i64,
nanos: i32,
) -> Result<Timestamp, RangeError> {
debug_assert!(
-c::NANOS_PER_SEC_32 < nanos && nanos < c::NANOS_PER_SEC_32
);
let mut second =
rtry!(b::UnixEpochSeconds::checked_add(self.as_second(), seconds));
let mut nanosecond = self.nanosecond + nanos;
if nanosecond == 0 {
return Ok(Timestamp { second, nanosecond });
}
if nanosecond >= c::NANOS_PER_SEC_32 {
nanosecond -= c::NANOS_PER_SEC_32;
second = rtry!(b::UnixEpochSeconds::checked_add(second, 1));
} else if nanosecond <= -c::NANOS_PER_SEC_32 {
nanosecond += c::NANOS_PER_SEC_32;
second = rtry!(b::UnixEpochSeconds::checked_add(second, -1));
}
if second != 0
&& nanosecond != 0
&& second.signum() != (nanosecond.signum() as i64)
{
if second < 0 {
debug_assert!(nanosecond > 0);
second += 1;
nanosecond -= c::NANOS_PER_SEC_32;
} else {
debug_assert!(second > 0);
debug_assert!(nanosecond < 0);
second -= 1;
nanosecond += c::NANOS_PER_SEC_32;
}
}
Ok(Timestamp { second, nanosecond })
}
#[inline]
pub const fn checked_add_seconds(
self,
seconds: i64,
) -> Result<Timestamp, RangeError> {
let second =
rtry!(b::UnixEpochSeconds::checked_add(self.as_second(), seconds));
Ok(Timestamp { second, ..self })
}
#[inline]
pub const fn checked_sub_seconds(
self,
seconds: i64,
) -> Result<Timestamp, RangeError> {
let Some(seconds) = seconds.checked_neg() else {
rbail!(b::UnixEpochSeconds::error())
};
self.checked_add_seconds(seconds)
}
}
impl core::fmt::Debug for Timestamp {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let dt = self.to_datetime(Offset::UTC);
core::fmt::Debug::fmt(&dt, f)?;
f.write_str("Z")
}
}
impl Default for Timestamp {
#[inline]
fn default() -> Timestamp {
Timestamp::UNIX_EPOCH
}
}
impl core::ops::Add<i64> for Timestamp {
type Output = Timestamp;
fn add(self, seconds: i64) -> Timestamp {
self.checked_add_seconds(seconds).unwrap()
}
}
impl core::ops::Add<(i64, i32)> for Timestamp {
type Output = Timestamp;
fn add(self, (seconds, nanoseconds): (i64, i32)) -> Timestamp {
self.checked_add(seconds, nanoseconds).unwrap()
}
}
impl core::ops::AddAssign<i64> for Timestamp {
#[inline]
fn add_assign(&mut self, rhs: i64) {
*self = *self + rhs;
}
}
impl core::ops::AddAssign<(i64, i32)> for Timestamp {
#[inline]
fn add_assign(&mut self, rhs: (i64, i32)) {
*self = *self + rhs;
}
}
impl core::ops::Sub<i64> for Timestamp {
type Output = Timestamp;
fn sub(self, seconds: i64) -> Timestamp {
self.checked_sub_seconds(seconds).unwrap()
}
}
impl core::ops::Sub<(i64, i32)> for Timestamp {
type Output = Timestamp;
fn sub(self, (seconds, nanoseconds): (i64, i32)) -> Timestamp {
self.checked_sub(seconds, nanoseconds).unwrap()
}
}
impl core::ops::SubAssign<i64> for Timestamp {
#[inline]
fn sub_assign(&mut self, rhs: i64) {
*self = *self + rhs;
}
}
impl core::ops::SubAssign<(i64, i32)> for Timestamp {
#[inline]
fn sub_assign(&mut self, rhs: (i64, i32)) {
*self = *self + rhs;
}
}
#[cfg(test)]
impl quickcheck::Arbitrary for Timestamp {
fn arbitrary(g: &mut quickcheck::Gen) -> Timestamp {
let secs = b::UnixEpochSeconds::arbitrary(g);
let mut nanos = b::SignedSubsecNanosecond::arbitrary(g);
if secs == b::UnixEpochSeconds::MIN && nanos < 0 {
nanos = 0;
}
Timestamp::new(secs, nanos).unwrap_or(Timestamp::UNIX_EPOCH)
}
fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = Self>> {
let secs = self.as_second();
let nanos = self.subsec_nanosecond();
alloc::boxed::Box::new((secs, nanos).shrink().filter_map(
|(secs, nanos)| {
let secs = b::UnixEpochSeconds::check(secs).ok()?;
let nanos = b::SignedSubsecNanosecond::check(nanos).ok()?;
if secs == b::UnixEpochSeconds::MIN && nanos > 0 {
None
} else {
Timestamp::new(secs, nanos).ok()
}
},
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[track_caller]
fn datetime(
year: i16,
month: i8,
day: i8,
hour: i8,
minute: i8,
second: i8,
subsec_nanosecond: i32,
) -> DateTime {
DateTime::new(
year,
month,
day,
hour,
minute,
second,
subsec_nanosecond,
)
.unwrap()
}
#[track_caller]
fn stamp(second: i64, subsec: i32) -> Timestamp {
Timestamp::new(second, subsec).unwrap()
}
#[track_caller]
fn offset(second: i32) -> Offset {
Offset::from_seconds(second).unwrap()
}
#[test]
fn new_ok() {
let ts = stamp(0, 0);
assert_eq!(ts, Timestamp::UNIX_EPOCH);
let ts = stamp(0, 123_000_000);
assert_eq!(ts.as_second(), 0);
assert_eq!(ts.subsec_nanosecond(), 123_000_000);
let ts = stamp(0, -123_000_000);
assert_eq!(ts.as_second(), 0);
assert_eq!(ts.subsec_nanosecond(), -123_000_000);
let ts = stamp(1, 0);
assert_eq!(ts.as_second(), 1);
assert_eq!(ts.subsec_nanosecond(), 0);
let ts = stamp(-1, 0);
assert_eq!(ts.as_second(), -1);
assert_eq!(ts.subsec_nanosecond(), 0);
let ts = stamp(1, 123_000_000);
assert_eq!(ts.as_second(), 1);
assert_eq!(ts.subsec_nanosecond(), 123_000_000);
let ts = stamp(-1, -123_000_000);
assert_eq!(ts.as_second(), -1);
assert_eq!(ts.subsec_nanosecond(), -123_000_000);
let ts = stamp(1, -123_000_000);
assert_eq!(ts.as_second(), 0);
assert_eq!(ts.subsec_nanosecond(), 877_000_000);
let ts = stamp(-1, 123_000_000);
assert_eq!(ts.as_second(), 0);
assert_eq!(ts.subsec_nanosecond(), -877_000_000);
let ts = stamp(-377705023201, 0);
assert_eq!(ts, Timestamp::MIN);
let ts = stamp(253402207200, 999_999_999);
assert_eq!(ts, Timestamp::MAX);
}
#[test]
fn new_err() {
assert!(Timestamp::new(0, 1_000_000_000).is_err());
assert!(Timestamp::new(0, -1_000_000_000).is_err());
assert!(Timestamp::new(1, 1_000_000_000).is_err());
assert!(Timestamp::new(1, -1_000_000_000).is_err());
assert!(Timestamp::new(-1, 1_000_000_000).is_err());
assert!(Timestamp::new(-1, -1_000_000_000).is_err());
assert!(Timestamp::new(0, i32::MAX).is_err());
assert!(Timestamp::new(0, i32::MIN).is_err());
assert!(Timestamp::new(-377705023201, -1).is_err());
assert!(Timestamp::new(253402207201, 0).is_err());
}
#[test]
fn to_datetime_no_subsec() {
let dt = datetime(1970, 1, 1, 0, 0, 0, 0);
assert_eq!(stamp(0, 0).to_datetime(offset(0)), dt);
assert_eq!(stamp(-3600, 0).to_datetime(offset(3600)), dt);
assert_eq!(stamp(3600, 0).to_datetime(offset(-3600)), dt);
let dt = datetime(1969, 12, 31, 23, 30, 0, 0);
assert_eq!(stamp(-1800, 0).to_datetime(offset(0)), dt);
assert_eq!(stamp(-5400, 0).to_datetime(offset(3600)), dt);
assert_eq!(stamp(1800, 0).to_datetime(offset(-3600)), dt);
let dt = datetime(1970, 1, 1, 0, 30, 0, 0);
assert_eq!(stamp(1800, 0).to_datetime(offset(0)), dt);
assert_eq!(stamp(-1800, 0).to_datetime(offset(3600)), dt);
assert_eq!(stamp(5400, 0).to_datetime(offset(-3600)), dt);
}
#[test]
fn to_datetime_with_subsec() {
let dt = datetime(1970, 1, 1, 0, 0, 0, 123);
assert_eq!(stamp(0, 123).to_datetime(offset(0)), dt);
assert_eq!(stamp(-3599, -999_999_877).to_datetime(offset(3600)), dt);
assert_eq!(stamp(3600, 123).to_datetime(offset(-3600)), dt);
let dt = datetime(1969, 12, 31, 23, 30, 0, 123);
assert_eq!(stamp(-1799, -999_999_877).to_datetime(offset(0)), dt);
assert_eq!(stamp(-5399, -999_999_877).to_datetime(offset(3600)), dt);
assert_eq!(stamp(1800, 123).to_datetime(offset(-3600)), dt);
let dt = datetime(1970, 1, 1, 0, 30, 0, 123);
assert_eq!(stamp(1800, 123).to_datetime(offset(0)), dt);
assert_eq!(stamp(-1799, -999_999_877).to_datetime(offset(3600)), dt);
assert_eq!(stamp(5400, 123).to_datetime(offset(-3600)), dt);
}
#[test]
fn to_datetime_limits() {
assert_eq!(Timestamp::MIN.to_datetime(Offset::MIN), DateTime::MIN);
assert_eq!(Timestamp::MAX.to_datetime(Offset::MAX), DateTime::MAX);
}
quickcheck::quickcheck! {
fn prop_timestamp_datetime_roundtrip(
ts: Timestamp,
offset: Offset
) -> bool {
let dt = ts.to_datetime(offset);
let got = dt.to_timestamp(offset).unwrap();
got == ts
}
}
}