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