smos_domain/value_objects/
timestamp.rs1use crate::error::DomainError;
4use serde::{Deserialize, Serialize};
5use time::OffsetDateTime;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct Timestamp(OffsetDateTime);
15
16impl Timestamp {
17 pub fn from_offset_date_time(odt: OffsetDateTime) -> Self {
22 Self(odt)
23 }
24
25 #[allow(dead_code)] pub(crate) fn now_utc() -> Self {
34 Self(OffsetDateTime::now_utc())
35 }
36
37 pub fn from_unix_secs(secs: i64) -> Result<Self, DomainError> {
38 match OffsetDateTime::from_unix_timestamp(secs) {
39 Ok(odt) => Ok(Self(odt)),
40 Err(_) => Err(DomainError::InvalidTimestamp(format!(
41 "unix_secs out of range: {secs}"
42 ))),
43 }
44 }
45
46 pub fn from_unix_millis(ms: i64) -> Result<Self, DomainError> {
47 let secs = ms.div_euclid(1000);
48 let nanos = (ms.rem_euclid(1000)) as u32 * 1_000_000;
49 match OffsetDateTime::from_unix_timestamp_nanos(
50 (secs as i128) * 1_000_000_000 + nanos as i128,
51 ) {
52 Ok(odt) => Ok(Self(odt)),
53 Err(_) => Err(DomainError::InvalidTimestamp(format!(
54 "unix_millis out of range: {ms}"
55 ))),
56 }
57 }
58
59 pub fn as_unix_secs(&self) -> i64 {
60 self.0.unix_timestamp()
61 }
62
63 pub fn as_unix_millis(&self) -> i64 {
64 let nanos = self.0.unix_timestamp_nanos();
73 let millis = nanos / 1_000_000;
74 match i64::try_from(millis) {
75 Ok(v) => v,
76 Err(_) => {
77 if millis < 0 {
78 i64::MIN
79 } else {
80 i64::MAX
81 }
82 }
83 }
84 }
85
86 pub fn as_offset_date_time(&self) -> OffsetDateTime {
87 self.0
88 }
89}
90
91impl std::fmt::Display for Timestamp {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 write!(f, "{}", self.0)
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn now_utc_returns_reasonable_year() {
103 let ts = Timestamp::now_utc();
104 assert!(ts.as_offset_date_time().year() >= 2026);
105 }
106
107 #[test]
108 fn from_unix_secs_roundtrips() {
109 let ts = Timestamp::from_unix_secs(1_700_000_000).unwrap();
110 assert_eq!(ts.as_unix_secs(), 1_700_000_000);
111 }
112
113 #[test]
114 fn from_unix_millis_roundtrips() {
115 let ts = Timestamp::from_unix_millis(1_700_000_012).unwrap();
116 assert_eq!(ts.as_unix_millis(), 1_700_000_012);
117 }
118
119 #[test]
120 fn from_unix_secs_and_millis_agree() {
121 let secs = 1_234_567_890i64;
122 let from_s = Timestamp::from_unix_secs(secs).unwrap();
123 let from_ms = Timestamp::from_unix_millis(secs * 1000).unwrap();
124 assert_eq!(from_s.as_unix_secs(), from_ms.as_unix_secs());
125 }
126
127 #[test]
128 fn ordering_works() {
129 let earlier = Timestamp::from_unix_secs(1000).unwrap();
130 let later = Timestamp::from_unix_secs(2000).unwrap();
131 assert!(earlier < later);
132 }
133
134 #[test]
135 fn serde_roundtrip_preserves_value() {
136 let ts = Timestamp::from_unix_secs(1_700_000_000).unwrap();
137 let json = serde_json::to_string(&ts).unwrap();
138 let back: Timestamp = serde_json::from_str(&json).unwrap();
139 assert_eq!(ts, back);
140 }
141}