Skip to main content

elo_rust/runtime/
temporal.rs

1//! Temporal type operations for ELO runtime
2//!
3//! Provides date, datetime, and duration handling with comprehensive operations
4//! for temporal arithmetic, comparisons, and calculations.
5
6use chrono::{DateTime, Duration, Local, NaiveDate, TimeZone, Utc};
7use std::fmt;
8
9/// Represents a temporal value (Date, DateTime, or Duration)
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum TemporalValue {
12    /// A date without time (ISO8601: YYYY-MM-DD)
13    Date(NaiveDate),
14
15    /// A datetime with timezone
16    DateTime(DateTime<Utc>),
17
18    /// A duration/interval
19    Duration(Duration),
20}
21
22impl TemporalValue {
23    /// Parse an ISO8601 date string (YYYY-MM-DD)
24    pub fn parse_date(date_str: &str) -> Result<Self, String> {
25        NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
26            .map(TemporalValue::Date)
27            .map_err(|e| format!("Invalid date format: {} (expected YYYY-MM-DD)", e))
28    }
29
30    /// Parse an ISO8601 datetime string
31    pub fn parse_datetime(datetime_str: &str) -> Result<Self, String> {
32        DateTime::parse_from_rfc3339(datetime_str)
33            .map(|dt| TemporalValue::DateTime(dt.with_timezone(&Utc)))
34            .map_err(|e| format!("Invalid datetime format: {}", e))
35    }
36
37    /// Parse an ISO8601 duration string
38    pub fn parse_duration(duration_str: &str) -> Result<Self, String> {
39        // Simple ISO8601 duration parsing
40        // Format: P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W
41        if let Some(weeks_part) = duration_str.strip_prefix('P') {
42            if let Some(weeks) = weeks_part.strip_suffix('W') {
43                let weeks: i64 = weeks
44                    .parse()
45                    .map_err(|_| format!("Invalid duration: {}", duration_str))?;
46                return Ok(TemporalValue::Duration(Duration::weeks(weeks)));
47            }
48        }
49
50        // Basic day parsing (P1D, P2D, etc.)
51        if duration_str.starts_with('P') && duration_str.ends_with('D') {
52            let days_str = &duration_str[1..duration_str.len() - 1];
53            let days: i64 = days_str
54                .parse()
55                .map_err(|_| format!("Invalid duration: {}", duration_str))?;
56            return Ok(TemporalValue::Duration(Duration::days(days)));
57        }
58
59        // PT parsing for time durations (PT1H, PT30M, PT1H30M)
60        if let Some(time_part) = duration_str.strip_prefix("PT") {
61            let mut total_secs = 0i64;
62
63            // Simple parser for PTnHnMnS format
64            let mut current = String::new();
65            for ch in time_part.chars() {
66                match ch {
67                    'H' => {
68                        if let Ok(hours) = current.parse::<i64>() {
69                            total_secs += hours * 3600;
70                        }
71                        current.clear();
72                    }
73                    'M' => {
74                        if let Ok(mins) = current.parse::<i64>() {
75                            total_secs += mins * 60;
76                        }
77                        current.clear();
78                    }
79                    'S' => {
80                        if let Ok(secs) = current.parse::<i64>() {
81                            total_secs += secs;
82                        }
83                        current.clear();
84                    }
85                    '.' => {
86                        // Handle fractional seconds (simplified - just truncate)
87                        current.clear();
88                    }
89                    _ => current.push(ch),
90                }
91            }
92
93            return Ok(TemporalValue::Duration(Duration::seconds(total_secs)));
94        }
95
96        Err(format!(
97            "Invalid duration format: {} (expected ISO8601 format)",
98            duration_str
99        ))
100    }
101
102    /// Get the type name
103    pub fn type_name(&self) -> &'static str {
104        match self {
105            TemporalValue::Date(_) => "date",
106            TemporalValue::DateTime(_) => "datetime",
107            TemporalValue::Duration(_) => "duration",
108        }
109    }
110
111    /// Get today's date
112    pub fn today() -> Self {
113        TemporalValue::Date(Local::now().naive_local().date())
114    }
115
116    /// Get current datetime
117    pub fn now() -> Self {
118        TemporalValue::DateTime(Utc::now())
119    }
120
121    /// Add a duration to this temporal value
122    pub fn add_duration(&self, duration: &TemporalValue) -> Result<TemporalValue, String> {
123        let dur = match duration {
124            TemporalValue::Duration(d) => *d,
125            _ => return Err("Can only add Duration to temporal values".to_string()),
126        };
127
128        match self {
129            TemporalValue::Date(date) => {
130                let naive_dt = date
131                    .and_hms_opt(0, 0, 0)
132                    .ok_or("Invalid date time combination")?;
133                let dt = Utc.from_utc_datetime(&naive_dt);
134                let result_dt = dt + dur;
135                Ok(TemporalValue::DateTime(result_dt))
136            }
137            TemporalValue::DateTime(dt) => Ok(TemporalValue::DateTime(*dt + dur)),
138            TemporalValue::Duration(d) => Ok(TemporalValue::Duration(*d + dur)),
139        }
140    }
141
142    /// Subtract a duration from this temporal value
143    pub fn subtract_duration(&self, duration: &TemporalValue) -> Result<TemporalValue, String> {
144        let dur = match duration {
145            TemporalValue::Duration(d) => *d,
146            _ => return Err("Can only subtract Duration from temporal values".to_string()),
147        };
148
149        match self {
150            TemporalValue::Date(date) => {
151                let naive_dt = date
152                    .and_hms_opt(0, 0, 0)
153                    .ok_or("Invalid date time combination")?;
154                let dt = Utc.from_utc_datetime(&naive_dt);
155                let result_dt = dt - dur;
156                Ok(TemporalValue::DateTime(result_dt))
157            }
158            TemporalValue::DateTime(dt) => Ok(TemporalValue::DateTime(*dt - dur)),
159            TemporalValue::Duration(d) => Ok(TemporalValue::Duration(*d - dur)),
160        }
161    }
162
163    /// Get the difference between two temporal values
164    pub fn difference(&self, other: &TemporalValue) -> Result<TemporalValue, String> {
165        match (self, other) {
166            (TemporalValue::Date(d1), TemporalValue::Date(d2)) => {
167                let dt1 = d1
168                    .and_hms_opt(0, 0, 0)
169                    .ok_or("Invalid date time combination")?;
170                let dt2 = d2
171                    .and_hms_opt(0, 0, 0)
172                    .ok_or("Invalid date time combination")?;
173                let diff = dt1.signed_duration_since(dt2);
174                Ok(TemporalValue::Duration(diff))
175            }
176            (TemporalValue::DateTime(dt1), TemporalValue::DateTime(dt2)) => {
177                let diff = dt1.signed_duration_since(*dt2);
178                Ok(TemporalValue::Duration(diff))
179            }
180            _ => Err(format!(
181                "Cannot compute difference between {} and {}",
182                self.type_name(),
183                other.type_name()
184            )),
185        }
186    }
187
188    /// Compare two temporal values
189    pub fn compare(&self, other: &TemporalValue) -> Result<std::cmp::Ordering, String> {
190        match (self, other) {
191            (TemporalValue::Date(d1), TemporalValue::Date(d2)) => Ok(d1.cmp(d2)),
192            (TemporalValue::DateTime(dt1), TemporalValue::DateTime(dt2)) => Ok(dt1.cmp(dt2)),
193            (TemporalValue::Duration(d1), TemporalValue::Duration(d2)) => Ok(d1.cmp(d2)),
194            _ => Err(format!(
195                "Cannot compare {} with {}",
196                self.type_name(),
197                other.type_name()
198            )),
199        }
200    }
201
202    /// Check if this is before another temporal value
203    pub fn is_before(&self, other: &TemporalValue) -> Result<bool, String> {
204        Ok(self.compare(other)? == std::cmp::Ordering::Less)
205    }
206
207    /// Check if this is after another temporal value
208    pub fn is_after(&self, other: &TemporalValue) -> Result<bool, String> {
209        Ok(self.compare(other)? == std::cmp::Ordering::Greater)
210    }
211
212    /// Get the number of days in a duration
213    pub fn days(&self) -> Result<i64, String> {
214        match self {
215            TemporalValue::Duration(d) => Ok(d.num_days()),
216            _ => Err(format!("Cannot get days from {}", self.type_name())),
217        }
218    }
219
220    /// Get the number of seconds in a duration
221    pub fn seconds(&self) -> Result<i64, String> {
222        match self {
223            TemporalValue::Duration(d) => Ok(d.num_seconds()),
224            _ => Err(format!("Cannot get seconds from {}", self.type_name())),
225        }
226    }
227
228    /// Get start of day for a date
229    pub fn start_of_day(&self) -> Result<TemporalValue, String> {
230        match self {
231            TemporalValue::Date(date) => {
232                let dt = date
233                    .and_hms_opt(0, 0, 0)
234                    .ok_or("Invalid date time combination")?;
235                Ok(TemporalValue::DateTime(Utc.from_utc_datetime(&dt)))
236            }
237            TemporalValue::DateTime(dt) => {
238                let date = dt.date_naive();
239                let start_dt = date
240                    .and_hms_opt(0, 0, 0)
241                    .ok_or("Invalid date time combination")?;
242                Ok(TemporalValue::DateTime(Utc.from_utc_datetime(&start_dt)))
243            }
244            _ => Err(format!("Cannot get start of day from {}", self.type_name())),
245        }
246    }
247
248    /// Get end of day for a date
249    pub fn end_of_day(&self) -> Result<TemporalValue, String> {
250        match self {
251            TemporalValue::Date(date) => {
252                let dt = date
253                    .and_hms_opt(23, 59, 59)
254                    .ok_or("Invalid date time combination")?;
255                Ok(TemporalValue::DateTime(Utc.from_utc_datetime(&dt)))
256            }
257            TemporalValue::DateTime(dt) => {
258                let date = dt.date_naive();
259                let end_dt = date
260                    .and_hms_opt(23, 59, 59)
261                    .ok_or("Invalid date time combination")?;
262                Ok(TemporalValue::DateTime(Utc.from_utc_datetime(&end_dt)))
263            }
264            _ => Err(format!("Cannot get end of day from {}", self.type_name())),
265        }
266    }
267
268    /// Format as ISO8601 string
269    pub fn to_iso8601(&self) -> String {
270        match self {
271            TemporalValue::Date(date) => date.to_string(),
272            TemporalValue::DateTime(dt) => dt.to_rfc3339(),
273            TemporalValue::Duration(d) => {
274                let days = d.num_days();
275                let secs = d.num_seconds() % 86400;
276                format!("P{}DT{}S", days, secs)
277            }
278        }
279    }
280}
281
282impl fmt::Display for TemporalValue {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        write!(f, "{}", self.to_iso8601())
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use chrono::Datelike;
292
293    #[test]
294    fn test_parse_date() {
295        let date = TemporalValue::parse_date("2024-01-15").unwrap();
296        match date {
297            TemporalValue::Date(d) => {
298                assert_eq!(d.year(), 2024);
299                assert_eq!(d.month(), 1);
300                assert_eq!(d.day(), 15);
301            }
302            _ => panic!("Expected Date"),
303        }
304    }
305
306    #[test]
307    fn test_parse_duration_days() {
308        let duration = TemporalValue::parse_duration("P5D").unwrap();
309        match duration {
310            TemporalValue::Duration(d) => {
311                assert_eq!(d.num_days(), 5);
312            }
313            _ => panic!("Expected Duration"),
314        }
315    }
316
317    #[test]
318    fn test_parse_duration_hours() {
319        let duration = TemporalValue::parse_duration("PT2H").unwrap();
320        match duration {
321            TemporalValue::Duration(d) => {
322                assert_eq!(d.num_hours(), 2);
323            }
324            _ => panic!("Expected Duration"),
325        }
326    }
327
328    #[test]
329    fn test_add_duration_to_date() {
330        let date = TemporalValue::parse_date("2024-01-15").unwrap();
331        let duration = TemporalValue::parse_duration("P5D").unwrap();
332        let result = date.add_duration(&duration).unwrap();
333
334        match result {
335            TemporalValue::DateTime(dt) => {
336                assert_eq!(dt.date_naive().day(), 20);
337            }
338            _ => panic!("Expected DateTime"),
339        }
340    }
341
342    #[test]
343    fn test_subtract_duration_from_date() {
344        let date = TemporalValue::parse_date("2024-01-15").unwrap();
345        let duration = TemporalValue::parse_duration("P10D").unwrap();
346        let result = date.subtract_duration(&duration).unwrap();
347
348        match result {
349            TemporalValue::DateTime(dt) => {
350                assert_eq!(dt.date_naive().day(), 5);
351            }
352            _ => panic!("Expected DateTime"),
353        }
354    }
355
356    #[test]
357    fn test_date_comparison() {
358        let date1 = TemporalValue::parse_date("2024-01-15").unwrap();
359        let date2 = TemporalValue::parse_date("2024-01-20").unwrap();
360
361        assert!(date1.is_before(&date2).unwrap());
362        assert!(date2.is_after(&date1).unwrap());
363    }
364
365    #[test]
366    fn test_difference_between_dates() {
367        let date1 = TemporalValue::parse_date("2024-01-15").unwrap();
368        let date2 = TemporalValue::parse_date("2024-01-20").unwrap();
369
370        let diff = date2.difference(&date1).unwrap();
371        match diff {
372            TemporalValue::Duration(d) => {
373                assert_eq!(d.num_days(), 5);
374            }
375            _ => panic!("Expected Duration"),
376        }
377    }
378
379    #[test]
380    fn test_duration_arithmetic() {
381        let d1 = TemporalValue::parse_duration("P2D").unwrap();
382        let d2 = TemporalValue::parse_duration("P3D").unwrap();
383
384        let sum = d1.add_duration(&d2).unwrap();
385        match sum {
386            TemporalValue::Duration(d) => {
387                assert_eq!(d.num_days(), 5);
388            }
389            _ => panic!("Expected Duration"),
390        }
391    }
392
393    #[test]
394    fn test_start_end_of_day() {
395        let date = TemporalValue::parse_date("2024-01-15").unwrap();
396
397        let start = date.start_of_day().unwrap();
398        let end = date.end_of_day().unwrap();
399
400        match (&start, &end) {
401            (TemporalValue::DateTime(dt1), TemporalValue::DateTime(dt2)) => {
402                assert_eq!(dt1.date_naive().day(), dt2.date_naive().day());
403                assert_eq!(dt1.format("%H:%M:%S").to_string(), "00:00:00");
404                assert_eq!(dt2.format("%H:%M:%S").to_string(), "23:59:59");
405            }
406            _ => panic!("Expected DateTimes"),
407        }
408    }
409
410    #[test]
411    fn test_invalid_date_parsing() {
412        let result = TemporalValue::parse_date("2024-13-45");
413        assert!(result.is_err());
414    }
415
416    #[test]
417    fn test_today() {
418        let today = TemporalValue::today();
419        match today {
420            TemporalValue::Date(_) => {}
421            _ => panic!("Expected Date"),
422        }
423    }
424
425    #[test]
426    fn test_type_name() {
427        assert_eq!(
428            TemporalValue::parse_date("2024-01-15").unwrap().type_name(),
429            "date"
430        );
431        assert_eq!(
432            TemporalValue::parse_duration("P1D").unwrap().type_name(),
433            "duration"
434        );
435    }
436}