use std::fmt;
#[cfg(feature = "chrono")]
use crate::datetime;
#[cfg(feature = "chrono")]
use chrono::{DateTime, Datelike, Utc};
#[cfg(feature = "chrono")]
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimestampError {
InvalidYear(i32),
Parse(String),
}
impl fmt::Display for TimestampError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TimestampError::InvalidYear(year) => write!(
f,
"invalid year {year}, supported years are between 0 and 9999 included"
),
TimestampError::Parse(s) => {
write!(f, "failed to parse date {s} in rfc3339 format")
}
}
}
}
impl std::error::Error for TimestampError {}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))]
pub struct Timestamp(i64);
impl Timestamp {
#[cfg(feature = "chrono")]
pub fn new(datetime: DateTime<Utc>) -> Result<Timestamp, TimestampError> {
let year = datetime.year();
#[expect(clippy::manual_range_contains)]
if year < 0 || year > 9999 {
return Err(TimestampError::InvalidYear(year));
}
let truncated_timestamp = datetime.timestamp_millis();
Ok(Timestamp(truncated_timestamp))
}
#[cfg(not(feature = "chrono"))]
pub fn new(val: i64) -> Result<Timestamp, TimestampError> {
Ok(Self(val))
}
#[cfg(feature = "chrono")]
pub fn from_millis(milliseconds: i64) -> Option<Self> {
(Self::MIN.as_millis()..=Self::MAX.as_millis())
.contains(&milliseconds)
.then_some(Self(milliseconds))
}
#[cfg(not(feature = "chrono"))]
pub fn from_millis(milliseconds: i64) -> Option<Self> {
Some(Self(milliseconds))
}
pub fn as_millis(&self) -> i64 {
self.0
}
#[cfg(feature = "chrono")]
pub(crate) fn as_datetime(&self) -> DateTime<Utc> {
DateTime::from_timestamp_millis(self.0)
.expect("roundtrips with `DateTime::timestamp_millis`")
}
#[cfg(feature = "chrono")]
pub const MIN: Timestamp = Timestamp(datetime!(0000-01-01 00:00:00 Z).timestamp_millis());
#[cfg(feature = "chrono")]
pub const MAX: Timestamp = Timestamp(datetime!(10000-01-01 00:00:00 Z).timestamp_millis() - 1);
}
#[cfg(feature = "chrono")]
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_datetime().fmt(f)
}
}
#[cfg(not(feature = "chrono"))]
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl fmt::Debug for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
#[cfg(all(feature = "json", feature = "chrono"))]
impl From<Timestamp> for serde_json::Value {
fn from(value: Timestamp) -> Self {
serde_json::Value::String(
value
.as_datetime()
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
.to_string(),
)
}
}
#[cfg(feature = "chrono")]
impl FromStr for Timestamp {
type Err = TimestampError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let datetime =
DateTime::parse_from_rfc3339(s).map_err(|_| TimestampError::Parse(s.to_string()))?;
Timestamp::new(datetime.to_utc())
}
}
#[cfg(all(test, feature = "chrono"))]
mod tests {
use super::*;
use chrono::{DateTime, Utc};
#[test]
fn new_timestamp_truncates_at_millisecond_precision() {
assert_eq!(
"1996-12-19T16:39:57.123555Z".parse::<Timestamp>().unwrap(),
"1996-12-19T16:39:57.123Z".parse::<Timestamp>().unwrap()
)
}
#[test]
fn constants_are_correctly_computed() {
assert_eq!(
"0000-01-01T00:00:00Z".parse::<Timestamp>().unwrap(),
Timestamp::MIN
);
assert_eq!(
"9999-12-31T23:59:59.999Z".parse::<Timestamp>().unwrap(),
Timestamp::MAX
);
}
#[test]
fn timestamp_constructors() {
let unparsable_timestamp: Result<Timestamp, _> = "0000-01-01T00:00:00ZTR".parse();
assert!(unparsable_timestamp.is_err());
let out_of_range_year = DateTime::<Utc>::UNIX_EPOCH.with_year(10_000).unwrap();
assert!(Timestamp::new(out_of_range_year).is_err());
let parseable_timestamp: Result<Timestamp, _> = "0000-01-01T00:00:00Z".parse();
assert!(parseable_timestamp.is_ok())
}
#[test]
fn parse_accepts_any_timezone() {
assert_eq!(
"0000-01-01T00:00:00Z".parse::<Timestamp>().unwrap(),
"0000-01-01T01:00:00+01:00".parse::<Timestamp>().unwrap()
);
}
}