aleph_types/
timestamp.rs

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/// Timestamp type on the Aleph Cloud network.
14///
15/// Time in Aleph messages is usually represented as a floating-point epoch timestamp. This type
16/// keeps the floating point representation for fast serialization/deserialization and to avoid
17/// loss of precision, but provides helpers to convert to datetime for human readability.
18#[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 to_datetime(&self) -> Result<DateTime<Utc>, TimestampError> {
35        let secs = self.0.floor() as i64;
36        let nsecs = ((self.0.fract() * 1_000_000_000.0).round() as u32).min(999_999_999);
37        match Utc.timestamp_opt(secs, nsecs) {
38            chrono::LocalResult::Single(dt) => Ok(dt),
39            chrono::LocalResult::Ambiguous(earliest, latest) => {
40                panic!(
41                    "Ambiguous timestamp (earliest: {} - latest: {}), which should be impossible when importing a timestamp to UTC datetime",
42                    earliest.to_rfc3339(),
43                    latest.to_rfc3339()
44                )
45            }
46            chrono::LocalResult::None => Err(TimestampError::OutOfBounds),
47        }
48    }
49}
50
51impl Display for Timestamp {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        let datetime_str = match self.to_datetime() {
54            Ok(dt) => dt.to_rfc3339(),
55            Err(_) => "invalid datetime".to_string(),
56        };
57
58        write!(f, "{} ({})", self.0, datetime_str)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use chrono::TimeZone;
66
67    #[test]
68    fn test_timestamp_serialization() {
69        let dt = Utc.timestamp_opt(1635789600, 500_000_000).unwrap();
70        let timestamp = Timestamp::from(dt);
71        let serialized = serde_json::to_string(&timestamp).unwrap();
72        assert_eq!(serialized, "1635789600.5");
73    }
74
75    #[test]
76    fn test_timestamp_deserialization() {
77        let json = "1635789600.5";
78        let timestamp: Timestamp = serde_json::from_str(json).unwrap();
79        assert_eq!(timestamp.0, 1635789600.5);
80    }
81
82    #[test]
83    fn test_timestamp_display() {
84        let dt = Utc.timestamp_opt(1635789600, 500_000_000).unwrap();
85        let timestamp = Timestamp::from(dt);
86        assert_eq!(
87            format!("{}", timestamp),
88            "1635789600.5 (2021-11-01T18:00:00.500+00:00)"
89        );
90    }
91}