use anyhow::Context;
use chrono::DateTime;
use crate::{de::Deserialize, impl_st, ser::Serialize, time_duration::TimeDuration, AlgebraicType, AlgebraicValue};
use std::fmt;
use std::ops::{Add, AddAssign, Sub, SubAssign};
use std::time::{Duration, SystemTime};
#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)]
#[sats(crate = crate)]
pub struct Timestamp {
__timestamp_micros_since_unix_epoch__: i64,
}
impl_st!([] Timestamp, AlgebraicType::timestamp());
impl Timestamp {
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
pub fn now() -> Self {
Self::from_system_time(SystemTime::now())
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
#[deprecated = "Timestamp::now() is stubbed and will panic. Read the `.timestamp` field of a `ReducerContext` instead."]
pub fn now() -> Self {
unimplemented!()
}
pub const UNIX_EPOCH: Self = Self {
__timestamp_micros_since_unix_epoch__: 0,
};
pub fn to_micros_since_unix_epoch(self) -> i64 {
self.__timestamp_micros_since_unix_epoch__
}
pub fn from_micros_since_unix_epoch(micros: i64) -> Self {
Self {
__timestamp_micros_since_unix_epoch__: micros,
}
}
pub fn from_time_duration_since_unix_epoch(time_duration: TimeDuration) -> Self {
Self::from_micros_since_unix_epoch(time_duration.to_micros())
}
pub fn to_time_duration_since_unix_epoch(self) -> TimeDuration {
TimeDuration::from_micros(self.to_micros_since_unix_epoch())
}
pub fn to_duration_since_unix_epoch(self) -> Result<Duration, Duration> {
let micros = self.to_micros_since_unix_epoch();
if micros >= 0 {
Ok(Duration::from_micros(micros as u64))
} else {
Err(Duration::from_micros((-micros) as u64))
}
}
pub fn from_duration_since_unix_epoch(duration: Duration) -> Self {
Self::from_micros_since_unix_epoch(
duration
.as_micros()
.try_into()
.expect("Duration since Unix epoch overflows i64 microseconds"),
)
}
pub fn to_system_time(self) -> SystemTime {
match self.to_duration_since_unix_epoch() {
Ok(positive) => SystemTime::UNIX_EPOCH
.checked_add(positive)
.expect("Timestamp with i64 microseconds since Unix epoch overflows SystemTime"),
Err(negative) => SystemTime::UNIX_EPOCH
.checked_sub(negative)
.expect("Timestamp with i64 microseconds before Unix epoch overflows SystemTime"),
}
}
pub fn from_system_time(system_time: SystemTime) -> Self {
let duration = system_time
.duration_since(SystemTime::UNIX_EPOCH)
.expect("SystemTime predates the Unix epoch");
Self::from_duration_since_unix_epoch(duration)
}
pub fn duration_since(self, earlier: Timestamp) -> Option<Duration> {
self.time_duration_since(earlier)?.to_duration().ok()
}
pub fn time_duration_since(self, earlier: Timestamp) -> Option<TimeDuration> {
let delta = self
.to_micros_since_unix_epoch()
.checked_sub(earlier.to_micros_since_unix_epoch())?;
Some(TimeDuration::from_micros(delta))
}
pub fn parse_from_rfc3339(str: &str) -> anyhow::Result<Timestamp> {
DateTime::parse_from_rfc3339(str)
.map_err(|err| anyhow::anyhow!(err))
.with_context(|| "Invalid timestamp format. Expected RFC 3339 format (e.g. '2025-02-10 15:45:30').")
.map(|dt| dt.timestamp_micros())
.map(Timestamp::from_micros_since_unix_epoch)
}
pub fn checked_add(&self, duration: TimeDuration) -> Option<Self> {
self.__timestamp_micros_since_unix_epoch__
.checked_add(duration.to_micros())
.map(Timestamp::from_micros_since_unix_epoch)
}
pub fn checked_sub(&self, duration: TimeDuration) -> Option<Self> {
self.__timestamp_micros_since_unix_epoch__
.checked_sub(duration.to_micros())
.map(Timestamp::from_micros_since_unix_epoch)
}
pub fn checked_add_duration(&self, duration: Duration) -> Option<Self> {
self.checked_add(TimeDuration::from_duration(duration))
}
pub fn checked_sub_duration(&self, duration: Duration) -> Option<Self> {
self.checked_sub(TimeDuration::from_duration(duration))
}
pub fn to_chrono_date_time(&self) -> anyhow::Result<DateTime<chrono::Utc>> {
DateTime::from_timestamp_micros(self.to_micros_since_unix_epoch())
.ok_or_else(|| anyhow::anyhow!("Timestamp with i64 microseconds since Unix epoch overflows DateTime"))
.with_context(|| self.to_micros_since_unix_epoch())
}
pub fn to_rfc3339(&self) -> anyhow::Result<String> {
Ok(self.to_chrono_date_time()?.to_rfc3339())
}
}
impl Add<TimeDuration> for Timestamp {
type Output = Self;
fn add(self, other: TimeDuration) -> Self::Output {
self.checked_add(other).unwrap()
}
}
impl Add<Duration> for Timestamp {
type Output = Self;
fn add(self, other: Duration) -> Self::Output {
self.checked_add_duration(other).unwrap()
}
}
impl Sub<TimeDuration> for Timestamp {
type Output = Self;
fn sub(self, other: TimeDuration) -> Self::Output {
self.checked_sub(other).unwrap()
}
}
impl Sub<Duration> for Timestamp {
type Output = Self;
fn sub(self, other: Duration) -> Self::Output {
self.checked_sub_duration(other).unwrap()
}
}
impl AddAssign<TimeDuration> for Timestamp {
fn add_assign(&mut self, other: TimeDuration) {
*self = *self + other;
}
}
impl AddAssign<Duration> for Timestamp {
fn add_assign(&mut self, other: Duration) {
*self = *self + other;
}
}
impl SubAssign<TimeDuration> for Timestamp {
fn sub_assign(&mut self, rhs: TimeDuration) {
*self = *self - rhs;
}
}
impl SubAssign<Duration> for Timestamp {
fn sub_assign(&mut self, rhs: Duration) {
*self = *self - rhs;
}
}
pub(crate) const MICROSECONDS_PER_SECOND: i64 = 1_000_000;
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_rfc3339().unwrap())
}
}
impl From<SystemTime> for Timestamp {
fn from(system_time: SystemTime) -> Self {
Self::from_system_time(system_time)
}
}
impl From<Timestamp> for SystemTime {
fn from(timestamp: Timestamp) -> Self {
timestamp.to_system_time()
}
}
impl From<Timestamp> for AlgebraicValue {
fn from(value: Timestamp) -> Self {
AlgebraicValue::product([value.to_micros_since_unix_epoch().into()])
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::GroundSpacetimeType;
use proptest::prelude::*;
fn round_to_micros(st: SystemTime) -> SystemTime {
let duration = st.duration_since(SystemTime::UNIX_EPOCH).unwrap();
let micros = duration.as_micros();
SystemTime::UNIX_EPOCH + Duration::from_micros(micros as _)
}
#[test]
fn timestamp_type_matches() {
assert_eq!(AlgebraicType::timestamp(), Timestamp::get_type());
assert!(Timestamp::get_type().is_timestamp());
assert!(Timestamp::get_type().is_special());
}
#[test]
fn round_trip_systemtime_through_timestamp() {
let now = round_to_micros(SystemTime::now());
let timestamp = Timestamp::from(now);
let now_prime = SystemTime::from(timestamp);
assert_eq!(now, now_prime);
}
proptest! {
#[test]
fn round_trip_timestamp_through_systemtime(micros in any::<i64>().prop_map(|n| n.abs())) {
let timestamp = Timestamp::from_micros_since_unix_epoch(micros);
let system_time = SystemTime::from(timestamp);
let timestamp_prime = Timestamp::from(system_time);
prop_assert_eq!(timestamp_prime, timestamp);
prop_assert_eq!(timestamp_prime.to_micros_since_unix_epoch(), micros);
}
#[test]
fn arithmetic_with_timeduration(lhs in any::<i64>(), rhs in any::<i64>()) {
let lhs_timestamp = Timestamp::from_micros_since_unix_epoch(lhs);
let rhs_time_duration = TimeDuration::from_micros(rhs);
if let Some(sum) = lhs.checked_add(rhs) {
let sum_timestamp = lhs_timestamp.checked_add(rhs_time_duration);
prop_assert!(sum_timestamp.is_some());
prop_assert_eq!(sum_timestamp.unwrap().to_micros_since_unix_epoch(), sum);
prop_assert_eq!((lhs_timestamp + rhs_time_duration).to_micros_since_unix_epoch(), sum);
let mut sum_assign = lhs_timestamp;
sum_assign += rhs_time_duration;
prop_assert_eq!(sum_assign.to_micros_since_unix_epoch(), sum);
} else {
prop_assert!(lhs_timestamp.checked_add(rhs_time_duration).is_none());
}
if let Some(diff) = lhs.checked_sub(rhs) {
let diff_timestamp = lhs_timestamp.checked_sub(rhs_time_duration);
prop_assert!(diff_timestamp.is_some());
prop_assert_eq!(diff_timestamp.unwrap().to_micros_since_unix_epoch(), diff);
prop_assert_eq!((lhs_timestamp - rhs_time_duration).to_micros_since_unix_epoch(), diff);
let mut diff_assign = lhs_timestamp;
diff_assign -= rhs_time_duration;
prop_assert_eq!(diff_assign.to_micros_since_unix_epoch(), diff);
} else {
prop_assert!(lhs_timestamp.checked_sub(rhs_time_duration).is_none());
}
}
}
}