1use chrono::{DateTime, TimeZone, Timelike, Utc};
2use serde::{Deserialize, Serialize};
3use std::fmt::Display;
4
5#[derive(Debug, thiserror::Error)]
6pub enum TimestampError {
7 #[error("Timestamp out of bounds")]
8 OutOfBounds,
9 #[error("Failed to parse timestamp")]
10 ParseError,
11}
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct Timestamp(f64);
20
21impl From<f64> for Timestamp {
22 fn from(value: f64) -> Self {
23 Self(value)
24 }
25}
26
27impl From<DateTime<Utc>> for Timestamp {
28 fn from(datetime: DateTime<Utc>) -> Self {
29 Self(datetime.timestamp() as f64 + datetime.nanosecond() as f64 / 1_000_000_000.0)
30 }
31}
32
33impl Timestamp {
34 pub fn now() -> Self {
35 Self::from(chrono::Utc::now())
36 }
37
38 pub fn as_f64(&self) -> f64 {
39 self.0
40 }
41
42 pub fn to_datetime(&self) -> Result<DateTime<Utc>, TimestampError> {
43 let secs = self.0.floor() as i64;
44 let nsecs = ((self.0.fract() * 1_000_000_000.0).round() as u32).min(999_999_999);
45 match Utc.timestamp_opt(secs, nsecs) {
46 chrono::LocalResult::Single(dt) => Ok(dt),
47 chrono::LocalResult::Ambiguous(earliest, latest) => {
48 panic!(
49 "Ambiguous timestamp (earliest: {} - latest: {}), which should be impossible when importing a timestamp to UTC datetime",
50 earliest.to_rfc3339(),
51 latest.to_rfc3339()
52 )
53 }
54 chrono::LocalResult::None => Err(TimestampError::OutOfBounds),
55 }
56 }
57}
58
59impl Display for Timestamp {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 let datetime_str = match self.to_datetime() {
62 Ok(dt) => dt.to_rfc3339(),
63 Err(_) => "invalid datetime".to_string(),
64 };
65
66 write!(f, "{} ({})", self.0, datetime_str)
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use chrono::TimeZone;
74
75 #[test]
76 fn test_timestamp_serialization() {
77 let dt = Utc.timestamp_opt(1635789600, 500_000_000).unwrap();
78 let timestamp = Timestamp::from(dt);
79 let serialized = serde_json::to_string(×tamp).unwrap();
80 assert_eq!(serialized, "1635789600.5");
81 }
82
83 #[test]
84 fn test_timestamp_deserialization() {
85 let json = "1635789600.5";
86 let timestamp: Timestamp = serde_json::from_str(json).unwrap();
87 assert_eq!(timestamp.0, 1635789600.5);
88 }
89
90 #[test]
91 fn test_timestamp_now() {
92 let before = chrono::Utc::now().timestamp() as f64;
93 let ts = Timestamp::now();
94 let after = chrono::Utc::now().timestamp() as f64;
95
96 let ts_f64 = ts.as_f64();
97 assert!(ts_f64 >= before);
98 assert!(ts_f64 <= after + 1.0);
99 }
100
101 #[test]
102 fn test_timestamp_display() {
103 let dt = Utc.timestamp_opt(1635789600, 500_000_000).unwrap();
104 let timestamp = Timestamp::from(dt);
105 assert_eq!(
106 format!("{}", timestamp),
107 "1635789600.5 (2021-11-01T18:00:00.500+00:00)"
108 );
109 }
110}