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 arithmetic operations, returning OperationResult (Veto on error)
313pub fn time_arithmetic(
314    left: &LiteralValue,
315    op: &ArithmeticComputation,
316    right: &LiteralValue,
317) -> OperationResult {
318    match (&left.value, &right.value, op) {
319        (Value::Time(time), Value::Duration(value, unit), ArithmeticComputation::Add) => {
320            let seconds = super::units::duration_to_seconds(*value, unit);
321            let time_aware = match time_value_to_chrono_datetime(time) {
322                Ok(d) => d,
323                Err(msg) => return OperationResult::Veto(Some(msg)),
324            };
325            let duration = match seconds_to_chrono_duration(seconds) {
326                Ok(d) => d,
327                Err(msg) => return OperationResult::Veto(Some(msg)),
328            };
329            let result_dt = time_aware + duration;
330            OperationResult::Value(LiteralValue::time_with_type(
331                chrono_datetime_to_time_value(result_dt),
332                left.lemma_type.clone(),
333            ))
334        }
335
336        (Value::Time(time), Value::Duration(value, unit), ArithmeticComputation::Subtract) => {
337            let seconds = super::units::duration_to_seconds(*value, unit);
338            let time_aware = match time_value_to_chrono_datetime(time) {
339                Ok(d) => d,
340                Err(msg) => return OperationResult::Veto(Some(msg)),
341            };
342            let duration = match seconds_to_chrono_duration(seconds) {
343                Ok(d) => d,
344                Err(msg) => return OperationResult::Veto(Some(msg)),
345            };
346            let result_dt = time_aware - duration;
347            OperationResult::Value(LiteralValue::time_with_type(
348                chrono_datetime_to_time_value(result_dt),
349                left.lemma_type.clone(),
350            ))
351        }
352
353        (Value::Time(left_time), Value::Time(right_time), ArithmeticComputation::Subtract) => {
354            let left_dt = match time_value_to_chrono_datetime(left_time) {
355                Ok(d) => d,
356                Err(msg) => return OperationResult::Veto(Some(msg)),
357            };
358            let right_dt = match time_value_to_chrono_datetime(right_time) {
359                Ok(d) => d,
360                Err(msg) => return OperationResult::Veto(Some(msg)),
361            };
362
363            let diff = left_dt.naive_utc() - right_dt.naive_utc();
364            let diff_seconds = diff.num_seconds();
365            let seconds = Decimal::from(diff_seconds);
366
367            OperationResult::Value(LiteralValue::duration(seconds, crate::DurationUnit::Second))
368        }
369
370        (Value::Time(time), Value::Date(date), ArithmeticComputation::Subtract) => {
371            // Time - Date: Create a datetime from the date's date components and the time's time components
372            // Then subtract to get the duration
373            let time_dt = match time_value_to_chrono_datetime(time) {
374                Ok(d) => d,
375                Err(msg) => return OperationResult::Veto(Some(msg)),
376            };
377
378            // Create a datetime using the date's date components and the time's time components
379            let naive_date = match NaiveDate::from_ymd_opt(date.year, date.month, date.day) {
380                Some(d) => d,
381                None => {
382                    return OperationResult::Veto(Some(format!(
383                        "Invalid date: {}-{}-{}",
384                        date.year, date.month, date.day
385                    )))
386                }
387            };
388            let naive_time = match NaiveTime::from_hms_opt(
389                time.hour as u32,
390                time.minute as u32,
391                time.second as u32,
392            ) {
393                Some(t) => t,
394                None => {
395                    return OperationResult::Veto(Some(format!(
396                        "Invalid time: {}:{}:{}",
397                        time.hour, time.minute, time.second
398                    )))
399                }
400            };
401            let naive_dt = NaiveDateTime::new(naive_date, naive_time);
402
403            // Use the time's timezone, or UTC if not specified
404            let offset = match create_timezone_offset(&time.timezone) {
405                Ok(o) => o,
406                Err(msg) => return OperationResult::Veto(Some(msg)),
407            };
408            let date_dt = match offset.from_local_datetime(&naive_dt).single() {
409                Some(dt) => dt,
410                None => {
411                    return OperationResult::Veto(Some(
412                        "Ambiguous or invalid datetime for timezone".to_string(),
413                    ))
414                }
415            };
416
417            let duration = time_dt - date_dt;
418            let seconds = Decimal::from(duration.num_seconds());
419            OperationResult::Value(LiteralValue::duration(seconds, crate::DurationUnit::Second))
420        }
421
422        _ => OperationResult::Veto(Some(format!(
423            "Time arithmetic operation {:?} not supported for these operand types",
424            op
425        ))),
426    }
427}
428
429fn time_value_to_chrono_datetime(time: &TimeValue) -> Result<DateTime<FixedOffset>, String> {
430    let naive_date =
431        NaiveDate::from_ymd_opt(EPOCH_YEAR, EPOCH_MONTH, EPOCH_DAY).ok_or_else(|| {
432            format!(
433                "Invalid epoch date: {}-{}-{}",
434                EPOCH_YEAR, EPOCH_MONTH, EPOCH_DAY
435            )
436        })?;
437    let naive_time =
438        NaiveTime::from_hms_opt(time.hour as u32, time.minute as u32, time.second as u32)
439            .ok_or_else(|| {
440                format!(
441                    "Invalid time: {}:{}:{}",
442                    time.hour, time.minute, time.second
443                )
444            })?;
445
446    let naive_dt = NaiveDateTime::new(naive_date, naive_time);
447
448    let offset = create_timezone_offset(&time.timezone)?;
449    offset
450        .from_local_datetime(&naive_dt)
451        .single()
452        .ok_or_else(|| "Ambiguous or invalid time for timezone".to_string())
453}
454
455fn chrono_datetime_to_time_value(dt: DateTime<FixedOffset>) -> TimeValue {
456    let offset_seconds = dt.offset().local_minus_utc();
457    let offset_hours = (offset_seconds / SECONDS_PER_HOUR) as i8;
458    let offset_minutes = ((offset_seconds.abs() % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
459
460    TimeValue {
461        hour: dt.hour() as u8,
462        minute: dt.minute() as u8,
463        second: dt.second() as u8,
464        timezone: Some(TimezoneValue {
465            offset_hours,
466            offset_minutes,
467        }),
468    }
469}