spatial_narrative/core/
timestamp.rs1use crate::error::{Error, Result};
4use chrono::{DateTime, TimeZone, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(
12 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
13)]
14#[serde(rename_all = "lowercase")]
15pub enum TemporalPrecision {
16 Year,
18 Month,
20 Day,
22 Hour,
24 Minute,
26 #[default]
28 Second,
29 Millisecond,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct Timestamp {
57 pub datetime: DateTime<Utc>,
59 #[serde(default)]
61 pub precision: TemporalPrecision,
62}
63
64impl Timestamp {
65 pub fn new(datetime: DateTime<Utc>) -> Self {
67 Self {
68 datetime,
69 precision: TemporalPrecision::Second,
70 }
71 }
72
73 pub fn with_precision(datetime: DateTime<Utc>, precision: TemporalPrecision) -> Self {
75 Self {
76 datetime,
77 precision,
78 }
79 }
80
81 pub fn now() -> Self {
83 Self::new(Utc::now())
84 }
85
86 pub fn parse(s: &str) -> Result<Self> {
95 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
97 return Ok(Self::new(dt.with_timezone(&Utc)));
98 }
99
100 if let Ok(dt) = s.parse::<DateTime<Utc>>() {
102 return Ok(Self::new(dt));
103 }
104
105 if s.len() == 10 {
107 if let Ok(naive) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
108 let dt = naive
109 .and_hms_opt(0, 0, 0)
110 .map(|ndt| Utc.from_utc_datetime(&ndt))
111 .ok_or_else(|| Error::InvalidTimestamp(s.to_string()))?;
112 return Ok(Self::with_precision(dt, TemporalPrecision::Day));
113 }
114 }
115
116 if s.len() == 7 && s.chars().nth(4) == Some('-') {
118 let year: i32 = s[0..4]
119 .parse()
120 .map_err(|_| Error::InvalidTimestamp(s.to_string()))?;
121 let month: u32 = s[5..7]
122 .parse()
123 .map_err(|_| Error::InvalidTimestamp(s.to_string()))?;
124
125 if let Some(naive) = chrono::NaiveDate::from_ymd_opt(year, month, 1) {
126 let dt = naive
127 .and_hms_opt(0, 0, 0)
128 .map(|ndt| Utc.from_utc_datetime(&ndt))
129 .ok_or_else(|| Error::InvalidTimestamp(s.to_string()))?;
130 return Ok(Self::with_precision(dt, TemporalPrecision::Month));
131 }
132 }
133
134 if s.len() == 4 {
136 let year: i32 = s
137 .parse()
138 .map_err(|_| Error::InvalidTimestamp(s.to_string()))?;
139
140 if let Some(naive) = chrono::NaiveDate::from_ymd_opt(year, 1, 1) {
141 let dt = naive
142 .and_hms_opt(0, 0, 0)
143 .map(|ndt| Utc.from_utc_datetime(&ndt))
144 .ok_or_else(|| Error::InvalidTimestamp(s.to_string()))?;
145 return Ok(Self::with_precision(dt, TemporalPrecision::Year));
146 }
147 }
148
149 Err(Error::InvalidTimestamp(s.to_string()))
150 }
151
152 pub fn from_unix(secs: i64) -> Option<Self> {
154 DateTime::from_timestamp(secs, 0).map(Self::new)
155 }
156
157 pub fn from_unix_millis(millis: i64) -> Option<Self> {
159 DateTime::from_timestamp_millis(millis)
160 .map(|dt| Self::with_precision(dt, TemporalPrecision::Millisecond))
161 }
162
163 pub fn unix_timestamp(&self) -> i64 {
165 self.datetime.timestamp()
166 }
167
168 pub fn unix_timestamp_millis(&self) -> i64 {
170 self.datetime.timestamp_millis()
171 }
172
173 pub fn to_unix_millis(&self) -> i64 {
175 self.datetime.timestamp_millis()
176 }
177
178 pub fn to_rfc3339(&self) -> String {
180 self.datetime.to_rfc3339()
181 }
182
183 pub fn format_with_precision(&self) -> String {
185 match self.precision {
186 TemporalPrecision::Year => self.datetime.format("%Y").to_string(),
187 TemporalPrecision::Month => self.datetime.format("%Y-%m").to_string(),
188 TemporalPrecision::Day => self.datetime.format("%Y-%m-%d").to_string(),
189 TemporalPrecision::Hour => self.datetime.format("%Y-%m-%dT%H:00:00Z").to_string(),
190 TemporalPrecision::Minute => self.datetime.format("%Y-%m-%dT%H:%M:00Z").to_string(),
191 TemporalPrecision::Second | TemporalPrecision::Millisecond => {
192 self.datetime.to_rfc3339()
193 },
194 }
195 }
196
197 pub fn is_before(&self, other: &Timestamp) -> bool {
199 self.datetime < other.datetime
200 }
201
202 pub fn is_after(&self, other: &Timestamp) -> bool {
204 self.datetime > other.datetime
205 }
206
207 pub fn duration_since(&self, earlier: &Timestamp) -> chrono::Duration {
209 self.datetime.signed_duration_since(earlier.datetime)
210 }
211}
212
213impl Default for Timestamp {
214 fn default() -> Self {
215 Self::now()
216 }
217}
218
219impl PartialOrd for Timestamp {
220 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
221 Some(self.cmp(other))
222 }
223}
224
225impl Ord for Timestamp {
226 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
227 self.datetime.cmp(&other.datetime)
228 }
229}
230
231impl std::hash::Hash for Timestamp {
232 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
233 self.datetime.hash(state);
234 self.precision.hash(state);
235 }
236}
237
238impl std::fmt::Display for Timestamp {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 write!(f, "{}", self.format_with_precision())
241 }
242}
243
244impl From<DateTime<Utc>> for Timestamp {
245 fn from(datetime: DateTime<Utc>) -> Self {
246 Self::new(datetime)
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_timestamp_now() {
256 let ts = Timestamp::now();
257 assert_eq!(ts.precision, TemporalPrecision::Second);
258 }
259
260 #[test]
261 fn test_timestamp_parse_rfc3339() {
262 let ts = Timestamp::parse("2024-03-15T14:30:00Z").unwrap();
263 assert_eq!(ts.datetime.year(), 2024);
264 assert_eq!(ts.datetime.month(), 3);
265 assert_eq!(ts.datetime.day(), 15);
266 }
267
268 #[test]
269 fn test_timestamp_parse_date_only() {
270 let ts = Timestamp::parse("2024-03-15").unwrap();
271 assert_eq!(ts.precision, TemporalPrecision::Day);
272 assert_eq!(ts.datetime.year(), 2024);
273 assert_eq!(ts.datetime.month(), 3);
274 assert_eq!(ts.datetime.day(), 15);
275 }
276
277 #[test]
278 fn test_timestamp_parse_year_month() {
279 let ts = Timestamp::parse("2024-03").unwrap();
280 assert_eq!(ts.precision, TemporalPrecision::Month);
281 assert_eq!(ts.datetime.year(), 2024);
282 assert_eq!(ts.datetime.month(), 3);
283 }
284
285 #[test]
286 fn test_timestamp_parse_year() {
287 let ts = Timestamp::parse("2024").unwrap();
288 assert_eq!(ts.precision, TemporalPrecision::Year);
289 assert_eq!(ts.datetime.year(), 2024);
290 }
291
292 #[test]
293 fn test_timestamp_parse_invalid() {
294 assert!(Timestamp::parse("not a timestamp").is_err());
295 assert!(Timestamp::parse("").is_err());
296 }
297
298 #[test]
299 fn test_timestamp_from_unix() {
300 let ts = Timestamp::from_unix(1710510600).unwrap(); assert_eq!(ts.datetime.year(), 2024);
302 }
303
304 #[test]
305 fn test_timestamp_format_with_precision() {
306 let ts = Timestamp::parse("2024-03").unwrap();
307 assert_eq!(ts.format_with_precision(), "2024-03");
308 }
309
310 #[test]
311 fn test_timestamp_ordering() {
312 let ts1 = Timestamp::parse("2024-01-01T00:00:00Z").unwrap();
313 let ts2 = Timestamp::parse("2024-06-01T00:00:00Z").unwrap();
314 assert!(ts1 < ts2);
315 assert!(ts1.is_before(&ts2));
316 assert!(ts2.is_after(&ts1));
317 }
318
319 #[test]
320 fn test_timestamp_duration() {
321 let ts1 = Timestamp::parse("2024-01-01T00:00:00Z").unwrap();
322 let ts2 = Timestamp::parse("2024-01-02T00:00:00Z").unwrap();
323 let duration = ts2.duration_since(&ts1);
324 assert_eq!(duration.num_days(), 1);
325 }
326
327 #[test]
328 fn test_timestamp_serialization() {
329 let ts = Timestamp::parse("2024-03-15T14:30:00Z").unwrap();
330 let json = serde_json::to_string(&ts).unwrap();
331 let parsed: Timestamp = serde_json::from_str(&json).unwrap();
332 assert_eq!(ts.datetime, parsed.datetime);
333 }
334
335 use chrono::Datelike;
336}