1use 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
42pub 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 let date_dt = match datetime_value_to_chrono(date) {
172 Ok(d) => d,
173 Err(msg) => return OperationResult::Veto(Some(msg)),
174 };
175
176 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 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
276pub 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
312pub 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
349pub 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 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 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 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}