1use anyhow::{Result, anyhow};
10use chrono::{
11 DateTime, Datelike, Duration, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Offset,
12 TimeZone, Timelike, Utc, Weekday,
13};
14use chrono_tz::Tz;
15use std::collections::HashMap;
16pub use uni_common::TemporalType;
19use uni_common::{TemporalValue, Value};
20
21const MICROS_PER_SECOND: i64 = 1_000_000;
26const MICROS_PER_MINUTE: i64 = 60 * MICROS_PER_SECOND;
27const MICROS_PER_HOUR: i64 = 60 * MICROS_PER_MINUTE;
28const MICROS_PER_DAY: i64 = 24 * MICROS_PER_HOUR;
29const SECONDS_PER_DAY: i64 = 86_400;
30const NANOS_PER_SECOND: i64 = 1_000_000_000;
31const NANOS_PER_DAY: i64 = 24 * 3600 * NANOS_PER_SECOND;
32
33pub fn classify_temporal(s: &str) -> Option<TemporalType> {
35 let base = if let Some(bracket_pos) = s.find('[') {
37 &s[..bracket_pos]
38 } else {
39 s
40 };
41
42 if base.starts_with(['P', 'p']) {
44 return Some(TemporalType::Duration);
45 }
46
47 let has_date = base.len() >= 10
49 && base.as_bytes().get(4) == Some(&b'-')
50 && base.as_bytes().get(7) == Some(&b'-')
51 && base[..4].bytes().all(|b| b.is_ascii_digit())
52 && base[5..7].bytes().all(|b| b.is_ascii_digit())
53 && base[8..10].bytes().all(|b| b.is_ascii_digit());
54
55 let has_t = has_date && base.len() > 10 && base.as_bytes().get(10) == Some(&b'T');
57
58 if has_date && has_t {
59 let after_t = &base[11..];
61 if has_timezone_suffix(after_t) {
62 Some(TemporalType::DateTime)
63 } else {
64 Some(TemporalType::LocalDateTime)
65 }
66 } else if has_date {
67 Some(TemporalType::Date)
68 } else {
69 let has_time = base.len() >= 5
71 && base.as_bytes().get(2) == Some(&b':')
72 && base[..2].bytes().all(|b| b.is_ascii_digit())
73 && base[3..5].bytes().all(|b| b.is_ascii_digit());
74
75 if has_time {
76 if has_timezone_suffix(base) {
77 Some(TemporalType::Time)
78 } else {
79 Some(TemporalType::LocalTime)
80 }
81 } else {
82 None
83 }
84 }
85}
86
87fn has_timezone_suffix(s: &str) -> bool {
89 if s.ends_with(['Z', 'z']) {
90 return true;
91 }
92 for (i, b) in s.bytes().enumerate().rev() {
95 if b == b'+' || b == b'-' {
96 let after = &s[i + 1..];
97 if after.len() >= 4
98 && after[..2].bytes().all(|b| b.is_ascii_digit())
99 && after.as_bytes().get(2) == Some(&b':')
100 {
101 return true;
102 }
103 if after.len() >= 4 && after[..4].bytes().all(|b| b.is_ascii_digit()) {
105 return true;
106 }
107 }
108 }
109 false
110}
111
112pub fn parse_duration_from_value(val: &Value) -> Result<CypherDuration> {
114 match val {
115 Value::Temporal(TemporalValue::Duration {
116 months,
117 days,
118 nanos,
119 }) => Ok(CypherDuration::new(*months, *days, *nanos)),
120 Value::Map(map) => {
121 if let Some(Value::Map(inner)) = map.get("Duration")
122 && let (Some(months), Some(days), Some(nanos)) = (
123 inner.get("months").and_then(Value::as_i64),
124 inner.get("days").and_then(Value::as_i64),
125 inner.get("nanos").and_then(Value::as_i64),
126 )
127 {
128 return Ok(CypherDuration::new(months, days, nanos));
129 }
130 Err(anyhow!("Expected duration value"))
131 }
132 Value::String(s) => parse_duration_to_cypher(s),
133 Value::Int(micros) => Ok(CypherDuration::from_micros(*micros)),
134 _ => Err(anyhow!("Expected duration value")),
135 }
136}
137
138#[derive(Debug, Clone)]
144pub enum TimezoneInfo {
145 FixedOffset(FixedOffset),
147 Named(Tz),
149}
150
151impl TimezoneInfo {
152 pub fn offset_for_local(&self, ndt: &NaiveDateTime) -> Result<FixedOffset> {
154 match self {
155 TimezoneInfo::FixedOffset(fo) => Ok(*fo),
156 TimezoneInfo::Named(tz) => {
157 match tz.from_local_datetime(ndt) {
159 chrono::LocalResult::Single(dt) => Ok(dt.offset().fix()),
160 chrono::LocalResult::Ambiguous(dt1, _dt2) => {
161 Ok(dt1.offset().fix())
163 }
164 chrono::LocalResult::None => {
165 Err(anyhow!("Local time does not exist in timezone (DST gap)"))
167 }
168 }
169 }
170 }
171 }
172
173 pub fn offset_for_utc(&self, utc_ndt: &NaiveDateTime) -> FixedOffset {
175 match self {
176 TimezoneInfo::FixedOffset(fo) => *fo,
177 TimezoneInfo::Named(tz) => tz.from_utc_datetime(utc_ndt).offset().fix(),
178 }
179 }
180
181 fn name(&self) -> Option<&str> {
183 match self {
184 TimezoneInfo::FixedOffset(_) => None,
185 TimezoneInfo::Named(tz) => Some(tz.name()),
186 }
187 }
188
189 fn offset_seconds_with_date(&self, date: &NaiveDate) -> i32 {
191 match self {
192 TimezoneInfo::FixedOffset(fo) => fo.local_minus_utc(),
193 TimezoneInfo::Named(tz) => {
194 let noon = NaiveTime::from_hms_opt(12, 0, 0).unwrap();
196 let ndt = NaiveDateTime::new(*date, noon);
197 match tz.from_local_datetime(&ndt) {
198 chrono::LocalResult::Single(dt) => dt.offset().fix().local_minus_utc(),
199 chrono::LocalResult::Ambiguous(dt1, _) => dt1.offset().fix().local_minus_utc(),
200 chrono::LocalResult::None => 0, }
202 }
203 }
204 }
205}
206
207fn parse_timezone(tz_str: &str) -> Result<TimezoneInfo> {
209 let tz_str = tz_str.trim();
210
211 if let Ok(tz) = tz_str.parse::<Tz>() {
213 return Ok(TimezoneInfo::Named(tz));
214 }
215
216 let offset_secs = parse_timezone_offset(tz_str)?;
218 let offset = FixedOffset::east_opt(offset_secs)
219 .ok_or_else(|| anyhow!("Invalid timezone offset: {}", offset_secs))?;
220 Ok(TimezoneInfo::FixedOffset(offset))
221}
222
223pub fn parse_datetime_utc(s: &str) -> Result<DateTime<Utc>> {
237 let s = s.trim();
241 let parse_input = match s.rfind('[') {
242 Some(pos) if s.ends_with(']') => &s[..pos],
243 _ => s,
244 };
245
246 DateTime::parse_from_rfc3339(parse_input)
247 .map(|dt: DateTime<FixedOffset>| dt.with_timezone(&Utc))
248 .or_else(|_| {
249 if let Some(base) = parse_input.strip_suffix('Z') {
251 NaiveDateTime::parse_from_str(base, "%Y-%m-%dT%H:%M")
252 .map(|ndt| DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc))
253 } else {
254 DateTime::parse_from_str(parse_input, "%Y-%m-%dT%H:%M%:z")
256 .map(|dt: DateTime<FixedOffset>| dt.with_timezone(&Utc))
257 }
258 })
259 .or_else(|_| {
260 DateTime::parse_from_str(parse_input, "%Y-%m-%d %H:%M:%S %z")
261 .map(|dt: DateTime<FixedOffset>| dt.with_timezone(&Utc))
262 })
263 .or_else(|_| {
264 NaiveDateTime::parse_from_str(parse_input, "%Y-%m-%d %H:%M:%S")
265 .map(|ndt| DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc))
266 })
267 .map_err(|_| anyhow!("Invalid datetime format: {}", s))
268}
269
270pub fn eval_datetime_function_with_clock(
282 name: &str,
283 args: &[Value],
284 frozen_now: chrono::DateTime<chrono::Utc>,
285) -> Result<Value> {
286 if args.is_empty() {
288 match name {
289 "DATE" | "DATE.STATEMENT" | "DATE.TRANSACTION" => {
290 let d = frozen_now.date_naive();
291 return Ok(Value::Temporal(TemporalValue::Date {
292 days_since_epoch: date_to_days_since_epoch(&d),
293 }));
294 }
295 "TIME" | "TIME.STATEMENT" | "TIME.TRANSACTION" => {
296 let t = frozen_now.time();
297 return Ok(Value::Temporal(TemporalValue::Time {
298 nanos_since_midnight: time_to_nanos(&t),
299 offset_seconds: 0,
300 }));
301 }
302 "LOCALTIME" | "LOCALTIME.STATEMENT" | "LOCALTIME.TRANSACTION" => {
303 let local = frozen_now.with_timezone(&chrono::Local).time();
304 return Ok(Value::Temporal(TemporalValue::LocalTime {
305 nanos_since_midnight: time_to_nanos(&local),
306 }));
307 }
308 "DATETIME" | "DATETIME.STATEMENT" | "DATETIME.TRANSACTION" => {
309 return Ok(Value::Temporal(TemporalValue::DateTime {
310 nanos_since_epoch: frozen_now.timestamp_nanos_opt().unwrap_or(0),
311 offset_seconds: 0,
312 timezone_name: None,
313 }));
314 }
315 "LOCALDATETIME" | "LOCALDATETIME.STATEMENT" | "LOCALDATETIME.TRANSACTION" => {
316 let local = frozen_now.with_timezone(&chrono::Local).naive_local();
317 let epoch = NaiveDateTime::new(
318 NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(),
319 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
320 );
321 let nanos = local
322 .signed_duration_since(epoch)
323 .num_nanoseconds()
324 .unwrap_or(0);
325 return Ok(Value::Temporal(TemporalValue::LocalDateTime {
326 nanos_since_epoch: nanos,
327 }));
328 }
329 _ => {}
330 }
331 }
332 eval_datetime_function(name, args)
334}
335
336pub fn eval_datetime_function(name: &str, args: &[Value]) -> Result<Value> {
337 match name {
338 "DATE" => eval_date(args),
340 "TIME" => eval_time(args),
341 "DATETIME" => eval_datetime(args),
342 "LOCALDATETIME" => eval_localdatetime(args),
343 "LOCALTIME" => eval_localtime(args),
344 "DURATION" => eval_duration(args),
345
346 "YEAR" => eval_extract(args, Component::Year),
348 "MONTH" => eval_extract(args, Component::Month),
349 "DAY" => eval_extract(args, Component::Day),
350 "HOUR" => eval_extract(args, Component::Hour),
351 "MINUTE" => eval_extract(args, Component::Minute),
352 "SECOND" => eval_extract(args, Component::Second),
353
354 "DATETIME.FROMEPOCH" => eval_datetime_fromepoch(args),
356 "DATETIME.FROMEPOCHMILLIS" => eval_datetime_fromepochmillis(args),
357
358 "DATE.TRUNCATE" => eval_truncate("date", args),
360 "TIME.TRUNCATE" => eval_truncate("time", args),
361 "DATETIME.TRUNCATE" => eval_truncate("datetime", args),
362 "LOCALDATETIME.TRUNCATE" => eval_truncate("localdatetime", args),
363 "LOCALTIME.TRUNCATE" => eval_truncate("localtime", args),
364
365 "DATETIME.TRANSACTION" | "DATETIME.STATEMENT" | "DATETIME.REALTIME" => eval_datetime(args),
367 "DATE.TRANSACTION" | "DATE.STATEMENT" | "DATE.REALTIME" => eval_date(args),
368 "TIME.TRANSACTION" | "TIME.STATEMENT" | "TIME.REALTIME" => eval_time(args),
369 "LOCALTIME.TRANSACTION" | "LOCALTIME.STATEMENT" | "LOCALTIME.REALTIME" => {
370 eval_localtime(args)
371 }
372 "LOCALDATETIME.TRANSACTION" | "LOCALDATETIME.STATEMENT" | "LOCALDATETIME.REALTIME" => {
373 eval_localdatetime(args)
374 }
375
376 "DURATION.BETWEEN" => eval_duration_between(args),
378 "DURATION.INMONTHS" => eval_duration_in_months(args),
379 "DURATION.INDAYS" => eval_duration_in_days(args),
380 "DURATION.INSECONDS" => eval_duration_in_seconds(args),
381
382 _ => Err(anyhow!("Unknown datetime function: {}", name)),
383 }
384}
385
386pub fn is_datetime_value(val: &Value) -> bool {
388 match val {
389 Value::Temporal(TemporalValue::DateTime { .. }) => true,
390 Value::String(s) => parse_datetime_utc(s).is_ok(),
391 _ => false,
392 }
393}
394
395pub fn is_date_value(val: &Value) -> bool {
397 match val {
398 Value::Temporal(TemporalValue::Date { .. }) => true,
399 Value::String(s) => NaiveDate::parse_from_str(s, "%Y-%m-%d").is_ok(),
400 _ => false,
401 }
402}
403
404pub fn is_duration_value(val: &Value) -> bool {
410 match val {
411 Value::Temporal(TemporalValue::Duration { .. }) => true,
412 Value::String(s) => is_duration_string(s),
413 _ => false,
414 }
415}
416
417pub fn is_duration_or_micros(val: &Value) -> bool {
423 is_duration_value(val) || matches!(val, Value::Int(_))
424}
425
426pub fn duration_to_micros(val: &Value) -> Result<i64> {
428 match val {
429 Value::String(s) => {
430 let duration = parse_duration_to_cypher(s)?;
431 Ok(duration.to_micros())
432 }
433 Value::Int(i) => Ok(*i),
434 _ => Err(anyhow!("Expected duration value")),
435 }
436}
437
438pub fn add_duration_to_datetime(dt_str: &str, micros: i64) -> Result<String> {
440 let dt = parse_datetime_utc(dt_str)?;
441 let result = dt + Duration::microseconds(micros);
442 Ok(result.to_rfc3339())
443}
444
445pub fn add_duration_to_date(date_str: &str, micros: i64) -> Result<String> {
447 let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
448 let dt = date
449 .and_hms_opt(0, 0, 0)
450 .ok_or_else(|| anyhow!("Invalid date"))?;
451 let result = dt + Duration::microseconds(micros);
452 Ok(result.format("%Y-%m-%d").to_string())
453}
454
455pub fn datetime_difference(dt1_str: &str, dt2_str: &str) -> Result<i64> {
457 let dt1 = parse_datetime_utc(dt1_str)?;
458 let dt2 = parse_datetime_utc(dt2_str)?;
459 dt1.signed_duration_since(dt2)
460 .num_microseconds()
461 .ok_or_else(|| anyhow!("Duration overflow"))
462}
463
464pub fn parse_duration_to_micros(s: &str) -> Result<i64> {
468 let s = s.trim();
469
470 if s.starts_with(['P', 'p']) {
472 return parse_iso8601_duration(s);
473 }
474
475 parse_simple_duration(s)
477}
478
479pub fn parse_duration_to_cypher(s: &str) -> Result<CypherDuration> {
481 let s = s.trim();
482
483 if s.starts_with(['P', 'p']) {
485 return parse_iso8601_duration_cypher(s);
486 }
487
488 let micros = parse_simple_duration(s)?;
490 Ok(CypherDuration::from_micros(micros))
491}
492
493fn parse_datetime_style_duration(s: &str) -> Result<CypherDuration> {
497 let body = &s[1..]; let (date_part, time_part) = if let Some(t_pos) = body.find('T') {
501 (&body[..t_pos], Some(&body[t_pos + 1..]))
502 } else {
503 (body, None)
504 };
505
506 let date_parts: Vec<&str> = date_part.split('-').collect();
508 if date_parts.len() != 3 {
509 return Err(anyhow!(
510 "Invalid date-time style duration date: {}",
511 date_part
512 ));
513 }
514 let years: i64 = date_parts[0]
515 .parse()
516 .map_err(|_| anyhow!("Invalid years"))?;
517 let month_val: i64 = date_parts[1]
518 .parse()
519 .map_err(|_| anyhow!("Invalid months"))?;
520 let day_val: i64 = date_parts[2].parse().map_err(|_| anyhow!("Invalid days"))?;
521
522 let months = years * 12 + month_val;
523 let days = day_val;
524
525 let nanos = if let Some(tp) = time_part {
527 let time_parts: Vec<&str> = tp.split(':').collect();
528 if time_parts.len() != 3 {
529 return Err(anyhow!("Invalid date-time style duration time: {}", tp));
530 }
531 let hours: f64 = time_parts[0]
532 .parse()
533 .map_err(|_| anyhow!("Invalid hours"))?;
534 let minutes: f64 = time_parts[1]
535 .parse()
536 .map_err(|_| anyhow!("Invalid minutes"))?;
537 let seconds: f64 = time_parts[2]
538 .parse()
539 .map_err(|_| anyhow!("Invalid seconds"))?;
540 (hours * 3600.0 * NANOS_PER_SECOND as f64
541 + minutes * 60.0 * NANOS_PER_SECOND as f64
542 + seconds * NANOS_PER_SECOND as f64) as i64
543 } else {
544 0
545 };
546
547 Ok(CypherDuration::new(months, days, nanos))
548}
549
550fn parse_iso8601_duration_cypher(s: &str) -> Result<CypherDuration> {
552 if s.len() >= 11
555 && s.as_bytes().get(5) == Some(&b'-')
556 && s.as_bytes().get(1).is_some_and(|b| b.is_ascii_digit())
557 {
558 return parse_datetime_style_duration(s);
559 }
560
561 let s = &s[1..]; let mut months: i64 = 0;
563 let mut days: i64 = 0;
564 let mut nanos: i64 = 0;
565 let mut in_time_part = false;
566 let mut num_buf = String::new();
567
568 for c in s.chars() {
569 if c == 'T' || c == 't' {
570 in_time_part = true;
571 continue;
572 }
573
574 if c.is_ascii_digit() || c == '.' || c == '-' {
575 num_buf.push(c);
576 } else {
577 if num_buf.is_empty() {
578 continue;
579 }
580 let num: f64 = num_buf
581 .parse()
582 .map_err(|_| anyhow!("Invalid duration number"))?;
583 num_buf.clear();
584
585 match c {
586 'Y' | 'y' => {
587 let whole = num.trunc() as i64;
589 let frac = num.fract();
590 months += whole * 12;
591 if frac != 0.0 {
592 let frac_months = frac * 12.0;
594 let whole_frac_months = frac_months.trunc() as i64;
595 let frac_frac_months = frac_months.fract();
596 months += whole_frac_months;
597 let frac_secs = frac_frac_months * 2_629_746.0;
599 let extra_days = (frac_secs / SECONDS_PER_DAY as f64).trunc() as i64;
600 let remaining_secs =
601 frac_secs - (extra_days as f64 * SECONDS_PER_DAY as f64);
602 days += extra_days;
603 nanos += (remaining_secs * NANOS_PER_SECOND as f64) as i64;
604 }
605 }
606 'M' if !in_time_part => {
607 let whole = num.trunc() as i64;
609 let frac = num.fract();
610 months += whole;
611 if frac != 0.0 {
612 let frac_secs = frac * 2_629_746.0;
613 let extra_days = (frac_secs / SECONDS_PER_DAY as f64).trunc() as i64;
614 let remaining_secs =
615 frac_secs - (extra_days as f64 * SECONDS_PER_DAY as f64);
616 days += extra_days;
617 nanos += (remaining_secs * NANOS_PER_SECOND as f64) as i64;
618 }
619 }
620 'W' | 'w' => {
621 let total_days_f = num * 7.0;
623 let whole = total_days_f.trunc() as i64;
624 let frac = total_days_f.fract();
625 days += whole;
626 nanos += (frac * NANOS_PER_DAY as f64) as i64;
627 }
628 'D' | 'd' => {
629 let whole = num.trunc() as i64;
631 let frac = num.fract();
632 days += whole;
633 nanos += (frac * NANOS_PER_DAY as f64) as i64;
634 }
635 'H' | 'h' => nanos += (num * 3600.0 * NANOS_PER_SECOND as f64) as i64,
636 'M' | 'm' if in_time_part => nanos += (num * 60.0 * NANOS_PER_SECOND as f64) as i64,
637 'S' | 's' => nanos += (num * NANOS_PER_SECOND as f64) as i64,
638 _ => return Err(anyhow!("Invalid ISO 8601 duration designator: {}", c)),
639 }
640 }
641 }
642
643 Ok(CypherDuration::new(months, days, nanos))
644}
645
646enum Component {
651 Year,
652 Month,
653 Day,
654 Hour,
655 Minute,
656 Second,
657}
658
659fn eval_extract(args: &[Value], component: Component) -> Result<Value> {
660 if args.len() != 1 {
661 return Err(anyhow!("Extract function requires 1 argument"));
662 }
663 match &args[0] {
664 Value::Temporal(tv) => {
665 let result = match component {
666 Component::Year => tv.year(),
667 Component::Month => tv.month(),
668 Component::Day => tv.day(),
669 Component::Hour => tv.hour(),
670 Component::Minute => tv.minute(),
671 Component::Second => tv.second(),
672 };
673 match result {
674 Some(v) => Ok(Value::Int(v)),
675 None => Err(anyhow!("Temporal value does not have requested component")),
676 }
677 }
678 Value::String(s) => {
679 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
681 return Ok(Value::Int(extract_component(&dt, &component) as i64));
682 }
683 if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
684 return Ok(Value::Int(extract_component(&dt, &component) as i64));
685 }
686
687 match component {
688 Component::Year | Component::Month | Component::Day => {
689 if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
690 return Ok(Value::Int(match component {
691 Component::Year => d.year() as i64,
692 Component::Month => d.month() as i64,
693 Component::Day => d.day() as i64,
694 _ => unreachable!(),
695 }));
696 }
697 }
698 Component::Hour | Component::Minute | Component::Second => {
699 if let Ok(t) = NaiveTime::parse_from_str(s, "%H:%M:%S") {
700 return Ok(Value::Int(match component {
701 Component::Hour => t.hour() as i64,
702 Component::Minute => t.minute() as i64,
703 Component::Second => t.second() as i64,
704 _ => unreachable!(),
705 }));
706 }
707 }
708 }
709
710 Err(anyhow!("Could not parse date/time string for extraction"))
711 }
712 Value::Null => Ok(Value::Null),
713 _ => Err(anyhow!(
714 "Extract function expects a temporal or string argument"
715 )),
716 }
717}
718
719fn extract_component<T: Datelike + Timelike>(dt: &T, component: &Component) -> i32 {
720 match component {
721 Component::Year => dt.year(),
722 Component::Month => dt.month() as i32,
723 Component::Day => dt.day() as i32,
724 Component::Hour => dt.hour() as i32,
725 Component::Minute => dt.minute() as i32,
726 Component::Second => dt.second() as i32,
727 }
728}
729
730pub fn eval_temporal_accessor(temporal_str: &str, component: &str) -> Result<Value> {
739 let component_lower = component.to_lowercase();
740 match component_lower.as_str() {
741 "year" => extract_year(temporal_str),
743 "month" => extract_month(temporal_str),
744 "day" => extract_day(temporal_str),
745 "hour" => extract_hour(temporal_str),
746 "minute" => extract_minute(temporal_str),
747 "second" => extract_second(temporal_str),
748
749 "quarter" => extract_quarter(temporal_str),
751 "week" => extract_week(temporal_str),
752 "weekyear" => extract_week_year(temporal_str),
753 "ordinalday" => extract_ordinal_day(temporal_str),
754 "dayofweek" | "weekday" => extract_day_of_week(temporal_str),
755 "dayofquarter" => extract_day_of_quarter(temporal_str),
756
757 "millisecond" => extract_millisecond(temporal_str),
759 "microsecond" => extract_microsecond(temporal_str),
760 "nanosecond" => extract_nanosecond(temporal_str),
761
762 "timezone" => extract_timezone_name_from_str(temporal_str),
764 "offset" => extract_offset_string(temporal_str),
765 "offsetminutes" => extract_offset_minutes(temporal_str),
766 "offsetseconds" => extract_offset_seconds(temporal_str),
767
768 "epochseconds" => extract_epoch_seconds(temporal_str),
770 "epochmillis" => extract_epoch_millis(temporal_str),
771
772 _ => Err(anyhow!("Unknown temporal component: {}", component)),
773 }
774}
775
776pub fn eval_temporal_accessor_value(val: &Value, component: &str) -> Result<Value> {
781 match val {
782 Value::Null => Ok(Value::Null),
783 Value::Map(map) => Ok(map.get(component).cloned().unwrap_or(Value::Null)),
787 Value::Temporal(tv) => {
788 let comp_lower = component.to_lowercase();
791 match comp_lower.as_str() {
792 "timezone" => {
793 return match tv {
794 TemporalValue::DateTime {
795 timezone_name,
796 offset_seconds,
797 ..
798 } => Ok(match timezone_name {
799 Some(name) => Value::String(name.clone()),
800 None => Value::String(format_timezone_offset(*offset_seconds)),
801 }),
802 TemporalValue::Time { offset_seconds, .. } => {
803 Ok(Value::String(format_timezone_offset(*offset_seconds)))
804 }
805 _ => Ok(Value::Null),
806 };
807 }
808 "offset" => {
809 return match tv {
810 TemporalValue::DateTime { offset_seconds, .. }
811 | TemporalValue::Time { offset_seconds, .. } => {
812 Ok(Value::String(format_timezone_offset(*offset_seconds)))
813 }
814 _ => Ok(Value::Null),
815 };
816 }
817 "offsetminutes" => {
818 return match tv {
819 TemporalValue::DateTime { offset_seconds, .. }
820 | TemporalValue::Time { offset_seconds, .. } => {
821 Ok(Value::Int((*offset_seconds / 60) as i64))
822 }
823 _ => Ok(Value::Null),
824 };
825 }
826 "offsetseconds" => {
827 return match tv {
828 TemporalValue::DateTime { offset_seconds, .. }
829 | TemporalValue::Time { offset_seconds, .. } => {
830 Ok(Value::Int(*offset_seconds as i64))
831 }
832 _ => Ok(Value::Null),
833 };
834 }
835 "epochseconds" => {
836 return match tv {
837 TemporalValue::DateTime {
838 nanos_since_epoch, ..
839 } => Ok(Value::Int(nanos_since_epoch / 1_000_000_000)),
840 TemporalValue::LocalDateTime { nanos_since_epoch } => {
841 Ok(Value::Int(nanos_since_epoch / 1_000_000_000))
842 }
843 TemporalValue::Date { days_since_epoch } => {
844 Ok(Value::Int(*days_since_epoch as i64 * 86400))
845 }
846 _ => Ok(Value::Null),
847 };
848 }
849 "epochmillis" => {
850 return match tv {
851 TemporalValue::DateTime {
852 nanos_since_epoch, ..
853 } => Ok(Value::Int(nanos_since_epoch / 1_000_000)),
854 TemporalValue::LocalDateTime { nanos_since_epoch } => {
855 Ok(Value::Int(nanos_since_epoch / 1_000_000))
856 }
857 TemporalValue::Date { days_since_epoch } => {
858 Ok(Value::Int(*days_since_epoch as i64 * 86400 * 1000))
859 }
860 _ => Ok(Value::Null),
861 };
862 }
863 _ => {}
864 }
865 let temporal_str = tv.to_string();
867 eval_temporal_accessor(&temporal_str, component)
868 }
869 Value::String(s) => eval_temporal_accessor(s, component),
870 _ => Err(anyhow!(
871 "Cannot access temporal property '{}' on non-temporal value",
872 component
873 )),
874 }
875}
876
877pub fn is_temporal_accessor(property: &str) -> bool {
879 let property_lower = property.to_lowercase();
880 matches!(
881 property_lower.as_str(),
882 "year"
883 | "month"
884 | "day"
885 | "hour"
886 | "minute"
887 | "second"
888 | "quarter"
889 | "week"
890 | "weekyear"
891 | "ordinalday"
892 | "dayofweek"
893 | "weekday"
894 | "dayofquarter"
895 | "millisecond"
896 | "microsecond"
897 | "nanosecond"
898 | "timezone"
899 | "offset"
900 | "offsetminutes"
901 | "offsetseconds"
902 | "epochseconds"
903 | "epochmillis"
904 )
905}
906
907pub fn is_temporal_string(s: &str) -> bool {
909 let bytes = s.as_bytes();
910 if bytes.len() < 8 {
911 return false;
912 }
913
914 (bytes.len() >= 10 && bytes[4] == b'-' && bytes[7] == b'-')
916 || (bytes[2] == b':' && bytes[5] == b':')
918 || (bytes[0] == b'P' || bytes[0] == b'p')
920}
921
922pub fn is_duration_string(s: &str) -> bool {
924 s.starts_with(['P', 'p'])
925}
926
927fn extract_date_component(s: &str, f: impl FnOnce(NaiveDate) -> i64) -> Result<Value> {
930 let (date, _, _) = parse_datetime_with_tz(s)?;
931 Ok(Value::Int(f(date)))
932}
933
934fn extract_time_component(s: &str, f: impl FnOnce(NaiveTime) -> i64) -> Result<Value> {
935 let (_, time, _) = parse_datetime_with_tz(s)?;
936 Ok(Value::Int(f(time)))
937}
938
939fn extract_year(s: &str) -> Result<Value> {
940 extract_date_component(s, |d| d.year() as i64)
941}
942
943fn extract_month(s: &str) -> Result<Value> {
944 extract_date_component(s, |d| d.month() as i64)
945}
946
947fn extract_day(s: &str) -> Result<Value> {
948 extract_date_component(s, |d| d.day() as i64)
949}
950
951fn extract_hour(s: &str) -> Result<Value> {
952 extract_time_component(s, |t| t.hour() as i64)
953}
954
955fn extract_minute(s: &str) -> Result<Value> {
956 extract_time_component(s, |t| t.minute() as i64)
957}
958
959fn extract_second(s: &str) -> Result<Value> {
960 extract_time_component(s, |t| t.second() as i64)
961}
962
963fn extract_quarter(s: &str) -> Result<Value> {
964 extract_date_component(s, |d| ((d.month() - 1) / 3 + 1) as i64)
965}
966
967fn extract_week(s: &str) -> Result<Value> {
968 extract_date_component(s, |d| d.iso_week().week() as i64)
969}
970
971fn extract_week_year(s: &str) -> Result<Value> {
972 extract_date_component(s, |d| d.iso_week().year() as i64)
973}
974
975fn extract_ordinal_day(s: &str) -> Result<Value> {
976 extract_date_component(s, |d| d.ordinal() as i64)
977}
978
979fn extract_day_of_week(s: &str) -> Result<Value> {
980 extract_date_component(s, |d| (d.weekday().num_days_from_monday() + 1) as i64)
982}
983
984fn extract_day_of_quarter(s: &str) -> Result<Value> {
985 let (date, _, _) = parse_datetime_with_tz(s)?;
986 let quarter = (date.month() - 1) / 3;
987 let first_month_of_quarter = quarter * 3 + 1;
988 let quarter_start = NaiveDate::from_ymd_opt(date.year(), first_month_of_quarter, 1)
989 .ok_or_else(|| {
990 anyhow!(
991 "Invalid quarter start for year={}, month={}",
992 date.year(),
993 first_month_of_quarter
994 )
995 })?;
996 let day_of_quarter = (date - quarter_start).num_days() + 1;
997 Ok(Value::Int(day_of_quarter))
998}
999
1000fn extract_millisecond(s: &str) -> Result<Value> {
1001 extract_time_component(s, |t| (t.nanosecond() / 1_000_000) as i64)
1002}
1003
1004fn extract_microsecond(s: &str) -> Result<Value> {
1005 extract_time_component(s, |t| (t.nanosecond() / 1_000) as i64)
1006}
1007
1008fn extract_nanosecond(s: &str) -> Result<Value> {
1009 extract_time_component(s, |t| t.nanosecond() as i64)
1010}
1011
1012fn extract_timezone_name_from_str(s: &str) -> Result<Value> {
1013 let (_, _, tz_info) = parse_datetime_with_tz(s)?;
1014 match tz_info {
1015 Some(TimezoneInfo::Named(tz)) => Ok(Value::String(tz.name().to_string())),
1016 Some(TimezoneInfo::FixedOffset(offset)) => {
1017 let secs = offset.local_minus_utc();
1019 Ok(Value::String(format_timezone_offset(secs)))
1020 }
1021 None => Ok(Value::Null),
1022 }
1023}
1024
1025fn extract_offset_string(s: &str) -> Result<Value> {
1026 let (date, time, tz_info) = parse_datetime_with_tz(s)?;
1027 match tz_info {
1028 Some(ref tz) => {
1029 let ndt = NaiveDateTime::new(date, time);
1030 let offset = tz.offset_for_local(&ndt)?;
1031 Ok(Value::String(format_timezone_offset(
1032 offset.local_minus_utc(),
1033 )))
1034 }
1035 None => Ok(Value::Null),
1036 }
1037}
1038
1039fn extract_offset_total_seconds(s: &str) -> Result<i32> {
1040 let (date, time, tz_info) = parse_datetime_with_tz(s)?;
1041 match tz_info {
1042 Some(ref tz) => {
1043 let ndt = NaiveDateTime::new(date, time);
1044 let offset = tz.offset_for_local(&ndt)?;
1045 Ok(offset.local_minus_utc())
1046 }
1047 None => Ok(0),
1048 }
1049}
1050
1051fn extract_offset_minutes(s: &str) -> Result<Value> {
1052 Ok(Value::Int((extract_offset_total_seconds(s)? / 60) as i64))
1053}
1054
1055fn extract_offset_seconds(s: &str) -> Result<Value> {
1056 Ok(Value::Int(extract_offset_total_seconds(s)? as i64))
1057}
1058
1059fn parse_as_utc(s: &str) -> Result<DateTime<Utc>> {
1060 let (date, time, tz_info) = parse_datetime_with_tz(s)?;
1061 let local_ndt = NaiveDateTime::new(date, time);
1062
1063 if let Some(tz) = tz_info {
1064 let offset = tz.offset_for_local(&local_ndt)?;
1065 let utc_ndt = local_ndt - Duration::seconds(offset.local_minus_utc() as i64);
1066 Ok(DateTime::<Utc>::from_naive_utc_and_offset(utc_ndt, Utc))
1067 } else {
1068 Ok(DateTime::<Utc>::from_naive_utc_and_offset(local_ndt, Utc))
1069 }
1070}
1071
1072fn extract_epoch_seconds(s: &str) -> Result<Value> {
1073 Ok(Value::Int(parse_as_utc(s)?.timestamp()))
1074}
1075
1076fn extract_epoch_millis(s: &str) -> Result<Value> {
1077 Ok(Value::Int(parse_as_utc(s)?.timestamp_millis()))
1078}
1079
1080pub fn eval_duration_accessor(duration_str: &str, component: &str) -> Result<Value> {
1089 let duration = parse_duration_to_cypher(duration_str)?;
1090 let component_lower = component.to_lowercase();
1091
1092 let total_months = duration.months;
1093 let total_nanos = duration.nanos;
1094 let total_secs = total_nanos.div_euclid(NANOS_PER_SECOND);
1095
1096 match component_lower.as_str() {
1097 "years" => Ok(Value::Int(total_months.div_euclid(12))),
1099 "quarters" => Ok(Value::Int(total_months.div_euclid(3))),
1100 "months" => Ok(Value::Int(total_months)),
1101 "weeks" => Ok(Value::Int(duration.days.div_euclid(7))),
1102 "days" => Ok(Value::Int(duration.days)),
1103 "hours" => Ok(Value::Int(total_secs.div_euclid(3600))),
1104 "minutes" => Ok(Value::Int(total_secs.div_euclid(60))),
1105 "seconds" => Ok(Value::Int(total_secs)),
1106 "milliseconds" => Ok(Value::Int(total_nanos.div_euclid(1_000_000))),
1107 "microseconds" => Ok(Value::Int(total_nanos.div_euclid(1_000))),
1108 "nanoseconds" => Ok(Value::Int(total_nanos)),
1109
1110 "quartersofyear" => Ok(Value::Int(total_months.rem_euclid(12) / 3)),
1112 "monthsofquarter" => Ok(Value::Int(total_months.rem_euclid(3))),
1113 "monthsofyear" => Ok(Value::Int(total_months.rem_euclid(12))),
1114 "daysofweek" => Ok(Value::Int(duration.days.rem_euclid(7))),
1115 "hoursofday" => Ok(Value::Int(total_secs.div_euclid(3600).rem_euclid(24))),
1116 "minutesofhour" => Ok(Value::Int(total_secs.div_euclid(60).rem_euclid(60))),
1117 "secondsofminute" => Ok(Value::Int(total_secs.rem_euclid(60))),
1118 "millisecondsofsecond" => Ok(Value::Int(
1119 total_nanos.div_euclid(1_000_000).rem_euclid(1000),
1120 )),
1121 "microsecondsofsecond" => Ok(Value::Int(
1122 total_nanos.div_euclid(1_000).rem_euclid(1_000_000),
1123 )),
1124 "nanosecondsofsecond" => Ok(Value::Int(total_nanos.rem_euclid(NANOS_PER_SECOND))),
1125
1126 _ => Err(anyhow!("Unknown duration component: {}", component)),
1127 }
1128}
1129
1130pub fn is_duration_accessor(property: &str) -> bool {
1132 let property_lower = property.to_lowercase();
1133 matches!(
1134 property_lower.as_str(),
1135 "years"
1136 | "quarters"
1137 | "months"
1138 | "weeks"
1139 | "days"
1140 | "hours"
1141 | "minutes"
1142 | "seconds"
1143 | "milliseconds"
1144 | "microseconds"
1145 | "nanoseconds"
1146 | "quartersofyear"
1147 | "monthsofquarter"
1148 | "monthsofyear"
1149 | "daysofweek"
1150 | "hoursofday"
1151 | "minutesofhour"
1152 | "secondsofminute"
1153 | "millisecondsofsecond"
1154 | "microsecondsofsecond"
1155 | "nanosecondsofsecond"
1156 )
1157}
1158
1159fn eval_date(args: &[Value]) -> Result<Value> {
1164 if args.is_empty() {
1165 let now = Utc::now().date_naive();
1167 return Ok(Value::Temporal(TemporalValue::Date {
1168 days_since_epoch: date_to_days_since_epoch(&now),
1169 }));
1170 }
1171
1172 match &args[0] {
1173 Value::String(s) => {
1174 match parse_date_string(s) {
1175 Ok(date) => Ok(Value::Temporal(TemporalValue::Date {
1176 days_since_epoch: date_to_days_since_epoch(&date),
1177 })),
1178 Err(e) => {
1179 if parse_extended_date_string(s).is_some() {
1180 Ok(Value::String(s.clone()))
1182 } else {
1183 Err(e)
1184 }
1185 }
1186 }
1187 }
1188 Value::Temporal(TemporalValue::Date { .. }) => Ok(args[0].clone()),
1189 Value::Temporal(tv) => {
1191 if let Some(date) = tv.to_date() {
1192 Ok(Value::Temporal(TemporalValue::Date {
1193 days_since_epoch: date_to_days_since_epoch(&date),
1194 }))
1195 } else {
1196 Err(anyhow!("date(): temporal value has no date component"))
1197 }
1198 }
1199 Value::Map(map) => eval_date_from_map(map),
1200 Value::Null => Ok(Value::Null),
1201 _ => Err(anyhow!("date() expects a string or map argument")),
1202 }
1203}
1204
1205fn date_to_days_since_epoch(date: &NaiveDate) -> i32 {
1207 let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
1208 (date.signed_duration_since(epoch)).num_days() as i32
1209}
1210
1211fn eval_date_from_map(map: &HashMap<String, Value>) -> Result<Value> {
1212 if let Some(dt_val) = map.get("date") {
1214 return eval_date_from_projection(map, dt_val);
1215 }
1216
1217 let date = build_date_from_map(map)?;
1218 Ok(Value::Temporal(TemporalValue::Date {
1219 days_since_epoch: date_to_days_since_epoch(&date),
1220 }))
1221}
1222
1223fn eval_date_from_projection(map: &HashMap<String, Value>, source: &Value) -> Result<Value> {
1225 let source_date = temporal_or_string_to_date(source)?;
1226 let date = build_date_from_projection(map, &source_date)?;
1227 Ok(Value::Temporal(TemporalValue::Date {
1228 days_since_epoch: date_to_days_since_epoch(&date),
1229 }))
1230}
1231
1232fn temporal_or_string_to_date(val: &Value) -> Result<NaiveDate> {
1234 match val {
1235 Value::Temporal(tv) => tv
1236 .to_date()
1237 .ok_or_else(|| anyhow!("Temporal value has no date component")),
1238 Value::String(s) => parse_datetime_with_tz(s).map(|(date, _, _)| date),
1239 _ => Err(anyhow!(
1240 "Expected temporal or string value for date extraction"
1241 )),
1242 }
1243}
1244
1245fn build_date_from_projection(
1253 map: &HashMap<String, Value>,
1254 source_date: &NaiveDate,
1255) -> Result<NaiveDate> {
1256 if map.contains_key("week") {
1258 let week_year = map
1259 .get("weekYear")
1260 .and_then(|v| v.as_i64())
1261 .map(|v| v as i32)
1262 .unwrap_or_else(|| source_date.iso_week().year());
1263 let week = map.get("week").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
1264 let dow = map
1265 .get("dayOfWeek")
1266 .and_then(|v| v.as_i64())
1267 .unwrap_or_else(|| source_date.weekday().number_from_monday() as i64)
1268 as u32;
1269 return build_date_from_week(week_year, week, dow);
1270 }
1271
1272 if map.contains_key("ordinalDay") {
1274 let year = map
1275 .get("year")
1276 .and_then(|v| v.as_i64())
1277 .map(|v| v as i32)
1278 .unwrap_or(source_date.year());
1279 let ordinal = map
1280 .get("ordinalDay")
1281 .and_then(|v| v.as_i64())
1282 .unwrap_or(source_date.ordinal() as i64) as u32;
1283 return NaiveDate::from_yo_opt(year, ordinal)
1284 .ok_or_else(|| anyhow!("Invalid ordinal day: {} for year {}", ordinal, year));
1285 }
1286
1287 if map.contains_key("quarter") {
1289 let year = map
1290 .get("year")
1291 .and_then(|v| v.as_i64())
1292 .map(|v| v as i32)
1293 .unwrap_or(source_date.year());
1294 let quarter = map.get("quarter").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
1295 let doq = map
1296 .get("dayOfQuarter")
1297 .and_then(|v| v.as_i64())
1298 .unwrap_or_else(|| day_of_quarter(source_date) as i64) as u32;
1299 return build_date_from_quarter(year, quarter, doq);
1300 }
1301
1302 let year = map
1304 .get("year")
1305 .and_then(|v| v.as_i64())
1306 .map(|v| v as i32)
1307 .unwrap_or(source_date.year());
1308 let month = map
1309 .get("month")
1310 .and_then(|v| v.as_i64())
1311 .map(|v| v as u32)
1312 .unwrap_or(source_date.month());
1313 let day = map
1314 .get("day")
1315 .and_then(|v| v.as_i64())
1316 .map(|v| v as u32)
1317 .unwrap_or(source_date.day());
1318
1319 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow!("Invalid date in projection"))
1320}
1321
1322fn build_date_from_map(map: &HashMap<String, Value>) -> Result<NaiveDate> {
1330 let year = map
1332 .get("year")
1333 .and_then(|v| v.as_i64())
1334 .ok_or_else(|| anyhow!("date/datetime map requires 'year' field"))? as i32;
1335
1336 if let Some(week) = map.get("week").and_then(|v| v.as_i64()) {
1338 let dow = map.get("dayOfWeek").and_then(|v| v.as_i64()).unwrap_or(1);
1339 return build_date_from_week(year, week as u32, dow as u32);
1340 }
1341
1342 if let Some(ordinal) = map.get("ordinalDay").and_then(|v| v.as_i64()) {
1344 return NaiveDate::from_yo_opt(year, ordinal as u32)
1345 .ok_or_else(|| anyhow!("Invalid ordinal day: {} for year {}", ordinal, year));
1346 }
1347
1348 if let Some(quarter) = map.get("quarter").and_then(|v| v.as_i64()) {
1350 let doq = map
1351 .get("dayOfQuarter")
1352 .and_then(|v| v.as_i64())
1353 .unwrap_or(1);
1354 return build_date_from_quarter(year, quarter as u32, doq as u32);
1355 }
1356
1357 let month = map.get("month").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
1359 let day = map.get("day").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
1360
1361 NaiveDate::from_ymd_opt(year, month, day)
1362 .ok_or_else(|| anyhow!("Invalid date: year={}, month={}, day={}", year, month, day))
1363}
1364
1365fn build_date_from_week(year: i32, week: u32, day_of_week: u32) -> Result<NaiveDate> {
1367 if !(1..=53).contains(&week) {
1368 return Err(anyhow!("Week must be between 1 and 53"));
1369 }
1370 if !(1..=7).contains(&day_of_week) {
1371 return Err(anyhow!("Day of week must be between 1 and 7"));
1372 }
1373
1374 let jan4 =
1376 NaiveDate::from_ymd_opt(year, 1, 4).ok_or_else(|| anyhow!("Invalid year: {}", year))?;
1377
1378 let iso_week_day = jan4.weekday().num_days_from_monday();
1380 let week1_monday = jan4 - Duration::days(iso_week_day as i64);
1381
1382 let days_offset = ((week - 1) * 7 + (day_of_week - 1)) as i64;
1384 Ok(week1_monday + Duration::days(days_offset))
1385}
1386
1387fn day_of_quarter(date: &NaiveDate) -> u32 {
1389 let quarter_start_month = ((date.month() - 1) / 3) * 3 + 1;
1390 let quarter_start = NaiveDate::from_ymd_opt(date.year(), quarter_start_month, 1).unwrap();
1391 (date.signed_duration_since(quarter_start).num_days() + 1) as u32
1392}
1393
1394fn build_date_from_quarter(year: i32, quarter: u32, day_of_quarter: u32) -> Result<NaiveDate> {
1396 if !(1..=4).contains(&quarter) {
1397 return Err(anyhow!("Quarter must be between 1 and 4"));
1398 }
1399
1400 let first_month = (quarter - 1) * 3 + 1;
1402 let quarter_start = NaiveDate::from_ymd_opt(year, first_month, 1)
1403 .ok_or_else(|| anyhow!("Invalid quarter start"))?;
1404
1405 let result = quarter_start + Duration::days((day_of_quarter - 1) as i64);
1407
1408 let result_quarter = (result.month() - 1) / 3 + 1;
1410 if result_quarter != quarter || result.year() != year {
1411 return Err(anyhow!(
1412 "Day {} is out of range for quarter {}",
1413 day_of_quarter,
1414 quarter
1415 ));
1416 }
1417
1418 Ok(result)
1419}
1420
1421fn parse_date_string(s: &str) -> Result<NaiveDate> {
1422 NaiveDate::parse_from_str(s, "%Y-%m-%d")
1423 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S").map(|dt| dt.date()))
1424 .or_else(|_| {
1425 DateTime::parse_from_rfc3339(s).map(|dt| dt.date_naive())
1427 })
1428 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f").map(|dt| dt.date()))
1430 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").map(|dt| dt.date()))
1431 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M").map(|dt| dt.date()))
1432 .or_else(|e| try_parse_compact_date(s).ok_or(e))
1434 .or_else(|_| {
1435 parse_datetime_with_tz(s).map(|(date, _, _)| date)
1437 })
1438 .map_err(|e| anyhow!("Invalid date format: {}", e))
1439}
1440
1441fn eval_time(args: &[Value]) -> Result<Value> {
1446 if args.is_empty() {
1447 let now = Utc::now();
1448 let time = now.time();
1449 return Ok(Value::Temporal(TemporalValue::Time {
1450 nanos_since_midnight: time_to_nanos(&time),
1451 offset_seconds: 0,
1452 }));
1453 }
1454
1455 match &args[0] {
1456 Value::String(s) => {
1457 let (time, tz_info) = parse_time_string_with_tz(s)?;
1458 let offset = match tz_info {
1459 Some(ref info) => info
1460 .offset_for_local(&NaiveDateTime::new(Utc::now().date_naive(), time))?
1461 .local_minus_utc(),
1462 None => 0,
1463 };
1464 Ok(Value::Temporal(TemporalValue::Time {
1465 nanos_since_midnight: time_to_nanos(&time),
1466 offset_seconds: offset,
1467 }))
1468 }
1469 Value::Temporal(TemporalValue::Time { .. }) => Ok(args[0].clone()),
1470 Value::Temporal(tv) => {
1472 let time = tv
1473 .to_time()
1474 .ok_or_else(|| anyhow!("time(): temporal value has no time component"))?;
1475 let offset = match tv {
1476 TemporalValue::DateTime { offset_seconds, .. } => *offset_seconds,
1477 TemporalValue::Time { offset_seconds, .. } => *offset_seconds,
1478 _ => 0, };
1480 Ok(Value::Temporal(TemporalValue::Time {
1481 nanos_since_midnight: time_to_nanos(&time),
1482 offset_seconds: offset,
1483 }))
1484 }
1485 Value::Map(map) => eval_time_from_map(map, true),
1486 Value::Null => Ok(Value::Null),
1487 _ => Err(anyhow!("time() expects a string or map argument")),
1488 }
1489}
1490
1491fn eval_localtime(args: &[Value]) -> Result<Value> {
1492 if args.is_empty() {
1493 let now = chrono::Local::now().time();
1494 return Ok(Value::Temporal(TemporalValue::LocalTime {
1495 nanos_since_midnight: time_to_nanos(&now),
1496 }));
1497 }
1498
1499 match &args[0] {
1500 Value::String(s) => {
1501 let time = parse_time_string(s)?;
1502 Ok(Value::Temporal(TemporalValue::LocalTime {
1503 nanos_since_midnight: time_to_nanos(&time),
1504 }))
1505 }
1506 Value::Temporal(TemporalValue::LocalTime { .. }) => Ok(args[0].clone()),
1507 Value::Temporal(tv) => {
1509 let time = tv
1510 .to_time()
1511 .ok_or_else(|| anyhow!("localtime(): temporal value has no time component"))?;
1512 Ok(Value::Temporal(TemporalValue::LocalTime {
1513 nanos_since_midnight: time_to_nanos(&time),
1514 }))
1515 }
1516 Value::Map(map) => eval_time_from_map(map, false),
1517 Value::Null => Ok(Value::Null),
1518 _ => Err(anyhow!("localtime() expects a string or map argument")),
1519 }
1520}
1521
1522fn eval_time_from_map(map: &HashMap<String, Value>, with_timezone: bool) -> Result<Value> {
1523 if let Some(time_val) = map.get("time") {
1525 return eval_time_from_projection(map, time_val, with_timezone);
1526 }
1527
1528 let hour = map.get("hour").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1529 let minute = map.get("minute").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1530 let second = map.get("second").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1531 let nanos = build_nanoseconds(map);
1532
1533 let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos).ok_or_else(|| {
1534 anyhow!(
1535 "Invalid time: hour={}, minute={}, second={}",
1536 hour,
1537 minute,
1538 second
1539 )
1540 })?;
1541
1542 let nanos = time_to_nanos(&time);
1543
1544 if with_timezone {
1545 let offset = if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
1547 parse_timezone_offset(tz_str)?
1548 } else {
1549 0
1550 };
1551 Ok(Value::Temporal(TemporalValue::Time {
1552 nanos_since_midnight: nanos,
1553 offset_seconds: offset,
1554 }))
1555 } else {
1556 Ok(Value::Temporal(TemporalValue::LocalTime {
1557 nanos_since_midnight: nanos,
1558 }))
1559 }
1560}
1561
1562fn eval_time_from_projection(
1564 map: &HashMap<String, Value>,
1565 source: &Value,
1566 with_timezone: bool,
1567) -> Result<Value> {
1568 let (source_time, source_offset) = match source {
1570 Value::Temporal(TemporalValue::Time {
1571 nanos_since_midnight,
1572 offset_seconds,
1573 }) => (nanos_to_time(*nanos_since_midnight), Some(*offset_seconds)),
1574 Value::Temporal(TemporalValue::LocalTime {
1575 nanos_since_midnight,
1576 }) => (nanos_to_time(*nanos_since_midnight), None),
1577 Value::Temporal(TemporalValue::DateTime {
1578 nanos_since_epoch,
1579 offset_seconds,
1580 ..
1581 }) => {
1582 let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
1584 let dt = chrono::DateTime::from_timestamp_nanos(local_nanos);
1585 (dt.naive_utc().time(), Some(*offset_seconds))
1586 }
1587 Value::Temporal(TemporalValue::LocalDateTime { nanos_since_epoch }) => {
1588 let dt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch);
1589 (dt.naive_utc().time(), None)
1590 }
1591 Value::Temporal(TemporalValue::Date { .. }) => {
1592 (NaiveTime::from_hms_opt(0, 0, 0).unwrap(), None)
1594 }
1595 Value::String(s) => {
1596 let (_, time, tz_info) = parse_datetime_with_tz(s)?;
1597 let offset = tz_info.as_ref().map(|tz| {
1598 let today = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
1599 let ndt = NaiveDateTime::new(today, time);
1600 tz.offset_for_local(&ndt)
1601 .map(|o| o.local_minus_utc())
1602 .unwrap_or(0)
1603 });
1604 (time, offset)
1605 }
1606 _ => return Err(anyhow!("time field must be a string or temporal")),
1607 };
1608
1609 let hour = map
1611 .get("hour")
1612 .and_then(|v| v.as_i64())
1613 .map(|v| v as u32)
1614 .unwrap_or(source_time.hour());
1615 let minute = map
1616 .get("minute")
1617 .and_then(|v| v.as_i64())
1618 .map(|v| v as u32)
1619 .unwrap_or(source_time.minute());
1620 let second = map
1621 .get("second")
1622 .and_then(|v| v.as_i64())
1623 .map(|v| v as u32)
1624 .unwrap_or(source_time.second());
1625
1626 let nanos = if map.contains_key("millisecond")
1627 || map.contains_key("microsecond")
1628 || map.contains_key("nanosecond")
1629 {
1630 build_nanoseconds(map)
1631 } else {
1632 source_time.nanosecond()
1633 };
1634
1635 let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
1636 .ok_or_else(|| anyhow!("Invalid time in projection"))?;
1637 let nanos = time_to_nanos(&time);
1638
1639 if with_timezone {
1640 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
1641 let new_offset = parse_timezone_offset(tz_str)?;
1642 let converted_nanos = if let Some(src_offset) = source_offset {
1645 let utc_nanos = nanos - (src_offset as i64) * 1_000_000_000;
1646 let target_nanos = utc_nanos + (new_offset as i64) * 1_000_000_000;
1647 target_nanos.rem_euclid(NANOS_PER_DAY)
1649 } else {
1650 nanos
1652 };
1653 Ok(Value::Temporal(TemporalValue::Time {
1654 nanos_since_midnight: converted_nanos,
1655 offset_seconds: new_offset,
1656 }))
1657 } else {
1658 let offset = source_offset.unwrap_or(0);
1659 Ok(Value::Temporal(TemporalValue::Time {
1660 nanos_since_midnight: nanos,
1661 offset_seconds: offset,
1662 }))
1663 }
1664 } else {
1665 Ok(Value::Temporal(TemporalValue::LocalTime {
1666 nanos_since_midnight: nanos,
1667 }))
1668 }
1669}
1670
1671fn parse_time_string(s: &str) -> Result<NaiveTime> {
1672 NaiveTime::parse_from_str(s, "%H:%M:%S")
1674 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S%.f"))
1675 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S%.9f"))
1676 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M"))
1677 .or_else(|e| try_parse_compact_time(s).ok_or(e))
1680 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S").map(|dt| dt.time()))
1681 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f").map(|dt| dt.time()))
1682 .or_else(|_| DateTime::parse_from_rfc3339(s).map(|dt| dt.time()))
1683 .or_else(|_| {
1684 parse_datetime_with_tz(s).map(|(_, time, _)| time)
1686 })
1687 .map_err(|_| anyhow!("Invalid time format"))
1688}
1689
1690fn parse_time_string_with_tz(s: &str) -> Result<(NaiveTime, Option<TimezoneInfo>)> {
1696 let (datetime_part, tz_name) = if let Some(bracket_pos) = s.find('[') {
1698 let tz_name = s[bracket_pos + 1..s.len() - 1].to_string();
1699 (&s[..bracket_pos], Some(tz_name))
1700 } else {
1701 (s, None)
1702 };
1703
1704 if let Ok(time) = try_parse_naive_time(datetime_part) {
1706 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?;
1707 return Ok((time, tz_info));
1708 }
1709
1710 if let Some(base) = datetime_part
1712 .strip_suffix('Z')
1713 .or_else(|| datetime_part.strip_suffix('z'))
1714 && let Ok(time) = try_parse_naive_time(base)
1715 {
1716 let utc_tz = TimezoneInfo::FixedOffset(FixedOffset::east_opt(0).unwrap());
1717 let tz_info = tz_name
1718 .map(|n| parse_timezone(&n))
1719 .transpose()?
1720 .or(Some(utc_tz));
1721 return Ok((time, tz_info));
1722 }
1723
1724 if let Some(tz_pos) = datetime_part.rfind('+').or_else(|| {
1726 datetime_part.rfind('-').filter(|&pos| pos >= 2)
1728 }) {
1729 let left_part = &datetime_part[..tz_pos];
1730 let tz_part = &datetime_part[tz_pos..];
1731
1732 if let Ok(time) = try_parse_naive_time(left_part) {
1733 let tz_info = if let Some(name) = tz_name {
1734 Some(parse_timezone(&name)?)
1735 } else {
1736 let offset = parse_timezone_offset(tz_part)?;
1737 let fo = FixedOffset::east_opt(offset)
1738 .ok_or_else(|| anyhow!("Invalid timezone offset"))?;
1739 Some(TimezoneInfo::FixedOffset(fo))
1740 };
1741 return Ok((time, tz_info));
1742 }
1743 }
1744
1745 let (_, time, tz_info) = parse_datetime_with_tz(s)?;
1747 Ok((time, tz_info))
1748}
1749
1750fn build_nanoseconds(map: &HashMap<String, Value>) -> u32 {
1751 let millis = map.get("millisecond").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1752 let micros = map.get("microsecond").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1753 let nanos = map.get("nanosecond").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1754
1755 millis * 1_000_000 + micros * 1_000 + nanos
1756}
1757
1758fn build_nanoseconds_with_base(map: &HashMap<String, Value>, base_nanos: u32) -> u32 {
1763 let base_millis = base_nanos / 1_000_000;
1764 let base_micros = (base_nanos % 1_000_000) / 1_000;
1765 let base_nano_part = base_nanos % 1_000;
1766
1767 let millis = map
1768 .get("millisecond")
1769 .and_then(|v| v.as_i64())
1770 .unwrap_or(base_millis as i64) as u32;
1771 let micros = map
1772 .get("microsecond")
1773 .and_then(|v| v.as_i64())
1774 .unwrap_or(base_micros as i64) as u32;
1775 let nanos = map
1776 .get("nanosecond")
1777 .and_then(|v| v.as_i64())
1778 .unwrap_or(base_nano_part as i64) as u32;
1779
1780 millis * 1_000_000 + micros * 1_000 + nanos
1781}
1782
1783fn format_timezone_offset(offset_secs: i32) -> String {
1785 if offset_secs == 0 {
1786 "Z".to_string()
1787 } else {
1788 let hours = offset_secs / 3600;
1789 let remaining = offset_secs.abs() % 3600;
1790 let mins = remaining / 60;
1791 let secs = remaining % 60;
1792 if secs != 0 {
1793 format!("{:+03}:{:02}:{:02}", hours, mins, secs)
1794 } else {
1795 format!("{:+03}:{:02}", hours, mins)
1796 }
1797 }
1798}
1799
1800fn format_time_with_nanos(time: &NaiveTime) -> String {
1801 let nanos = time.nanosecond();
1802 let secs = time.second();
1803
1804 if nanos == 0 && secs == 0 {
1805 time.format("%H:%M").to_string()
1807 } else if nanos == 0 {
1808 time.format("%H:%M:%S").to_string()
1809 } else if nanos.is_multiple_of(1_000_000) {
1810 time.format("%H:%M:%S%.3f").to_string()
1812 } else if nanos.is_multiple_of(1_000) {
1813 time.format("%H:%M:%S%.6f").to_string()
1815 } else {
1816 time.format("%H:%M:%S%.9f").to_string()
1818 }
1819}
1820
1821fn parse_timezone_offset(tz: &str) -> Result<i32> {
1822 let tz = tz.trim();
1823 if tz == "Z" || tz == "z" {
1824 return Ok(0);
1825 }
1826
1827 if tz.len() >= 3 && (tz.starts_with('+') || tz.starts_with('-')) {
1829 let sign = if tz.starts_with('-') { -1 } else { 1 };
1830 let hours: i32 = tz[1..3]
1831 .parse()
1832 .map_err(|_| anyhow!("Invalid timezone hours"))?;
1833
1834 let rest = &tz[3..];
1835 let (mins, secs) = if rest.is_empty() {
1836 (0, 0)
1838 } else if let Some(after_colon) = rest.strip_prefix(':') {
1839 let mins: i32 = if after_colon.len() >= 2 {
1841 after_colon[..2]
1842 .parse()
1843 .map_err(|_| anyhow!("Invalid timezone minutes"))?
1844 } else {
1845 0
1846 };
1847 let secs: i32 = if after_colon.len() >= 5 && after_colon.as_bytes()[2] == b':' {
1848 after_colon[3..5]
1850 .parse()
1851 .map_err(|_| anyhow!("Invalid timezone seconds"))?
1852 } else {
1853 0
1854 };
1855 (mins, secs)
1856 } else {
1857 let mins: i32 = if rest.len() >= 2 {
1859 rest[..2]
1860 .parse()
1861 .map_err(|_| anyhow!("Invalid timezone minutes"))?
1862 } else {
1863 0
1864 };
1865 let secs: i32 = if rest.len() >= 4 {
1866 rest[2..4]
1867 .parse()
1868 .map_err(|_| anyhow!("Invalid timezone seconds"))?
1869 } else {
1870 0
1871 };
1872 (mins, secs)
1873 };
1874
1875 return Ok(sign * (hours * 3600 + mins * 60 + secs));
1876 }
1877
1878 Err(anyhow!("Unsupported timezone format: {}", tz))
1879}
1880
1881fn eval_datetime(args: &[Value]) -> Result<Value> {
1886 if args.is_empty() {
1887 let now = Utc::now();
1888 return Ok(Value::Temporal(TemporalValue::DateTime {
1889 nanos_since_epoch: now.timestamp_nanos_opt().unwrap_or(0),
1890 offset_seconds: 0,
1891 timezone_name: None,
1892 }));
1893 }
1894
1895 match &args[0] {
1896 Value::String(s) => {
1897 let (date, time, tz_info) = parse_datetime_with_tz(s)?;
1898 let ndt = NaiveDateTime::new(date, time);
1899 let (offset_secs, tz_name) = match tz_info {
1900 Some(ref info) => {
1901 let fo = info.offset_for_local(&ndt)?;
1902 (fo.local_minus_utc(), info.name().map(|s| s.to_string()))
1903 }
1904 None => (0, None),
1905 };
1906 Ok(datetime_value_from_local_and_offset(
1907 &ndt,
1908 offset_secs,
1909 tz_name,
1910 ))
1911 }
1912 Value::Temporal(TemporalValue::DateTime { .. }) => Ok(args[0].clone()),
1913 Value::Temporal(tv) => {
1915 let date = tv.to_date().unwrap_or_else(|| Utc::now().date_naive());
1916 let time = tv
1917 .to_time()
1918 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
1919 let ndt = NaiveDateTime::new(date, time);
1920 let offset = match tv {
1921 TemporalValue::Time { offset_seconds, .. } => *offset_seconds,
1922 _ => 0,
1923 };
1924 Ok(datetime_value_from_local_and_offset(&ndt, offset, None))
1925 }
1926 Value::Map(map) => eval_datetime_from_map(map, true),
1927 Value::Null => Ok(Value::Null),
1928 _ => Err(anyhow!("datetime() expects a string or map argument")),
1929 }
1930}
1931
1932fn eval_localdatetime(args: &[Value]) -> Result<Value> {
1933 if args.is_empty() {
1934 let now = chrono::Local::now().naive_local();
1935 let epoch = NaiveDateTime::new(
1936 NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(),
1937 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
1938 );
1939 let nanos = now
1940 .signed_duration_since(epoch)
1941 .num_nanoseconds()
1942 .unwrap_or(0);
1943 return Ok(Value::Temporal(TemporalValue::LocalDateTime {
1944 nanos_since_epoch: nanos,
1945 }));
1946 }
1947
1948 match &args[0] {
1949 Value::String(s) => {
1950 match parse_datetime_with_tz(s) {
1951 Ok((date, time, _)) => {
1952 let ndt = NaiveDateTime::new(date, time);
1953 Ok(localdatetime_value_from_naive(&ndt))
1954 }
1955 Err(e) => {
1956 if parse_extended_localdatetime_string(s).is_some() {
1957 Ok(Value::String(s.clone()))
1959 } else {
1960 Err(e)
1961 }
1962 }
1963 }
1964 }
1965 Value::Temporal(TemporalValue::LocalDateTime { .. }) => Ok(args[0].clone()),
1966 Value::Temporal(tv) => {
1968 let date = tv.to_date().unwrap_or_else(|| Utc::now().date_naive());
1969 let time = tv
1970 .to_time()
1971 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
1972 let ndt = NaiveDateTime::new(date, time);
1973 Ok(localdatetime_value_from_naive(&ndt))
1974 }
1975 Value::Map(map) => eval_datetime_from_map(map, false),
1976 Value::Null => Ok(Value::Null),
1977 _ => Err(anyhow!("localdatetime() expects a string or map argument")),
1978 }
1979}
1980
1981fn extract_time_and_tz_from_value(val: &Value) -> Result<(NaiveTime, Option<TimezoneInfo>)> {
1983 match val {
1984 Value::Temporal(tv) => {
1985 let time = tv
1986 .to_time()
1987 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
1988 let tz = match tv {
1989 TemporalValue::DateTime {
1990 offset_seconds,
1991 timezone_name,
1992 ..
1993 } => {
1994 if let Some(name) = timezone_name {
1995 Some(parse_timezone(name)?)
1996 } else {
1997 let fo = FixedOffset::east_opt(*offset_seconds)
1998 .ok_or_else(|| anyhow!("Invalid offset"))?;
1999 Some(TimezoneInfo::FixedOffset(fo))
2000 }
2001 }
2002 TemporalValue::Time { offset_seconds, .. } => {
2003 let fo = FixedOffset::east_opt(*offset_seconds)
2004 .ok_or_else(|| anyhow!("Invalid offset"))?;
2005 Some(TimezoneInfo::FixedOffset(fo))
2006 }
2007 _ => None,
2008 };
2009 Ok((time, tz))
2010 }
2011 Value::String(s) => {
2012 let (_, time, tz_info) = parse_datetime_with_tz(s)?;
2013 Ok((time, tz_info))
2014 }
2015 _ => Err(anyhow!("time must be a string or temporal")),
2016 }
2017}
2018
2019fn naive_datetime_to_nanos(ndt: &NaiveDateTime) -> Option<i64> {
2022 let epoch = NaiveDateTime::new(
2023 NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(),
2024 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
2025 );
2026 ndt.signed_duration_since(epoch).num_nanoseconds()
2027}
2028
2029fn localdatetime_value_from_naive(ndt: &NaiveDateTime) -> Value {
2030 if let Some(nanos) = naive_datetime_to_nanos(ndt) {
2031 Value::Temporal(TemporalValue::LocalDateTime {
2032 nanos_since_epoch: nanos,
2033 })
2034 } else {
2035 Value::String(format_naive_datetime(ndt))
2036 }
2037}
2038
2039fn datetime_value_from_local_and_offset(
2040 local_ndt: &NaiveDateTime,
2041 offset_seconds: i32,
2042 timezone_name: Option<String>,
2043) -> Value {
2044 let utc_ndt = *local_ndt - Duration::seconds(offset_seconds as i64);
2045 let utc_dt = DateTime::<Utc>::from_naive_utc_and_offset(utc_ndt, Utc);
2046
2047 if let Some(nanos) = utc_dt.timestamp_nanos_opt() {
2048 Value::Temporal(TemporalValue::DateTime {
2049 nanos_since_epoch: nanos,
2050 offset_seconds,
2051 timezone_name,
2052 })
2053 } else {
2054 let rendered = if let Some(offset) = FixedOffset::east_opt(offset_seconds) {
2055 if let Some(dt) = offset.from_local_datetime(local_ndt).single() {
2056 format_datetime_with_offset_and_tz(&dt, timezone_name.as_deref())
2057 } else {
2058 let base = format!(
2059 "{}{}",
2060 format_naive_datetime(local_ndt),
2061 format_timezone_offset(offset_seconds)
2062 );
2063 if let Some(name) = timezone_name.as_deref() {
2064 format!("{base}[{name}]")
2065 } else {
2066 base
2067 }
2068 }
2069 } else {
2070 let base = format!(
2071 "{}{}",
2072 format_naive_datetime(local_ndt),
2073 format_timezone_offset(offset_seconds)
2074 );
2075 if let Some(name) = timezone_name.as_deref() {
2076 format!("{base}[{name}]")
2077 } else {
2078 base
2079 }
2080 };
2081 Value::String(rendered)
2082 }
2083}
2084
2085fn eval_datetime_from_map(map: &HashMap<String, Value>, with_timezone: bool) -> Result<Value> {
2086 if let Some(dt_val) = map.get("datetime") {
2088 return eval_datetime_from_projection(map, dt_val, with_timezone);
2089 }
2090
2091 if let (Some(date_val), Some(time_val)) = (map.get("date"), map.get("time")) {
2093 return eval_datetime_from_date_and_time(map, date_val, time_val, with_timezone);
2094 }
2095
2096 if let Some(date_val) = map.get("date") {
2100 let source_date = temporal_or_string_to_date(date_val)?;
2101 let date = build_date_from_projection(map, &source_date)?;
2102 let hour = map.get("hour").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2103 let minute = map.get("minute").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2104 let second = map.get("second").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2105 let nanos = build_nanoseconds(map);
2106 let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
2107 .ok_or_else(|| anyhow!("Invalid time in datetime map"))?;
2108 let ndt = NaiveDateTime::new(date, time);
2109
2110 if with_timezone {
2111 let (offset_secs, tz_name) =
2112 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
2113 let tz_info = parse_timezone(tz_str)?;
2114 let offset = tz_info.offset_for_local(&ndt)?;
2115 (
2116 offset.local_minus_utc(),
2117 tz_info.name().map(|s| s.to_string()),
2118 )
2119 } else {
2120 (0, None) };
2122
2123 return Ok(datetime_value_from_local_and_offset(
2124 &ndt,
2125 offset_secs,
2126 tz_name,
2127 ));
2128 } else {
2129 return Ok(localdatetime_value_from_naive(&ndt));
2130 }
2131 }
2132
2133 let (time, source_tz) = if let Some(time_val) = map.get("time") {
2136 let (t, tz) = extract_time_and_tz_from_value(time_val)?;
2137 let hour = map
2139 .get("hour")
2140 .and_then(|v| v.as_i64())
2141 .map(|v| v as u32)
2142 .unwrap_or(t.hour());
2143 let minute = map
2144 .get("minute")
2145 .and_then(|v| v.as_i64())
2146 .map(|v| v as u32)
2147 .unwrap_or(t.minute());
2148 let second = map
2149 .get("second")
2150 .and_then(|v| v.as_i64())
2151 .map(|v| v as u32)
2152 .unwrap_or(t.second());
2153 let nanos = if map.contains_key("millisecond")
2154 || map.contains_key("microsecond")
2155 || map.contains_key("nanosecond")
2156 {
2157 build_nanoseconds(map)
2158 } else {
2159 t.nanosecond()
2160 };
2161 let resolved_time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
2162 .ok_or_else(|| anyhow!("Invalid time in datetime map"))?;
2163 (resolved_time, tz)
2164 } else {
2165 let hour = map.get("hour").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2166 let minute = map.get("minute").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2167 let second = map.get("second").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2168 let nanos = build_nanoseconds(map);
2169 let t = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
2170 .ok_or_else(|| anyhow!("Invalid time in datetime map"))?;
2171 (t, None::<TimezoneInfo>)
2172 };
2173
2174 let date = build_date_from_map(map)?;
2176
2177 let ndt = NaiveDateTime::new(date, time);
2178
2179 if with_timezone {
2180 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
2184 let tz_info = parse_timezone(tz_str)?;
2185 if let Some(ref src_tz) = source_tz {
2186 let src_offset = src_tz.offset_for_local(&ndt)?;
2188 let utc_ndt = ndt - Duration::seconds(src_offset.local_minus_utc() as i64);
2189 let target_offset = tz_info.offset_for_utc(&utc_ndt);
2190 let offset_secs = target_offset.local_minus_utc();
2191 let tz_name = tz_info.name().map(|s| s.to_string());
2192 let target_local_ndt = utc_ndt + Duration::seconds(offset_secs as i64);
2193 Ok(datetime_value_from_local_and_offset(
2194 &target_local_ndt,
2195 offset_secs,
2196 tz_name,
2197 ))
2198 } else {
2199 let offset = tz_info.offset_for_local(&ndt)?;
2201 let offset_secs = offset.local_minus_utc();
2202 let tz_name = tz_info.name().map(|s| s.to_string());
2203 Ok(datetime_value_from_local_and_offset(
2204 &ndt,
2205 offset_secs,
2206 tz_name,
2207 ))
2208 }
2209 } else if let Some(ref tz) = source_tz {
2210 let offset = tz.offset_for_local(&ndt)?;
2211 let offset_secs = offset.local_minus_utc();
2212 let tz_name = tz.name().map(|s| s.to_string());
2213 Ok(datetime_value_from_local_and_offset(
2214 &ndt,
2215 offset_secs,
2216 tz_name,
2217 ))
2218 } else {
2219 Ok(datetime_value_from_local_and_offset(&ndt, 0, None))
2221 }
2222 } else {
2223 Ok(localdatetime_value_from_naive(&ndt))
2225 }
2226}
2227
2228fn eval_datetime_from_date_and_time(
2233 map: &HashMap<String, Value>,
2234 date_val: &Value,
2235 time_val: &Value,
2236 with_timezone: bool,
2237) -> Result<Value> {
2238 let source_date = temporal_or_string_to_date(date_val)?;
2239 let (source_time, source_tz) = match time_val {
2240 Value::Temporal(tv) => {
2241 let time = tv
2242 .to_time()
2243 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
2244 let tz = match tv {
2245 TemporalValue::DateTime {
2246 offset_seconds,
2247 timezone_name,
2248 ..
2249 } => {
2250 if let Some(name) = timezone_name {
2251 Some(parse_timezone(name)?)
2252 } else {
2253 let fo = FixedOffset::east_opt(*offset_seconds)
2254 .ok_or_else(|| anyhow!("Invalid offset"))?;
2255 Some(TimezoneInfo::FixedOffset(fo))
2256 }
2257 }
2258 TemporalValue::Time { offset_seconds, .. } => {
2259 let fo = FixedOffset::east_opt(*offset_seconds)
2260 .ok_or_else(|| anyhow!("Invalid offset"))?;
2261 Some(TimezoneInfo::FixedOffset(fo))
2262 }
2263 _ => None,
2264 };
2265 (time, tz)
2266 }
2267 Value::String(s) => {
2268 let (_, time, tz_info) = parse_datetime_with_tz(s)?;
2269 (time, tz_info)
2270 }
2271 _ => return Err(anyhow!("time field must be a string or temporal")),
2272 };
2273
2274 let date = build_date_from_projection(map, &source_date)?;
2276
2277 let hour = map
2279 .get("hour")
2280 .and_then(|v| v.as_i64())
2281 .map(|v| v as u32)
2282 .unwrap_or(source_time.hour());
2283 let minute = map
2284 .get("minute")
2285 .and_then(|v| v.as_i64())
2286 .map(|v| v as u32)
2287 .unwrap_or(source_time.minute());
2288 let second = map
2289 .get("second")
2290 .and_then(|v| v.as_i64())
2291 .map(|v| v as u32)
2292 .unwrap_or(source_time.second());
2293
2294 let nanos = if map.contains_key("millisecond")
2295 || map.contains_key("microsecond")
2296 || map.contains_key("nanosecond")
2297 {
2298 build_nanoseconds(map)
2299 } else {
2300 source_time.nanosecond()
2301 };
2302
2303 let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
2304 .ok_or_else(|| anyhow!("Invalid time in datetime(date+time) projection"))?;
2305
2306 let ndt = NaiveDateTime::new(date, time);
2307
2308 if with_timezone {
2309 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
2310 let tz_info = parse_timezone(tz_str)?;
2311 if let Some(ref src_tz) = source_tz {
2312 let src_offset = src_tz.offset_for_local(&ndt)?;
2314 let utc_ndt = ndt - Duration::seconds(src_offset.local_minus_utc() as i64);
2315 let target_offset = tz_info.offset_for_utc(&utc_ndt);
2316 let offset_secs = target_offset.local_minus_utc();
2317 let tz_name = tz_info.name().map(|s| s.to_string());
2318 let target_local_ndt = utc_ndt + Duration::seconds(offset_secs as i64);
2319 Ok(datetime_value_from_local_and_offset(
2320 &target_local_ndt,
2321 offset_secs,
2322 tz_name,
2323 ))
2324 } else {
2325 let offset = tz_info.offset_for_local(&ndt)?;
2327 let offset_secs = offset.local_minus_utc();
2328 let tz_name = tz_info.name().map(|s| s.to_string());
2329 Ok(datetime_value_from_local_and_offset(
2330 &ndt,
2331 offset_secs,
2332 tz_name,
2333 ))
2334 }
2335 } else if let Some(ref tz) = source_tz {
2336 let offset = tz.offset_for_local(&ndt)?;
2337 let offset_secs = offset.local_minus_utc();
2338 let tz_name = tz.name().map(|s| s.to_string());
2339 Ok(datetime_value_from_local_and_offset(
2340 &ndt,
2341 offset_secs,
2342 tz_name,
2343 ))
2344 } else {
2345 Ok(datetime_value_from_local_and_offset(&ndt, 0, None))
2347 }
2348 } else {
2349 Ok(localdatetime_value_from_naive(&ndt))
2350 }
2351}
2352
2353fn eval_datetime_from_projection(
2355 map: &HashMap<String, Value>,
2356 source: &Value,
2357 with_timezone: bool,
2358) -> Result<Value> {
2359 let (source_date, source_time, source_tz) = temporal_or_string_to_components(source)?;
2361
2362 let date = build_date_from_projection(map, &source_date)?;
2364
2365 let hour = map
2367 .get("hour")
2368 .and_then(|v| v.as_i64())
2369 .map(|v| v as u32)
2370 .unwrap_or(source_time.hour());
2371 let minute = map
2372 .get("minute")
2373 .and_then(|v| v.as_i64())
2374 .map(|v| v as u32)
2375 .unwrap_or(source_time.minute());
2376 let second = map
2377 .get("second")
2378 .and_then(|v| v.as_i64())
2379 .map(|v| v as u32)
2380 .unwrap_or(source_time.second());
2381
2382 let nanos = if map.contains_key("millisecond")
2386 || map.contains_key("microsecond")
2387 || map.contains_key("nanosecond")
2388 {
2389 build_nanoseconds(map)
2390 } else {
2391 source_time.nanosecond()
2392 };
2393
2394 let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
2395 .ok_or_else(|| anyhow!("Invalid time in projection"))?;
2396
2397 let ndt = NaiveDateTime::new(date, time);
2398
2399 if with_timezone {
2400 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
2401 let tz_info = parse_timezone(tz_str)?;
2402 if let Some(ref src_tz) = source_tz {
2403 let src_offset = src_tz.offset_for_local(&ndt)?;
2405 let utc_ndt = ndt - Duration::seconds(src_offset.local_minus_utc() as i64);
2406 let target_offset = tz_info.offset_for_utc(&utc_ndt);
2407 let offset_secs = target_offset.local_minus_utc();
2408 let tz_name = tz_info.name().map(|s| s.to_string());
2409 let target_local_ndt = utc_ndt + Duration::seconds(offset_secs as i64);
2410 Ok(datetime_value_from_local_and_offset(
2411 &target_local_ndt,
2412 offset_secs,
2413 tz_name,
2414 ))
2415 } else {
2416 let offset = tz_info.offset_for_local(&ndt)?;
2418 let offset_secs = offset.local_minus_utc();
2419 let tz_name = tz_info.name().map(|s| s.to_string());
2420 Ok(datetime_value_from_local_and_offset(
2421 &ndt,
2422 offset_secs,
2423 tz_name,
2424 ))
2425 }
2426 } else if let Some(ref tz) = source_tz {
2427 let offset = tz.offset_for_local(&ndt)?;
2428 let offset_secs = offset.local_minus_utc();
2429 let tz_name = tz.name().map(|s| s.to_string());
2430 Ok(datetime_value_from_local_and_offset(
2431 &ndt,
2432 offset_secs,
2433 tz_name,
2434 ))
2435 } else {
2436 Ok(datetime_value_from_local_and_offset(&ndt, 0, None))
2438 }
2439 } else {
2440 Ok(localdatetime_value_from_naive(&ndt))
2441 }
2442}
2443
2444fn temporal_or_string_to_components(
2446 val: &Value,
2447) -> Result<(NaiveDate, NaiveTime, Option<TimezoneInfo>)> {
2448 match val {
2449 Value::Temporal(tv) => {
2450 let date = tv.to_date().unwrap_or_else(|| Utc::now().date_naive());
2451 let time = tv
2452 .to_time()
2453 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
2454 let tz_info = match tv {
2455 TemporalValue::DateTime {
2456 offset_seconds,
2457 timezone_name,
2458 ..
2459 } => {
2460 if let Some(name) = timezone_name {
2461 Some(parse_timezone(name)?)
2462 } else {
2463 let fo = FixedOffset::east_opt(*offset_seconds)
2464 .ok_or_else(|| anyhow!("Invalid offset"))?;
2465 Some(TimezoneInfo::FixedOffset(fo))
2466 }
2467 }
2468 TemporalValue::Time { offset_seconds, .. } => {
2469 let fo = FixedOffset::east_opt(*offset_seconds)
2470 .ok_or_else(|| anyhow!("Invalid offset"))?;
2471 Some(TimezoneInfo::FixedOffset(fo))
2472 }
2473 _ => None,
2474 };
2475 Ok((date, time, tz_info))
2476 }
2477 Value::String(s) => parse_datetime_with_tz(s),
2478 _ => Err(anyhow!("Expected temporal or string value")),
2479 }
2480}
2481
2482fn iso_weekday(d: u32) -> Option<Weekday> {
2484 match d {
2485 1 => Some(Weekday::Mon),
2486 2 => Some(Weekday::Tue),
2487 3 => Some(Weekday::Wed),
2488 4 => Some(Weekday::Thu),
2489 5 => Some(Weekday::Fri),
2490 6 => Some(Weekday::Sat),
2491 7 => Some(Weekday::Sun),
2492 _ => None,
2493 }
2494}
2495
2496fn try_parse_compact_date(s: &str) -> Option<NaiveDate> {
2509 if let Some(w_pos) = s.find("-W") {
2511 if w_pos == 4 {
2512 let year: i32 = s[..4].parse().ok()?;
2513 let after_w = &s[w_pos + 2..]; if after_w.len() == 4 && after_w.as_bytes()[2] == b'-' {
2516 let week: u32 = after_w[..2].parse().ok()?;
2517 let d: u32 = after_w[3..4].parse().ok()?;
2518 let weekday = iso_weekday(d)?;
2519 return NaiveDate::from_isoywd_opt(year, week, weekday);
2520 }
2521 if after_w.len() == 2 && after_w.chars().all(|c| c.is_ascii_digit()) {
2523 let week: u32 = after_w.parse().ok()?;
2524 return NaiveDate::from_isoywd_opt(year, week, Weekday::Mon);
2525 }
2526 }
2527 return None;
2528 }
2529
2530 if let Some(w_pos) = s.find('W') {
2532 if w_pos == 4 && s.len() >= 7 {
2533 let year: i32 = s[..4].parse().ok()?;
2534 let after_w = &s[w_pos + 1..];
2535 if after_w.len() == 2 || after_w.len() == 3 {
2536 let week: u32 = after_w[..2].parse().ok()?;
2537 let weekday = if after_w.len() == 3 {
2538 let d: u32 = after_w[2..3].parse().ok()?;
2539 iso_weekday(d)?
2540 } else {
2541 Weekday::Mon
2542 };
2543 return NaiveDate::from_isoywd_opt(year, week, weekday);
2544 }
2545 }
2546 return None;
2547 }
2548
2549 if s.len() >= 7 && s.as_bytes()[4] == b'-' && s[..4].chars().all(|c| c.is_ascii_digit()) {
2551 let year: i32 = s[..4].parse().ok()?;
2552 let after_dash = &s[5..];
2553
2554 if after_dash.len() == 3 && after_dash.chars().all(|c| c.is_ascii_digit()) {
2556 let ordinal: u32 = after_dash.parse().ok()?;
2557 return NaiveDate::from_yo_opt(year, ordinal);
2558 }
2559
2560 if after_dash.len() == 2 && after_dash.chars().all(|c| c.is_ascii_digit()) {
2562 let month: u32 = after_dash.parse().ok()?;
2563 return NaiveDate::from_ymd_opt(year, month, 1);
2564 }
2565 }
2566
2567 if !s.chars().all(|c| c.is_ascii_digit()) {
2569 return None;
2570 }
2571
2572 match s.len() {
2573 8 => {
2575 let year: i32 = s[..4].parse().ok()?;
2576 let month: u32 = s[4..6].parse().ok()?;
2577 let day: u32 = s[6..8].parse().ok()?;
2578 NaiveDate::from_ymd_opt(year, month, day)
2579 }
2580 7 => {
2582 let year: i32 = s[..4].parse().ok()?;
2583 let ordinal: u32 = s[4..7].parse().ok()?;
2584 NaiveDate::from_yo_opt(year, ordinal)
2585 }
2586 6 => {
2588 let year: i32 = s[..4].parse().ok()?;
2589 let month: u32 = s[4..6].parse().ok()?;
2590 NaiveDate::from_ymd_opt(year, month, 1)
2591 }
2592 4 => {
2594 let year: i32 = s.parse().ok()?;
2595 NaiveDate::from_ymd_opt(year, 1, 1)
2596 }
2597 _ => None,
2598 }
2599}
2600
2601fn try_parse_compact_time(s: &str) -> Option<NaiveTime> {
2608 let (integer_part, frac_part) = if let Some(dot_pos) = s.find('.') {
2610 (&s[..dot_pos], Some(&s[dot_pos + 1..]))
2611 } else {
2612 (s, None)
2613 };
2614
2615 if !integer_part.chars().all(|c| c.is_ascii_digit()) {
2617 return None;
2618 }
2619
2620 match integer_part.len() {
2621 6 => {
2623 let hour: u32 = integer_part[..2].parse().ok()?;
2624 let min: u32 = integer_part[2..4].parse().ok()?;
2625 let sec: u32 = integer_part[4..6].parse().ok()?;
2626 if let Some(frac) = frac_part {
2627 let mut frac_str = frac.to_string();
2630 if frac_str.len() > 9 {
2631 frac_str.truncate(9);
2632 }
2633 while frac_str.len() < 9 {
2634 frac_str.push('0');
2635 }
2636 let nanos: u32 = frac_str.parse().ok()?;
2637 NaiveTime::from_hms_nano_opt(hour, min, sec, nanos)
2638 } else {
2639 NaiveTime::from_hms_opt(hour, min, sec)
2640 }
2641 }
2642 4 => {
2644 if frac_part.is_some() {
2645 return None; }
2647 let hour: u32 = integer_part[..2].parse().ok()?;
2648 let min: u32 = integer_part[2..4].parse().ok()?;
2649 NaiveTime::from_hms_opt(hour, min, 0)
2650 }
2651 2 => {
2653 if frac_part.is_some() {
2654 return None; }
2656 let hour: u32 = integer_part.parse().ok()?;
2657 NaiveTime::from_hms_opt(hour, 0, 0)
2658 }
2659 _ => None,
2660 }
2661}
2662
2663fn try_parse_naive_time(s: &str) -> Result<NaiveTime, chrono::ParseError> {
2666 NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
2667 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S"))
2668 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M"))
2669 .or_else(|e| try_parse_compact_time(s).ok_or(e))
2670}
2671
2672fn try_parse_naive_datetime(s: &str) -> Result<NaiveDateTime, chrono::ParseError> {
2675 NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")
2676 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f"))
2677 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M"))
2678 .or_else(|e| {
2679 if let Some(t_pos) = s.find('T') {
2681 let date_part = &s[..t_pos];
2682 let time_part = &s[t_pos + 1..];
2683 let date = try_parse_compact_date(date_part);
2684 let time = try_parse_compact_time(time_part)
2685 .or_else(|| try_parse_naive_time(time_part).ok());
2686 if let (Some(d), Some(t)) = (date, time) {
2687 return Ok(d.and_time(t));
2688 }
2689 }
2690 if let Some(date) = try_parse_compact_date(s) {
2692 let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
2693 return Ok(date.and_time(midnight));
2694 }
2695 Err(e)
2696 })
2697}
2698
2699pub fn parse_datetime_with_tz(s: &str) -> Result<(NaiveDate, NaiveTime, Option<TimezoneInfo>)> {
2701 let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
2702 let today = Utc::now().date_naive();
2703
2704 let (datetime_part, tz_name) = if let Some(bracket_pos) = s.find('[') {
2706 let tz_name = s[bracket_pos + 1..s.len() - 1].to_string();
2707 (&s[..bracket_pos], Some(tz_name))
2708 } else {
2709 (s, None)
2710 };
2711
2712 if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_part) {
2714 let tz_info = if let Some(name) = tz_name {
2715 Some(parse_timezone(&name)?)
2716 } else {
2717 Some(TimezoneInfo::FixedOffset(dt.offset().fix()))
2718 };
2719 return Ok((dt.date_naive(), dt.time(), tz_info));
2720 }
2721
2722 if let Ok(ndt) = try_parse_naive_datetime(datetime_part) {
2724 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?;
2725 return Ok((ndt.date(), ndt.time(), tz_info));
2726 }
2727
2728 if let Ok(d) = NaiveDate::parse_from_str(datetime_part, "%Y-%m-%d") {
2730 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?;
2731 return Ok((d, midnight, tz_info));
2732 }
2733
2734 if let Some(d) = try_parse_compact_date(datetime_part) {
2736 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?;
2737 return Ok((d, midnight, tz_info));
2738 }
2739
2740 if let Some(tz_pos) = datetime_part.rfind('+').or_else(|| {
2747 datetime_part.rfind('-').filter(|&pos| {
2751 if let Some(t_pos) = datetime_part.find('T') {
2752 pos >= t_pos + 3
2754 } else {
2755 pos >= 2
2757 }
2758 })
2759 }) {
2760 let left_part = &datetime_part[..tz_pos];
2761 let tz_part = &datetime_part[tz_pos..];
2762
2763 let resolve_tz = |tz_name: Option<String>, tz_part: &str| -> Result<Option<TimezoneInfo>> {
2764 if let Some(name) = tz_name {
2765 Ok(Some(parse_timezone(&name)?))
2766 } else {
2767 let offset = parse_timezone_offset(tz_part)?;
2768 let fo = FixedOffset::east_opt(offset)
2769 .ok_or_else(|| anyhow!("Invalid timezone offset"))?;
2770 Ok(Some(TimezoneInfo::FixedOffset(fo)))
2771 }
2772 };
2773
2774 if !left_part.contains('T')
2778 && let Ok(time) = try_parse_naive_time(left_part)
2779 && let Ok(tz_info) = resolve_tz(tz_name.clone(), tz_part)
2780 {
2781 return Ok((today, time, tz_info));
2782 }
2783
2784 if let Ok(ndt) = try_parse_naive_datetime(left_part) {
2786 let tz_info = resolve_tz(tz_name, tz_part)?;
2787 return Ok((ndt.date(), ndt.time(), tz_info));
2788 }
2789
2790 if left_part.contains('T')
2792 && let Ok(time) = try_parse_naive_time(left_part)
2793 {
2794 let tz_info = resolve_tz(tz_name, tz_part)?;
2795 return Ok((today, time, tz_info));
2796 }
2797 }
2798
2799 if let Some(base) = datetime_part
2801 .strip_suffix('Z')
2802 .or_else(|| datetime_part.strip_suffix('z'))
2803 {
2804 let utc_tz = Some(TimezoneInfo::FixedOffset(FixedOffset::east_opt(0).unwrap()));
2805 if let Ok(ndt) = try_parse_naive_datetime(base) {
2807 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?.or(utc_tz);
2808 return Ok((ndt.date(), ndt.time(), tz_info));
2809 }
2810 if let Ok(time) = try_parse_naive_time(base) {
2812 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?.or(utc_tz);
2813 return Ok((today, time, tz_info));
2814 }
2815 }
2816
2817 if let Ok(time) = try_parse_naive_time(datetime_part) {
2819 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?;
2820 return Ok((today, time, tz_info));
2821 }
2822
2823 Err(anyhow!("Cannot parse datetime: {}", s))
2824}
2825
2826fn nanos_precision_format(nanos: u32, seconds: u32) -> &'static str {
2828 if nanos == 0 && seconds == 0 {
2829 "%Y-%m-%dT%H:%M"
2830 } else if nanos == 0 {
2831 "%Y-%m-%dT%H:%M:%S"
2832 } else if nanos.is_multiple_of(1_000_000) {
2833 "%Y-%m-%dT%H:%M:%S%.3f"
2834 } else if nanos.is_multiple_of(1_000) {
2835 "%Y-%m-%dT%H:%M:%S%.6f"
2836 } else {
2837 "%Y-%m-%dT%H:%M:%S%.9f"
2838 }
2839}
2840
2841fn format_datetime_with_nanos(dt: &DateTime<Utc>) -> String {
2842 let fmt = nanos_precision_format(dt.nanosecond(), dt.second());
2843 format!("{}Z", dt.format(fmt))
2844}
2845
2846fn format_datetime_with_offset_and_tz(dt: &DateTime<FixedOffset>, tz_name: Option<&str>) -> String {
2847 let fmt = nanos_precision_format(dt.nanosecond(), dt.second());
2848 let tz_suffix = format_timezone_offset(dt.offset().local_minus_utc());
2849 let base = format!("{}{}", dt.format(fmt), tz_suffix);
2850
2851 if let Some(name) = tz_name {
2852 format!("{}[{}]", base, name)
2853 } else {
2854 base
2855 }
2856}
2857
2858fn format_naive_datetime(ndt: &NaiveDateTime) -> String {
2859 let fmt = nanos_precision_format(ndt.nanosecond(), ndt.second());
2860 ndt.format(fmt).to_string()
2861}
2862
2863#[derive(Debug, Clone, PartialEq)]
2871pub struct CypherDuration {
2872 pub months: i64,
2874 pub days: i64,
2876 pub nanos: i64,
2878}
2879
2880impl CypherDuration {
2881 pub fn new(months: i64, days: i64, nanos: i64) -> Self {
2882 Self {
2883 months,
2884 days,
2885 nanos,
2886 }
2887 }
2888
2889 pub fn to_temporal_value(&self) -> Value {
2891 Value::Temporal(TemporalValue::Duration {
2892 months: self.months,
2893 days: self.days,
2894 nanos: self.nanos,
2895 })
2896 }
2897
2898 pub fn from_micros(micros: i64) -> Self {
2900 let total_nanos = micros * 1000;
2901 let total_secs = total_nanos / NANOS_PER_SECOND;
2902 let remaining_nanos = total_nanos % NANOS_PER_SECOND;
2903
2904 let days = total_secs / (24 * 3600);
2905 let day_secs = total_secs % (24 * 3600);
2906
2907 Self {
2908 months: 0,
2909 days,
2910 nanos: day_secs * NANOS_PER_SECOND + remaining_nanos,
2911 }
2912 }
2913
2914 pub fn to_iso8601(&self) -> String {
2918 let mut result = String::from("P");
2919
2920 let years = self.months / 12;
2921 let months = self.months % 12;
2922
2923 if years != 0 {
2924 result.push_str(&format!("{}Y", years));
2925 }
2926 if months != 0 {
2927 result.push_str(&format!("{}M", months));
2928 }
2929 if self.days != 0 {
2930 result.push_str(&format!("{}D", self.days));
2931 }
2932
2933 let nanos = self.nanos;
2936 let total_secs = nanos / NANOS_PER_SECOND; let remaining_nanos = nanos % NANOS_PER_SECOND; let hours = total_secs / 3600;
2940 let rem_after_hours = total_secs % 3600;
2941 let minutes = rem_after_hours / 60;
2942 let seconds = rem_after_hours % 60;
2943
2944 if hours != 0 || minutes != 0 || seconds != 0 || remaining_nanos != 0 {
2945 result.push('T');
2946
2947 if hours != 0 {
2948 result.push_str(&format!("{}H", hours));
2949 }
2950 if minutes != 0 {
2951 result.push_str(&format!("{}M", minutes));
2952 }
2953 if seconds != 0 || remaining_nanos != 0 {
2954 if remaining_nanos != 0 {
2955 let secs_with_nanos = seconds as f64 + (remaining_nanos as f64 / 1e9);
2958 let formatted = format!("{:.9}", secs_with_nanos);
2959 let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
2960 result.push_str(trimmed);
2961 result.push('S');
2962 } else {
2963 result.push_str(&format!("{}S", seconds));
2964 }
2965 }
2966 }
2967
2968 if result == "P" {
2970 result.push_str("T0S");
2971 }
2972
2973 result
2974 }
2975
2976 pub fn to_micros(&self) -> i64 {
2978 let month_days = self.months * 30; let total_days = month_days + self.days;
2980 let day_micros = total_days * MICROS_PER_DAY;
2981 let nano_micros = self.nanos / 1000;
2982 day_micros + nano_micros
2983 }
2984
2985 pub fn add(&self, other: &CypherDuration) -> CypherDuration {
2987 CypherDuration::new(
2988 self.months + other.months,
2989 self.days + other.days,
2990 self.nanos + other.nanos,
2991 )
2992 }
2993
2994 pub fn sub(&self, other: &CypherDuration) -> CypherDuration {
2996 CypherDuration::new(
2997 self.months - other.months,
2998 self.days - other.days,
2999 self.nanos - other.nanos,
3000 )
3001 }
3002
3003 pub fn negate(&self) -> CypherDuration {
3005 CypherDuration::new(-self.months, -self.days, -self.nanos)
3006 }
3007
3008 pub fn multiply(&self, factor: f64) -> CypherDuration {
3010 let months_f = self.months as f64 * factor;
3011 let whole_months = months_f.trunc() as i64;
3012 let frac_months = months_f.fract();
3013
3014 let frac_month_seconds = frac_months * 2_629_746.0;
3016 let extra_days_from_months = (frac_month_seconds / SECONDS_PER_DAY as f64).trunc();
3017 let remaining_secs_from_months =
3018 frac_month_seconds - extra_days_from_months * SECONDS_PER_DAY as f64;
3019
3020 let days_f = self.days as f64 * factor + extra_days_from_months;
3021 let whole_days = days_f.trunc() as i64;
3022 let frac_days = days_f.fract();
3023
3024 let nanos_f = self.nanos as f64 * factor
3025 + remaining_secs_from_months * NANOS_PER_SECOND as f64
3026 + frac_days * NANOS_PER_DAY as f64;
3027
3028 CypherDuration::new(whole_months, whole_days, nanos_f.trunc() as i64)
3029 }
3030
3031 pub fn divide(&self, divisor: f64) -> CypherDuration {
3033 if divisor == 0.0 {
3034 return CypherDuration::new(0, 0, 0);
3036 }
3037 self.multiply(1.0 / divisor)
3038 }
3039}
3040
3041pub fn add_months_to_date(date: NaiveDate, months: i64) -> NaiveDate {
3051 if months == 0 {
3052 return date;
3053 }
3054
3055 let total_months = date.year() as i64 * 12 + (date.month() as i64 - 1) + months;
3056 let new_year = total_months.div_euclid(12) as i32;
3057 let new_month = (total_months.rem_euclid(12) + 1) as u32;
3058
3059 let max_day = days_in_month(new_year, new_month);
3061 let new_day = date.day().min(max_day);
3062
3063 NaiveDate::from_ymd_opt(new_year, new_month, new_day)
3064 .unwrap_or_else(|| NaiveDate::from_ymd_opt(new_year, new_month, 1).unwrap())
3065}
3066
3067fn days_in_month(year: i32, month: u32) -> u32 {
3069 match month {
3070 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
3071 4 | 6 | 9 | 11 => 30,
3072 2 => {
3073 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
3074 29
3075 } else {
3076 28
3077 }
3078 }
3079 _ => 30,
3080 }
3081}
3082
3083pub fn add_cypher_duration_to_date(date_str: &str, dur: &CypherDuration) -> Result<String> {
3087 let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
3088
3089 let after_months = add_months_to_date(date, dur.months);
3091
3092 let after_days = after_months + Duration::days(dur.days);
3094
3095 let extra_days = dur.nanos / NANOS_PER_DAY;
3098 let result = after_days + Duration::days(extra_days);
3099
3100 Ok(result.format("%Y-%m-%d").to_string())
3101}
3102
3103pub fn add_cypher_duration_to_localtime(time_str: &str, dur: &CypherDuration) -> Result<String> {
3107 let time = parse_time_string(time_str)?;
3108 let total_nanos = time_to_nanos(&time) + dur.nanos;
3109 let wrapped = total_nanos.rem_euclid(NANOS_PER_DAY);
3111 let result = nanos_to_time(wrapped);
3112 Ok(format_time_with_nanos(&result))
3113}
3114
3115pub fn add_cypher_duration_to_time(time_str: &str, dur: &CypherDuration) -> Result<String> {
3119 let (_, time, tz_info) = parse_datetime_with_tz(time_str)?;
3120 let total_nanos = time_to_nanos(&time) + dur.nanos;
3121 let wrapped = total_nanos.rem_euclid(NANOS_PER_DAY);
3122 let result_time = nanos_to_time(wrapped);
3123
3124 let time_part = format_time_with_nanos(&result_time);
3125 if let Some(ref tz) = tz_info {
3126 let today = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
3127 let ndt = NaiveDateTime::new(today, result_time);
3128 let offset = tz.offset_for_local(&ndt)?;
3129 let offset_str = format_timezone_offset(offset.local_minus_utc());
3130 Ok(format!("{}{}", time_part, offset_str))
3131 } else {
3132 Ok(time_part)
3133 }
3134}
3135
3136pub fn add_cypher_duration_to_localdatetime(dt_str: &str, dur: &CypherDuration) -> Result<String> {
3138 let ndt = NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M:%S")
3139 .or_else(|_| NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M:%S%.f"))
3140 .or_else(|_| NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M"))
3141 .map_err(|_| anyhow!("Invalid localdatetime: {}", dt_str))?;
3142
3143 let after_months = add_months_to_date(ndt.date(), dur.months);
3145 let after_days = after_months + Duration::days(dur.days);
3147 let result_ndt = NaiveDateTime::new(after_days, ndt.time()) + Duration::nanoseconds(dur.nanos);
3149
3150 Ok(format_naive_datetime(&result_ndt))
3151}
3152
3153pub fn add_cypher_duration_to_datetime(dt_str: &str, dur: &CypherDuration) -> Result<String> {
3155 let (date, time, tz_info) = parse_datetime_with_tz(dt_str)?;
3156
3157 let after_months = add_months_to_date(date, dur.months);
3159 let after_days = after_months + Duration::days(dur.days);
3161 let ndt = NaiveDateTime::new(after_days, time) + Duration::nanoseconds(dur.nanos);
3163
3164 if let Some(ref tz) = tz_info {
3165 let offset = tz.offset_for_local(&ndt)?;
3166 let dt = offset
3167 .from_local_datetime(&ndt)
3168 .single()
3169 .ok_or_else(|| anyhow!("Ambiguous local time after duration addition"))?;
3170 Ok(format_datetime_with_offset_and_tz(&dt, tz.name()))
3171 } else {
3172 let dt = DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc);
3173 Ok(format_datetime_with_nanos(&dt))
3174 }
3175}
3176
3177fn time_to_nanos(t: &NaiveTime) -> i64 {
3179 t.hour() as i64 * 3_600 * NANOS_PER_SECOND
3180 + t.minute() as i64 * 60 * NANOS_PER_SECOND
3181 + t.second() as i64 * NANOS_PER_SECOND
3182 + t.nanosecond() as i64
3183}
3184
3185fn nanos_to_time(nanos: i64) -> NaiveTime {
3187 let total_secs = nanos / NANOS_PER_SECOND;
3188 let remaining_nanos = (nanos % NANOS_PER_SECOND) as u32;
3189 let h = (total_secs / 3600) as u32;
3190 let m = ((total_secs % 3600) / 60) as u32;
3191 let s = (total_secs % 60) as u32;
3192 NaiveTime::from_hms_nano_opt(h, m, s, remaining_nanos)
3193 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap())
3194}
3195
3196fn eval_duration(args: &[Value]) -> Result<Value> {
3201 if args.len() != 1 {
3202 return Err(anyhow!("duration() requires 1 argument"));
3203 }
3204
3205 match &args[0] {
3206 Value::String(s) => {
3207 let duration = parse_duration_to_cypher(s)?;
3208 Ok(Value::Temporal(TemporalValue::Duration {
3209 months: duration.months,
3210 days: duration.days,
3211 nanos: duration.nanos,
3212 }))
3213 }
3214 Value::Temporal(TemporalValue::Duration { .. }) => Ok(args[0].clone()),
3215 Value::Map(map) => eval_duration_from_map(map),
3216 Value::Int(_) | Value::Float(_) => {
3217 if let Some(micros) = args[0].as_i64() {
3218 let duration = CypherDuration::from_micros(micros);
3219 Ok(Value::Temporal(TemporalValue::Duration {
3220 months: duration.months,
3221 days: duration.days,
3222 nanos: duration.nanos,
3223 }))
3224 } else {
3225 Ok(args[0].clone())
3226 }
3227 }
3228 Value::Null => Ok(Value::Null),
3229 _ => Err(anyhow!("duration() expects a string, map, or number")),
3230 }
3231}
3232
3233fn eval_duration_from_map(map: &HashMap<String, Value>) -> Result<Value> {
3239 let mut months_f: f64 = 0.0;
3240 let mut days_f: f64 = 0.0;
3241 let mut nanos_f: f64 = 0.0;
3242
3243 if let Some(years) = map.get("years").and_then(get_numeric_value) {
3245 months_f += years * 12.0;
3246 }
3247 if let Some(m) = map.get("months").and_then(get_numeric_value) {
3248 months_f += m;
3249 }
3250
3251 let whole_months = months_f.trunc() as i64;
3254 let frac_months = months_f.fract();
3255 let frac_month_seconds = frac_months * 2_629_746.0;
3256 let extra_days_from_months = (frac_month_seconds / SECONDS_PER_DAY as f64).trunc();
3257 let remaining_secs_from_months =
3258 frac_month_seconds - extra_days_from_months * SECONDS_PER_DAY as f64;
3259 days_f += extra_days_from_months;
3260 nanos_f += remaining_secs_from_months * NANOS_PER_SECOND as f64;
3261
3262 if let Some(weeks) = map.get("weeks").and_then(get_numeric_value) {
3263 days_f += weeks * 7.0;
3264 }
3265 if let Some(d) = map.get("days").and_then(get_numeric_value) {
3266 days_f += d;
3267 }
3268
3269 let whole_days = days_f.trunc() as i64;
3271 let frac_days = days_f.fract();
3272 nanos_f += frac_days * NANOS_PER_DAY as f64;
3273
3274 if let Some(hours) = map.get("hours").and_then(get_numeric_value) {
3276 nanos_f += hours * 3600.0 * NANOS_PER_SECOND as f64;
3277 }
3278 if let Some(minutes) = map.get("minutes").and_then(get_numeric_value) {
3279 nanos_f += minutes * 60.0 * NANOS_PER_SECOND as f64;
3280 }
3281 if let Some(seconds) = map.get("seconds").and_then(get_numeric_value) {
3282 nanos_f += seconds * NANOS_PER_SECOND as f64;
3283 }
3284 if let Some(millis) = map.get("milliseconds").and_then(get_numeric_value) {
3285 nanos_f += millis * 1_000_000.0;
3286 }
3287 if let Some(micros) = map.get("microseconds").and_then(get_numeric_value) {
3288 nanos_f += micros * 1_000.0;
3289 }
3290 if let Some(n) = map.get("nanoseconds").and_then(get_numeric_value) {
3291 nanos_f += n;
3292 }
3293
3294 let duration = CypherDuration::new(whole_months, whole_days, nanos_f.trunc() as i64);
3295 Ok(Value::Temporal(TemporalValue::Duration {
3296 months: duration.months,
3297 days: duration.days,
3298 nanos: duration.nanos,
3299 }))
3300}
3301
3302fn get_numeric_value(v: &Value) -> Option<f64> {
3304 v.as_f64().or_else(|| v.as_i64().map(|i| i as f64))
3305}
3306
3307fn parse_iso8601_duration(s: &str) -> Result<i64> {
3309 let s = &s[1..]; let mut total_micros: i64 = 0;
3311 let mut in_time_part = false;
3312 let mut num_buf = String::new();
3313
3314 for c in s.chars() {
3315 if c == 'T' || c == 't' {
3316 in_time_part = true;
3317 continue;
3318 }
3319
3320 if c.is_ascii_digit() || c == '.' || c == '-' {
3321 num_buf.push(c);
3322 } else {
3323 if num_buf.is_empty() {
3324 continue;
3325 }
3326 let num: f64 = num_buf
3327 .parse()
3328 .map_err(|_| anyhow!("Invalid duration number"))?;
3329 num_buf.clear();
3330
3331 let micros = match c {
3332 'Y' | 'y' => (num * 365.0 * MICROS_PER_DAY as f64) as i64,
3333 'M' if !in_time_part => (num * 30.0 * MICROS_PER_DAY as f64) as i64, 'W' | 'w' => (num * 7.0 * MICROS_PER_DAY as f64) as i64,
3335 'D' | 'd' => (num * MICROS_PER_DAY as f64) as i64,
3336 'H' | 'h' => (num * MICROS_PER_HOUR as f64) as i64,
3337 'M' | 'm' if in_time_part => (num * MICROS_PER_MINUTE as f64) as i64, 'S' | 's' => (num * MICROS_PER_SECOND as f64) as i64,
3339 _ => return Err(anyhow!("Invalid ISO 8601 duration designator: {}", c)),
3340 };
3341 total_micros += micros;
3342 }
3343 }
3344
3345 Ok(total_micros)
3346}
3347
3348fn parse_simple_duration(s: &str) -> Result<i64> {
3350 let mut total_micros: i64 = 0;
3351 let mut num_buf = String::new();
3352
3353 for c in s.chars() {
3354 if c.is_ascii_digit() || c == '.' || c == '-' {
3355 num_buf.push(c);
3356 } else if c.is_ascii_alphabetic() {
3357 if num_buf.is_empty() {
3358 return Err(anyhow!("Invalid duration format"));
3359 }
3360 let num: f64 = num_buf
3361 .parse()
3362 .map_err(|_| anyhow!("Invalid duration number"))?;
3363 num_buf.clear();
3364
3365 let micros = match c {
3366 'w' => (num * 7.0 * MICROS_PER_DAY as f64) as i64,
3367 'd' => (num * MICROS_PER_DAY as f64) as i64,
3368 'h' => (num * MICROS_PER_HOUR as f64) as i64,
3369 'm' => (num * MICROS_PER_MINUTE as f64) as i64,
3370 's' => (num * MICROS_PER_SECOND as f64) as i64,
3371 _ => return Err(anyhow!("Invalid duration unit: {}", c)),
3372 };
3373 total_micros += micros;
3374 }
3375 }
3376
3377 if !num_buf.is_empty() {
3379 let num: f64 = num_buf
3380 .parse()
3381 .map_err(|_| anyhow!("Invalid duration number"))?;
3382 total_micros += (num * MICROS_PER_SECOND as f64) as i64;
3383 }
3384
3385 Ok(total_micros)
3386}
3387
3388fn eval_datetime_fromepoch(args: &[Value]) -> Result<Value> {
3393 let seconds = args
3394 .first()
3395 .and_then(|v| v.as_i64())
3396 .ok_or_else(|| anyhow!("datetime.fromepoch requires seconds argument"))?;
3397 let nanos = args.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as u32;
3398
3399 let dt = DateTime::from_timestamp(seconds, nanos)
3400 .ok_or_else(|| anyhow!("Invalid epoch timestamp: {}", seconds))?;
3401 let epoch_nanos = dt.timestamp_nanos_opt().unwrap_or(0);
3402 Ok(Value::Temporal(TemporalValue::DateTime {
3403 nanos_since_epoch: epoch_nanos,
3404 offset_seconds: 0,
3405 timezone_name: None,
3406 }))
3407}
3408
3409fn eval_datetime_fromepochmillis(args: &[Value]) -> Result<Value> {
3410 let millis = args
3411 .first()
3412 .and_then(|v| v.as_i64())
3413 .ok_or_else(|| anyhow!("datetime.fromepochmillis requires milliseconds argument"))?;
3414
3415 let dt = DateTime::from_timestamp_millis(millis)
3416 .ok_or_else(|| anyhow!("Invalid epoch millis: {}", millis))?;
3417 let epoch_nanos = dt.timestamp_nanos_opt().unwrap_or(0);
3418 Ok(Value::Temporal(TemporalValue::DateTime {
3419 nanos_since_epoch: epoch_nanos,
3420 offset_seconds: 0,
3421 timezone_name: None,
3422 }))
3423}
3424
3425fn eval_truncate(type_name: &str, args: &[Value]) -> Result<Value> {
3430 if args.is_empty() {
3431 return Err(anyhow!(
3432 "{}.truncate requires at least a unit argument",
3433 type_name
3434 ));
3435 }
3436
3437 let unit = args
3438 .first()
3439 .and_then(|v| v.as_str())
3440 .ok_or_else(|| anyhow!("truncate requires unit as first argument"))?;
3441
3442 let temporal = args.get(1);
3443 let adjust_map = args.get(2).and_then(|v| v.as_object());
3444
3445 match type_name {
3446 "date" => truncate_date(unit, temporal, adjust_map),
3447 "time" => truncate_time(unit, temporal, adjust_map, true),
3448 "localtime" => truncate_time(unit, temporal, adjust_map, false),
3449 "datetime" | "localdatetime" => truncate_datetime(unit, temporal, adjust_map, type_name),
3450 _ => Err(anyhow!("Unknown truncate type: {}", type_name)),
3451 }
3452}
3453
3454fn truncate_date(
3455 unit: &str,
3456 temporal: Option<&Value>,
3457 adjust_map: Option<&HashMap<String, Value>>,
3458) -> Result<Value> {
3459 let date = match temporal {
3460 Some(Value::Temporal(_)) => temporal_or_string_to_date(temporal.unwrap())?,
3461 Some(Value::String(s)) => parse_date_string(s)?,
3462 Some(Value::Null) | None => Utc::now().date_naive(),
3463 _ => return Err(anyhow!("truncate expects a date string")),
3464 };
3465
3466 let truncated = truncate_date_to_unit(date, unit)?;
3467
3468 if let Some(map) = adjust_map {
3469 apply_date_adjustments(truncated, map)
3470 } else {
3471 Ok(Value::Temporal(TemporalValue::Date {
3472 days_since_epoch: date_to_days_since_epoch(&truncated),
3473 }))
3474 }
3475}
3476
3477fn truncate_date_to_unit(date: NaiveDate, unit: &str) -> Result<NaiveDate> {
3478 let unit_lower = unit.to_lowercase();
3479 match unit_lower.as_str() {
3480 "millennium" => {
3481 let millennium_year = (date.year() / 1000) * 1000;
3483 NaiveDate::from_ymd_opt(millennium_year, 1, 1)
3484 .ok_or_else(|| anyhow!("Invalid millennium truncation"))
3485 }
3486 "century" => {
3487 let century_year = (date.year() / 100) * 100;
3489 NaiveDate::from_ymd_opt(century_year, 1, 1)
3490 .ok_or_else(|| anyhow!("Invalid century truncation"))
3491 }
3492 "decade" => {
3493 let decade_year = (date.year() / 10) * 10;
3494 NaiveDate::from_ymd_opt(decade_year, 1, 1)
3495 .ok_or_else(|| anyhow!("Invalid decade truncation"))
3496 }
3497 "year" => NaiveDate::from_ymd_opt(date.year(), 1, 1)
3498 .ok_or_else(|| anyhow!("Invalid year truncation")),
3499 "weekyear" => {
3500 let iso_week = date.iso_week();
3502 let week_year = iso_week.year();
3503 let jan4 =
3504 NaiveDate::from_ymd_opt(week_year, 1, 4).ok_or_else(|| anyhow!("Invalid date"))?;
3505 let iso_week_day = jan4.weekday().num_days_from_monday();
3506 Ok(jan4 - Duration::days(iso_week_day as i64))
3507 }
3508 "quarter" => {
3509 let quarter = (date.month() - 1) / 3;
3510 let first_month = quarter * 3 + 1;
3511 NaiveDate::from_ymd_opt(date.year(), first_month, 1)
3512 .ok_or_else(|| anyhow!("Invalid quarter truncation"))
3513 }
3514 "month" => NaiveDate::from_ymd_opt(date.year(), date.month(), 1)
3515 .ok_or_else(|| anyhow!("Invalid month truncation")),
3516 "week" => {
3517 let weekday = date.weekday().num_days_from_monday();
3519 Ok(date - Duration::days(weekday as i64))
3520 }
3521 "day" => Ok(date),
3522 _ => Err(anyhow!("Unknown truncation unit for date: {}", unit)),
3523 }
3524}
3525
3526fn apply_date_adjustments(date: NaiveDate, map: &HashMap<String, Value>) -> Result<Value> {
3527 let mut result = date;
3528
3529 if let Some(dow) = map.get("dayOfWeek").and_then(|v| v.as_i64()) {
3531 let current_dow = result.weekday().num_days_from_monday() as i64 + 1;
3534 let diff = dow - current_dow;
3535 result += Duration::days(diff);
3536 }
3537
3538 if let Some(month) = map.get("month").and_then(|v| v.as_i64()) {
3539 result = NaiveDate::from_ymd_opt(result.year(), month as u32, result.day())
3540 .ok_or_else(|| anyhow!("Invalid month adjustment"))?;
3541 }
3542 if let Some(day) = map.get("day").and_then(|v| v.as_i64()) {
3543 result = NaiveDate::from_ymd_opt(result.year(), result.month(), day as u32)
3544 .ok_or_else(|| anyhow!("Invalid day adjustment"))?;
3545 }
3546
3547 Ok(Value::Temporal(TemporalValue::Date {
3548 days_since_epoch: date_to_days_since_epoch(&result),
3549 }))
3550}
3551
3552fn truncate_time(
3553 unit: &str,
3554 temporal: Option<&Value>,
3555 adjust_map: Option<&HashMap<String, Value>>,
3556 with_timezone: bool,
3557) -> Result<Value> {
3558 let (date, time, tz_info) = match temporal {
3559 Some(Value::Temporal(tv)) => {
3560 let t = tv
3561 .to_time()
3562 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
3563 let offset = match tv {
3564 TemporalValue::Time { offset_seconds, .. }
3565 | TemporalValue::DateTime { offset_seconds, .. } => Some(
3566 TimezoneInfo::FixedOffset(FixedOffset::east_opt(*offset_seconds).unwrap()),
3567 ),
3568 _ => None,
3569 };
3570 (Utc::now().date_naive(), t, offset)
3571 }
3572 Some(Value::String(s)) => {
3573 if let Ok((date, time, tz)) = parse_datetime_with_tz(s) {
3575 (date, time, tz)
3576 } else if let Ok(t) = parse_time_string(s) {
3577 (Utc::now().date_naive(), t, None)
3579 } else {
3580 return Err(anyhow!("truncate expects a time string"));
3581 }
3582 }
3583 Some(Value::Null) | None => {
3584 let now = Utc::now();
3585 (now.date_naive(), now.time(), None)
3586 }
3587 _ => return Err(anyhow!("truncate expects a time string")),
3588 };
3589
3590 let effective_tz = if let Some(map) = adjust_map {
3592 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
3593 Some(parse_timezone(tz_str)?)
3594 } else {
3595 tz_info
3596 }
3597 } else {
3598 tz_info
3599 };
3600
3601 let truncated = truncate_time_to_unit(time, unit)?;
3602
3603 let final_time = if let Some(map) = adjust_map {
3604 apply_time_adjustments(truncated, map)?
3605 } else {
3606 truncated
3607 };
3608
3609 let nanos = time_to_nanos(&final_time);
3611 if with_timezone {
3612 let offset = if let Some(ref tz) = effective_tz {
3613 tz.offset_seconds_with_date(&date)
3614 } else {
3615 0
3616 };
3617 Ok(Value::Temporal(TemporalValue::Time {
3618 nanos_since_midnight: nanos,
3619 offset_seconds: offset,
3620 }))
3621 } else {
3622 Ok(Value::Temporal(TemporalValue::LocalTime {
3623 nanos_since_midnight: nanos,
3624 }))
3625 }
3626}
3627
3628fn truncate_time_to_unit(time: NaiveTime, unit: &str) -> Result<NaiveTime> {
3629 let unit_lower = unit.to_lowercase();
3630 match unit_lower.as_str() {
3631 "day" => NaiveTime::from_hms_opt(0, 0, 0).ok_or_else(|| anyhow!("Invalid truncation")),
3632 "hour" => {
3633 NaiveTime::from_hms_opt(time.hour(), 0, 0).ok_or_else(|| anyhow!("Invalid truncation"))
3634 }
3635 "minute" => NaiveTime::from_hms_opt(time.hour(), time.minute(), 0)
3636 .ok_or_else(|| anyhow!("Invalid truncation")),
3637 "second" => NaiveTime::from_hms_opt(time.hour(), time.minute(), time.second())
3638 .ok_or_else(|| anyhow!("Invalid truncation")),
3639 "millisecond" => {
3640 let millis = time.nanosecond() / 1_000_000;
3641 NaiveTime::from_hms_nano_opt(
3642 time.hour(),
3643 time.minute(),
3644 time.second(),
3645 millis * 1_000_000,
3646 )
3647 .ok_or_else(|| anyhow!("Invalid truncation"))
3648 }
3649 "microsecond" => {
3650 let micros = time.nanosecond() / 1_000;
3651 NaiveTime::from_hms_nano_opt(time.hour(), time.minute(), time.second(), micros * 1_000)
3652 .ok_or_else(|| anyhow!("Invalid truncation"))
3653 }
3654 _ => Err(anyhow!("Unknown truncation unit for time: {}", unit)),
3655 }
3656}
3657
3658fn apply_time_adjustments(time: NaiveTime, map: &HashMap<String, Value>) -> Result<NaiveTime> {
3660 let hour = map
3661 .get("hour")
3662 .and_then(|v| v.as_i64())
3663 .unwrap_or(time.hour() as i64) as u32;
3664 let minute = map
3665 .get("minute")
3666 .and_then(|v| v.as_i64())
3667 .unwrap_or(time.minute() as i64) as u32;
3668 let second = map
3669 .get("second")
3670 .and_then(|v| v.as_i64())
3671 .unwrap_or(time.second() as i64) as u32;
3672 let nanos = build_nanoseconds_with_base(map, time.nanosecond());
3673
3674 NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
3675 .ok_or_else(|| anyhow!("Invalid time adjustment"))
3676}
3677
3678fn truncate_datetime(
3679 unit: &str,
3680 temporal: Option<&Value>,
3681 adjust_map: Option<&HashMap<String, Value>>,
3682 type_name: &str,
3683) -> Result<Value> {
3684 let (date, time, tz_info) = match temporal {
3685 Some(Value::Temporal(_)) => temporal_or_string_to_components(temporal.unwrap())?,
3686 Some(Value::String(s)) => {
3687 parse_datetime_with_tz(s)?
3689 }
3690 Some(Value::Null) | None => {
3691 let now = Utc::now();
3692 (
3693 now.date_naive(),
3694 now.time(),
3695 Some(TimezoneInfo::FixedOffset(FixedOffset::east_opt(0).unwrap())),
3696 )
3697 }
3698 _ => return Err(anyhow!("truncate expects a datetime string")),
3699 };
3700
3701 let effective_tz = if let Some(map) = adjust_map {
3703 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
3704 Some(parse_timezone(tz_str)?)
3705 } else {
3706 tz_info
3707 }
3708 } else {
3709 tz_info
3710 };
3711
3712 let (truncated_date, truncated_time) = truncate_datetime_to_unit(date, time, unit)?;
3714
3715 if let Some(map) = adjust_map {
3716 apply_datetime_adjustments(
3717 truncated_date,
3718 truncated_time,
3719 map,
3720 type_name,
3721 effective_tz.as_ref(),
3722 )
3723 } else {
3724 let ndt = NaiveDateTime::new(truncated_date, truncated_time);
3725 if type_name == "localdatetime" {
3726 Ok(localdatetime_value_from_naive(&ndt))
3727 } else if let Some(ref tz) = effective_tz {
3728 let offset = tz.offset_for_local(&ndt)?;
3729 let offset_secs = offset.local_minus_utc();
3730 Ok(datetime_value_from_local_and_offset(
3731 &ndt,
3732 offset_secs,
3733 tz.name().map(|s| s.to_string()),
3734 ))
3735 } else {
3736 Ok(datetime_value_from_local_and_offset(&ndt, 0, None))
3737 }
3738 }
3739}
3740
3741fn truncate_datetime_to_unit(
3742 date: NaiveDate,
3743 time: NaiveTime,
3744 unit: &str,
3745) -> Result<(NaiveDate, NaiveTime)> {
3746 let unit_lower = unit.to_lowercase();
3747 let midnight =
3748 NaiveTime::from_hms_opt(0, 0, 0).ok_or_else(|| anyhow!("Failed to create midnight"))?;
3749
3750 match unit_lower.as_str() {
3751 "millennium" | "century" | "decade" | "year" | "weekyear" | "quarter" | "month"
3753 | "week" | "day" => {
3754 let truncated_date = truncate_date_to_unit(date, unit)?;
3755 Ok((truncated_date, midnight))
3756 }
3757 "hour" | "minute" | "second" | "millisecond" | "microsecond" => {
3759 let truncated_time = truncate_time_to_unit(time, unit)?;
3760 Ok((date, truncated_time))
3761 }
3762 _ => Err(anyhow!("Unknown truncation unit: {}", unit)),
3763 }
3764}
3765
3766fn apply_datetime_adjustments(
3767 date: NaiveDate,
3768 time: NaiveTime,
3769 map: &HashMap<String, Value>,
3770 type_name: &str,
3771 tz_info: Option<&TimezoneInfo>,
3772) -> Result<Value> {
3773 let year = map
3775 .get("year")
3776 .and_then(|v| v.as_i64())
3777 .unwrap_or(date.year() as i64) as i32;
3778 let month = map
3779 .get("month")
3780 .and_then(|v| v.as_i64())
3781 .unwrap_or(date.month() as i64) as u32;
3782 let day = map
3783 .get("day")
3784 .and_then(|v| v.as_i64())
3785 .unwrap_or(date.day() as i64) as u32;
3786
3787 let hour = map
3789 .get("hour")
3790 .and_then(|v| v.as_i64())
3791 .unwrap_or(time.hour() as i64) as u32;
3792 let minute = map
3793 .get("minute")
3794 .and_then(|v| v.as_i64())
3795 .unwrap_or(time.minute() as i64) as u32;
3796 let second = map
3797 .get("second")
3798 .and_then(|v| v.as_i64())
3799 .unwrap_or(time.second() as i64) as u32;
3800 let nanos = build_nanoseconds_with_base(map, time.nanosecond());
3801
3802 let mut adjusted_date = NaiveDate::from_ymd_opt(year, month, day)
3803 .ok_or_else(|| anyhow!("Invalid date in adjustment"))?;
3804
3805 if let Some(dow) = map.get("dayOfWeek").and_then(|v| v.as_i64()) {
3807 let current_dow = adjusted_date.weekday().num_days_from_monday() as i64 + 1;
3808 let diff = dow - current_dow;
3809 adjusted_date += Duration::days(diff);
3810 }
3811
3812 let adjusted_time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
3813 .ok_or_else(|| anyhow!("Invalid time in adjustment"))?;
3814
3815 let ndt = NaiveDateTime::new(adjusted_date, adjusted_time);
3816
3817 if type_name == "localdatetime" {
3818 Ok(localdatetime_value_from_naive(&ndt))
3819 } else if let Some(tz) = tz_info {
3820 let offset = tz.offset_for_local(&ndt)?;
3821 let offset_secs = offset.local_minus_utc();
3822 Ok(datetime_value_from_local_and_offset(
3823 &ndt,
3824 offset_secs,
3825 tz.name().map(|s| s.to_string()),
3826 ))
3827 } else {
3828 Ok(datetime_value_from_local_and_offset(&ndt, 0, None))
3829 }
3830}
3831
3832#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3837struct ExtendedDate {
3838 year: i64,
3839 month: u32,
3840 day: u32,
3841}
3842
3843#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3844struct ExtendedLocalDateTime {
3845 date: ExtendedDate,
3846 hour: u32,
3847 minute: u32,
3848 second: u32,
3849 nanosecond: u32,
3850}
3851
3852fn is_leap_year_i64(year: i64) -> bool {
3853 year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
3854}
3855
3856fn days_in_month_i64(year: i64, month: u32) -> Option<u32> {
3857 let days = match month {
3858 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
3859 4 | 6 | 9 | 11 => 30,
3860 2 => {
3861 if is_leap_year_i64(year) {
3862 29
3863 } else {
3864 28
3865 }
3866 }
3867 _ => return None,
3868 };
3869 Some(days)
3870}
3871
3872fn parse_extended_date_string(s: &str) -> Option<ExtendedDate> {
3873 let bytes = s.as_bytes();
3874 if bytes.is_empty() {
3875 return None;
3876 }
3877
3878 let mut idx = 0usize;
3879 if matches!(bytes[0], b'+' | b'-') {
3880 idx += 1;
3881 }
3882 if idx >= bytes.len() || !bytes[idx].is_ascii_digit() {
3883 return None;
3884 }
3885
3886 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
3887 idx += 1;
3888 }
3889 if idx >= bytes.len() || bytes[idx] != b'-' {
3890 return None;
3891 }
3892
3893 let year: i64 = s[..idx].parse().ok()?;
3894 let rest = &s[idx + 1..];
3895 let (month_str, day_str) = rest.split_once('-')?;
3896 if month_str.len() != 2 || day_str.len() != 2 {
3897 return None;
3898 }
3899 let month: u32 = month_str.parse().ok()?;
3900 let day: u32 = day_str.parse().ok()?;
3901 let max_day = days_in_month_i64(year, month)?;
3902 if day == 0 || day > max_day {
3903 return None;
3904 }
3905 Some(ExtendedDate { year, month, day })
3906}
3907
3908fn parse_extended_localdatetime_string(s: &str) -> Option<ExtendedLocalDateTime> {
3909 let (date_part, time_part) = if let Some((d, t)) = s.split_once('T') {
3910 (d, Some(t))
3911 } else {
3912 (s, None)
3913 };
3914
3915 let date = parse_extended_date_string(date_part)?;
3916
3917 let Some(time_part) = time_part else {
3918 return Some(ExtendedLocalDateTime {
3919 date,
3920 hour: 0,
3921 minute: 0,
3922 second: 0,
3923 nanosecond: 0,
3924 });
3925 };
3926
3927 if time_part.contains('+') || time_part.contains('Z') || time_part.contains('z') {
3928 return None;
3929 }
3930 let (hms_part, frac_part) = if let Some((hms, frac)) = time_part.split_once('.') {
3931 (hms, Some(frac))
3932 } else {
3933 (time_part, None)
3934 };
3935 let mut parts = hms_part.split(':');
3936 let hour: u32 = parts.next()?.parse().ok()?;
3937 let minute: u32 = parts.next()?.parse().ok()?;
3938 let second: u32 = parts.next().map(|v| v.parse().ok()).unwrap_or(Some(0))?;
3939 if parts.next().is_some() {
3940 return None;
3941 }
3942 if hour > 23 || minute > 59 || second > 59 {
3943 return None;
3944 }
3945
3946 let nanosecond = if let Some(frac) = frac_part {
3947 if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
3948 return None;
3949 }
3950 let mut frac_buf = frac.to_string();
3951 if frac_buf.len() > 9 {
3952 frac_buf.truncate(9);
3953 }
3954 while frac_buf.len() < 9 {
3955 frac_buf.push('0');
3956 }
3957 frac_buf.parse().ok()?
3958 } else {
3959 0
3960 };
3961
3962 Some(ExtendedLocalDateTime {
3963 date,
3964 hour,
3965 minute,
3966 second,
3967 nanosecond,
3968 })
3969}
3970
3971fn days_from_civil(date: ExtendedDate) -> i128 {
3972 let mut y = date.year;
3974 let m = date.month as i64;
3975 let d = date.day as i64;
3976 y -= if m <= 2 { 1 } else { 0 };
3977 let era = y.div_euclid(400);
3978 let yoe = y - era * 400;
3979 let mp = m + if m > 2 { -3 } else { 9 };
3980 let doy = (153 * mp + 2) / 5 + d - 1;
3981 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
3982 era as i128 * 146_097 + doe as i128 - 719_468
3983}
3984
3985fn calendar_months_between_extended(start: &ExtendedDate, end: &ExtendedDate) -> i64 {
3986 let year_diff = end.year - start.year;
3987 let month_diff = end.month as i64 - start.month as i64;
3988 let total_months = year_diff * 12 + month_diff;
3989
3990 if total_months > 0 && end.day < start.day {
3991 total_months - 1
3992 } else if total_months < 0 && end.day > start.day {
3993 total_months + 1
3994 } else {
3995 total_months
3996 }
3997}
3998
3999fn add_months_to_extended_date(date: ExtendedDate, months: i64) -> ExtendedDate {
4000 if months == 0 {
4001 return date;
4002 }
4003
4004 let total_months = date.year as i128 * 12 + (date.month as i128 - 1) + months as i128;
4005 let year = total_months.div_euclid(12) as i64;
4006 let month = (total_months.rem_euclid(12) + 1) as u32;
4007 let max_day = days_in_month_i64(year, month).unwrap_or(31);
4008 let day = date.day.min(max_day);
4009
4010 ExtendedDate { year, month, day }
4011}
4012
4013fn remaining_days_after_months_extended(
4014 start: &ExtendedDate,
4015 end: &ExtendedDate,
4016 months: i64,
4017) -> i64 {
4018 let after_months = add_months_to_extended_date(*start, months);
4019 (days_from_civil(*end) - days_from_civil(after_months)) as i64
4020}
4021
4022fn try_extended_date_from_value(val: &Value) -> Option<ExtendedDate> {
4023 match val {
4024 Value::String(s) => parse_extended_date_string(s),
4025 _ => None,
4026 }
4027}
4028
4029fn try_extended_localdatetime_from_value(val: &Value) -> Option<ExtendedLocalDateTime> {
4030 match val {
4031 Value::String(s) => parse_extended_localdatetime_string(s),
4032 _ => None,
4033 }
4034}
4035
4036fn try_eval_duration_between_extended(args: &[Value]) -> Result<Option<Value>> {
4037 let Some(start) = try_extended_date_from_value(&args[0]) else {
4038 return Ok(None);
4039 };
4040 let Some(end) = try_extended_date_from_value(&args[1]) else {
4041 return Ok(None);
4042 };
4043
4044 let months = calendar_months_between_extended(&start, &end);
4045 let remaining_days = remaining_days_after_months_extended(&start, &end, months);
4046 let dur = CypherDuration::new(months, remaining_days, 0);
4047 Ok(Some(Value::String(dur.to_iso8601())))
4048}
4049
4050fn format_time_only_duration_nanos(total_nanos: i128) -> String {
4051 if total_nanos == 0 {
4052 return "PT0S".to_string();
4053 }
4054 let total_secs = total_nanos / NANOS_PER_SECOND as i128;
4055 let rem_nanos = total_nanos % NANOS_PER_SECOND as i128;
4056
4057 let hours = total_secs / 3600;
4058 let rem_after_hours = total_secs % 3600;
4059 let minutes = rem_after_hours / 60;
4060 let seconds = rem_after_hours % 60;
4061
4062 let mut out = String::from("PT");
4063 if hours != 0 {
4064 out.push_str(&format!("{hours}H"));
4065 }
4066 if minutes != 0 {
4067 out.push_str(&format!("{minutes}M"));
4068 }
4069 if seconds != 0 || rem_nanos != 0 {
4070 if rem_nanos == 0 {
4071 out.push_str(&format!("{seconds}S"));
4072 } else {
4073 let sign = if total_nanos < 0 && seconds == 0 {
4074 "-"
4075 } else {
4076 ""
4077 };
4078 let secs_abs = seconds.abs();
4079 let nanos_abs = rem_nanos.abs();
4080 let frac = format!("{nanos_abs:09}");
4081 let trimmed = frac.trim_end_matches('0');
4082 out.push_str(&format!("{sign}{secs_abs}.{trimmed}S"));
4083 }
4084 }
4085 if out == "PT" { "PT0S".to_string() } else { out }
4086}
4087
4088fn try_eval_duration_in_seconds_extended(args: &[Value]) -> Result<Option<Value>> {
4089 let Some(start) = try_extended_localdatetime_from_value(&args[0]) else {
4090 return Ok(None);
4091 };
4092 let Some(end) = try_extended_localdatetime_from_value(&args[1]) else {
4093 return Ok(None);
4094 };
4095
4096 let start_days = days_from_civil(start.date);
4097 let end_days = days_from_civil(end.date);
4098 let start_tod_nanos =
4099 (start.hour as i128 * 3600 + start.minute as i128 * 60 + start.second as i128)
4100 * NANOS_PER_SECOND as i128
4101 + start.nanosecond as i128;
4102 let end_tod_nanos = (end.hour as i128 * 3600 + end.minute as i128 * 60 + end.second as i128)
4103 * NANOS_PER_SECOND as i128
4104 + end.nanosecond as i128;
4105 let total_nanos =
4106 (end_days - start_days) * NANOS_PER_DAY as i128 + (end_tod_nanos - start_tod_nanos);
4107
4108 if total_nanos >= i64::MIN as i128 && total_nanos <= i64::MAX as i128 {
4109 let dur = CypherDuration::new(0, 0, total_nanos as i64);
4110 Ok(Some(dur.to_temporal_value()))
4111 } else {
4112 Ok(Some(Value::String(format_time_only_duration_nanos(
4113 total_nanos,
4114 ))))
4115 }
4116}
4117
4118fn calendar_months_between(start: &NaiveDate, end: &NaiveDate) -> i64 {
4123 let year_diff = end.year() as i64 - start.year() as i64;
4124 let month_diff = end.month() as i64 - start.month() as i64;
4125 let total_months = year_diff * 12 + month_diff;
4126
4127 if total_months > 0 && end.day() < start.day() {
4129 total_months - 1
4130 } else if total_months < 0 && end.day() > start.day() {
4131 total_months + 1
4132 } else {
4133 total_months
4134 }
4135}
4136
4137fn remaining_days_after_months(start: &NaiveDate, end: &NaiveDate, months: i64) -> i64 {
4139 let after_months = add_months_to_date(*start, months);
4140 end.signed_duration_since(after_months).num_days()
4141}
4142
4143fn eval_duration_between(args: &[Value]) -> Result<Value> {
4144 if args.len() < 2 {
4145 return Err(anyhow!("duration.between requires two temporal arguments"));
4146 }
4147 if args[0].is_null() || args[1].is_null() {
4148 return Ok(Value::Null);
4149 }
4150
4151 let start_res = parse_temporal_value_typed(&args[0]);
4152 let end_res = parse_temporal_value_typed(&args[1]);
4153 let (start, end) = match (start_res, end_res) {
4154 (Ok(start), Ok(end)) => (start, end),
4155 (start_res, end_res) => {
4156 if let Some(value) = try_eval_duration_between_extended(args)? {
4157 return Ok(value);
4158 }
4159 return Err(start_res
4160 .err()
4161 .or_else(|| end_res.err())
4162 .unwrap_or_else(|| anyhow!("duration.between requires two temporal arguments")));
4163 }
4164 };
4165
4166 let start_has_date = has_date_component(start.ttype);
4167 let end_has_date = has_date_component(end.ttype);
4168 let start_has_time = has_time_component(start.ttype);
4169 let end_has_time = has_time_component(end.ttype);
4170
4171 if start.ttype == TemporalType::Date && end.ttype == TemporalType::Date {
4173 let months = calendar_months_between(&start.local_date, &end.local_date);
4174 let remaining_days =
4175 remaining_days_after_months(&start.local_date, &end.local_date, months);
4176 let dur = CypherDuration::new(months, remaining_days, 0);
4177 return Ok(dur.to_temporal_value());
4178 }
4179
4180 if start_has_date && end_has_date && start_has_time && end_has_time {
4183 let tz_aware = both_tz_aware(&start, &end);
4184 let (s_date, s_time, e_date, e_time) = if tz_aware {
4185 (
4186 start.utc_datetime.date(),
4187 start.utc_datetime.time(),
4188 end.utc_datetime.date(),
4189 end.utc_datetime.time(),
4190 )
4191 } else {
4192 (
4193 start.local_date,
4194 start.local_time,
4195 end.local_date,
4196 end.local_time,
4197 )
4198 };
4199
4200 let months = calendar_months_between(&s_date, &e_date);
4201 let date_after_months = add_months_to_date(s_date, months);
4202 let start_dt = NaiveDateTime::new(date_after_months, s_time);
4203 let end_dt = NaiveDateTime::new(e_date, e_time);
4204 let remaining_nanos = end_dt
4205 .signed_duration_since(start_dt)
4206 .num_nanoseconds()
4207 .unwrap_or(0);
4208
4209 let dur = CypherDuration::new(months, 0, remaining_nanos);
4210 return Ok(dur.to_temporal_value());
4211 }
4212
4213 if start_has_date && end_has_date {
4215 let tz_aware = both_tz_aware(&start, &end);
4216 let (s_date, s_time, e_date, e_time) = if tz_aware {
4217 (
4218 start.utc_datetime.date(),
4219 start.utc_datetime.time(),
4220 end.utc_datetime.date(),
4221 end.utc_datetime.time(),
4222 )
4223 } else {
4224 (
4225 start.local_date,
4226 start.local_time,
4227 end.local_date,
4228 end.local_time,
4229 )
4230 };
4231
4232 let months = calendar_months_between(&s_date, &e_date);
4233 let date_after_months = add_months_to_date(s_date, months);
4234 let start_dt = NaiveDateTime::new(date_after_months, s_time);
4235 let end_dt = NaiveDateTime::new(e_date, e_time);
4236 let remaining = end_dt.signed_duration_since(start_dt);
4237 let remaining_days = remaining.num_days();
4238 let remaining_nanos =
4239 remaining.num_nanoseconds().unwrap_or(0) - remaining_days * 86_400_000_000_000;
4240
4241 let dur = CypherDuration::new(months, remaining_days, remaining_nanos);
4242 return Ok(dur.to_temporal_value());
4243 }
4244
4245 let tz_aware = both_tz_aware(&start, &end);
4248 let start_time = if tz_aware {
4249 start.utc_datetime.time()
4250 } else {
4251 start.local_time
4252 };
4253 let end_time = if tz_aware {
4254 end.utc_datetime.time()
4255 } else {
4256 end.local_time
4257 };
4258
4259 let start_nanos = time_to_nanos(&start_time);
4260 let end_nanos = time_to_nanos(&end_time);
4261 let nanos_diff = end_nanos - start_nanos;
4262
4263 let dur = CypherDuration::new(0, 0, nanos_diff);
4264 Ok(dur.to_temporal_value())
4265}
4266
4267fn has_date_component(ttype: TemporalType) -> bool {
4269 matches!(
4270 ttype,
4271 TemporalType::Date | TemporalType::LocalDateTime | TemporalType::DateTime
4272 )
4273}
4274
4275fn has_time_component(ttype: TemporalType) -> bool {
4277 matches!(
4278 ttype,
4279 TemporalType::LocalTime
4280 | TemporalType::Time
4281 | TemporalType::LocalDateTime
4282 | TemporalType::DateTime
4283 )
4284}
4285
4286fn eval_duration_in_months(args: &[Value]) -> Result<Value> {
4287 if args.len() < 2 {
4288 return Err(anyhow!("duration.inMonths requires two temporal arguments"));
4289 }
4290 if args[0].is_null() || args[1].is_null() {
4291 return Ok(Value::Null);
4292 }
4293
4294 let start = parse_temporal_value_typed(&args[0])?;
4295 let end = parse_temporal_value_typed(&args[1])?;
4296
4297 if has_date_component(start.ttype) && has_date_component(end.ttype) {
4298 let tz_aware = both_tz_aware(&start, &end);
4300 let (s_date, s_time, e_date, e_time) = if tz_aware {
4301 (
4302 start.utc_datetime.date(),
4303 start.utc_datetime.time(),
4304 end.utc_datetime.date(),
4305 end.utc_datetime.time(),
4306 )
4307 } else {
4308 (
4309 start.local_date,
4310 start.local_time,
4311 end.local_date,
4312 end.local_time,
4313 )
4314 };
4315 let mut months = calendar_months_between(&s_date, &e_date);
4316 if s_date.day() == e_date.day() {
4321 if months > 0 && e_time < s_time {
4322 months -= 1;
4323 } else if months < 0 && e_time > s_time {
4324 months += 1;
4325 }
4326 }
4327 let dur = CypherDuration::new(months, 0, 0);
4328 Ok(dur.to_temporal_value())
4329 } else {
4330 Ok(Value::Temporal(TemporalValue::Duration {
4331 months: 0,
4332 days: 0,
4333 nanos: 0,
4334 }))
4335 }
4336}
4337
4338fn eval_duration_in_days(args: &[Value]) -> Result<Value> {
4339 if args.len() < 2 {
4340 return Err(anyhow!("duration.inDays requires two temporal arguments"));
4341 }
4342 if args[0].is_null() || args[1].is_null() {
4343 return Ok(Value::Null);
4344 }
4345
4346 let start = parse_temporal_value_typed(&args[0])?;
4347 let end = parse_temporal_value_typed(&args[1])?;
4348
4349 if has_date_component(start.ttype) && has_date_component(end.ttype) {
4350 let tz_aware = both_tz_aware(&start, &end);
4352 let (s_dt, e_dt) = if tz_aware {
4353 (start.utc_datetime, end.utc_datetime)
4354 } else {
4355 (
4356 NaiveDateTime::new(start.local_date, start.local_time),
4357 NaiveDateTime::new(end.local_date, end.local_time),
4358 )
4359 };
4360 let total_nanos = e_dt
4362 .signed_duration_since(s_dt)
4363 .num_nanoseconds()
4364 .ok_or_else(|| anyhow!("Duration overflow in inDays"))?;
4365 let days = total_nanos / 86_400_000_000_000;
4366 let dur = CypherDuration::new(0, days, 0);
4367 Ok(dur.to_temporal_value())
4368 } else {
4369 Ok(Value::Temporal(TemporalValue::Duration {
4370 months: 0,
4371 days: 0,
4372 nanos: 0,
4373 }))
4374 }
4375}
4376
4377fn normalize_local_to_utc(ndt: NaiveDateTime, tz: Tz) -> Result<NaiveDateTime> {
4383 use chrono::TimeZone;
4384 match tz.from_local_datetime(&ndt) {
4385 chrono::LocalResult::Single(dt) => Ok(dt.naive_utc()),
4386 chrono::LocalResult::Ambiguous(earliest, _) => Ok(earliest.naive_utc()),
4387 chrono::LocalResult::None => {
4388 let shifted = ndt + chrono::Duration::hours(1);
4390 match tz.from_local_datetime(&shifted) {
4391 chrono::LocalResult::Single(dt) => Ok(dt.naive_utc()),
4392 chrono::LocalResult::Ambiguous(earliest, _) => Ok(earliest.naive_utc()),
4393 _ => Err(anyhow!("Cannot resolve local time in timezone")),
4394 }
4395 }
4396 }
4397}
4398
4399fn eval_duration_in_seconds(args: &[Value]) -> Result<Value> {
4400 if args.len() < 2 {
4401 return Err(anyhow!(
4402 "duration.inSeconds requires two temporal arguments"
4403 ));
4404 }
4405 if args[0].is_null() || args[1].is_null() {
4406 return Ok(Value::Null);
4407 }
4408
4409 let start_res = parse_temporal_value_typed(&args[0]);
4410 let end_res = parse_temporal_value_typed(&args[1]);
4411 let (start, end) = match (start_res, end_res) {
4412 (Ok(start), Ok(end)) => (start, end),
4413 (start_res, end_res) => {
4414 if let Some(value) = try_eval_duration_in_seconds_extended(args)? {
4415 return Ok(value);
4416 }
4417 return Err(start_res
4418 .err()
4419 .or_else(|| end_res.err())
4420 .unwrap_or_else(|| anyhow!("duration.inSeconds requires two temporal arguments")));
4421 }
4422 };
4423
4424 let start_has_date = has_date_component(start.ttype);
4425 let end_has_date = has_date_component(end.ttype);
4426
4427 let shared_named_tz = start.named_tz.or(end.named_tz);
4431
4432 let have_tz = both_tz_aware(&start, &end);
4441
4442 let resolve =
4443 |pt: &ParsedTemporal, date_override: Option<NaiveDate>| -> Result<NaiveDateTime> {
4444 let local_date = date_override.unwrap_or(pt.local_date);
4445 let local_ndt = NaiveDateTime::new(local_date, pt.local_time);
4446
4447 if let Some(tz) = shared_named_tz {
4448 if pt.named_tz.is_some() && date_override.is_none() {
4450 Ok(pt.utc_datetime)
4452 } else {
4453 normalize_local_to_utc(local_ndt, tz)
4455 }
4456 } else if have_tz {
4457 if date_override.is_some() {
4459 let offset = pt.utc_offset_secs.unwrap_or(0);
4460 Ok(local_ndt - chrono::Duration::seconds(offset as i64))
4461 } else {
4462 Ok(pt.utc_datetime)
4463 }
4464 } else {
4465 Ok(local_ndt)
4467 }
4468 };
4469
4470 if !start_has_date || !end_has_date {
4472 if shared_named_tz.is_some() {
4473 let ref_date = if start_has_date {
4476 start.local_date
4477 } else if end_has_date {
4478 end.local_date
4479 } else {
4480 NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()
4481 };
4482 let s_dt = resolve(&start, Some(ref_date))?;
4483 let e_dt = resolve(&end, Some(ref_date))?;
4484 let total_nanos = e_dt
4485 .signed_duration_since(s_dt)
4486 .num_nanoseconds()
4487 .ok_or_else(|| anyhow!("Duration overflow in inSeconds"))?;
4488 let dur = CypherDuration::new(0, 0, total_nanos);
4489 return Ok(dur.to_temporal_value());
4490 }
4491
4492 let s_time = if have_tz {
4494 start.utc_datetime.time()
4495 } else {
4496 start.local_time
4497 };
4498 let e_time = if have_tz {
4499 end.utc_datetime.time()
4500 } else {
4501 end.local_time
4502 };
4503 let s_nanos = time_to_nanos(&s_time);
4504 let e_nanos = time_to_nanos(&e_time);
4505 let dur = CypherDuration::new(0, 0, e_nanos - s_nanos);
4506 return Ok(dur.to_temporal_value());
4507 }
4508
4509 let s_dt = resolve(&start, None)?;
4511 let e_dt = resolve(&end, None)?;
4512 let total_nanos = e_dt
4513 .signed_duration_since(s_dt)
4514 .num_nanoseconds()
4515 .ok_or_else(|| anyhow!("Duration overflow in inSeconds"))?;
4516
4517 let dur = CypherDuration::new(0, 0, total_nanos);
4518 Ok(dur.to_temporal_value())
4519}
4520
4521struct ParsedTemporal {
4523 local_date: NaiveDate,
4525 local_time: NaiveTime,
4527 utc_datetime: NaiveDateTime,
4529 ttype: TemporalType,
4531 utc_offset_secs: Option<i32>,
4533 named_tz: Option<Tz>,
4535}
4536
4537fn both_tz_aware(a: &ParsedTemporal, b: &ParsedTemporal) -> bool {
4539 a.utc_offset_secs.is_some() && b.utc_offset_secs.is_some()
4540}
4541
4542fn parse_temporal_value_typed(val: &Value) -> Result<ParsedTemporal> {
4544 let midnight =
4545 NaiveTime::from_hms_opt(0, 0, 0).ok_or_else(|| anyhow!("Failed to create midnight"))?;
4546 let epoch_date = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
4547
4548 match val {
4549 Value::String(s) => {
4550 let ttype = classify_temporal(s)
4551 .ok_or_else(|| anyhow!("Cannot classify temporal value: {}", s))?;
4552
4553 match ttype {
4554 TemporalType::DateTime => {
4555 let (date, time, tz_info) = parse_datetime_with_tz(s)?;
4556 let local_ndt = NaiveDateTime::new(date, time);
4557 let iana_tz = tz_info.as_ref().and_then(|info| match info {
4558 TimezoneInfo::Named(tz) => Some(*tz),
4559 _ => None,
4560 });
4561 let offset_secs = if let Some(ref info) = tz_info {
4562 info.offset_for_local(&local_ndt)?.local_minus_utc()
4563 } else {
4564 0
4565 };
4566 let utc_ndt = local_ndt - chrono::Duration::seconds(offset_secs as i64);
4567 Ok(ParsedTemporal {
4568 local_date: date,
4569 local_time: time,
4570 utc_datetime: utc_ndt,
4571 ttype,
4572 utc_offset_secs: Some(offset_secs),
4573
4574 named_tz: iana_tz,
4575 })
4576 }
4577 TemporalType::LocalDateTime => {
4578 let ndt = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")
4579 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f"))
4580 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M"))
4581 .map_err(|_| anyhow!("Cannot parse localdatetime: {}", s))?;
4582 Ok(ParsedTemporal {
4583 local_date: ndt.date(),
4584 local_time: ndt.time(),
4585 utc_datetime: ndt,
4586 ttype,
4587 utc_offset_secs: None,
4588
4589 named_tz: None,
4590 })
4591 }
4592 TemporalType::Date => {
4593 let d = NaiveDate::parse_from_str(s, "%Y-%m-%d")
4594 .map_err(|_| anyhow!("Cannot parse date: {}", s))?;
4595 let ndt = NaiveDateTime::new(d, midnight);
4596 Ok(ParsedTemporal {
4597 local_date: d,
4598 local_time: midnight,
4599 utc_datetime: ndt,
4600 ttype,
4601 utc_offset_secs: None,
4602
4603 named_tz: None,
4604 })
4605 }
4606 TemporalType::Time => {
4607 let (_, time, tz_info) = parse_datetime_with_tz(s)?;
4608 let offset_secs = if let Some(ref info) = tz_info {
4609 let dummy_ndt = NaiveDateTime::new(epoch_date, time);
4610 info.offset_for_local(&dummy_ndt)?.local_minus_utc()
4611 } else {
4612 0
4613 };
4614 let local_ndt = NaiveDateTime::new(epoch_date, time);
4615 let utc_ndt = local_ndt - chrono::Duration::seconds(offset_secs as i64);
4616 Ok(ParsedTemporal {
4617 local_date: epoch_date,
4618 local_time: time,
4619 utc_datetime: utc_ndt,
4620 ttype,
4621 utc_offset_secs: Some(offset_secs),
4622
4623 named_tz: None,
4624 })
4625 }
4626 TemporalType::LocalTime => {
4627 let time = parse_time_string(s)?;
4628 let ndt = NaiveDateTime::new(epoch_date, time);
4629 Ok(ParsedTemporal {
4630 local_date: epoch_date,
4631 local_time: time,
4632 utc_datetime: ndt,
4633 ttype,
4634 utc_offset_secs: None,
4635
4636 named_tz: None,
4637 })
4638 }
4639 TemporalType::Duration => Err(anyhow!("Cannot use duration as temporal argument")),
4640 }
4641 }
4642 Value::Temporal(tv) => {
4643 let ttype = tv.temporal_type();
4644 match tv {
4645 TemporalValue::Date { days_since_epoch } => {
4646 let d = epoch_date + chrono::Duration::days(*days_since_epoch as i64);
4647 let ndt = NaiveDateTime::new(d, midnight);
4648 Ok(ParsedTemporal {
4649 local_date: d,
4650 local_time: midnight,
4651 utc_datetime: ndt,
4652 ttype,
4653 utc_offset_secs: None,
4654 named_tz: None,
4655 })
4656 }
4657 TemporalValue::LocalTime {
4658 nanos_since_midnight,
4659 } => {
4660 let time = nanos_to_time(*nanos_since_midnight);
4661 let ndt = NaiveDateTime::new(epoch_date, time);
4662 Ok(ParsedTemporal {
4663 local_date: epoch_date,
4664 local_time: time,
4665 utc_datetime: ndt,
4666 ttype,
4667 utc_offset_secs: None,
4668 named_tz: None,
4669 })
4670 }
4671 TemporalValue::Time {
4672 nanos_since_midnight,
4673 offset_seconds,
4674 } => {
4675 let time = nanos_to_time(*nanos_since_midnight);
4676 let local_ndt = NaiveDateTime::new(epoch_date, time);
4677 let utc_ndt = local_ndt - chrono::Duration::seconds(*offset_seconds as i64);
4678 Ok(ParsedTemporal {
4679 local_date: epoch_date,
4680 local_time: time,
4681 utc_datetime: utc_ndt,
4682 ttype,
4683 utc_offset_secs: Some(*offset_seconds),
4684 named_tz: None,
4685 })
4686 }
4687 TemporalValue::LocalDateTime { nanos_since_epoch } => {
4688 let ndt =
4689 chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch).naive_utc();
4690 Ok(ParsedTemporal {
4691 local_date: ndt.date(),
4692 local_time: ndt.time(),
4693 utc_datetime: ndt,
4694 ttype,
4695 utc_offset_secs: None,
4696 named_tz: None,
4697 })
4698 }
4699 TemporalValue::DateTime {
4700 nanos_since_epoch,
4701 offset_seconds,
4702 timezone_name,
4703 } => {
4704 let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
4706 let local_ndt = chrono::DateTime::from_timestamp_nanos(local_nanos).naive_utc();
4707 let utc_ndt =
4708 chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch).naive_utc();
4709 let iana_tz = timezone_name
4710 .as_deref()
4711 .and_then(|name| name.parse::<chrono_tz::Tz>().ok());
4712 Ok(ParsedTemporal {
4713 local_date: local_ndt.date(),
4714 local_time: local_ndt.time(),
4715 utc_datetime: utc_ndt,
4716 ttype,
4717 utc_offset_secs: Some(*offset_seconds),
4718 named_tz: iana_tz,
4719 })
4720 }
4721 TemporalValue::Duration { .. } => {
4722 Err(anyhow!("Cannot use duration as temporal argument"))
4723 }
4724 }
4725 }
4726 _ => Err(anyhow!("Expected temporal value, got: {:?}", val)),
4727 }
4728}
4729
4730#[cfg(test)]
4735mod tests {
4736 use super::*;
4737
4738 fn map_val(pairs: Vec<(&str, Value)>) -> Value {
4740 Value::Map(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
4741 }
4742
4743 #[test]
4744 fn test_parse_datetime_utc_accepts_bracketed_timezone_suffix() {
4745 let dt = parse_datetime_utc("2020-01-01T00:00Z[UTC]").unwrap();
4746 assert_eq!(dt.to_rfc3339(), "2020-01-01T00:00:00+00:00");
4747
4748 let dt = parse_datetime_utc("2020-01-01T01:00:00+01:00[Europe/Paris]").unwrap();
4749 assert_eq!(dt.to_rfc3339(), "2020-01-01T00:00:00+00:00");
4750 }
4751
4752 #[test]
4753 fn test_date_from_map_calendar() {
4754 let result = eval_date(&[map_val(vec![
4755 ("year", Value::Int(1984)),
4756 ("month", Value::Int(10)),
4757 ("day", Value::Int(11)),
4758 ])])
4759 .unwrap();
4760 assert_eq!(result.to_string(), "1984-10-11");
4761 }
4762
4763 #[test]
4764 fn test_date_from_map_defaults() {
4765 let result = eval_date(&[map_val(vec![("year", Value::Int(1984))])]).unwrap();
4766 assert_eq!(result.to_string(), "1984-01-01");
4767 }
4768
4769 #[test]
4770 fn test_date_from_week() {
4771 let result = eval_date(&[map_val(vec![
4773 ("year", Value::Int(1984)),
4774 ("week", Value::Int(10)),
4775 ("dayOfWeek", Value::Int(3)),
4776 ])])
4777 .unwrap();
4778 assert!(result.to_string().starts_with("1984-03"));
4779 }
4780
4781 #[test]
4782 fn test_date_from_ordinal() {
4783 let result = eval_date(&[map_val(vec![
4785 ("year", Value::Int(1984)),
4786 ("ordinalDay", Value::Int(202)),
4787 ])])
4788 .unwrap();
4789 assert_eq!(result.to_string(), "1984-07-20");
4790 }
4791
4792 #[test]
4793 fn test_date_from_quarter() {
4794 let result = eval_date(&[map_val(vec![
4796 ("year", Value::Int(1984)),
4797 ("quarter", Value::Int(3)),
4798 ("dayOfQuarter", Value::Int(45)),
4799 ])])
4800 .unwrap();
4801 assert_eq!(result.to_string(), "1984-08-14");
4802 }
4803
4804 #[test]
4805 fn test_time_from_map() {
4806 let result = eval_time(&[map_val(vec![
4807 ("hour", Value::Int(12)),
4808 ("minute", Value::Int(31)),
4809 ("second", Value::Int(14)),
4810 ])])
4811 .unwrap();
4812 assert_eq!(result.to_string(), "12:31:14Z");
4813 }
4814
4815 #[test]
4816 fn test_time_from_map_with_nanos() {
4817 let result = eval_time(&[map_val(vec![
4818 ("hour", Value::Int(12)),
4819 ("minute", Value::Int(31)),
4820 ("second", Value::Int(14)),
4821 ("millisecond", Value::Int(645)),
4822 ("microsecond", Value::Int(876)),
4823 ("nanosecond", Value::Int(123)),
4824 ])])
4825 .unwrap();
4826 assert!(result.to_string().starts_with("12:31:14.645876"));
4828 }
4829
4830 #[test]
4831 fn test_datetime_from_map() {
4832 let result = eval_datetime(&[map_val(vec![
4833 ("year", Value::Int(1984)),
4834 ("month", Value::Int(10)),
4835 ("day", Value::Int(11)),
4836 ("hour", Value::Int(12)),
4837 ])])
4838 .unwrap();
4839 assert!(result.to_string().contains("1984-10-11T12:00"));
4840 }
4841
4842 #[test]
4843 fn test_localdatetime_from_week() {
4844 let result = eval_localdatetime(&[map_val(vec![
4846 ("year", Value::Int(1816)),
4847 ("week", Value::Int(1)),
4848 ])])
4849 .unwrap();
4850 assert_eq!(result.to_string(), "1816-01-01T00:00");
4851
4852 let result = eval_localdatetime(&[map_val(vec![
4854 ("year", Value::Int(1816)),
4855 ("week", Value::Int(52)),
4856 ])])
4857 .unwrap();
4858 assert_eq!(result.to_string(), "1816-12-23T00:00");
4859
4860 let result = eval_localdatetime(&[map_val(vec![
4862 ("year", Value::Int(1817)),
4863 ("week", Value::Int(1)),
4864 ])])
4865 .unwrap();
4866 assert_eq!(result.to_string(), "1816-12-30T00:00");
4867 }
4868
4869 #[test]
4870 fn test_duration_from_map_extended() {
4871 let result = eval_duration(&[map_val(vec![
4872 ("years", Value::Int(1)),
4873 ("months", Value::Int(2)),
4874 ("days", Value::Int(3)),
4875 ])])
4876 .unwrap();
4877 let dur_str = result.to_string();
4879 assert!(dur_str.starts_with('P'));
4880 assert!(dur_str.contains('Y')); assert!(dur_str.contains('D')); }
4883
4884 #[test]
4885 fn test_datetime_fromepoch() {
4886 let result = eval_datetime_fromepoch(&[Value::Int(0)]).unwrap();
4887 assert_eq!(result.to_string(), "1970-01-01T00:00Z");
4888 }
4889
4890 #[test]
4891 fn test_datetime_fromepochmillis() {
4892 let result = eval_datetime_fromepochmillis(&[Value::Int(0)]).unwrap();
4893 assert_eq!(result.to_string(), "1970-01-01T00:00Z");
4894 }
4895
4896 #[test]
4897 fn test_truncate_date_year() {
4898 let result = eval_truncate(
4899 "date",
4900 &[
4901 Value::String("year".to_string()),
4902 Value::String("1984-10-11".to_string()),
4903 ],
4904 )
4905 .unwrap();
4906 assert_eq!(result.to_string(), "1984-01-01");
4907 }
4908
4909 #[test]
4910 fn test_truncate_date_month() {
4911 let result = eval_truncate(
4912 "date",
4913 &[
4914 Value::String("month".to_string()),
4915 Value::String("1984-10-11".to_string()),
4916 ],
4917 )
4918 .unwrap();
4919 assert_eq!(result.to_string(), "1984-10-01");
4920 }
4921
4922 #[test]
4923 fn test_truncate_datetime_hour() {
4924 let result = eval_truncate(
4925 "datetime",
4926 &[
4927 Value::String("hour".to_string()),
4928 Value::String("1984-10-11T12:31:14Z".to_string()),
4929 ],
4930 )
4931 .unwrap();
4932 assert!(result.to_string().contains("1984-10-11T12:00"));
4933 }
4934
4935 #[test]
4936 fn test_duration_between() {
4937 let result = eval_duration_between(&[
4938 Value::String("1984-10-11".to_string()),
4939 Value::String("1984-10-12".to_string()),
4940 ])
4941 .unwrap();
4942 assert_eq!(result.to_string(), "P1D");
4943 }
4944
4945 #[test]
4946 fn test_duration_in_days() {
4947 let result = eval_duration_in_days(&[
4948 Value::String("1984-10-11".to_string()),
4949 Value::String("1984-10-21".to_string()),
4950 ])
4951 .unwrap();
4952 assert_eq!(result.to_string(), "P10D");
4953 }
4954
4955 #[test]
4956 fn test_duration_in_months() {
4957 let result = eval_duration_in_months(&[
4958 Value::String("1984-10-11".to_string()),
4959 Value::String("1985-01-11".to_string()),
4960 ])
4961 .unwrap();
4962 assert_eq!(result.to_string(), "P3M");
4963 }
4964
4965 #[test]
4966 fn test_duration_in_seconds() {
4967 let result = eval_duration_in_seconds(&[
4968 Value::String("1984-10-11T12:00:00".to_string()),
4969 Value::String("1984-10-11T13:00:00".to_string()),
4970 ])
4971 .unwrap();
4972 assert_eq!(result.to_string(), "PT1H");
4973 }
4974
4975 #[test]
4976 fn test_classify_temporal() {
4977 assert_eq!(classify_temporal("1984-10-11"), Some(TemporalType::Date));
4978 assert_eq!(classify_temporal("12:31:14"), Some(TemporalType::LocalTime));
4979 assert_eq!(
4980 classify_temporal("12:31:14+01:00"),
4981 Some(TemporalType::Time)
4982 );
4983 assert_eq!(
4984 classify_temporal("1984-10-11T12:31:14"),
4985 Some(TemporalType::LocalDateTime)
4986 );
4987 assert_eq!(
4988 classify_temporal("1984-10-11T12:31:14Z"),
4989 Some(TemporalType::DateTime)
4990 );
4991 assert_eq!(
4992 classify_temporal("1984-10-11T12:31:14+01:00"),
4993 Some(TemporalType::DateTime)
4994 );
4995 assert_eq!(classify_temporal("P1Y2M3D"), Some(TemporalType::Duration));
4996 }
4997
4998 #[test]
4999 fn test_add_months_to_date_clamping() {
5000 let date = NaiveDate::from_ymd_opt(2023, 1, 31).unwrap();
5002 let result = add_months_to_date(date, 1);
5003 assert_eq!(result, NaiveDate::from_ymd_opt(2023, 2, 28).unwrap());
5004
5005 let date = NaiveDate::from_ymd_opt(2024, 1, 31).unwrap();
5007 let result = add_months_to_date(date, 1);
5008 assert_eq!(result, NaiveDate::from_ymd_opt(2024, 2, 29).unwrap());
5009 }
5010
5011 #[test]
5012 fn test_cypher_duration_multiply() {
5013 let dur = CypherDuration::new(1, 1, 0);
5014 let result = dur.multiply(2.0);
5015 assert_eq!(result.months, 2);
5016 assert_eq!(result.days, 2);
5017 }
5018
5019 #[test]
5020 fn test_fractional_cascading_in_map() {
5021 let result = eval_duration(&[map_val(vec![
5024 ("months", Value::Float(5.5)),
5025 ("days", Value::Int(0)),
5026 ])])
5027 .unwrap();
5028 let s = result.to_string();
5029 assert_eq!(s, "P5M15DT5H14M33S");
5030 }
5031
5032 #[test]
5033 fn test_fractional_cascading_full() {
5034 let result = eval_duration(&[map_val(vec![
5035 ("years", Value::Float(12.5)),
5036 ("months", Value::Float(5.5)),
5037 ("days", Value::Float(14.5)),
5038 ("hours", Value::Float(16.5)),
5039 ("minutes", Value::Float(12.5)),
5040 ("seconds", Value::Float(70.5)),
5041 ("nanoseconds", Value::Int(3)),
5042 ])])
5043 .unwrap();
5044 let s = result.to_string();
5045 let dur = parse_duration_to_cypher(&s).unwrap();
5047 assert_eq!(dur.months, 155);
5048 assert_eq!(dur.days, 29);
5049 }
5050
5051 #[test]
5052 fn test_parse_iso8601_duration_with_weeks() {
5053 let micros = parse_duration_to_micros("P1W").unwrap();
5054 assert_eq!(micros, 7 * MICROS_PER_DAY);
5055 }
5056
5057 #[test]
5058 fn test_parse_iso8601_duration_complex() {
5059 let micros = parse_duration_to_micros("P1DT2H30M").unwrap();
5060 let expected = MICROS_PER_DAY + 2 * MICROS_PER_HOUR + 30 * MICROS_PER_MINUTE;
5061 assert_eq!(micros, expected);
5062 }
5063}