1use 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
16const 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
22const EPOCH_YEAR: i32 = 1970;
24const EPOCH_MONTH: u32 = 1;
25const EPOCH_DAY: u32 = 1;
26
27fn 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 Ok(FixedOffset::east_opt(0).unwrap())
42 }
43}
44
45pub fn datetime_arithmetic(
47 left: &LiteralValue,
48 op: &ArithmeticOperation,
49 right: &LiteralValue,
50) -> LemmaResult<LiteralValue> {
51 match (left, right, op) {
52 (
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 (
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 (
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
148fn 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
174fn 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
194fn 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 let milliseconds = (seconds_f64 * MILLISECONDS_PER_SECOND) as i64;
202 Ok(ChronoDuration::milliseconds(milliseconds))
203}
204
205pub fn datetime_comparison(
207 left: &LiteralValue,
208 op: &ComparisonOperator,
209 right: &LiteralValue,
210) -> LemmaResult<bool> {
211 match (left, right) {
212 (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 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
237pub fn time_arithmetic(
239 left: &LiteralValue,
240 op: &ArithmeticOperation,
241 right: &LiteralValue,
242) -> LemmaResult<LiteralValue> {
243 match (left, right, op) {
244 (
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 (
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 (
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 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
297fn time_value_to_chrono_datetime(time: &TimeValue) -> LemmaResult<DateTime<FixedOffset>> {
299 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
319fn 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}