lemma/evaluator/
datetime.rs

1//! DateTime operations
2//!
3//! Handles arithmetic and comparisons with dates and datetimes.
4
5use crate::{
6    ArithmeticOperation, ComparisonOperator, DateTimeValue, LemmaError, LemmaResult, LiteralValue,
7    TimeValue, TimezoneValue,
8};
9use chrono::{
10    DateTime, Datelike, Duration as ChronoDuration, FixedOffset, NaiveDate, NaiveDateTime,
11    NaiveTime, TimeZone, Timelike,
12};
13use rust_decimal::prelude::ToPrimitive;
14use rust_decimal::Decimal;
15
16// Time constants
17const SECONDS_PER_HOUR: i32 = 3600;
18const SECONDS_PER_MINUTE: i32 = 60;
19const MONTHS_PER_YEAR: u32 = 12;
20const MILLISECONDS_PER_SECOND: f64 = 1000.0;
21
22// Reference date for time-only calculations (Unix epoch)
23const EPOCH_YEAR: i32 = 1970;
24const EPOCH_MONTH: u32 = 1;
25const EPOCH_DAY: u32 = 1;
26
27/// Create a timezone-aware FixedOffset from an optional TimezoneValue.
28/// Defaults to UTC if no timezone is specified.
29fn create_timezone_offset(timezone: &Option<TimezoneValue>) -> LemmaResult<FixedOffset> {
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            LemmaError::Engine(format!(
35                "Invalid timezone offset: {}:{}",
36                tz.offset_hours, tz.offset_minutes
37            ))
38        })
39    } else {
40        // Default to UTC (zero offset)
41        Ok(FixedOffset::east_opt(0).unwrap())
42    }
43}
44
45/// Perform date/datetime arithmetic
46pub fn datetime_arithmetic(
47    left: &LiteralValue,
48    op: &ArithmeticOperation,
49    right: &LiteralValue,
50) -> LemmaResult<LiteralValue> {
51    match (left, right, op) {
52        // Date + Duration
53        (
54            LiteralValue::Date(date),
55            LiteralValue::Unit(crate::NumericUnit::Duration(value, unit)),
56            ArithmeticOperation::Add,
57        ) => {
58            let dt = datetime_value_to_chrono(date)?;
59
60            let new_dt = match unit {
61                crate::DurationUnit::Month => {
62                    let months = value
63                        .to_i32()
64                        .ok_or_else(|| LemmaError::Engine("Month value too large".to_string()))?;
65                    dt.checked_add_months(chrono::Months::new(months as u32))
66                        .ok_or_else(|| LemmaError::Engine("Date overflow".to_string()))?
67                }
68                crate::DurationUnit::Year => {
69                    let years = value
70                        .to_i32()
71                        .ok_or_else(|| LemmaError::Engine("Year value too large".to_string()))?;
72                    dt.checked_add_months(chrono::Months::new(
73                        (years * MONTHS_PER_YEAR as i32) as u32,
74                    ))
75                    .ok_or_else(|| LemmaError::Engine("Date overflow".to_string()))?
76                }
77                _ => {
78                    let seconds = crate::parser::units::duration_to_seconds(*value, unit);
79                    let duration = seconds_to_chrono_duration(seconds)?;
80                    dt.checked_add_signed(duration)
81                        .ok_or_else(|| LemmaError::Engine("Date overflow".to_string()))?
82                }
83            };
84
85            Ok(LiteralValue::Date(chrono_to_datetime_value(new_dt)))
86        }
87
88        // Date - Duration
89        (
90            LiteralValue::Date(date),
91            LiteralValue::Unit(crate::NumericUnit::Duration(value, unit)),
92            ArithmeticOperation::Subtract,
93        ) => {
94            let dt = datetime_value_to_chrono(date)?;
95
96            let new_dt = match unit {
97                crate::DurationUnit::Month => {
98                    let months = value
99                        .to_i32()
100                        .ok_or_else(|| LemmaError::Engine("Month value too large".to_string()))?;
101                    dt.checked_sub_months(chrono::Months::new(months as u32))
102                        .ok_or_else(|| LemmaError::Engine("Date overflow".to_string()))?
103                }
104                crate::DurationUnit::Year => {
105                    let years = value
106                        .to_i32()
107                        .ok_or_else(|| LemmaError::Engine("Year value too large".to_string()))?;
108                    dt.checked_sub_months(chrono::Months::new(
109                        (years * MONTHS_PER_YEAR as i32) as u32,
110                    ))
111                    .ok_or_else(|| LemmaError::Engine("Date overflow".to_string()))?
112                }
113                _ => {
114                    let seconds = crate::parser::units::duration_to_seconds(*value, unit);
115                    let duration = seconds_to_chrono_duration(seconds)?;
116                    dt.checked_sub_signed(duration)
117                        .ok_or_else(|| LemmaError::Engine("Date overflow".to_string()))?
118                }
119            };
120
121            Ok(LiteralValue::Date(chrono_to_datetime_value(new_dt)))
122        }
123
124        // Date - Date = Duration (in seconds)
125        (
126            LiteralValue::Date(left_date),
127            LiteralValue::Date(right_date),
128            ArithmeticOperation::Subtract,
129        ) => {
130            let left_dt = datetime_value_to_chrono(left_date)?;
131            let right_dt = datetime_value_to_chrono(right_date)?;
132            let duration = left_dt - right_dt;
133
134            let seconds = Decimal::from(duration.num_seconds());
135            Ok(LiteralValue::Unit(crate::NumericUnit::Duration(
136                seconds,
137                crate::DurationUnit::Second,
138            )))
139        }
140
141        _ => Err(LemmaError::Engine(format!(
142            "DateTime arithmetic operation {:?} not supported for these operand types",
143            op
144        ))),
145    }
146}
147
148/// Convert DateTimeValue to chrono DateTime, handling timezone if present
149fn datetime_value_to_chrono(date: &DateTimeValue) -> LemmaResult<DateTime<FixedOffset>> {
150    let naive_date = NaiveDate::from_ymd_opt(date.year, date.month, date.day).ok_or_else(|| {
151        LemmaError::Engine(format!(
152            "Invalid date: {}-{}-{}",
153            date.year, date.month, date.day
154        ))
155    })?;
156
157    let naive_time = chrono::NaiveTime::from_hms_opt(date.hour, date.minute, date.second)
158        .ok_or_else(|| {
159            LemmaError::Engine(format!(
160                "Invalid time: {}:{}:{}",
161                date.hour, date.minute, date.second
162            ))
163        })?;
164
165    let naive_dt = NaiveDateTime::new(naive_date, naive_time);
166
167    let offset = create_timezone_offset(&date.timezone)?;
168    offset
169        .from_local_datetime(&naive_dt)
170        .single()
171        .ok_or_else(|| LemmaError::Engine("Ambiguous or invalid datetime for timezone".to_string()))
172}
173
174/// Convert chrono DateTime back to DateTimeValue
175fn chrono_to_datetime_value(dt: DateTime<FixedOffset>) -> DateTimeValue {
176    let offset_seconds = dt.offset().local_minus_utc();
177    let offset_hours = (offset_seconds / SECONDS_PER_HOUR) as i8;
178    let offset_minutes = ((offset_seconds.abs() % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
179
180    DateTimeValue {
181        year: dt.year(),
182        month: dt.month(),
183        day: dt.day(),
184        hour: dt.hour(),
185        minute: dt.minute(),
186        second: dt.second(),
187        timezone: Some(TimezoneValue {
188            offset_hours,
189            offset_minutes,
190        }),
191    }
192}
193
194/// Convert seconds (Decimal) to chrono Duration
195fn seconds_to_chrono_duration(seconds: Decimal) -> LemmaResult<ChronoDuration> {
196    let seconds_f64 = seconds
197        .to_f64()
198        .ok_or_else(|| LemmaError::Engine("Duration conversion failed".to_string()))?;
199
200    // Handle fractional seconds by converting to milliseconds for precision
201    let milliseconds = (seconds_f64 * MILLISECONDS_PER_SECOND) as i64;
202    Ok(ChronoDuration::milliseconds(milliseconds))
203}
204
205/// Perform date/datetime comparisons
206pub fn datetime_comparison(
207    left: &LiteralValue,
208    op: &ComparisonOperator,
209    right: &LiteralValue,
210) -> LemmaResult<bool> {
211    match (left, right) {
212        // Date comparisons - convert both to UTC for fair comparison
213        (LiteralValue::Date(l), LiteralValue::Date(r)) => {
214            let l_dt = datetime_value_to_chrono(l)?;
215            let r_dt = datetime_value_to_chrono(r)?;
216
217            // Convert to UTC for comparison
218            let l_utc = l_dt.naive_utc();
219            let r_utc = r_dt.naive_utc();
220
221            Ok(match op {
222                ComparisonOperator::GreaterThan => l_utc > r_utc,
223                ComparisonOperator::LessThan => l_utc < r_utc,
224                ComparisonOperator::GreaterThanOrEqual => l_utc >= r_utc,
225                ComparisonOperator::LessThanOrEqual => l_utc <= r_utc,
226                ComparisonOperator::Equal | ComparisonOperator::Is => l_utc == r_utc,
227                ComparisonOperator::NotEqual | ComparisonOperator::IsNot => l_utc != r_utc,
228            })
229        }
230
231        _ => Err(LemmaError::Engine(
232            "Invalid datetime comparison operands".to_string(),
233        )),
234    }
235}
236
237/// Perform time arithmetic operations
238pub fn time_arithmetic(
239    left: &LiteralValue,
240    op: &ArithmeticOperation,
241    right: &LiteralValue,
242) -> LemmaResult<LiteralValue> {
243    match (left, right, op) {
244        // Time + Duration = Time
245        (
246            LiteralValue::Time(time),
247            LiteralValue::Unit(crate::NumericUnit::Duration(value, unit)),
248            ArithmeticOperation::Add,
249        ) => {
250            let seconds = crate::parser::units::duration_to_seconds(*value, unit);
251            let time_aware = time_value_to_chrono_datetime(time)?;
252            let duration = seconds_to_chrono_duration(seconds)?;
253            let result_dt = time_aware + duration;
254            Ok(LiteralValue::Time(chrono_datetime_to_time_value(result_dt)))
255        }
256
257        // Time - Duration = Time
258        (
259            LiteralValue::Time(time),
260            LiteralValue::Unit(crate::NumericUnit::Duration(value, unit)),
261            ArithmeticOperation::Subtract,
262        ) => {
263            let seconds = crate::parser::units::duration_to_seconds(*value, unit);
264            let time_aware = time_value_to_chrono_datetime(time)?;
265            let duration = seconds_to_chrono_duration(seconds)?;
266            let result_dt = time_aware - duration;
267            Ok(LiteralValue::Time(chrono_datetime_to_time_value(result_dt)))
268        }
269
270        // Time - Time = Duration (in seconds)
271        (
272            LiteralValue::Time(left_time),
273            LiteralValue::Time(right_time),
274            ArithmeticOperation::Subtract,
275        ) => {
276            let left_dt = time_value_to_chrono_datetime(left_time)?;
277            let right_dt = time_value_to_chrono_datetime(right_time)?;
278
279            // Convert to UTC and get difference in seconds
280            let diff = left_dt.naive_utc() - right_dt.naive_utc();
281            let diff_seconds = diff.num_seconds();
282            let seconds = Decimal::from(diff_seconds);
283
284            Ok(LiteralValue::Unit(crate::NumericUnit::Duration(
285                seconds,
286                crate::DurationUnit::Second,
287            )))
288        }
289
290        _ => Err(LemmaError::Engine(format!(
291            "Time arithmetic operation {:?} not supported for these operand types",
292            op
293        ))),
294    }
295}
296
297/// Convert TimeValue to timezone-aware DateTime (using epoch date for calculation)
298fn time_value_to_chrono_datetime(time: &TimeValue) -> LemmaResult<DateTime<FixedOffset>> {
299    // Use Unix epoch as reference date for time-only arithmetic
300    let naive_date = NaiveDate::from_ymd_opt(EPOCH_YEAR, EPOCH_MONTH, EPOCH_DAY).unwrap();
301    let naive_time =
302        NaiveTime::from_hms_opt(time.hour as u32, time.minute as u32, time.second as u32)
303            .ok_or_else(|| {
304                LemmaError::Engine(format!(
305                    "Invalid time: {}:{}:{}",
306                    time.hour, time.minute, time.second
307                ))
308            })?;
309
310    let naive_dt = NaiveDateTime::new(naive_date, naive_time);
311
312    let offset = create_timezone_offset(&time.timezone)?;
313    offset
314        .from_local_datetime(&naive_dt)
315        .single()
316        .ok_or_else(|| LemmaError::Engine("Ambiguous or invalid time for timezone".to_string()))
317}
318
319/// Convert chrono DateTime back to TimeValue
320fn chrono_datetime_to_time_value(dt: DateTime<FixedOffset>) -> TimeValue {
321    let offset_seconds = dt.offset().local_minus_utc();
322    let offset_hours = (offset_seconds / SECONDS_PER_HOUR) as i8;
323    let offset_minutes = ((offset_seconds.abs() % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8;
324
325    TimeValue {
326        hour: dt.hour() as u8,
327        minute: dt.minute() as u8,
328        second: dt.second() as u8,
329        timezone: Some(TimezoneValue {
330            offset_hours,
331            offset_minutes,
332        }),
333    }
334}