Skip to main content

lemma/computation/
datetime.rs

1//! DateTime operations
2//!
3//! Handles arithmetic and comparisons with dates and datetimes.
4//! Returns OperationResult with Veto for errors instead of Result.
5
6use crate::evaluation::OperationResult;
7use crate::{
8    ArithmeticComputation, ComparisonComputation, DateTimeValue, LiteralValue, TimeValue,
9    TimezoneValue, Value,
10};
11use chrono::{
12    DateTime, Datelike, Duration as ChronoDuration, FixedOffset, NaiveDate, NaiveDateTime,
13    NaiveTime, TimeZone, Timelike,
14};
15use rust_decimal::prelude::ToPrimitive;
16use rust_decimal::Decimal;
17
18const SECONDS_PER_HOUR: i32 = 3600;
19const SECONDS_PER_MINUTE: i32 = 60;
20const MONTHS_PER_YEAR: u32 = 12;
21const MILLISECONDS_PER_SECOND: f64 = 1000.0;
22
23const EPOCH_YEAR: i32 = 1970;
24const EPOCH_MONTH: u32 = 1;
25const EPOCH_DAY: u32 = 1;
26
27fn create_timezone_offset(timezone: &Option<TimezoneValue>) -> Result<FixedOffset, String> {
28    if let Some(tz) = timezone {
29        let offset_seconds = (tz.offset_hours as i32 * SECONDS_PER_HOUR)
30            + (tz.offset_minutes as i32 * SECONDS_PER_MINUTE);
31        FixedOffset::east_opt(offset_seconds).ok_or_else(|| {
32            format!(
33                "Invalid timezone offset: {}:{}",
34                tz.offset_hours, tz.offset_minutes
35            )
36        })
37    } else {
38        FixedOffset::east_opt(0).ok_or_else(|| "Failed to create UTC offset".to_string())
39    }
40}
41
42/// Perform date/datetime arithmetic, returning OperationResult (Veto on error)
43pub fn datetime_arithmetic(
44    left: &LiteralValue,
45    op: &ArithmeticComputation,
46    right: &LiteralValue,
47) -> OperationResult {
48    match (&left.value, &right.value, op) {
49        (Value::Date(date), Value::Duration(value, unit), ArithmeticComputation::Add) => {
50            let dt = match datetime_value_to_chrono(date) {
51                Ok(d) => d,
52                Err(msg) => return OperationResult::Veto(Some(msg)),
53            };
54
55            let new_dt = match unit {
56                crate::DurationUnit::Month => {
57                    let months = match value.to_i32() {
58                        Some(m) => m,
59                        None => {
60                            return OperationResult::Veto(Some("Month value too large".to_string()))
61                        }
62                    };
63                    match dt.checked_add_months(chrono::Months::new(months as u32)) {
64                        Some(d) => d,
65                        None => return OperationResult::Veto(Some("Date overflow".to_string())),
66                    }
67                }
68                crate::DurationUnit::Year => {
69                    let years = match value.to_i32() {
70                        Some(y) => y,
71                        None => {
72                            return OperationResult::Veto(Some("Year value too large".to_string()))
73                        }
74                    };
75                    match dt.checked_add_months(chrono::Months::new(
76                        (years * MONTHS_PER_YEAR as i32) as u32,
77                    )) {
78                        Some(d) => d,
79                        None => return OperationResult::Veto(Some("Date overflow".to_string())),
80                    }
81                }
82                _ => {
83                    let seconds = super::units::duration_to_seconds(*value, unit);
84                    let duration = match seconds_to_chrono_duration(seconds) {
85                        Ok(d) => d,
86                        Err(msg) => return OperationResult::Veto(Some(msg)),
87                    };
88                    match dt.checked_add_signed(duration) {
89                        Some(d) => d,
90                        None => return OperationResult::Veto(Some("Date overflow".to_string())),
91                    }
92                }
93            };
94
95            OperationResult::Value(LiteralValue::date_with_type(
96                chrono_to_datetime_value(new_dt),
97                left.lemma_type.clone(),
98            ))
99        }
100
101        (Value::Date(date), Value::Duration(value, unit), ArithmeticComputation::Subtract) => {
102            let dt = match datetime_value_to_chrono(date) {
103                Ok(d) => d,
104                Err(msg) => return OperationResult::Veto(Some(msg)),
105            };
106
107            let new_dt = match unit {
108                crate::DurationUnit::Month => {
109                    let months = match value.to_i32() {
110                        Some(m) => m,
111                        None => {
112                            return OperationResult::Veto(Some("Month value too large".to_string()))
113                        }
114                    };
115                    match dt.checked_sub_months(chrono::Months::new(months as u32)) {
116                        Some(d) => d,
117                        None => return OperationResult::Veto(Some("Date overflow".to_string())),
118                    }
119                }
120                crate::DurationUnit::Year => {
121                    let years = match value.to_i32() {
122                        Some(y) => y,
123                        None => {
124                            return OperationResult::Veto(Some("Year value too large".to_string()))
125                        }
126                    };
127                    match dt.checked_sub_months(chrono::Months::new(
128                        (years * MONTHS_PER_YEAR as i32) as u32,
129                    )) {
130                        Some(d) => d,
131                        None => return OperationResult::Veto(Some("Date overflow".to_string())),
132                    }
133                }
134                _ => {
135                    let seconds = super::units::duration_to_seconds(*value, unit);
136                    let duration = match seconds_to_chrono_duration(seconds) {
137                        Ok(d) => d,
138                        Err(msg) => return OperationResult::Veto(Some(msg)),
139                    };
140                    match dt.checked_sub_signed(duration) {
141                        Some(d) => d,
142                        None => return OperationResult::Veto(Some("Date overflow".to_string())),
143                    }
144                }
145            };
146
147            OperationResult::Value(LiteralValue::date_with_type(
148                chrono_to_datetime_value(new_dt),
149                left.lemma_type.clone(),
150            ))
151        }
152
153        (Value::Date(left_date), Value::Date(right_date), ArithmeticComputation::Subtract) => {
154            let left_dt = match datetime_value_to_chrono(left_date) {
155                Ok(d) => d,
156                Err(msg) => return OperationResult::Veto(Some(msg)),
157            };
158            let right_dt = match datetime_value_to_chrono(right_date) {
159                Ok(d) => d,
160                Err(msg) => return OperationResult::Veto(Some(msg)),
161            };
162            let duration = left_dt - right_dt;
163
164            let seconds = Decimal::from(duration.num_seconds());
165            OperationResult::Value(LiteralValue::duration(seconds, crate::DurationUnit::Second))
166        }
167
168        (Value::Date(date), Value::Time(time), ArithmeticComputation::Subtract) => {
169            // Date - Time: Create a datetime from the date's date components and the time's time components
170            // Then subtract to get the duration
171            let date_dt = match datetime_value_to_chrono(date) {
172                Ok(d) => d,
173                Err(msg) => return OperationResult::Veto(Some(msg)),
174            };
175
176            // Create a datetime using the date's date components and the time's time components
177            let naive_date = match NaiveDate::from_ymd_opt(date.year, date.month, date.day) {
178                Some(d) => d,
179                None => {
180                    return OperationResult::Veto(Some(format!(
181                        "Invalid date: {}-{}-{}",
182                        date.year, date.month, date.day
183                    )))
184                }
185            };
186            let naive_time = match NaiveTime::from_hms_opt(
187                time.hour as u32,
188                time.minute as u32,
189                time.second as u32,
190            ) {
191                Some(t) => t,
192                None => {
193                    return OperationResult::Veto(Some(format!(
194                        "Invalid time: {}:{}:{}",
195                        time.hour, time.minute, time.second
196                    )))
197                }
198            };
199            let naive_dt = NaiveDateTime::new(naive_date, naive_time);
200
201            // Use the date's timezone, or UTC if not specified
202            let offset = match create_timezone_offset(&date.timezone) {
203                Ok(o) => o,
204                Err(msg) => return OperationResult::Veto(Some(msg)),
205            };
206            let time_dt = match offset.from_local_datetime(&naive_dt).single() {
207                Some(dt) => dt,
208                None => {
209                    return OperationResult::Veto(Some(
210                        "Ambiguous or invalid datetime for timezone".to_string(),
211                    ))
212                }
213            };
214
215            let duration = date_dt - time_dt;
216            let seconds = Decimal::from(duration.num_seconds());
217            OperationResult::Value(LiteralValue::duration(seconds, crate::DurationUnit::Second))
218        }
219
220        _ => OperationResult::Veto(Some(format!(
221            "DateTime arithmetic operation {:?} not supported for these operand types",
222            op
223        ))),
224    }
225}
226
227fn datetime_value_to_chrono(date: &DateTimeValue) -> Result<DateTime<FixedOffset>, String> {
228    let naive_date = NaiveDate::from_ymd_opt(date.year, date.month, date.day)
229        .ok_or_else(|| format!("Invalid date: {}-{}-{}", date.year, date.month, date.day))?;
230
231    let naive_time =
232        NaiveTime::from_hms_opt(date.hour, date.minute, date.second).ok_or_else(|| {
233            format!(
234                "Invalid time: {}:{}:{}",
235                date.hour, date.minute, date.second
236            )
237        })?;
238
239    let naive_dt = NaiveDateTime::new(naive_date, naive_time);
240
241    let offset = create_timezone_offset(&date.timezone)?;
242    offset
243        .from_local_datetime(&naive_dt)
244        .single()
245        .ok_or_else(|| "Ambiguous or invalid datetime for timezone".to_string())
246}
247
248fn chrono_to_datetime_value(dt: DateTime<FixedOffset>) -> DateTimeValue {
249    let offset_seconds = dt.offset().local_minus_utc();
250    let offset_hours = (offset_seconds / SECONDS_PER_HOUR) as i8;
251    let offset_minutes = ((offset_seconds.abs() % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
252
253    DateTimeValue {
254        year: dt.year(),
255        month: dt.month(),
256        day: dt.day(),
257        hour: dt.hour(),
258        minute: dt.minute(),
259        second: dt.second(),
260        timezone: Some(TimezoneValue {
261            offset_hours,
262            offset_minutes,
263        }),
264    }
265}
266
267fn seconds_to_chrono_duration(seconds: Decimal) -> Result<ChronoDuration, String> {
268    let seconds_f64 = seconds
269        .to_f64()
270        .ok_or_else(|| "Duration conversion failed".to_string())?;
271
272    let milliseconds = (seconds_f64 * MILLISECONDS_PER_SECOND) as i64;
273    Ok(ChronoDuration::milliseconds(milliseconds))
274}
275
276/// Perform date/datetime comparisons, returning OperationResult (Veto on error)
277pub fn datetime_comparison(
278    left: &LiteralValue,
279    op: &ComparisonComputation,
280    right: &LiteralValue,
281) -> OperationResult {
282    match (&left.value, &right.value) {
283        (Value::Date(l), Value::Date(r)) => {
284            let l_dt = match datetime_value_to_chrono(l) {
285                Ok(d) => d,
286                Err(msg) => return OperationResult::Veto(Some(msg)),
287            };
288            let r_dt = match datetime_value_to_chrono(r) {
289                Ok(d) => d,
290                Err(msg) => return OperationResult::Veto(Some(msg)),
291            };
292
293            let l_utc = l_dt.naive_utc();
294            let r_utc = r_dt.naive_utc();
295
296            let result = match op {
297                ComparisonComputation::GreaterThan => l_utc > r_utc,
298                ComparisonComputation::LessThan => l_utc < r_utc,
299                ComparisonComputation::GreaterThanOrEqual => l_utc >= r_utc,
300                ComparisonComputation::LessThanOrEqual => l_utc <= r_utc,
301                ComparisonComputation::Equal | ComparisonComputation::Is => l_utc == r_utc,
302                ComparisonComputation::NotEqual | ComparisonComputation::IsNot => l_utc != r_utc,
303            };
304
305            OperationResult::Value(LiteralValue::boolean(result.into()))
306        }
307
308        _ => OperationResult::Veto(Some("Invalid datetime comparison operands".to_string())),
309    }
310}
311
312/// Perform time comparisons, returning OperationResult (Veto on error)
313pub fn time_comparison(
314    left: &LiteralValue,
315    op: &ComparisonComputation,
316    right: &LiteralValue,
317) -> OperationResult {
318    match (&left.value, &right.value) {
319        (Value::Time(l), Value::Time(r)) => {
320            let l_dt = match time_value_to_chrono_datetime(l) {
321                Ok(d) => d,
322                Err(msg) => return OperationResult::Veto(Some(msg)),
323            };
324            let r_dt = match time_value_to_chrono_datetime(r) {
325                Ok(d) => d,
326                Err(msg) => return OperationResult::Veto(Some(msg)),
327            };
328
329            let l_utc = l_dt.naive_utc();
330            let r_utc = r_dt.naive_utc();
331
332            let result = match op {
333                ComparisonComputation::GreaterThan => l_utc > r_utc,
334                ComparisonComputation::LessThan => l_utc < r_utc,
335                ComparisonComputation::GreaterThanOrEqual => l_utc >= r_utc,
336                ComparisonComputation::LessThanOrEqual => l_utc <= r_utc,
337                ComparisonComputation::Equal | ComparisonComputation::Is => l_utc == r_utc,
338                ComparisonComputation::NotEqual | ComparisonComputation::IsNot => l_utc != r_utc,
339            };
340
341            OperationResult::Value(LiteralValue::boolean(result.into()))
342        }
343        _ => unreachable!(
344            "BUG: time_comparison called with non-time operands; this should be enforced by planning and dispatch"
345        ),
346    }
347}
348
349/// Perform time arithmetic operations, returning OperationResult (Veto on error)
350pub fn time_arithmetic(
351    left: &LiteralValue,
352    op: &ArithmeticComputation,
353    right: &LiteralValue,
354) -> OperationResult {
355    match (&left.value, &right.value, op) {
356        (Value::Time(time), Value::Duration(value, unit), ArithmeticComputation::Add) => {
357            let seconds = super::units::duration_to_seconds(*value, unit);
358            let time_aware = match time_value_to_chrono_datetime(time) {
359                Ok(d) => d,
360                Err(msg) => return OperationResult::Veto(Some(msg)),
361            };
362            let duration = match seconds_to_chrono_duration(seconds) {
363                Ok(d) => d,
364                Err(msg) => return OperationResult::Veto(Some(msg)),
365            };
366            let result_dt = time_aware + duration;
367            OperationResult::Value(LiteralValue::time_with_type(
368                chrono_datetime_to_time_value(result_dt),
369                left.lemma_type.clone(),
370            ))
371        }
372
373        (Value::Time(time), Value::Duration(value, unit), ArithmeticComputation::Subtract) => {
374            let seconds = super::units::duration_to_seconds(*value, unit);
375            let time_aware = match time_value_to_chrono_datetime(time) {
376                Ok(d) => d,
377                Err(msg) => return OperationResult::Veto(Some(msg)),
378            };
379            let duration = match seconds_to_chrono_duration(seconds) {
380                Ok(d) => d,
381                Err(msg) => return OperationResult::Veto(Some(msg)),
382            };
383            let result_dt = time_aware - duration;
384            OperationResult::Value(LiteralValue::time_with_type(
385                chrono_datetime_to_time_value(result_dt),
386                left.lemma_type.clone(),
387            ))
388        }
389
390        (Value::Time(left_time), Value::Time(right_time), ArithmeticComputation::Subtract) => {
391            let left_dt = match time_value_to_chrono_datetime(left_time) {
392                Ok(d) => d,
393                Err(msg) => return OperationResult::Veto(Some(msg)),
394            };
395            let right_dt = match time_value_to_chrono_datetime(right_time) {
396                Ok(d) => d,
397                Err(msg) => return OperationResult::Veto(Some(msg)),
398            };
399
400            let diff = left_dt.naive_utc() - right_dt.naive_utc();
401            let diff_seconds = diff.num_seconds();
402            let seconds = Decimal::from(diff_seconds);
403
404            OperationResult::Value(LiteralValue::duration(seconds, crate::DurationUnit::Second))
405        }
406
407        (Value::Time(time), Value::Date(date), ArithmeticComputation::Subtract) => {
408            // Time - Date: Create a datetime from the date's date components and the time's time components
409            // Then subtract to get the duration
410            let time_dt = match time_value_to_chrono_datetime(time) {
411                Ok(d) => d,
412                Err(msg) => return OperationResult::Veto(Some(msg)),
413            };
414
415            // Create a datetime using the date's date components and the time's time components
416            let naive_date = match NaiveDate::from_ymd_opt(date.year, date.month, date.day) {
417                Some(d) => d,
418                None => {
419                    return OperationResult::Veto(Some(format!(
420                        "Invalid date: {}-{}-{}",
421                        date.year, date.month, date.day
422                    )))
423                }
424            };
425            let naive_time = match NaiveTime::from_hms_opt(
426                time.hour as u32,
427                time.minute as u32,
428                time.second as u32,
429            ) {
430                Some(t) => t,
431                None => {
432                    return OperationResult::Veto(Some(format!(
433                        "Invalid time: {}:{}:{}",
434                        time.hour, time.minute, time.second
435                    )))
436                }
437            };
438            let naive_dt = NaiveDateTime::new(naive_date, naive_time);
439
440            // Use the time's timezone, or UTC if not specified
441            let offset = match create_timezone_offset(&time.timezone) {
442                Ok(o) => o,
443                Err(msg) => return OperationResult::Veto(Some(msg)),
444            };
445            let date_dt = match offset.from_local_datetime(&naive_dt).single() {
446                Some(dt) => dt,
447                None => {
448                    return OperationResult::Veto(Some(
449                        "Ambiguous or invalid datetime for timezone".to_string(),
450                    ))
451                }
452            };
453
454            let duration = time_dt - date_dt;
455            let seconds = Decimal::from(duration.num_seconds());
456            OperationResult::Value(LiteralValue::duration(seconds, crate::DurationUnit::Second))
457        }
458
459        _ => OperationResult::Veto(Some(format!(
460            "Time arithmetic operation {:?} not supported for these operand types",
461            op
462        ))),
463    }
464}
465
466fn time_value_to_chrono_datetime(time: &TimeValue) -> Result<DateTime<FixedOffset>, String> {
467    let naive_date =
468        NaiveDate::from_ymd_opt(EPOCH_YEAR, EPOCH_MONTH, EPOCH_DAY).ok_or_else(|| {
469            format!(
470                "Invalid epoch date: {}-{}-{}",
471                EPOCH_YEAR, EPOCH_MONTH, EPOCH_DAY
472            )
473        })?;
474    let naive_time =
475        NaiveTime::from_hms_opt(time.hour as u32, time.minute as u32, time.second as u32)
476            .ok_or_else(|| {
477                format!(
478                    "Invalid time: {}:{}:{}",
479                    time.hour, time.minute, time.second
480                )
481            })?;
482
483    let naive_dt = NaiveDateTime::new(naive_date, naive_time);
484
485    let offset = create_timezone_offset(&time.timezone)?;
486    offset
487        .from_local_datetime(&naive_dt)
488        .single()
489        .ok_or_else(|| "Ambiguous or invalid time for timezone".to_string())
490}
491
492fn chrono_datetime_to_time_value(dt: DateTime<FixedOffset>) -> TimeValue {
493    let offset_seconds = dt.offset().local_minus_utc();
494    let offset_hours = (offset_seconds / SECONDS_PER_HOUR) as i8;
495    let offset_minutes = ((offset_seconds.abs() % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
496
497    TimeValue {
498        hour: dt.hour() as u8,
499        minute: dt.minute() as u8,
500        second: dt.second() as u8,
501        timezone: Some(TimezoneValue {
502            offset_hours,
503            offset_minutes,
504        }),
505    }
506}