use crate::error::{Error, Result};
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
#[serde(rename_all = "lowercase")]
pub enum TemporalPrecision {
Year,
Month,
Day,
Hour,
Minute,
#[default]
Second,
Millisecond,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Timestamp {
pub datetime: DateTime<Utc>,
#[serde(default)]
pub precision: TemporalPrecision,
}
impl Timestamp {
pub fn new(datetime: DateTime<Utc>) -> Self {
Self {
datetime,
precision: TemporalPrecision::Second,
}
}
pub fn with_precision(datetime: DateTime<Utc>, precision: TemporalPrecision) -> Self {
Self {
datetime,
precision,
}
}
pub fn now() -> Self {
Self::new(Utc::now())
}
pub fn parse(s: &str) -> Result<Self> {
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Ok(Self::new(dt.with_timezone(&Utc)));
}
if let Ok(dt) = s.parse::<DateTime<Utc>>() {
return Ok(Self::new(dt));
}
if s.len() == 10 {
if let Ok(naive) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
let dt = naive
.and_hms_opt(0, 0, 0)
.map(|ndt| Utc.from_utc_datetime(&ndt))
.ok_or_else(|| Error::InvalidTimestamp(s.to_string()))?;
return Ok(Self::with_precision(dt, TemporalPrecision::Day));
}
}
if s.len() == 7 && s.chars().nth(4) == Some('-') {
let year: i32 = s[0..4]
.parse()
.map_err(|_| Error::InvalidTimestamp(s.to_string()))?;
let month: u32 = s[5..7]
.parse()
.map_err(|_| Error::InvalidTimestamp(s.to_string()))?;
if let Some(naive) = chrono::NaiveDate::from_ymd_opt(year, month, 1) {
let dt = naive
.and_hms_opt(0, 0, 0)
.map(|ndt| Utc.from_utc_datetime(&ndt))
.ok_or_else(|| Error::InvalidTimestamp(s.to_string()))?;
return Ok(Self::with_precision(dt, TemporalPrecision::Month));
}
}
if s.len() == 4 {
let year: i32 = s
.parse()
.map_err(|_| Error::InvalidTimestamp(s.to_string()))?;
if let Some(naive) = chrono::NaiveDate::from_ymd_opt(year, 1, 1) {
let dt = naive
.and_hms_opt(0, 0, 0)
.map(|ndt| Utc.from_utc_datetime(&ndt))
.ok_or_else(|| Error::InvalidTimestamp(s.to_string()))?;
return Ok(Self::with_precision(dt, TemporalPrecision::Year));
}
}
Err(Error::InvalidTimestamp(s.to_string()))
}
pub fn from_unix(secs: i64) -> Option<Self> {
DateTime::from_timestamp(secs, 0).map(Self::new)
}
pub fn from_unix_millis(millis: i64) -> Option<Self> {
DateTime::from_timestamp_millis(millis)
.map(|dt| Self::with_precision(dt, TemporalPrecision::Millisecond))
}
pub fn unix_timestamp(&self) -> i64 {
self.datetime.timestamp()
}
pub fn unix_timestamp_millis(&self) -> i64 {
self.datetime.timestamp_millis()
}
pub fn to_unix_millis(&self) -> i64 {
self.datetime.timestamp_millis()
}
pub fn to_rfc3339(&self) -> String {
self.datetime.to_rfc3339()
}
pub fn format_with_precision(&self) -> String {
match self.precision {
TemporalPrecision::Year => self.datetime.format("%Y").to_string(),
TemporalPrecision::Month => self.datetime.format("%Y-%m").to_string(),
TemporalPrecision::Day => self.datetime.format("%Y-%m-%d").to_string(),
TemporalPrecision::Hour => self.datetime.format("%Y-%m-%dT%H:00:00Z").to_string(),
TemporalPrecision::Minute => self.datetime.format("%Y-%m-%dT%H:%M:00Z").to_string(),
TemporalPrecision::Second | TemporalPrecision::Millisecond => {
self.datetime.to_rfc3339()
},
}
}
pub fn is_before(&self, other: &Timestamp) -> bool {
self.datetime < other.datetime
}
pub fn is_after(&self, other: &Timestamp) -> bool {
self.datetime > other.datetime
}
pub fn duration_since(&self, earlier: &Timestamp) -> chrono::Duration {
self.datetime.signed_duration_since(earlier.datetime)
}
}
impl Default for Timestamp {
fn default() -> Self {
Self::now()
}
}
impl PartialOrd for Timestamp {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Timestamp {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.datetime.cmp(&other.datetime)
}
}
impl std::hash::Hash for Timestamp {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.datetime.hash(state);
self.precision.hash(state);
}
}
impl std::fmt::Display for Timestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.format_with_precision())
}
}
impl From<DateTime<Utc>> for Timestamp {
fn from(datetime: DateTime<Utc>) -> Self {
Self::new(datetime)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timestamp_now() {
let ts = Timestamp::now();
assert_eq!(ts.precision, TemporalPrecision::Second);
}
#[test]
fn test_timestamp_parse_rfc3339() {
let ts = Timestamp::parse("2024-03-15T14:30:00Z").unwrap();
assert_eq!(ts.datetime.year(), 2024);
assert_eq!(ts.datetime.month(), 3);
assert_eq!(ts.datetime.day(), 15);
}
#[test]
fn test_timestamp_parse_date_only() {
let ts = Timestamp::parse("2024-03-15").unwrap();
assert_eq!(ts.precision, TemporalPrecision::Day);
assert_eq!(ts.datetime.year(), 2024);
assert_eq!(ts.datetime.month(), 3);
assert_eq!(ts.datetime.day(), 15);
}
#[test]
fn test_timestamp_parse_year_month() {
let ts = Timestamp::parse("2024-03").unwrap();
assert_eq!(ts.precision, TemporalPrecision::Month);
assert_eq!(ts.datetime.year(), 2024);
assert_eq!(ts.datetime.month(), 3);
}
#[test]
fn test_timestamp_parse_year() {
let ts = Timestamp::parse("2024").unwrap();
assert_eq!(ts.precision, TemporalPrecision::Year);
assert_eq!(ts.datetime.year(), 2024);
}
#[test]
fn test_timestamp_parse_invalid() {
assert!(Timestamp::parse("not a timestamp").is_err());
assert!(Timestamp::parse("").is_err());
}
#[test]
fn test_timestamp_from_unix() {
let ts = Timestamp::from_unix(1710510600).unwrap(); assert_eq!(ts.datetime.year(), 2024);
}
#[test]
fn test_timestamp_format_with_precision() {
let ts = Timestamp::parse("2024-03").unwrap();
assert_eq!(ts.format_with_precision(), "2024-03");
}
#[test]
fn test_timestamp_ordering() {
let ts1 = Timestamp::parse("2024-01-01T00:00:00Z").unwrap();
let ts2 = Timestamp::parse("2024-06-01T00:00:00Z").unwrap();
assert!(ts1 < ts2);
assert!(ts1.is_before(&ts2));
assert!(ts2.is_after(&ts1));
}
#[test]
fn test_timestamp_duration() {
let ts1 = Timestamp::parse("2024-01-01T00:00:00Z").unwrap();
let ts2 = Timestamp::parse("2024-01-02T00:00:00Z").unwrap();
let duration = ts2.duration_since(&ts1);
assert_eq!(duration.num_days(), 1);
}
#[test]
fn test_timestamp_serialization() {
let ts = Timestamp::parse("2024-03-15T14:30:00Z").unwrap();
let json = serde_json::to_string(&ts).unwrap();
let parsed: Timestamp = serde_json::from_str(&json).unwrap();
assert_eq!(ts.datetime, parsed.datetime);
}
use chrono::Datelike;
}