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 "BTIC" => eval_btic(args),
384
385 _ => Err(anyhow!("Unknown datetime function: {}", name)),
386 }
387}
388
389fn eval_btic(args: &[Value]) -> Result<Value> {
398 if args.is_empty() {
399 return Err(anyhow!("btic() requires exactly 1 argument"));
400 }
401 if args.len() > 1 {
402 return Err(anyhow!("btic() accepts 1 argument, got {}", args.len()));
403 }
404 match &args[0] {
405 Value::Null => Ok(Value::Null),
406 Value::String(s) => {
407 let btic = uni_btic::parse::parse_btic_literal(s).map_err(|e| {
408 anyhow!(
409 "TypeError: InvalidArgumentValue - btic() failed to parse '{}': {}",
410 s,
411 e
412 )
413 })?;
414 Ok(Value::Temporal(uni_common::TemporalValue::Btic {
415 lo: btic.lo(),
416 hi: btic.hi(),
417 meta: btic.meta(),
418 }))
419 }
420 other => Err(anyhow!("btic() argument must be a string, got: {}", other)),
421 }
422}
423
424pub fn is_datetime_value(val: &Value) -> bool {
426 match val {
427 Value::Temporal(TemporalValue::DateTime { .. }) => true,
428 Value::String(s) => parse_datetime_utc(s).is_ok(),
429 _ => false,
430 }
431}
432
433pub fn is_date_value(val: &Value) -> bool {
435 match val {
436 Value::Temporal(TemporalValue::Date { .. }) => true,
437 Value::String(s) => NaiveDate::parse_from_str(s, "%Y-%m-%d").is_ok(),
438 _ => false,
439 }
440}
441
442pub fn is_duration_value(val: &Value) -> bool {
448 match val {
449 Value::Temporal(TemporalValue::Duration { .. }) => true,
450 Value::String(s) => is_duration_string(s),
451 _ => false,
452 }
453}
454
455pub fn is_duration_or_micros(val: &Value) -> bool {
461 is_duration_value(val) || matches!(val, Value::Int(_))
462}
463
464pub fn duration_to_micros(val: &Value) -> Result<i64> {
466 match val {
467 Value::String(s) => {
468 let duration = parse_duration_to_cypher(s)?;
469 Ok(duration.to_micros())
470 }
471 Value::Int(i) => Ok(*i),
472 _ => Err(anyhow!("Expected duration value")),
473 }
474}
475
476pub fn add_duration_to_datetime(dt_str: &str, micros: i64) -> Result<String> {
478 let dt = parse_datetime_utc(dt_str)?;
479 let result = dt + Duration::microseconds(micros);
480 Ok(result.to_rfc3339())
481}
482
483pub fn add_duration_to_date(date_str: &str, micros: i64) -> Result<String> {
485 let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
486 let dt = date
487 .and_hms_opt(0, 0, 0)
488 .ok_or_else(|| anyhow!("Invalid date"))?;
489 let result = dt + Duration::microseconds(micros);
490 Ok(result.format("%Y-%m-%d").to_string())
491}
492
493pub fn datetime_difference(dt1_str: &str, dt2_str: &str) -> Result<i64> {
495 let dt1 = parse_datetime_utc(dt1_str)?;
496 let dt2 = parse_datetime_utc(dt2_str)?;
497 dt1.signed_duration_since(dt2)
498 .num_microseconds()
499 .ok_or_else(|| anyhow!("Duration overflow"))
500}
501
502pub fn parse_duration_to_micros(s: &str) -> Result<i64> {
506 let s = s.trim();
507
508 if s.starts_with(['P', 'p']) {
510 return parse_iso8601_duration(s);
511 }
512
513 parse_simple_duration(s)
515}
516
517pub fn parse_duration_to_cypher(s: &str) -> Result<CypherDuration> {
519 let s = s.trim();
520
521 if s.starts_with(['P', 'p']) {
523 return parse_iso8601_duration_cypher(s);
524 }
525
526 let micros = parse_simple_duration(s)?;
528 Ok(CypherDuration::from_micros(micros))
529}
530
531fn parse_datetime_style_duration(s: &str) -> Result<CypherDuration> {
535 let body = &s[1..]; let (date_part, time_part) = if let Some(t_pos) = body.find('T') {
539 (&body[..t_pos], Some(&body[t_pos + 1..]))
540 } else {
541 (body, None)
542 };
543
544 let date_parts: Vec<&str> = date_part.split('-').collect();
546 if date_parts.len() != 3 {
547 return Err(anyhow!(
548 "Invalid date-time style duration date: {}",
549 date_part
550 ));
551 }
552 let years: i64 = date_parts[0]
553 .parse()
554 .map_err(|_| anyhow!("Invalid years"))?;
555 let month_val: i64 = date_parts[1]
556 .parse()
557 .map_err(|_| anyhow!("Invalid months"))?;
558 let day_val: i64 = date_parts[2].parse().map_err(|_| anyhow!("Invalid days"))?;
559
560 let months = years * 12 + month_val;
561 let days = day_val;
562
563 let nanos = if let Some(tp) = time_part {
565 let time_parts: Vec<&str> = tp.split(':').collect();
566 if time_parts.len() != 3 {
567 return Err(anyhow!("Invalid date-time style duration time: {}", tp));
568 }
569 let hours: f64 = time_parts[0]
570 .parse()
571 .map_err(|_| anyhow!("Invalid hours"))?;
572 let minutes: f64 = time_parts[1]
573 .parse()
574 .map_err(|_| anyhow!("Invalid minutes"))?;
575 let seconds: f64 = time_parts[2]
576 .parse()
577 .map_err(|_| anyhow!("Invalid seconds"))?;
578 (hours * 3600.0 * NANOS_PER_SECOND as f64
579 + minutes * 60.0 * NANOS_PER_SECOND as f64
580 + seconds * NANOS_PER_SECOND as f64) as i64
581 } else {
582 0
583 };
584
585 Ok(CypherDuration::new(months, days, nanos))
586}
587
588fn parse_iso8601_duration_cypher(s: &str) -> Result<CypherDuration> {
590 if s.len() >= 11
593 && s.as_bytes().get(5) == Some(&b'-')
594 && s.as_bytes().get(1).is_some_and(|b| b.is_ascii_digit())
595 {
596 return parse_datetime_style_duration(s);
597 }
598
599 let s = &s[1..]; let mut months: i64 = 0;
601 let mut days: i64 = 0;
602 let mut nanos: i64 = 0;
603 let mut in_time_part = false;
604 let mut num_buf = String::new();
605
606 for c in s.chars() {
607 if c == 'T' || c == 't' {
608 in_time_part = true;
609 continue;
610 }
611
612 if c.is_ascii_digit() || c == '.' || c == '-' {
613 num_buf.push(c);
614 } else {
615 if num_buf.is_empty() {
616 continue;
617 }
618 let num: f64 = num_buf
619 .parse()
620 .map_err(|_| anyhow!("Invalid duration number"))?;
621 num_buf.clear();
622
623 match c {
624 'Y' | 'y' => {
625 let whole = num.trunc() as i64;
627 let frac = num.fract();
628 months += whole * 12;
629 if frac != 0.0 {
630 let frac_months = frac * 12.0;
632 let whole_frac_months = frac_months.trunc() as i64;
633 let frac_frac_months = frac_months.fract();
634 months += whole_frac_months;
635 let frac_secs = frac_frac_months * 2_629_746.0;
637 let extra_days = (frac_secs / SECONDS_PER_DAY as f64).trunc() as i64;
638 let remaining_secs =
639 frac_secs - (extra_days as f64 * SECONDS_PER_DAY as f64);
640 days += extra_days;
641 nanos += (remaining_secs * NANOS_PER_SECOND as f64) as i64;
642 }
643 }
644 'M' if !in_time_part => {
645 let whole = num.trunc() as i64;
647 let frac = num.fract();
648 months += whole;
649 if frac != 0.0 {
650 let frac_secs = frac * 2_629_746.0;
651 let extra_days = (frac_secs / SECONDS_PER_DAY as f64).trunc() as i64;
652 let remaining_secs =
653 frac_secs - (extra_days as f64 * SECONDS_PER_DAY as f64);
654 days += extra_days;
655 nanos += (remaining_secs * NANOS_PER_SECOND as f64) as i64;
656 }
657 }
658 'W' | 'w' => {
659 let total_days_f = num * 7.0;
661 let whole = total_days_f.trunc() as i64;
662 let frac = total_days_f.fract();
663 days += whole;
664 nanos += (frac * NANOS_PER_DAY as f64) as i64;
665 }
666 'D' | 'd' => {
667 let whole = num.trunc() as i64;
669 let frac = num.fract();
670 days += whole;
671 nanos += (frac * NANOS_PER_DAY as f64) as i64;
672 }
673 'H' | 'h' => nanos += (num * 3600.0 * NANOS_PER_SECOND as f64) as i64,
674 'M' | 'm' if in_time_part => nanos += (num * 60.0 * NANOS_PER_SECOND as f64) as i64,
675 'S' | 's' => nanos += (num * NANOS_PER_SECOND as f64) as i64,
676 _ => return Err(anyhow!("Invalid ISO 8601 duration designator: {}", c)),
677 }
678 }
679 }
680
681 Ok(CypherDuration::new(months, days, nanos))
682}
683
684enum Component {
689 Year,
690 Month,
691 Day,
692 Hour,
693 Minute,
694 Second,
695}
696
697fn eval_extract(args: &[Value], component: Component) -> Result<Value> {
698 if args.len() != 1 {
699 return Err(anyhow!("Extract function requires 1 argument"));
700 }
701 match &args[0] {
702 Value::Temporal(tv) => {
703 let result = match component {
704 Component::Year => tv.year(),
705 Component::Month => tv.month(),
706 Component::Day => tv.day(),
707 Component::Hour => tv.hour(),
708 Component::Minute => tv.minute(),
709 Component::Second => tv.second(),
710 };
711 match result {
712 Some(v) => Ok(Value::Int(v)),
713 None => Err(anyhow!("Temporal value does not have requested component")),
714 }
715 }
716 Value::String(s) => {
717 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
719 return Ok(Value::Int(extract_component(&dt, &component) as i64));
720 }
721 if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
722 return Ok(Value::Int(extract_component(&dt, &component) as i64));
723 }
724
725 match component {
726 Component::Year | Component::Month | Component::Day => {
727 if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
728 return Ok(Value::Int(match component {
729 Component::Year => d.year() as i64,
730 Component::Month => d.month() as i64,
731 Component::Day => d.day() as i64,
732 _ => unreachable!(),
733 }));
734 }
735 }
736 Component::Hour | Component::Minute | Component::Second => {
737 if let Ok(t) = NaiveTime::parse_from_str(s, "%H:%M:%S") {
738 return Ok(Value::Int(match component {
739 Component::Hour => t.hour() as i64,
740 Component::Minute => t.minute() as i64,
741 Component::Second => t.second() as i64,
742 _ => unreachable!(),
743 }));
744 }
745 }
746 }
747
748 Err(anyhow!("Could not parse date/time string for extraction"))
749 }
750 Value::Null => Ok(Value::Null),
751 _ => Err(anyhow!(
752 "Extract function expects a temporal or string argument"
753 )),
754 }
755}
756
757fn extract_component<T: Datelike + Timelike>(dt: &T, component: &Component) -> i32 {
758 match component {
759 Component::Year => dt.year(),
760 Component::Month => dt.month() as i32,
761 Component::Day => dt.day() as i32,
762 Component::Hour => dt.hour() as i32,
763 Component::Minute => dt.minute() as i32,
764 Component::Second => dt.second() as i32,
765 }
766}
767
768pub fn eval_temporal_accessor(temporal_str: &str, component: &str) -> Result<Value> {
777 let component_lower = component.to_lowercase();
778 match component_lower.as_str() {
779 "year" => extract_year(temporal_str),
781 "month" => extract_month(temporal_str),
782 "day" => extract_day(temporal_str),
783 "hour" => extract_hour(temporal_str),
784 "minute" => extract_minute(temporal_str),
785 "second" => extract_second(temporal_str),
786
787 "quarter" => extract_quarter(temporal_str),
789 "week" => extract_week(temporal_str),
790 "weekyear" => extract_week_year(temporal_str),
791 "ordinalday" => extract_ordinal_day(temporal_str),
792 "dayofweek" | "weekday" => extract_day_of_week(temporal_str),
793 "dayofquarter" => extract_day_of_quarter(temporal_str),
794
795 "millisecond" => extract_millisecond(temporal_str),
797 "microsecond" => extract_microsecond(temporal_str),
798 "nanosecond" => extract_nanosecond(temporal_str),
799
800 "timezone" => extract_timezone_name_from_str(temporal_str),
802 "offset" => extract_offset_string(temporal_str),
803 "offsetminutes" => extract_offset_minutes(temporal_str),
804 "offsetseconds" => extract_offset_seconds(temporal_str),
805
806 "epochseconds" => extract_epoch_seconds(temporal_str),
808 "epochmillis" => extract_epoch_millis(temporal_str),
809
810 _ => Err(anyhow!("Unknown temporal component: {}", component)),
811 }
812}
813
814pub fn eval_temporal_accessor_value(val: &Value, component: &str) -> Result<Value> {
819 match val {
820 Value::Null => Ok(Value::Null),
821 Value::Map(map) => Ok(map.get(component).cloned().unwrap_or(Value::Null)),
825 Value::Temporal(tv) => {
826 let comp_lower = component.to_lowercase();
829 match comp_lower.as_str() {
830 "timezone" => {
831 return match tv {
832 TemporalValue::DateTime {
833 timezone_name,
834 offset_seconds,
835 ..
836 } => Ok(match timezone_name {
837 Some(name) => Value::String(name.clone()),
838 None => Value::String(format_timezone_offset(*offset_seconds)),
839 }),
840 TemporalValue::Time { offset_seconds, .. } => {
841 Ok(Value::String(format_timezone_offset(*offset_seconds)))
842 }
843 _ => Ok(Value::Null),
844 };
845 }
846 "offset" => {
847 return match tv {
848 TemporalValue::DateTime { offset_seconds, .. }
849 | TemporalValue::Time { offset_seconds, .. } => {
850 Ok(Value::String(format_timezone_offset(*offset_seconds)))
851 }
852 _ => Ok(Value::Null),
853 };
854 }
855 "offsetminutes" => {
856 return match tv {
857 TemporalValue::DateTime { offset_seconds, .. }
858 | TemporalValue::Time { offset_seconds, .. } => {
859 Ok(Value::Int((*offset_seconds / 60) as i64))
860 }
861 _ => Ok(Value::Null),
862 };
863 }
864 "offsetseconds" => {
865 return match tv {
866 TemporalValue::DateTime { offset_seconds, .. }
867 | TemporalValue::Time { offset_seconds, .. } => {
868 Ok(Value::Int(*offset_seconds as i64))
869 }
870 _ => Ok(Value::Null),
871 };
872 }
873 "epochseconds" => {
874 return match tv {
875 TemporalValue::DateTime {
876 nanos_since_epoch, ..
877 } => Ok(Value::Int(nanos_since_epoch / 1_000_000_000)),
878 TemporalValue::LocalDateTime { nanos_since_epoch } => {
879 Ok(Value::Int(nanos_since_epoch / 1_000_000_000))
880 }
881 TemporalValue::Date { days_since_epoch } => {
882 Ok(Value::Int(*days_since_epoch as i64 * 86400))
883 }
884 _ => Ok(Value::Null),
885 };
886 }
887 "epochmillis" => {
888 return match tv {
889 TemporalValue::DateTime {
890 nanos_since_epoch, ..
891 } => Ok(Value::Int(nanos_since_epoch / 1_000_000)),
892 TemporalValue::LocalDateTime { nanos_since_epoch } => {
893 Ok(Value::Int(nanos_since_epoch / 1_000_000))
894 }
895 TemporalValue::Date { days_since_epoch } => {
896 Ok(Value::Int(*days_since_epoch as i64 * 86400 * 1000))
897 }
898 _ => Ok(Value::Null),
899 };
900 }
901 _ => {}
902 }
903 let temporal_str = tv.to_string();
905 eval_temporal_accessor(&temporal_str, component)
906 }
907 Value::String(s) => eval_temporal_accessor(s, component),
908 _ => Err(anyhow!(
909 "Cannot access temporal property '{}' on non-temporal value",
910 component
911 )),
912 }
913}
914
915pub fn is_temporal_accessor(property: &str) -> bool {
917 let property_lower = property.to_lowercase();
918 matches!(
919 property_lower.as_str(),
920 "year"
921 | "month"
922 | "day"
923 | "hour"
924 | "minute"
925 | "second"
926 | "quarter"
927 | "week"
928 | "weekyear"
929 | "ordinalday"
930 | "dayofweek"
931 | "weekday"
932 | "dayofquarter"
933 | "millisecond"
934 | "microsecond"
935 | "nanosecond"
936 | "timezone"
937 | "offset"
938 | "offsetminutes"
939 | "offsetseconds"
940 | "epochseconds"
941 | "epochmillis"
942 )
943}
944
945pub fn is_temporal_string(s: &str) -> bool {
947 let bytes = s.as_bytes();
948 if bytes.len() < 8 {
949 return false;
950 }
951
952 (bytes.len() >= 10 && bytes[4] == b'-' && bytes[7] == b'-')
954 || (bytes[2] == b':' && bytes[5] == b':')
956 || (bytes[0] == b'P' || bytes[0] == b'p')
958}
959
960pub fn is_duration_string(s: &str) -> bool {
962 s.starts_with(['P', 'p'])
963}
964
965fn extract_date_component(s: &str, f: impl FnOnce(NaiveDate) -> i64) -> Result<Value> {
968 let (date, _, _) = parse_datetime_with_tz(s)?;
969 Ok(Value::Int(f(date)))
970}
971
972fn extract_time_component(s: &str, f: impl FnOnce(NaiveTime) -> i64) -> Result<Value> {
973 let (_, time, _) = parse_datetime_with_tz(s)?;
974 Ok(Value::Int(f(time)))
975}
976
977fn extract_year(s: &str) -> Result<Value> {
978 extract_date_component(s, |d| d.year() as i64)
979}
980
981fn extract_month(s: &str) -> Result<Value> {
982 extract_date_component(s, |d| d.month() as i64)
983}
984
985fn extract_day(s: &str) -> Result<Value> {
986 extract_date_component(s, |d| d.day() as i64)
987}
988
989fn extract_hour(s: &str) -> Result<Value> {
990 extract_time_component(s, |t| t.hour() as i64)
991}
992
993fn extract_minute(s: &str) -> Result<Value> {
994 extract_time_component(s, |t| t.minute() as i64)
995}
996
997fn extract_second(s: &str) -> Result<Value> {
998 extract_time_component(s, |t| t.second() as i64)
999}
1000
1001fn extract_quarter(s: &str) -> Result<Value> {
1002 extract_date_component(s, |d| ((d.month() - 1) / 3 + 1) as i64)
1003}
1004
1005fn extract_week(s: &str) -> Result<Value> {
1006 extract_date_component(s, |d| d.iso_week().week() as i64)
1007}
1008
1009fn extract_week_year(s: &str) -> Result<Value> {
1010 extract_date_component(s, |d| d.iso_week().year() as i64)
1011}
1012
1013fn extract_ordinal_day(s: &str) -> Result<Value> {
1014 extract_date_component(s, |d| d.ordinal() as i64)
1015}
1016
1017fn extract_day_of_week(s: &str) -> Result<Value> {
1018 extract_date_component(s, |d| (d.weekday().num_days_from_monday() + 1) as i64)
1020}
1021
1022fn extract_day_of_quarter(s: &str) -> Result<Value> {
1023 let (date, _, _) = parse_datetime_with_tz(s)?;
1024 let quarter = (date.month() - 1) / 3;
1025 let first_month_of_quarter = quarter * 3 + 1;
1026 let quarter_start = NaiveDate::from_ymd_opt(date.year(), first_month_of_quarter, 1)
1027 .ok_or_else(|| {
1028 anyhow!(
1029 "Invalid quarter start for year={}, month={}",
1030 date.year(),
1031 first_month_of_quarter
1032 )
1033 })?;
1034 let day_of_quarter = (date - quarter_start).num_days() + 1;
1035 Ok(Value::Int(day_of_quarter))
1036}
1037
1038fn extract_millisecond(s: &str) -> Result<Value> {
1039 extract_time_component(s, |t| (t.nanosecond() / 1_000_000) as i64)
1040}
1041
1042fn extract_microsecond(s: &str) -> Result<Value> {
1043 extract_time_component(s, |t| (t.nanosecond() / 1_000) as i64)
1044}
1045
1046fn extract_nanosecond(s: &str) -> Result<Value> {
1047 extract_time_component(s, |t| t.nanosecond() as i64)
1048}
1049
1050fn extract_timezone_name_from_str(s: &str) -> Result<Value> {
1051 let (_, _, tz_info) = parse_datetime_with_tz(s)?;
1052 match tz_info {
1053 Some(TimezoneInfo::Named(tz)) => Ok(Value::String(tz.name().to_string())),
1054 Some(TimezoneInfo::FixedOffset(offset)) => {
1055 let secs = offset.local_minus_utc();
1057 Ok(Value::String(format_timezone_offset(secs)))
1058 }
1059 None => Ok(Value::Null),
1060 }
1061}
1062
1063fn extract_offset_string(s: &str) -> Result<Value> {
1064 let (date, time, tz_info) = parse_datetime_with_tz(s)?;
1065 match tz_info {
1066 Some(ref tz) => {
1067 let ndt = NaiveDateTime::new(date, time);
1068 let offset = tz.offset_for_local(&ndt)?;
1069 Ok(Value::String(format_timezone_offset(
1070 offset.local_minus_utc(),
1071 )))
1072 }
1073 None => Ok(Value::Null),
1074 }
1075}
1076
1077fn extract_offset_total_seconds(s: &str) -> Result<i32> {
1078 let (date, time, tz_info) = parse_datetime_with_tz(s)?;
1079 match tz_info {
1080 Some(ref tz) => {
1081 let ndt = NaiveDateTime::new(date, time);
1082 let offset = tz.offset_for_local(&ndt)?;
1083 Ok(offset.local_minus_utc())
1084 }
1085 None => Ok(0),
1086 }
1087}
1088
1089fn extract_offset_minutes(s: &str) -> Result<Value> {
1090 Ok(Value::Int((extract_offset_total_seconds(s)? / 60) as i64))
1091}
1092
1093fn extract_offset_seconds(s: &str) -> Result<Value> {
1094 Ok(Value::Int(extract_offset_total_seconds(s)? as i64))
1095}
1096
1097fn parse_as_utc(s: &str) -> Result<DateTime<Utc>> {
1098 let (date, time, tz_info) = parse_datetime_with_tz(s)?;
1099 let local_ndt = NaiveDateTime::new(date, time);
1100
1101 if let Some(tz) = tz_info {
1102 let offset = tz.offset_for_local(&local_ndt)?;
1103 let utc_ndt = local_ndt - Duration::seconds(offset.local_minus_utc() as i64);
1104 Ok(DateTime::<Utc>::from_naive_utc_and_offset(utc_ndt, Utc))
1105 } else {
1106 Ok(DateTime::<Utc>::from_naive_utc_and_offset(local_ndt, Utc))
1107 }
1108}
1109
1110fn extract_epoch_seconds(s: &str) -> Result<Value> {
1111 Ok(Value::Int(parse_as_utc(s)?.timestamp()))
1112}
1113
1114fn extract_epoch_millis(s: &str) -> Result<Value> {
1115 Ok(Value::Int(parse_as_utc(s)?.timestamp_millis()))
1116}
1117
1118pub fn eval_duration_accessor(duration_str: &str, component: &str) -> Result<Value> {
1127 let duration = parse_duration_to_cypher(duration_str)?;
1128 let component_lower = component.to_lowercase();
1129
1130 let total_months = duration.months;
1131 let total_nanos = duration.nanos;
1132 let total_secs = total_nanos.div_euclid(NANOS_PER_SECOND);
1133
1134 match component_lower.as_str() {
1135 "years" => Ok(Value::Int(total_months.div_euclid(12))),
1137 "quarters" => Ok(Value::Int(total_months.div_euclid(3))),
1138 "months" => Ok(Value::Int(total_months)),
1139 "weeks" => Ok(Value::Int(duration.days.div_euclid(7))),
1140 "days" => Ok(Value::Int(duration.days)),
1141 "hours" => Ok(Value::Int(total_secs.div_euclid(3600))),
1142 "minutes" => Ok(Value::Int(total_secs.div_euclid(60))),
1143 "seconds" => Ok(Value::Int(total_secs)),
1144 "milliseconds" => Ok(Value::Int(total_nanos.div_euclid(1_000_000))),
1145 "microseconds" => Ok(Value::Int(total_nanos.div_euclid(1_000))),
1146 "nanoseconds" => Ok(Value::Int(total_nanos)),
1147
1148 "quartersofyear" => Ok(Value::Int(total_months.rem_euclid(12) / 3)),
1150 "monthsofquarter" => Ok(Value::Int(total_months.rem_euclid(3))),
1151 "monthsofyear" => Ok(Value::Int(total_months.rem_euclid(12))),
1152 "daysofweek" => Ok(Value::Int(duration.days.rem_euclid(7))),
1153 "hoursofday" => Ok(Value::Int(total_secs.div_euclid(3600).rem_euclid(24))),
1154 "minutesofhour" => Ok(Value::Int(total_secs.div_euclid(60).rem_euclid(60))),
1155 "secondsofminute" => Ok(Value::Int(total_secs.rem_euclid(60))),
1156 "millisecondsofsecond" => Ok(Value::Int(
1157 total_nanos.div_euclid(1_000_000).rem_euclid(1000),
1158 )),
1159 "microsecondsofsecond" => Ok(Value::Int(
1160 total_nanos.div_euclid(1_000).rem_euclid(1_000_000),
1161 )),
1162 "nanosecondsofsecond" => Ok(Value::Int(total_nanos.rem_euclid(NANOS_PER_SECOND))),
1163
1164 _ => Err(anyhow!("Unknown duration component: {}", component)),
1165 }
1166}
1167
1168pub fn is_duration_accessor(property: &str) -> bool {
1170 let property_lower = property.to_lowercase();
1171 matches!(
1172 property_lower.as_str(),
1173 "years"
1174 | "quarters"
1175 | "months"
1176 | "weeks"
1177 | "days"
1178 | "hours"
1179 | "minutes"
1180 | "seconds"
1181 | "milliseconds"
1182 | "microseconds"
1183 | "nanoseconds"
1184 | "quartersofyear"
1185 | "monthsofquarter"
1186 | "monthsofyear"
1187 | "daysofweek"
1188 | "hoursofday"
1189 | "minutesofhour"
1190 | "secondsofminute"
1191 | "millisecondsofsecond"
1192 | "microsecondsofsecond"
1193 | "nanosecondsofsecond"
1194 )
1195}
1196
1197fn eval_date(args: &[Value]) -> Result<Value> {
1202 if args.is_empty() {
1203 let now = Utc::now().date_naive();
1205 return Ok(Value::Temporal(TemporalValue::Date {
1206 days_since_epoch: date_to_days_since_epoch(&now),
1207 }));
1208 }
1209
1210 match &args[0] {
1211 Value::String(s) => {
1212 match parse_date_string(s) {
1213 Ok(date) => Ok(Value::Temporal(TemporalValue::Date {
1214 days_since_epoch: date_to_days_since_epoch(&date),
1215 })),
1216 Err(e) => {
1217 if parse_extended_date_string(s).is_some() {
1218 Ok(Value::String(s.clone()))
1220 } else {
1221 Err(e)
1222 }
1223 }
1224 }
1225 }
1226 Value::Temporal(TemporalValue::Date { .. }) => Ok(args[0].clone()),
1227 Value::Temporal(tv) => {
1229 if let Some(date) = tv.to_date() {
1230 Ok(Value::Temporal(TemporalValue::Date {
1231 days_since_epoch: date_to_days_since_epoch(&date),
1232 }))
1233 } else {
1234 Err(anyhow!("date(): temporal value has no date component"))
1235 }
1236 }
1237 Value::Map(map) => eval_date_from_map(map),
1238 Value::Null => Ok(Value::Null),
1239 _ => Err(anyhow!("date() expects a string or map argument")),
1240 }
1241}
1242
1243fn date_to_days_since_epoch(date: &NaiveDate) -> i32 {
1245 let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
1246 (date.signed_duration_since(epoch)).num_days() as i32
1247}
1248
1249fn eval_date_from_map(map: &HashMap<String, Value>) -> Result<Value> {
1250 if let Some(dt_val) = map.get("date") {
1252 return eval_date_from_projection(map, dt_val);
1253 }
1254
1255 let date = build_date_from_map(map)?;
1256 Ok(Value::Temporal(TemporalValue::Date {
1257 days_since_epoch: date_to_days_since_epoch(&date),
1258 }))
1259}
1260
1261fn eval_date_from_projection(map: &HashMap<String, Value>, source: &Value) -> Result<Value> {
1263 let source_date = temporal_or_string_to_date(source)?;
1264 let date = build_date_from_projection(map, &source_date)?;
1265 Ok(Value::Temporal(TemporalValue::Date {
1266 days_since_epoch: date_to_days_since_epoch(&date),
1267 }))
1268}
1269
1270fn temporal_or_string_to_date(val: &Value) -> Result<NaiveDate> {
1272 match val {
1273 Value::Temporal(tv) => tv
1274 .to_date()
1275 .ok_or_else(|| anyhow!("Temporal value has no date component")),
1276 Value::String(s) => parse_datetime_with_tz(s).map(|(date, _, _)| date),
1277 _ => Err(anyhow!(
1278 "Expected temporal or string value for date extraction"
1279 )),
1280 }
1281}
1282
1283fn build_date_from_projection(
1291 map: &HashMap<String, Value>,
1292 source_date: &NaiveDate,
1293) -> Result<NaiveDate> {
1294 if map.contains_key("week") {
1296 let week_year = map
1297 .get("weekYear")
1298 .and_then(|v| v.as_i64())
1299 .map(|v| v as i32)
1300 .unwrap_or_else(|| source_date.iso_week().year());
1301 let week = map.get("week").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
1302 let dow = map
1303 .get("dayOfWeek")
1304 .and_then(|v| v.as_i64())
1305 .unwrap_or_else(|| source_date.weekday().number_from_monday() as i64)
1306 as u32;
1307 return build_date_from_week(week_year, week, dow);
1308 }
1309
1310 if map.contains_key("ordinalDay") {
1312 let year = map
1313 .get("year")
1314 .and_then(|v| v.as_i64())
1315 .map(|v| v as i32)
1316 .unwrap_or(source_date.year());
1317 let ordinal = map
1318 .get("ordinalDay")
1319 .and_then(|v| v.as_i64())
1320 .unwrap_or(source_date.ordinal() as i64) as u32;
1321 return NaiveDate::from_yo_opt(year, ordinal)
1322 .ok_or_else(|| anyhow!("Invalid ordinal day: {} for year {}", ordinal, year));
1323 }
1324
1325 if map.contains_key("quarter") {
1327 let year = map
1328 .get("year")
1329 .and_then(|v| v.as_i64())
1330 .map(|v| v as i32)
1331 .unwrap_or(source_date.year());
1332 let quarter = map.get("quarter").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
1333 let doq = map
1334 .get("dayOfQuarter")
1335 .and_then(|v| v.as_i64())
1336 .unwrap_or_else(|| day_of_quarter(source_date) as i64) as u32;
1337 return build_date_from_quarter(year, quarter, doq);
1338 }
1339
1340 let year = map
1342 .get("year")
1343 .and_then(|v| v.as_i64())
1344 .map(|v| v as i32)
1345 .unwrap_or(source_date.year());
1346 let month = map
1347 .get("month")
1348 .and_then(|v| v.as_i64())
1349 .map(|v| v as u32)
1350 .unwrap_or(source_date.month());
1351 let day = map
1352 .get("day")
1353 .and_then(|v| v.as_i64())
1354 .map(|v| v as u32)
1355 .unwrap_or(source_date.day());
1356
1357 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow!("Invalid date in projection"))
1358}
1359
1360fn build_date_from_map(map: &HashMap<String, Value>) -> Result<NaiveDate> {
1368 let year = map
1370 .get("year")
1371 .and_then(|v| v.as_i64())
1372 .ok_or_else(|| anyhow!("date/datetime map requires 'year' field"))? as i32;
1373
1374 if let Some(week) = map.get("week").and_then(|v| v.as_i64()) {
1376 let dow = map.get("dayOfWeek").and_then(|v| v.as_i64()).unwrap_or(1);
1377 return build_date_from_week(year, week as u32, dow as u32);
1378 }
1379
1380 if let Some(ordinal) = map.get("ordinalDay").and_then(|v| v.as_i64()) {
1382 return NaiveDate::from_yo_opt(year, ordinal as u32)
1383 .ok_or_else(|| anyhow!("Invalid ordinal day: {} for year {}", ordinal, year));
1384 }
1385
1386 if let Some(quarter) = map.get("quarter").and_then(|v| v.as_i64()) {
1388 let doq = map
1389 .get("dayOfQuarter")
1390 .and_then(|v| v.as_i64())
1391 .unwrap_or(1);
1392 return build_date_from_quarter(year, quarter as u32, doq as u32);
1393 }
1394
1395 let month = map.get("month").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
1397 let day = map.get("day").and_then(|v| v.as_i64()).unwrap_or(1) as u32;
1398
1399 NaiveDate::from_ymd_opt(year, month, day)
1400 .ok_or_else(|| anyhow!("Invalid date: year={}, month={}, day={}", year, month, day))
1401}
1402
1403fn build_date_from_week(year: i32, week: u32, day_of_week: u32) -> Result<NaiveDate> {
1405 if !(1..=53).contains(&week) {
1406 return Err(anyhow!("Week must be between 1 and 53"));
1407 }
1408 if !(1..=7).contains(&day_of_week) {
1409 return Err(anyhow!("Day of week must be between 1 and 7"));
1410 }
1411
1412 let jan4 =
1414 NaiveDate::from_ymd_opt(year, 1, 4).ok_or_else(|| anyhow!("Invalid year: {}", year))?;
1415
1416 let iso_week_day = jan4.weekday().num_days_from_monday();
1418 let week1_monday = jan4 - Duration::days(iso_week_day as i64);
1419
1420 let days_offset = ((week - 1) * 7 + (day_of_week - 1)) as i64;
1422 Ok(week1_monday + Duration::days(days_offset))
1423}
1424
1425fn day_of_quarter(date: &NaiveDate) -> u32 {
1427 let quarter_start_month = ((date.month() - 1) / 3) * 3 + 1;
1428 let quarter_start = NaiveDate::from_ymd_opt(date.year(), quarter_start_month, 1).unwrap();
1429 (date.signed_duration_since(quarter_start).num_days() + 1) as u32
1430}
1431
1432fn build_date_from_quarter(year: i32, quarter: u32, day_of_quarter: u32) -> Result<NaiveDate> {
1434 if !(1..=4).contains(&quarter) {
1435 return Err(anyhow!("Quarter must be between 1 and 4"));
1436 }
1437
1438 let first_month = (quarter - 1) * 3 + 1;
1440 let quarter_start = NaiveDate::from_ymd_opt(year, first_month, 1)
1441 .ok_or_else(|| anyhow!("Invalid quarter start"))?;
1442
1443 let result = quarter_start + Duration::days((day_of_quarter - 1) as i64);
1445
1446 let result_quarter = (result.month() - 1) / 3 + 1;
1448 if result_quarter != quarter || result.year() != year {
1449 return Err(anyhow!(
1450 "Day {} is out of range for quarter {}",
1451 day_of_quarter,
1452 quarter
1453 ));
1454 }
1455
1456 Ok(result)
1457}
1458
1459fn parse_date_string(s: &str) -> Result<NaiveDate> {
1460 NaiveDate::parse_from_str(s, "%Y-%m-%d")
1461 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S").map(|dt| dt.date()))
1462 .or_else(|_| {
1463 DateTime::parse_from_rfc3339(s).map(|dt| dt.date_naive())
1465 })
1466 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f").map(|dt| dt.date()))
1468 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S").map(|dt| dt.date()))
1469 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M").map(|dt| dt.date()))
1470 .or_else(|e| try_parse_compact_date(s).ok_or(e))
1472 .or_else(|_| {
1473 parse_datetime_with_tz(s).map(|(date, _, _)| date)
1475 })
1476 .map_err(|e| anyhow!("Invalid date format: {}", e))
1477}
1478
1479fn eval_time(args: &[Value]) -> Result<Value> {
1484 if args.is_empty() {
1485 let now = Utc::now();
1486 let time = now.time();
1487 return Ok(Value::Temporal(TemporalValue::Time {
1488 nanos_since_midnight: time_to_nanos(&time),
1489 offset_seconds: 0,
1490 }));
1491 }
1492
1493 match &args[0] {
1494 Value::String(s) => {
1495 let (time, tz_info) = parse_time_string_with_tz(s)?;
1496 let offset = match tz_info {
1497 Some(ref info) => info
1498 .offset_for_local(&NaiveDateTime::new(Utc::now().date_naive(), time))?
1499 .local_minus_utc(),
1500 None => 0,
1501 };
1502 Ok(Value::Temporal(TemporalValue::Time {
1503 nanos_since_midnight: time_to_nanos(&time),
1504 offset_seconds: offset,
1505 }))
1506 }
1507 Value::Temporal(TemporalValue::Time { .. }) => Ok(args[0].clone()),
1508 Value::Temporal(tv) => {
1510 let time = tv
1511 .to_time()
1512 .ok_or_else(|| anyhow!("time(): temporal value has no time component"))?;
1513 let offset = match tv {
1514 TemporalValue::DateTime { offset_seconds, .. } => *offset_seconds,
1515 TemporalValue::Time { offset_seconds, .. } => *offset_seconds,
1516 _ => 0, };
1518 Ok(Value::Temporal(TemporalValue::Time {
1519 nanos_since_midnight: time_to_nanos(&time),
1520 offset_seconds: offset,
1521 }))
1522 }
1523 Value::Map(map) => eval_time_from_map(map, true),
1524 Value::Null => Ok(Value::Null),
1525 _ => Err(anyhow!("time() expects a string or map argument")),
1526 }
1527}
1528
1529fn eval_localtime(args: &[Value]) -> Result<Value> {
1530 if args.is_empty() {
1531 let now = chrono::Local::now().time();
1532 return Ok(Value::Temporal(TemporalValue::LocalTime {
1533 nanos_since_midnight: time_to_nanos(&now),
1534 }));
1535 }
1536
1537 match &args[0] {
1538 Value::String(s) => {
1539 let time = parse_time_string(s)?;
1540 Ok(Value::Temporal(TemporalValue::LocalTime {
1541 nanos_since_midnight: time_to_nanos(&time),
1542 }))
1543 }
1544 Value::Temporal(TemporalValue::LocalTime { .. }) => Ok(args[0].clone()),
1545 Value::Temporal(tv) => {
1547 let time = tv
1548 .to_time()
1549 .ok_or_else(|| anyhow!("localtime(): temporal value has no time component"))?;
1550 Ok(Value::Temporal(TemporalValue::LocalTime {
1551 nanos_since_midnight: time_to_nanos(&time),
1552 }))
1553 }
1554 Value::Map(map) => eval_time_from_map(map, false),
1555 Value::Null => Ok(Value::Null),
1556 _ => Err(anyhow!("localtime() expects a string or map argument")),
1557 }
1558}
1559
1560fn eval_time_from_map(map: &HashMap<String, Value>, with_timezone: bool) -> Result<Value> {
1561 if let Some(time_val) = map.get("time") {
1563 return eval_time_from_projection(map, time_val, with_timezone);
1564 }
1565
1566 let hour = map.get("hour").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1567 let minute = map.get("minute").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1568 let second = map.get("second").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1569 let nanos = build_nanoseconds(map);
1570
1571 let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos).ok_or_else(|| {
1572 anyhow!(
1573 "Invalid time: hour={}, minute={}, second={}",
1574 hour,
1575 minute,
1576 second
1577 )
1578 })?;
1579
1580 let nanos = time_to_nanos(&time);
1581
1582 if with_timezone {
1583 let offset = if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
1585 parse_timezone_offset(tz_str)?
1586 } else {
1587 0
1588 };
1589 Ok(Value::Temporal(TemporalValue::Time {
1590 nanos_since_midnight: nanos,
1591 offset_seconds: offset,
1592 }))
1593 } else {
1594 Ok(Value::Temporal(TemporalValue::LocalTime {
1595 nanos_since_midnight: nanos,
1596 }))
1597 }
1598}
1599
1600fn eval_time_from_projection(
1602 map: &HashMap<String, Value>,
1603 source: &Value,
1604 with_timezone: bool,
1605) -> Result<Value> {
1606 let (source_time, source_offset) = match source {
1608 Value::Temporal(TemporalValue::Time {
1609 nanos_since_midnight,
1610 offset_seconds,
1611 }) => (nanos_to_time(*nanos_since_midnight), Some(*offset_seconds)),
1612 Value::Temporal(TemporalValue::LocalTime {
1613 nanos_since_midnight,
1614 }) => (nanos_to_time(*nanos_since_midnight), None),
1615 Value::Temporal(TemporalValue::DateTime {
1616 nanos_since_epoch,
1617 offset_seconds,
1618 ..
1619 }) => {
1620 let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
1622 let dt = chrono::DateTime::from_timestamp_nanos(local_nanos);
1623 (dt.naive_utc().time(), Some(*offset_seconds))
1624 }
1625 Value::Temporal(TemporalValue::LocalDateTime { nanos_since_epoch }) => {
1626 let dt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch);
1627 (dt.naive_utc().time(), None)
1628 }
1629 Value::Temporal(TemporalValue::Date { .. }) => {
1630 (NaiveTime::from_hms_opt(0, 0, 0).unwrap(), None)
1632 }
1633 Value::String(s) => {
1634 let (_, time, tz_info) = parse_datetime_with_tz(s)?;
1635 let offset = tz_info.as_ref().map(|tz| {
1636 let today = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
1637 let ndt = NaiveDateTime::new(today, time);
1638 tz.offset_for_local(&ndt)
1639 .map(|o| o.local_minus_utc())
1640 .unwrap_or(0)
1641 });
1642 (time, offset)
1643 }
1644 _ => return Err(anyhow!("time field must be a string or temporal")),
1645 };
1646
1647 let hour = map
1649 .get("hour")
1650 .and_then(|v| v.as_i64())
1651 .map(|v| v as u32)
1652 .unwrap_or(source_time.hour());
1653 let minute = map
1654 .get("minute")
1655 .and_then(|v| v.as_i64())
1656 .map(|v| v as u32)
1657 .unwrap_or(source_time.minute());
1658 let second = map
1659 .get("second")
1660 .and_then(|v| v.as_i64())
1661 .map(|v| v as u32)
1662 .unwrap_or(source_time.second());
1663
1664 let nanos = if map.contains_key("millisecond")
1665 || map.contains_key("microsecond")
1666 || map.contains_key("nanosecond")
1667 {
1668 build_nanoseconds(map)
1669 } else {
1670 source_time.nanosecond()
1671 };
1672
1673 let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
1674 .ok_or_else(|| anyhow!("Invalid time in projection"))?;
1675 let nanos = time_to_nanos(&time);
1676
1677 if with_timezone {
1678 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
1679 let new_offset = parse_timezone_offset(tz_str)?;
1680 let converted_nanos = if let Some(src_offset) = source_offset {
1683 let utc_nanos = nanos - (src_offset as i64) * 1_000_000_000;
1684 let target_nanos = utc_nanos + (new_offset as i64) * 1_000_000_000;
1685 target_nanos.rem_euclid(NANOS_PER_DAY)
1687 } else {
1688 nanos
1690 };
1691 Ok(Value::Temporal(TemporalValue::Time {
1692 nanos_since_midnight: converted_nanos,
1693 offset_seconds: new_offset,
1694 }))
1695 } else {
1696 let offset = source_offset.unwrap_or(0);
1697 Ok(Value::Temporal(TemporalValue::Time {
1698 nanos_since_midnight: nanos,
1699 offset_seconds: offset,
1700 }))
1701 }
1702 } else {
1703 Ok(Value::Temporal(TemporalValue::LocalTime {
1704 nanos_since_midnight: nanos,
1705 }))
1706 }
1707}
1708
1709fn parse_time_string(s: &str) -> Result<NaiveTime> {
1710 NaiveTime::parse_from_str(s, "%H:%M:%S")
1712 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S%.f"))
1713 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S%.9f"))
1714 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M"))
1715 .or_else(|e| try_parse_compact_time(s).ok_or(e))
1718 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S").map(|dt| dt.time()))
1719 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f").map(|dt| dt.time()))
1720 .or_else(|_| DateTime::parse_from_rfc3339(s).map(|dt| dt.time()))
1721 .or_else(|_| {
1722 parse_datetime_with_tz(s).map(|(_, time, _)| time)
1724 })
1725 .map_err(|_| anyhow!("Invalid time format"))
1726}
1727
1728fn parse_time_string_with_tz(s: &str) -> Result<(NaiveTime, Option<TimezoneInfo>)> {
1734 let (datetime_part, tz_name) = if let Some(bracket_pos) = s.find('[') {
1736 let tz_name = s[bracket_pos + 1..s.len() - 1].to_string();
1737 (&s[..bracket_pos], Some(tz_name))
1738 } else {
1739 (s, None)
1740 };
1741
1742 if let Ok(time) = try_parse_naive_time(datetime_part) {
1744 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?;
1745 return Ok((time, tz_info));
1746 }
1747
1748 if let Some(base) = datetime_part
1750 .strip_suffix('Z')
1751 .or_else(|| datetime_part.strip_suffix('z'))
1752 && let Ok(time) = try_parse_naive_time(base)
1753 {
1754 let utc_tz = TimezoneInfo::FixedOffset(FixedOffset::east_opt(0).unwrap());
1755 let tz_info = tz_name
1756 .map(|n| parse_timezone(&n))
1757 .transpose()?
1758 .or(Some(utc_tz));
1759 return Ok((time, tz_info));
1760 }
1761
1762 if let Some(tz_pos) = datetime_part.rfind('+').or_else(|| {
1764 datetime_part.rfind('-').filter(|&pos| pos >= 2)
1766 }) {
1767 let left_part = &datetime_part[..tz_pos];
1768 let tz_part = &datetime_part[tz_pos..];
1769
1770 if let Ok(time) = try_parse_naive_time(left_part) {
1771 let tz_info = if let Some(name) = tz_name {
1772 Some(parse_timezone(&name)?)
1773 } else {
1774 let offset = parse_timezone_offset(tz_part)?;
1775 let fo = FixedOffset::east_opt(offset)
1776 .ok_or_else(|| anyhow!("Invalid timezone offset"))?;
1777 Some(TimezoneInfo::FixedOffset(fo))
1778 };
1779 return Ok((time, tz_info));
1780 }
1781 }
1782
1783 let (_, time, tz_info) = parse_datetime_with_tz(s)?;
1785 Ok((time, tz_info))
1786}
1787
1788fn build_nanoseconds(map: &HashMap<String, Value>) -> u32 {
1789 let millis = map.get("millisecond").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1790 let micros = map.get("microsecond").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1791 let nanos = map.get("nanosecond").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
1792
1793 millis * 1_000_000 + micros * 1_000 + nanos
1794}
1795
1796fn build_nanoseconds_with_base(map: &HashMap<String, Value>, base_nanos: u32) -> u32 {
1801 let base_millis = base_nanos / 1_000_000;
1802 let base_micros = (base_nanos % 1_000_000) / 1_000;
1803 let base_nano_part = base_nanos % 1_000;
1804
1805 let millis = map
1806 .get("millisecond")
1807 .and_then(|v| v.as_i64())
1808 .unwrap_or(base_millis as i64) as u32;
1809 let micros = map
1810 .get("microsecond")
1811 .and_then(|v| v.as_i64())
1812 .unwrap_or(base_micros as i64) as u32;
1813 let nanos = map
1814 .get("nanosecond")
1815 .and_then(|v| v.as_i64())
1816 .unwrap_or(base_nano_part as i64) as u32;
1817
1818 millis * 1_000_000 + micros * 1_000 + nanos
1819}
1820
1821fn format_timezone_offset(offset_secs: i32) -> String {
1823 if offset_secs == 0 {
1824 "Z".to_string()
1825 } else {
1826 let hours = offset_secs / 3600;
1827 let remaining = offset_secs.abs() % 3600;
1828 let mins = remaining / 60;
1829 let secs = remaining % 60;
1830 if secs != 0 {
1831 format!("{:+03}:{:02}:{:02}", hours, mins, secs)
1832 } else {
1833 format!("{:+03}:{:02}", hours, mins)
1834 }
1835 }
1836}
1837
1838fn format_time_with_nanos(time: &NaiveTime) -> String {
1839 let nanos = time.nanosecond();
1840 let secs = time.second();
1841
1842 if nanos == 0 && secs == 0 {
1843 time.format("%H:%M").to_string()
1845 } else if nanos == 0 {
1846 time.format("%H:%M:%S").to_string()
1847 } else if nanos.is_multiple_of(1_000_000) {
1848 time.format("%H:%M:%S%.3f").to_string()
1850 } else if nanos.is_multiple_of(1_000) {
1851 time.format("%H:%M:%S%.6f").to_string()
1853 } else {
1854 time.format("%H:%M:%S%.9f").to_string()
1856 }
1857}
1858
1859fn parse_timezone_offset(tz: &str) -> Result<i32> {
1860 let tz = tz.trim();
1861 if tz == "Z" || tz == "z" {
1862 return Ok(0);
1863 }
1864
1865 if tz.len() >= 3 && (tz.starts_with('+') || tz.starts_with('-')) {
1867 let sign = if tz.starts_with('-') { -1 } else { 1 };
1868 let hours: i32 = tz[1..3]
1869 .parse()
1870 .map_err(|_| anyhow!("Invalid timezone hours"))?;
1871
1872 let rest = &tz[3..];
1873 let (mins, secs) = if rest.is_empty() {
1874 (0, 0)
1876 } else if let Some(after_colon) = rest.strip_prefix(':') {
1877 let mins: i32 = if after_colon.len() >= 2 {
1879 after_colon[..2]
1880 .parse()
1881 .map_err(|_| anyhow!("Invalid timezone minutes"))?
1882 } else {
1883 0
1884 };
1885 let secs: i32 = if after_colon.len() >= 5 && after_colon.as_bytes()[2] == b':' {
1886 after_colon[3..5]
1888 .parse()
1889 .map_err(|_| anyhow!("Invalid timezone seconds"))?
1890 } else {
1891 0
1892 };
1893 (mins, secs)
1894 } else {
1895 let mins: i32 = if rest.len() >= 2 {
1897 rest[..2]
1898 .parse()
1899 .map_err(|_| anyhow!("Invalid timezone minutes"))?
1900 } else {
1901 0
1902 };
1903 let secs: i32 = if rest.len() >= 4 {
1904 rest[2..4]
1905 .parse()
1906 .map_err(|_| anyhow!("Invalid timezone seconds"))?
1907 } else {
1908 0
1909 };
1910 (mins, secs)
1911 };
1912
1913 return Ok(sign * (hours * 3600 + mins * 60 + secs));
1914 }
1915
1916 Err(anyhow!("Unsupported timezone format: {}", tz))
1917}
1918
1919fn eval_datetime(args: &[Value]) -> Result<Value> {
1924 if args.is_empty() {
1925 let now = Utc::now();
1926 return Ok(Value::Temporal(TemporalValue::DateTime {
1927 nanos_since_epoch: now.timestamp_nanos_opt().unwrap_or(0),
1928 offset_seconds: 0,
1929 timezone_name: None,
1930 }));
1931 }
1932
1933 match &args[0] {
1934 Value::String(s) => {
1935 let (date, time, tz_info) = parse_datetime_with_tz(s)?;
1936 let ndt = NaiveDateTime::new(date, time);
1937 let (offset_secs, tz_name) = match tz_info {
1938 Some(ref info) => {
1939 let fo = info.offset_for_local(&ndt)?;
1940 (fo.local_minus_utc(), info.name().map(|s| s.to_string()))
1941 }
1942 None => (0, None),
1943 };
1944 Ok(datetime_value_from_local_and_offset(
1945 &ndt,
1946 offset_secs,
1947 tz_name,
1948 ))
1949 }
1950 Value::Temporal(TemporalValue::DateTime { .. }) => Ok(args[0].clone()),
1951 Value::Temporal(tv) => {
1953 let date = tv.to_date().unwrap_or_else(|| Utc::now().date_naive());
1954 let time = tv
1955 .to_time()
1956 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
1957 let ndt = NaiveDateTime::new(date, time);
1958 let offset = match tv {
1959 TemporalValue::Time { offset_seconds, .. } => *offset_seconds,
1960 _ => 0,
1961 };
1962 Ok(datetime_value_from_local_and_offset(&ndt, offset, None))
1963 }
1964 Value::Map(map) => eval_datetime_from_map(map, true),
1965 Value::Null => Ok(Value::Null),
1966 _ => Err(anyhow!("datetime() expects a string or map argument")),
1967 }
1968}
1969
1970fn eval_localdatetime(args: &[Value]) -> Result<Value> {
1971 if args.is_empty() {
1972 let now = chrono::Local::now().naive_local();
1973 let epoch = NaiveDateTime::new(
1974 NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(),
1975 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
1976 );
1977 let nanos = now
1978 .signed_duration_since(epoch)
1979 .num_nanoseconds()
1980 .unwrap_or(0);
1981 return Ok(Value::Temporal(TemporalValue::LocalDateTime {
1982 nanos_since_epoch: nanos,
1983 }));
1984 }
1985
1986 match &args[0] {
1987 Value::String(s) => {
1988 match parse_datetime_with_tz(s) {
1989 Ok((date, time, _)) => {
1990 let ndt = NaiveDateTime::new(date, time);
1991 Ok(localdatetime_value_from_naive(&ndt))
1992 }
1993 Err(e) => {
1994 if parse_extended_localdatetime_string(s).is_some() {
1995 Ok(Value::String(s.clone()))
1997 } else {
1998 Err(e)
1999 }
2000 }
2001 }
2002 }
2003 Value::Temporal(TemporalValue::LocalDateTime { .. }) => Ok(args[0].clone()),
2004 Value::Temporal(tv) => {
2006 let date = tv.to_date().unwrap_or_else(|| Utc::now().date_naive());
2007 let time = tv
2008 .to_time()
2009 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
2010 let ndt = NaiveDateTime::new(date, time);
2011 Ok(localdatetime_value_from_naive(&ndt))
2012 }
2013 Value::Map(map) => eval_datetime_from_map(map, false),
2014 Value::Null => Ok(Value::Null),
2015 _ => Err(anyhow!("localdatetime() expects a string or map argument")),
2016 }
2017}
2018
2019fn extract_time_and_tz_from_value(val: &Value) -> Result<(NaiveTime, Option<TimezoneInfo>)> {
2021 match val {
2022 Value::Temporal(tv) => {
2023 let time = tv
2024 .to_time()
2025 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
2026 let tz = match tv {
2027 TemporalValue::DateTime {
2028 offset_seconds,
2029 timezone_name,
2030 ..
2031 } => {
2032 if let Some(name) = timezone_name {
2033 Some(parse_timezone(name)?)
2034 } else {
2035 let fo = FixedOffset::east_opt(*offset_seconds)
2036 .ok_or_else(|| anyhow!("Invalid offset"))?;
2037 Some(TimezoneInfo::FixedOffset(fo))
2038 }
2039 }
2040 TemporalValue::Time { offset_seconds, .. } => {
2041 let fo = FixedOffset::east_opt(*offset_seconds)
2042 .ok_or_else(|| anyhow!("Invalid offset"))?;
2043 Some(TimezoneInfo::FixedOffset(fo))
2044 }
2045 _ => None,
2046 };
2047 Ok((time, tz))
2048 }
2049 Value::String(s) => {
2050 let (_, time, tz_info) = parse_datetime_with_tz(s)?;
2051 Ok((time, tz_info))
2052 }
2053 _ => Err(anyhow!("time must be a string or temporal")),
2054 }
2055}
2056
2057fn naive_datetime_to_nanos(ndt: &NaiveDateTime) -> Option<i64> {
2060 let epoch = NaiveDateTime::new(
2061 NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(),
2062 NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
2063 );
2064 ndt.signed_duration_since(epoch).num_nanoseconds()
2065}
2066
2067fn localdatetime_value_from_naive(ndt: &NaiveDateTime) -> Value {
2068 if let Some(nanos) = naive_datetime_to_nanos(ndt) {
2069 Value::Temporal(TemporalValue::LocalDateTime {
2070 nanos_since_epoch: nanos,
2071 })
2072 } else {
2073 Value::String(format_naive_datetime(ndt))
2074 }
2075}
2076
2077fn datetime_value_from_local_and_offset(
2078 local_ndt: &NaiveDateTime,
2079 offset_seconds: i32,
2080 timezone_name: Option<String>,
2081) -> Value {
2082 let utc_ndt = *local_ndt - Duration::seconds(offset_seconds as i64);
2083 let utc_dt = DateTime::<Utc>::from_naive_utc_and_offset(utc_ndt, Utc);
2084
2085 if let Some(nanos) = utc_dt.timestamp_nanos_opt() {
2086 Value::Temporal(TemporalValue::DateTime {
2087 nanos_since_epoch: nanos,
2088 offset_seconds,
2089 timezone_name,
2090 })
2091 } else {
2092 let rendered = if let Some(offset) = FixedOffset::east_opt(offset_seconds) {
2093 if let Some(dt) = offset.from_local_datetime(local_ndt).single() {
2094 format_datetime_with_offset_and_tz(&dt, timezone_name.as_deref())
2095 } else {
2096 let base = format!(
2097 "{}{}",
2098 format_naive_datetime(local_ndt),
2099 format_timezone_offset(offset_seconds)
2100 );
2101 if let Some(name) = timezone_name.as_deref() {
2102 format!("{base}[{name}]")
2103 } else {
2104 base
2105 }
2106 }
2107 } else {
2108 let base = format!(
2109 "{}{}",
2110 format_naive_datetime(local_ndt),
2111 format_timezone_offset(offset_seconds)
2112 );
2113 if let Some(name) = timezone_name.as_deref() {
2114 format!("{base}[{name}]")
2115 } else {
2116 base
2117 }
2118 };
2119 Value::String(rendered)
2120 }
2121}
2122
2123fn eval_datetime_from_map(map: &HashMap<String, Value>, with_timezone: bool) -> Result<Value> {
2124 if let Some(dt_val) = map.get("datetime") {
2126 return eval_datetime_from_projection(map, dt_val, with_timezone);
2127 }
2128
2129 if let (Some(date_val), Some(time_val)) = (map.get("date"), map.get("time")) {
2131 return eval_datetime_from_date_and_time(map, date_val, time_val, with_timezone);
2132 }
2133
2134 if let Some(date_val) = map.get("date") {
2138 let source_date = temporal_or_string_to_date(date_val)?;
2139 let date = build_date_from_projection(map, &source_date)?;
2140 let hour = map.get("hour").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2141 let minute = map.get("minute").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2142 let second = map.get("second").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2143 let nanos = build_nanoseconds(map);
2144 let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
2145 .ok_or_else(|| anyhow!("Invalid time in datetime map"))?;
2146 let ndt = NaiveDateTime::new(date, time);
2147
2148 if with_timezone {
2149 let (offset_secs, tz_name) =
2150 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
2151 let tz_info = parse_timezone(tz_str)?;
2152 let offset = tz_info.offset_for_local(&ndt)?;
2153 (
2154 offset.local_minus_utc(),
2155 tz_info.name().map(|s| s.to_string()),
2156 )
2157 } else {
2158 (0, None) };
2160
2161 return Ok(datetime_value_from_local_and_offset(
2162 &ndt,
2163 offset_secs,
2164 tz_name,
2165 ));
2166 } else {
2167 return Ok(localdatetime_value_from_naive(&ndt));
2168 }
2169 }
2170
2171 let (time, source_tz) = if let Some(time_val) = map.get("time") {
2174 let (t, tz) = extract_time_and_tz_from_value(time_val)?;
2175 let hour = map
2177 .get("hour")
2178 .and_then(|v| v.as_i64())
2179 .map(|v| v as u32)
2180 .unwrap_or(t.hour());
2181 let minute = map
2182 .get("minute")
2183 .and_then(|v| v.as_i64())
2184 .map(|v| v as u32)
2185 .unwrap_or(t.minute());
2186 let second = map
2187 .get("second")
2188 .and_then(|v| v.as_i64())
2189 .map(|v| v as u32)
2190 .unwrap_or(t.second());
2191 let nanos = if map.contains_key("millisecond")
2192 || map.contains_key("microsecond")
2193 || map.contains_key("nanosecond")
2194 {
2195 build_nanoseconds(map)
2196 } else {
2197 t.nanosecond()
2198 };
2199 let resolved_time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
2200 .ok_or_else(|| anyhow!("Invalid time in datetime map"))?;
2201 (resolved_time, tz)
2202 } else {
2203 let hour = map.get("hour").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2204 let minute = map.get("minute").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2205 let second = map.get("second").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
2206 let nanos = build_nanoseconds(map);
2207 let t = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
2208 .ok_or_else(|| anyhow!("Invalid time in datetime map"))?;
2209 (t, None::<TimezoneInfo>)
2210 };
2211
2212 let date = build_date_from_map(map)?;
2214
2215 let ndt = NaiveDateTime::new(date, time);
2216
2217 if with_timezone {
2218 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
2222 let tz_info = parse_timezone(tz_str)?;
2223 if let Some(ref src_tz) = source_tz {
2224 let src_offset = src_tz.offset_for_local(&ndt)?;
2226 let utc_ndt = ndt - Duration::seconds(src_offset.local_minus_utc() as i64);
2227 let target_offset = tz_info.offset_for_utc(&utc_ndt);
2228 let offset_secs = target_offset.local_minus_utc();
2229 let tz_name = tz_info.name().map(|s| s.to_string());
2230 let target_local_ndt = utc_ndt + Duration::seconds(offset_secs as i64);
2231 Ok(datetime_value_from_local_and_offset(
2232 &target_local_ndt,
2233 offset_secs,
2234 tz_name,
2235 ))
2236 } else {
2237 let offset = tz_info.offset_for_local(&ndt)?;
2239 let offset_secs = offset.local_minus_utc();
2240 let tz_name = tz_info.name().map(|s| s.to_string());
2241 Ok(datetime_value_from_local_and_offset(
2242 &ndt,
2243 offset_secs,
2244 tz_name,
2245 ))
2246 }
2247 } else if let Some(ref tz) = source_tz {
2248 let offset = tz.offset_for_local(&ndt)?;
2249 let offset_secs = offset.local_minus_utc();
2250 let tz_name = tz.name().map(|s| s.to_string());
2251 Ok(datetime_value_from_local_and_offset(
2252 &ndt,
2253 offset_secs,
2254 tz_name,
2255 ))
2256 } else {
2257 Ok(datetime_value_from_local_and_offset(&ndt, 0, None))
2259 }
2260 } else {
2261 Ok(localdatetime_value_from_naive(&ndt))
2263 }
2264}
2265
2266fn eval_datetime_from_date_and_time(
2271 map: &HashMap<String, Value>,
2272 date_val: &Value,
2273 time_val: &Value,
2274 with_timezone: bool,
2275) -> Result<Value> {
2276 let source_date = temporal_or_string_to_date(date_val)?;
2277 let (source_time, source_tz) = match time_val {
2278 Value::Temporal(tv) => {
2279 let time = tv
2280 .to_time()
2281 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
2282 let tz = match tv {
2283 TemporalValue::DateTime {
2284 offset_seconds,
2285 timezone_name,
2286 ..
2287 } => {
2288 if let Some(name) = timezone_name {
2289 Some(parse_timezone(name)?)
2290 } else {
2291 let fo = FixedOffset::east_opt(*offset_seconds)
2292 .ok_or_else(|| anyhow!("Invalid offset"))?;
2293 Some(TimezoneInfo::FixedOffset(fo))
2294 }
2295 }
2296 TemporalValue::Time { offset_seconds, .. } => {
2297 let fo = FixedOffset::east_opt(*offset_seconds)
2298 .ok_or_else(|| anyhow!("Invalid offset"))?;
2299 Some(TimezoneInfo::FixedOffset(fo))
2300 }
2301 _ => None,
2302 };
2303 (time, tz)
2304 }
2305 Value::String(s) => {
2306 let (_, time, tz_info) = parse_datetime_with_tz(s)?;
2307 (time, tz_info)
2308 }
2309 _ => return Err(anyhow!("time field must be a string or temporal")),
2310 };
2311
2312 let date = build_date_from_projection(map, &source_date)?;
2314
2315 let hour = map
2317 .get("hour")
2318 .and_then(|v| v.as_i64())
2319 .map(|v| v as u32)
2320 .unwrap_or(source_time.hour());
2321 let minute = map
2322 .get("minute")
2323 .and_then(|v| v.as_i64())
2324 .map(|v| v as u32)
2325 .unwrap_or(source_time.minute());
2326 let second = map
2327 .get("second")
2328 .and_then(|v| v.as_i64())
2329 .map(|v| v as u32)
2330 .unwrap_or(source_time.second());
2331
2332 let nanos = if map.contains_key("millisecond")
2333 || map.contains_key("microsecond")
2334 || map.contains_key("nanosecond")
2335 {
2336 build_nanoseconds(map)
2337 } else {
2338 source_time.nanosecond()
2339 };
2340
2341 let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
2342 .ok_or_else(|| anyhow!("Invalid time in datetime(date+time) projection"))?;
2343
2344 let ndt = NaiveDateTime::new(date, time);
2345
2346 if with_timezone {
2347 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
2348 let tz_info = parse_timezone(tz_str)?;
2349 if let Some(ref src_tz) = source_tz {
2350 let src_offset = src_tz.offset_for_local(&ndt)?;
2352 let utc_ndt = ndt - Duration::seconds(src_offset.local_minus_utc() as i64);
2353 let target_offset = tz_info.offset_for_utc(&utc_ndt);
2354 let offset_secs = target_offset.local_minus_utc();
2355 let tz_name = tz_info.name().map(|s| s.to_string());
2356 let target_local_ndt = utc_ndt + Duration::seconds(offset_secs as i64);
2357 Ok(datetime_value_from_local_and_offset(
2358 &target_local_ndt,
2359 offset_secs,
2360 tz_name,
2361 ))
2362 } else {
2363 let offset = tz_info.offset_for_local(&ndt)?;
2365 let offset_secs = offset.local_minus_utc();
2366 let tz_name = tz_info.name().map(|s| s.to_string());
2367 Ok(datetime_value_from_local_and_offset(
2368 &ndt,
2369 offset_secs,
2370 tz_name,
2371 ))
2372 }
2373 } else if let Some(ref tz) = source_tz {
2374 let offset = tz.offset_for_local(&ndt)?;
2375 let offset_secs = offset.local_minus_utc();
2376 let tz_name = tz.name().map(|s| s.to_string());
2377 Ok(datetime_value_from_local_and_offset(
2378 &ndt,
2379 offset_secs,
2380 tz_name,
2381 ))
2382 } else {
2383 Ok(datetime_value_from_local_and_offset(&ndt, 0, None))
2385 }
2386 } else {
2387 Ok(localdatetime_value_from_naive(&ndt))
2388 }
2389}
2390
2391fn eval_datetime_from_projection(
2393 map: &HashMap<String, Value>,
2394 source: &Value,
2395 with_timezone: bool,
2396) -> Result<Value> {
2397 let (source_date, source_time, source_tz) = temporal_or_string_to_components(source)?;
2399
2400 let date = build_date_from_projection(map, &source_date)?;
2402
2403 let hour = map
2405 .get("hour")
2406 .and_then(|v| v.as_i64())
2407 .map(|v| v as u32)
2408 .unwrap_or(source_time.hour());
2409 let minute = map
2410 .get("minute")
2411 .and_then(|v| v.as_i64())
2412 .map(|v| v as u32)
2413 .unwrap_or(source_time.minute());
2414 let second = map
2415 .get("second")
2416 .and_then(|v| v.as_i64())
2417 .map(|v| v as u32)
2418 .unwrap_or(source_time.second());
2419
2420 let nanos = if map.contains_key("millisecond")
2424 || map.contains_key("microsecond")
2425 || map.contains_key("nanosecond")
2426 {
2427 build_nanoseconds(map)
2428 } else {
2429 source_time.nanosecond()
2430 };
2431
2432 let time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
2433 .ok_or_else(|| anyhow!("Invalid time in projection"))?;
2434
2435 let ndt = NaiveDateTime::new(date, time);
2436
2437 if with_timezone {
2438 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
2439 let tz_info = parse_timezone(tz_str)?;
2440 if let Some(ref src_tz) = source_tz {
2441 let src_offset = src_tz.offset_for_local(&ndt)?;
2443 let utc_ndt = ndt - Duration::seconds(src_offset.local_minus_utc() as i64);
2444 let target_offset = tz_info.offset_for_utc(&utc_ndt);
2445 let offset_secs = target_offset.local_minus_utc();
2446 let tz_name = tz_info.name().map(|s| s.to_string());
2447 let target_local_ndt = utc_ndt + Duration::seconds(offset_secs as i64);
2448 Ok(datetime_value_from_local_and_offset(
2449 &target_local_ndt,
2450 offset_secs,
2451 tz_name,
2452 ))
2453 } else {
2454 let offset = tz_info.offset_for_local(&ndt)?;
2456 let offset_secs = offset.local_minus_utc();
2457 let tz_name = tz_info.name().map(|s| s.to_string());
2458 Ok(datetime_value_from_local_and_offset(
2459 &ndt,
2460 offset_secs,
2461 tz_name,
2462 ))
2463 }
2464 } else if let Some(ref tz) = source_tz {
2465 let offset = tz.offset_for_local(&ndt)?;
2466 let offset_secs = offset.local_minus_utc();
2467 let tz_name = tz.name().map(|s| s.to_string());
2468 Ok(datetime_value_from_local_and_offset(
2469 &ndt,
2470 offset_secs,
2471 tz_name,
2472 ))
2473 } else {
2474 Ok(datetime_value_from_local_and_offset(&ndt, 0, None))
2476 }
2477 } else {
2478 Ok(localdatetime_value_from_naive(&ndt))
2479 }
2480}
2481
2482fn temporal_or_string_to_components(
2484 val: &Value,
2485) -> Result<(NaiveDate, NaiveTime, Option<TimezoneInfo>)> {
2486 match val {
2487 Value::Temporal(tv) => {
2488 let date = tv.to_date().unwrap_or_else(|| Utc::now().date_naive());
2489 let time = tv
2490 .to_time()
2491 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
2492 let tz_info = match tv {
2493 TemporalValue::DateTime {
2494 offset_seconds,
2495 timezone_name,
2496 ..
2497 } => {
2498 if let Some(name) = timezone_name {
2499 Some(parse_timezone(name)?)
2500 } else {
2501 let fo = FixedOffset::east_opt(*offset_seconds)
2502 .ok_or_else(|| anyhow!("Invalid offset"))?;
2503 Some(TimezoneInfo::FixedOffset(fo))
2504 }
2505 }
2506 TemporalValue::Time { offset_seconds, .. } => {
2507 let fo = FixedOffset::east_opt(*offset_seconds)
2508 .ok_or_else(|| anyhow!("Invalid offset"))?;
2509 Some(TimezoneInfo::FixedOffset(fo))
2510 }
2511 _ => None,
2512 };
2513 Ok((date, time, tz_info))
2514 }
2515 Value::String(s) => parse_datetime_with_tz(s),
2516 _ => Err(anyhow!("Expected temporal or string value")),
2517 }
2518}
2519
2520fn iso_weekday(d: u32) -> Option<Weekday> {
2522 match d {
2523 1 => Some(Weekday::Mon),
2524 2 => Some(Weekday::Tue),
2525 3 => Some(Weekday::Wed),
2526 4 => Some(Weekday::Thu),
2527 5 => Some(Weekday::Fri),
2528 6 => Some(Weekday::Sat),
2529 7 => Some(Weekday::Sun),
2530 _ => None,
2531 }
2532}
2533
2534fn try_parse_compact_date(s: &str) -> Option<NaiveDate> {
2547 if let Some(w_pos) = s.find("-W") {
2549 if w_pos == 4 {
2550 let year: i32 = s[..4].parse().ok()?;
2551 let after_w = &s[w_pos + 2..]; if after_w.len() == 4 && after_w.as_bytes()[2] == b'-' {
2554 let week: u32 = after_w[..2].parse().ok()?;
2555 let d: u32 = after_w[3..4].parse().ok()?;
2556 let weekday = iso_weekday(d)?;
2557 return NaiveDate::from_isoywd_opt(year, week, weekday);
2558 }
2559 if after_w.len() == 2 && after_w.chars().all(|c| c.is_ascii_digit()) {
2561 let week: u32 = after_w.parse().ok()?;
2562 return NaiveDate::from_isoywd_opt(year, week, Weekday::Mon);
2563 }
2564 }
2565 return None;
2566 }
2567
2568 if let Some(w_pos) = s.find('W') {
2570 if w_pos == 4 && s.len() >= 7 {
2571 let year: i32 = s[..4].parse().ok()?;
2572 let after_w = &s[w_pos + 1..];
2573 if after_w.len() == 2 || after_w.len() == 3 {
2574 let week: u32 = after_w[..2].parse().ok()?;
2575 let weekday = if after_w.len() == 3 {
2576 let d: u32 = after_w[2..3].parse().ok()?;
2577 iso_weekday(d)?
2578 } else {
2579 Weekday::Mon
2580 };
2581 return NaiveDate::from_isoywd_opt(year, week, weekday);
2582 }
2583 }
2584 return None;
2585 }
2586
2587 if s.len() >= 7 && s.as_bytes()[4] == b'-' && s[..4].chars().all(|c| c.is_ascii_digit()) {
2589 let year: i32 = s[..4].parse().ok()?;
2590 let after_dash = &s[5..];
2591
2592 if after_dash.len() == 3 && after_dash.chars().all(|c| c.is_ascii_digit()) {
2594 let ordinal: u32 = after_dash.parse().ok()?;
2595 return NaiveDate::from_yo_opt(year, ordinal);
2596 }
2597
2598 if after_dash.len() == 2 && after_dash.chars().all(|c| c.is_ascii_digit()) {
2600 let month: u32 = after_dash.parse().ok()?;
2601 return NaiveDate::from_ymd_opt(year, month, 1);
2602 }
2603 }
2604
2605 if !s.chars().all(|c| c.is_ascii_digit()) {
2607 return None;
2608 }
2609
2610 match s.len() {
2611 8 => {
2613 let year: i32 = s[..4].parse().ok()?;
2614 let month: u32 = s[4..6].parse().ok()?;
2615 let day: u32 = s[6..8].parse().ok()?;
2616 NaiveDate::from_ymd_opt(year, month, day)
2617 }
2618 7 => {
2620 let year: i32 = s[..4].parse().ok()?;
2621 let ordinal: u32 = s[4..7].parse().ok()?;
2622 NaiveDate::from_yo_opt(year, ordinal)
2623 }
2624 6 => {
2626 let year: i32 = s[..4].parse().ok()?;
2627 let month: u32 = s[4..6].parse().ok()?;
2628 NaiveDate::from_ymd_opt(year, month, 1)
2629 }
2630 4 => {
2632 let year: i32 = s.parse().ok()?;
2633 NaiveDate::from_ymd_opt(year, 1, 1)
2634 }
2635 _ => None,
2636 }
2637}
2638
2639fn try_parse_compact_time(s: &str) -> Option<NaiveTime> {
2646 let (integer_part, frac_part) = if let Some(dot_pos) = s.find('.') {
2648 (&s[..dot_pos], Some(&s[dot_pos + 1..]))
2649 } else {
2650 (s, None)
2651 };
2652
2653 if !integer_part.chars().all(|c| c.is_ascii_digit()) {
2655 return None;
2656 }
2657
2658 match integer_part.len() {
2659 6 => {
2661 let hour: u32 = integer_part[..2].parse().ok()?;
2662 let min: u32 = integer_part[2..4].parse().ok()?;
2663 let sec: u32 = integer_part[4..6].parse().ok()?;
2664 if let Some(frac) = frac_part {
2665 let mut frac_str = frac.to_string();
2668 if frac_str.len() > 9 {
2669 frac_str.truncate(9);
2670 }
2671 while frac_str.len() < 9 {
2672 frac_str.push('0');
2673 }
2674 let nanos: u32 = frac_str.parse().ok()?;
2675 NaiveTime::from_hms_nano_opt(hour, min, sec, nanos)
2676 } else {
2677 NaiveTime::from_hms_opt(hour, min, sec)
2678 }
2679 }
2680 4 => {
2682 if frac_part.is_some() {
2683 return None; }
2685 let hour: u32 = integer_part[..2].parse().ok()?;
2686 let min: u32 = integer_part[2..4].parse().ok()?;
2687 NaiveTime::from_hms_opt(hour, min, 0)
2688 }
2689 2 => {
2691 if frac_part.is_some() {
2692 return None; }
2694 let hour: u32 = integer_part.parse().ok()?;
2695 NaiveTime::from_hms_opt(hour, 0, 0)
2696 }
2697 _ => None,
2698 }
2699}
2700
2701fn try_parse_naive_time(s: &str) -> Result<NaiveTime, chrono::ParseError> {
2704 NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
2705 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S"))
2706 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M"))
2707 .or_else(|e| try_parse_compact_time(s).ok_or(e))
2708}
2709
2710fn try_parse_naive_datetime(s: &str) -> Result<NaiveDateTime, chrono::ParseError> {
2713 NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")
2714 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f"))
2715 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M"))
2716 .or_else(|e| {
2717 if let Some(t_pos) = s.find('T') {
2719 let date_part = &s[..t_pos];
2720 let time_part = &s[t_pos + 1..];
2721 let date = try_parse_compact_date(date_part);
2722 let time = try_parse_compact_time(time_part)
2723 .or_else(|| try_parse_naive_time(time_part).ok());
2724 if let (Some(d), Some(t)) = (date, time) {
2725 return Ok(d.and_time(t));
2726 }
2727 }
2728 if let Some(date) = try_parse_compact_date(s) {
2730 let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
2731 return Ok(date.and_time(midnight));
2732 }
2733 Err(e)
2734 })
2735}
2736
2737pub fn parse_datetime_with_tz(s: &str) -> Result<(NaiveDate, NaiveTime, Option<TimezoneInfo>)> {
2739 let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
2740 let today = Utc::now().date_naive();
2741
2742 let (datetime_part, tz_name) = if let Some(bracket_pos) = s.find('[') {
2744 let tz_name = s[bracket_pos + 1..s.len() - 1].to_string();
2745 (&s[..bracket_pos], Some(tz_name))
2746 } else {
2747 (s, None)
2748 };
2749
2750 if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_part) {
2752 let tz_info = if let Some(name) = tz_name {
2753 Some(parse_timezone(&name)?)
2754 } else {
2755 Some(TimezoneInfo::FixedOffset(dt.offset().fix()))
2756 };
2757 return Ok((dt.date_naive(), dt.time(), tz_info));
2758 }
2759
2760 if let Ok(ndt) = try_parse_naive_datetime(datetime_part) {
2762 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?;
2763 return Ok((ndt.date(), ndt.time(), tz_info));
2764 }
2765
2766 if let Ok(d) = NaiveDate::parse_from_str(datetime_part, "%Y-%m-%d") {
2768 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?;
2769 return Ok((d, midnight, tz_info));
2770 }
2771
2772 if let Some(d) = try_parse_compact_date(datetime_part) {
2774 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?;
2775 return Ok((d, midnight, tz_info));
2776 }
2777
2778 if let Some(tz_pos) = datetime_part.rfind('+').or_else(|| {
2785 datetime_part.rfind('-').filter(|&pos| {
2789 if let Some(t_pos) = datetime_part.find('T') {
2790 pos >= t_pos + 3
2792 } else {
2793 pos >= 2
2795 }
2796 })
2797 }) {
2798 let left_part = &datetime_part[..tz_pos];
2799 let tz_part = &datetime_part[tz_pos..];
2800
2801 let resolve_tz = |tz_name: Option<String>, tz_part: &str| -> Result<Option<TimezoneInfo>> {
2802 if let Some(name) = tz_name {
2803 Ok(Some(parse_timezone(&name)?))
2804 } else {
2805 let offset = parse_timezone_offset(tz_part)?;
2806 let fo = FixedOffset::east_opt(offset)
2807 .ok_or_else(|| anyhow!("Invalid timezone offset"))?;
2808 Ok(Some(TimezoneInfo::FixedOffset(fo)))
2809 }
2810 };
2811
2812 if !left_part.contains('T')
2816 && let Ok(time) = try_parse_naive_time(left_part)
2817 && let Ok(tz_info) = resolve_tz(tz_name.clone(), tz_part)
2818 {
2819 return Ok((today, time, tz_info));
2820 }
2821
2822 if let Ok(ndt) = try_parse_naive_datetime(left_part) {
2824 let tz_info = resolve_tz(tz_name, tz_part)?;
2825 return Ok((ndt.date(), ndt.time(), tz_info));
2826 }
2827
2828 if left_part.contains('T')
2830 && let Ok(time) = try_parse_naive_time(left_part)
2831 {
2832 let tz_info = resolve_tz(tz_name, tz_part)?;
2833 return Ok((today, time, tz_info));
2834 }
2835 }
2836
2837 if let Some(base) = datetime_part
2839 .strip_suffix('Z')
2840 .or_else(|| datetime_part.strip_suffix('z'))
2841 {
2842 let utc_tz = Some(TimezoneInfo::FixedOffset(FixedOffset::east_opt(0).unwrap()));
2843 if let Ok(ndt) = try_parse_naive_datetime(base) {
2845 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?.or(utc_tz);
2846 return Ok((ndt.date(), ndt.time(), tz_info));
2847 }
2848 if let Ok(time) = try_parse_naive_time(base) {
2850 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?.or(utc_tz);
2851 return Ok((today, time, tz_info));
2852 }
2853 }
2854
2855 if let Ok(time) = try_parse_naive_time(datetime_part) {
2857 let tz_info = tz_name.map(|n| parse_timezone(&n)).transpose()?;
2858 return Ok((today, time, tz_info));
2859 }
2860
2861 Err(anyhow!("Cannot parse datetime: {}", s))
2862}
2863
2864fn nanos_precision_format(nanos: u32, seconds: u32) -> &'static str {
2866 if nanos == 0 && seconds == 0 {
2867 "%Y-%m-%dT%H:%M"
2868 } else if nanos == 0 {
2869 "%Y-%m-%dT%H:%M:%S"
2870 } else if nanos.is_multiple_of(1_000_000) {
2871 "%Y-%m-%dT%H:%M:%S%.3f"
2872 } else if nanos.is_multiple_of(1_000) {
2873 "%Y-%m-%dT%H:%M:%S%.6f"
2874 } else {
2875 "%Y-%m-%dT%H:%M:%S%.9f"
2876 }
2877}
2878
2879fn format_datetime_with_nanos(dt: &DateTime<Utc>) -> String {
2880 let fmt = nanos_precision_format(dt.nanosecond(), dt.second());
2881 format!("{}Z", dt.format(fmt))
2882}
2883
2884fn format_datetime_with_offset_and_tz(dt: &DateTime<FixedOffset>, tz_name: Option<&str>) -> String {
2885 let fmt = nanos_precision_format(dt.nanosecond(), dt.second());
2886 let tz_suffix = format_timezone_offset(dt.offset().local_minus_utc());
2887 let base = format!("{}{}", dt.format(fmt), tz_suffix);
2888
2889 if let Some(name) = tz_name {
2890 format!("{}[{}]", base, name)
2891 } else {
2892 base
2893 }
2894}
2895
2896fn format_naive_datetime(ndt: &NaiveDateTime) -> String {
2897 let fmt = nanos_precision_format(ndt.nanosecond(), ndt.second());
2898 ndt.format(fmt).to_string()
2899}
2900
2901#[derive(Debug, Clone, PartialEq)]
2909pub struct CypherDuration {
2910 pub months: i64,
2912 pub days: i64,
2914 pub nanos: i64,
2916}
2917
2918impl CypherDuration {
2919 pub fn new(months: i64, days: i64, nanos: i64) -> Self {
2920 Self {
2921 months,
2922 days,
2923 nanos,
2924 }
2925 }
2926
2927 pub fn to_temporal_value(&self) -> Value {
2929 Value::Temporal(TemporalValue::Duration {
2930 months: self.months,
2931 days: self.days,
2932 nanos: self.nanos,
2933 })
2934 }
2935
2936 pub fn from_micros(micros: i64) -> Self {
2938 let total_nanos = micros * 1000;
2939 let total_secs = total_nanos / NANOS_PER_SECOND;
2940 let remaining_nanos = total_nanos % NANOS_PER_SECOND;
2941
2942 let days = total_secs / (24 * 3600);
2943 let day_secs = total_secs % (24 * 3600);
2944
2945 Self {
2946 months: 0,
2947 days,
2948 nanos: day_secs * NANOS_PER_SECOND + remaining_nanos,
2949 }
2950 }
2951
2952 pub fn to_iso8601(&self) -> String {
2956 let mut result = String::from("P");
2957
2958 let years = self.months / 12;
2959 let months = self.months % 12;
2960
2961 if years != 0 {
2962 result.push_str(&format!("{}Y", years));
2963 }
2964 if months != 0 {
2965 result.push_str(&format!("{}M", months));
2966 }
2967 if self.days != 0 {
2968 result.push_str(&format!("{}D", self.days));
2969 }
2970
2971 let nanos = self.nanos;
2974 let total_secs = nanos / NANOS_PER_SECOND; let remaining_nanos = nanos % NANOS_PER_SECOND; let hours = total_secs / 3600;
2978 let rem_after_hours = total_secs % 3600;
2979 let minutes = rem_after_hours / 60;
2980 let seconds = rem_after_hours % 60;
2981
2982 if hours != 0 || minutes != 0 || seconds != 0 || remaining_nanos != 0 {
2983 result.push('T');
2984
2985 if hours != 0 {
2986 result.push_str(&format!("{}H", hours));
2987 }
2988 if minutes != 0 {
2989 result.push_str(&format!("{}M", minutes));
2990 }
2991 if seconds != 0 || remaining_nanos != 0 {
2992 if remaining_nanos != 0 {
2993 let secs_with_nanos = seconds as f64 + (remaining_nanos as f64 / 1e9);
2996 let formatted = format!("{:.9}", secs_with_nanos);
2997 let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
2998 result.push_str(trimmed);
2999 result.push('S');
3000 } else {
3001 result.push_str(&format!("{}S", seconds));
3002 }
3003 }
3004 }
3005
3006 if result == "P" {
3008 result.push_str("T0S");
3009 }
3010
3011 result
3012 }
3013
3014 pub fn to_micros(&self) -> i64 {
3016 let month_days = self.months * 30; let total_days = month_days + self.days;
3018 let day_micros = total_days * MICROS_PER_DAY;
3019 let nano_micros = self.nanos / 1000;
3020 day_micros + nano_micros
3021 }
3022
3023 pub fn add(&self, other: &CypherDuration) -> CypherDuration {
3025 CypherDuration::new(
3026 self.months + other.months,
3027 self.days + other.days,
3028 self.nanos + other.nanos,
3029 )
3030 }
3031
3032 pub fn sub(&self, other: &CypherDuration) -> CypherDuration {
3034 CypherDuration::new(
3035 self.months - other.months,
3036 self.days - other.days,
3037 self.nanos - other.nanos,
3038 )
3039 }
3040
3041 pub fn negate(&self) -> CypherDuration {
3043 CypherDuration::new(-self.months, -self.days, -self.nanos)
3044 }
3045
3046 pub fn multiply(&self, factor: f64) -> CypherDuration {
3048 let months_f = self.months as f64 * factor;
3049 let whole_months = months_f.trunc() as i64;
3050 let frac_months = months_f.fract();
3051
3052 let frac_month_seconds = frac_months * 2_629_746.0;
3054 let extra_days_from_months = (frac_month_seconds / SECONDS_PER_DAY as f64).trunc();
3055 let remaining_secs_from_months =
3056 frac_month_seconds - extra_days_from_months * SECONDS_PER_DAY as f64;
3057
3058 let days_f = self.days as f64 * factor + extra_days_from_months;
3059 let whole_days = days_f.trunc() as i64;
3060 let frac_days = days_f.fract();
3061
3062 let nanos_f = self.nanos as f64 * factor
3063 + remaining_secs_from_months * NANOS_PER_SECOND as f64
3064 + frac_days * NANOS_PER_DAY as f64;
3065
3066 CypherDuration::new(whole_months, whole_days, nanos_f.trunc() as i64)
3067 }
3068
3069 pub fn divide(&self, divisor: f64) -> CypherDuration {
3071 if divisor == 0.0 {
3072 return CypherDuration::new(0, 0, 0);
3074 }
3075 self.multiply(1.0 / divisor)
3076 }
3077}
3078
3079pub fn add_months_to_date(date: NaiveDate, months: i64) -> NaiveDate {
3089 if months == 0 {
3090 return date;
3091 }
3092
3093 let total_months = date.year() as i64 * 12 + (date.month() as i64 - 1) + months;
3094 let new_year = total_months.div_euclid(12) as i32;
3095 let new_month = (total_months.rem_euclid(12) + 1) as u32;
3096
3097 let max_day = days_in_month(new_year, new_month);
3099 let new_day = date.day().min(max_day);
3100
3101 NaiveDate::from_ymd_opt(new_year, new_month, new_day)
3102 .unwrap_or_else(|| NaiveDate::from_ymd_opt(new_year, new_month, 1).unwrap())
3103}
3104
3105fn days_in_month(year: i32, month: u32) -> u32 {
3107 match month {
3108 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
3109 4 | 6 | 9 | 11 => 30,
3110 2 => {
3111 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
3112 29
3113 } else {
3114 28
3115 }
3116 }
3117 _ => 30,
3118 }
3119}
3120
3121pub fn add_cypher_duration_to_date(date_str: &str, dur: &CypherDuration) -> Result<String> {
3125 let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
3126
3127 let after_months = add_months_to_date(date, dur.months);
3129
3130 let after_days = after_months + Duration::days(dur.days);
3132
3133 let extra_days = dur.nanos / NANOS_PER_DAY;
3136 let result = after_days + Duration::days(extra_days);
3137
3138 Ok(result.format("%Y-%m-%d").to_string())
3139}
3140
3141pub fn add_cypher_duration_to_localtime(time_str: &str, dur: &CypherDuration) -> Result<String> {
3145 let time = parse_time_string(time_str)?;
3146 let total_nanos = time_to_nanos(&time) + dur.nanos;
3147 let wrapped = total_nanos.rem_euclid(NANOS_PER_DAY);
3149 let result = nanos_to_time(wrapped);
3150 Ok(format_time_with_nanos(&result))
3151}
3152
3153pub fn add_cypher_duration_to_time(time_str: &str, dur: &CypherDuration) -> Result<String> {
3157 let (_, time, tz_info) = parse_datetime_with_tz(time_str)?;
3158 let total_nanos = time_to_nanos(&time) + dur.nanos;
3159 let wrapped = total_nanos.rem_euclid(NANOS_PER_DAY);
3160 let result_time = nanos_to_time(wrapped);
3161
3162 let time_part = format_time_with_nanos(&result_time);
3163 if let Some(ref tz) = tz_info {
3164 let today = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
3165 let ndt = NaiveDateTime::new(today, result_time);
3166 let offset = tz.offset_for_local(&ndt)?;
3167 let offset_str = format_timezone_offset(offset.local_minus_utc());
3168 Ok(format!("{}{}", time_part, offset_str))
3169 } else {
3170 Ok(time_part)
3171 }
3172}
3173
3174pub fn add_cypher_duration_to_localdatetime(dt_str: &str, dur: &CypherDuration) -> Result<String> {
3176 let ndt = NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M:%S")
3177 .or_else(|_| NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M:%S%.f"))
3178 .or_else(|_| NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M"))
3179 .map_err(|_| anyhow!("Invalid localdatetime: {}", dt_str))?;
3180
3181 let after_months = add_months_to_date(ndt.date(), dur.months);
3183 let after_days = after_months + Duration::days(dur.days);
3185 let result_ndt = NaiveDateTime::new(after_days, ndt.time()) + Duration::nanoseconds(dur.nanos);
3187
3188 Ok(format_naive_datetime(&result_ndt))
3189}
3190
3191pub fn add_cypher_duration_to_datetime(dt_str: &str, dur: &CypherDuration) -> Result<String> {
3193 let (date, time, tz_info) = parse_datetime_with_tz(dt_str)?;
3194
3195 let after_months = add_months_to_date(date, dur.months);
3197 let after_days = after_months + Duration::days(dur.days);
3199 let ndt = NaiveDateTime::new(after_days, time) + Duration::nanoseconds(dur.nanos);
3201
3202 if let Some(ref tz) = tz_info {
3203 let offset = tz.offset_for_local(&ndt)?;
3204 let dt = offset
3205 .from_local_datetime(&ndt)
3206 .single()
3207 .ok_or_else(|| anyhow!("Ambiguous local time after duration addition"))?;
3208 Ok(format_datetime_with_offset_and_tz(&dt, tz.name()))
3209 } else {
3210 let dt = DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc);
3211 Ok(format_datetime_with_nanos(&dt))
3212 }
3213}
3214
3215fn time_to_nanos(t: &NaiveTime) -> i64 {
3217 t.hour() as i64 * 3_600 * NANOS_PER_SECOND
3218 + t.minute() as i64 * 60 * NANOS_PER_SECOND
3219 + t.second() as i64 * NANOS_PER_SECOND
3220 + t.nanosecond() as i64
3221}
3222
3223fn nanos_to_time(nanos: i64) -> NaiveTime {
3225 let total_secs = nanos / NANOS_PER_SECOND;
3226 let remaining_nanos = (nanos % NANOS_PER_SECOND) as u32;
3227 let h = (total_secs / 3600) as u32;
3228 let m = ((total_secs % 3600) / 60) as u32;
3229 let s = (total_secs % 60) as u32;
3230 NaiveTime::from_hms_nano_opt(h, m, s, remaining_nanos)
3231 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap())
3232}
3233
3234fn eval_duration(args: &[Value]) -> Result<Value> {
3239 if args.len() != 1 {
3240 return Err(anyhow!("duration() requires 1 argument"));
3241 }
3242
3243 match &args[0] {
3244 Value::String(s) => {
3245 let duration = parse_duration_to_cypher(s)?;
3246 Ok(Value::Temporal(TemporalValue::Duration {
3247 months: duration.months,
3248 days: duration.days,
3249 nanos: duration.nanos,
3250 }))
3251 }
3252 Value::Temporal(TemporalValue::Duration { .. }) => Ok(args[0].clone()),
3253 Value::Map(map) => eval_duration_from_map(map),
3254 Value::Int(_) | Value::Float(_) => {
3255 if let Some(micros) = args[0].as_i64() {
3256 let duration = CypherDuration::from_micros(micros);
3257 Ok(Value::Temporal(TemporalValue::Duration {
3258 months: duration.months,
3259 days: duration.days,
3260 nanos: duration.nanos,
3261 }))
3262 } else {
3263 Ok(args[0].clone())
3264 }
3265 }
3266 Value::Null => Ok(Value::Null),
3267 _ => Err(anyhow!("duration() expects a string, map, or number")),
3268 }
3269}
3270
3271fn eval_duration_from_map(map: &HashMap<String, Value>) -> Result<Value> {
3277 let mut months_f: f64 = 0.0;
3278 let mut days_f: f64 = 0.0;
3279 let mut nanos_f: f64 = 0.0;
3280
3281 if let Some(years) = map.get("years").and_then(get_numeric_value) {
3283 months_f += years * 12.0;
3284 }
3285 if let Some(m) = map.get("months").and_then(get_numeric_value) {
3286 months_f += m;
3287 }
3288
3289 let whole_months = months_f.trunc() as i64;
3292 let frac_months = months_f.fract();
3293 let frac_month_seconds = frac_months * 2_629_746.0;
3294 let extra_days_from_months = (frac_month_seconds / SECONDS_PER_DAY as f64).trunc();
3295 let remaining_secs_from_months =
3296 frac_month_seconds - extra_days_from_months * SECONDS_PER_DAY as f64;
3297 days_f += extra_days_from_months;
3298 nanos_f += remaining_secs_from_months * NANOS_PER_SECOND as f64;
3299
3300 if let Some(weeks) = map.get("weeks").and_then(get_numeric_value) {
3301 days_f += weeks * 7.0;
3302 }
3303 if let Some(d) = map.get("days").and_then(get_numeric_value) {
3304 days_f += d;
3305 }
3306
3307 let whole_days = days_f.trunc() as i64;
3309 let frac_days = days_f.fract();
3310 nanos_f += frac_days * NANOS_PER_DAY as f64;
3311
3312 if let Some(hours) = map.get("hours").and_then(get_numeric_value) {
3314 nanos_f += hours * 3600.0 * NANOS_PER_SECOND as f64;
3315 }
3316 if let Some(minutes) = map.get("minutes").and_then(get_numeric_value) {
3317 nanos_f += minutes * 60.0 * NANOS_PER_SECOND as f64;
3318 }
3319 if let Some(seconds) = map.get("seconds").and_then(get_numeric_value) {
3320 nanos_f += seconds * NANOS_PER_SECOND as f64;
3321 }
3322 if let Some(millis) = map.get("milliseconds").and_then(get_numeric_value) {
3323 nanos_f += millis * 1_000_000.0;
3324 }
3325 if let Some(micros) = map.get("microseconds").and_then(get_numeric_value) {
3326 nanos_f += micros * 1_000.0;
3327 }
3328 if let Some(n) = map.get("nanoseconds").and_then(get_numeric_value) {
3329 nanos_f += n;
3330 }
3331
3332 let duration = CypherDuration::new(whole_months, whole_days, nanos_f.trunc() as i64);
3333 Ok(Value::Temporal(TemporalValue::Duration {
3334 months: duration.months,
3335 days: duration.days,
3336 nanos: duration.nanos,
3337 }))
3338}
3339
3340fn get_numeric_value(v: &Value) -> Option<f64> {
3342 v.as_f64().or_else(|| v.as_i64().map(|i| i as f64))
3343}
3344
3345fn parse_iso8601_duration(s: &str) -> Result<i64> {
3347 let s = &s[1..]; let mut total_micros: i64 = 0;
3349 let mut in_time_part = false;
3350 let mut num_buf = String::new();
3351
3352 for c in s.chars() {
3353 if c == 'T' || c == 't' {
3354 in_time_part = true;
3355 continue;
3356 }
3357
3358 if c.is_ascii_digit() || c == '.' || c == '-' {
3359 num_buf.push(c);
3360 } else {
3361 if num_buf.is_empty() {
3362 continue;
3363 }
3364 let num: f64 = num_buf
3365 .parse()
3366 .map_err(|_| anyhow!("Invalid duration number"))?;
3367 num_buf.clear();
3368
3369 let micros = match c {
3370 'Y' | 'y' => (num * 365.0 * MICROS_PER_DAY as f64) as i64,
3371 '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,
3373 'D' | 'd' => (num * MICROS_PER_DAY as f64) as i64,
3374 'H' | 'h' => (num * MICROS_PER_HOUR as f64) as i64,
3375 'M' | 'm' if in_time_part => (num * MICROS_PER_MINUTE as f64) as i64, 'S' | 's' => (num * MICROS_PER_SECOND as f64) as i64,
3377 _ => return Err(anyhow!("Invalid ISO 8601 duration designator: {}", c)),
3378 };
3379 total_micros += micros;
3380 }
3381 }
3382
3383 Ok(total_micros)
3384}
3385
3386fn parse_simple_duration(s: &str) -> Result<i64> {
3388 let mut total_micros: i64 = 0;
3389 let mut num_buf = String::new();
3390
3391 for c in s.chars() {
3392 if c.is_ascii_digit() || c == '.' || c == '-' {
3393 num_buf.push(c);
3394 } else if c.is_ascii_alphabetic() {
3395 if num_buf.is_empty() {
3396 return Err(anyhow!("Invalid duration format"));
3397 }
3398 let num: f64 = num_buf
3399 .parse()
3400 .map_err(|_| anyhow!("Invalid duration number"))?;
3401 num_buf.clear();
3402
3403 let micros = match c {
3404 'w' => (num * 7.0 * MICROS_PER_DAY as f64) as i64,
3405 'd' => (num * MICROS_PER_DAY as f64) as i64,
3406 'h' => (num * MICROS_PER_HOUR as f64) as i64,
3407 'm' => (num * MICROS_PER_MINUTE as f64) as i64,
3408 's' => (num * MICROS_PER_SECOND as f64) as i64,
3409 _ => return Err(anyhow!("Invalid duration unit: {}", c)),
3410 };
3411 total_micros += micros;
3412 }
3413 }
3414
3415 if !num_buf.is_empty() {
3417 let num: f64 = num_buf
3418 .parse()
3419 .map_err(|_| anyhow!("Invalid duration number"))?;
3420 total_micros += (num * MICROS_PER_SECOND as f64) as i64;
3421 }
3422
3423 Ok(total_micros)
3424}
3425
3426fn eval_datetime_fromepoch(args: &[Value]) -> Result<Value> {
3431 let seconds = args
3432 .first()
3433 .and_then(|v| v.as_i64())
3434 .ok_or_else(|| anyhow!("datetime.fromepoch requires seconds argument"))?;
3435 let nanos = args.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as u32;
3436
3437 let dt = DateTime::from_timestamp(seconds, nanos)
3438 .ok_or_else(|| anyhow!("Invalid epoch timestamp: {}", seconds))?;
3439 let epoch_nanos = dt.timestamp_nanos_opt().unwrap_or(0);
3440 Ok(Value::Temporal(TemporalValue::DateTime {
3441 nanos_since_epoch: epoch_nanos,
3442 offset_seconds: 0,
3443 timezone_name: None,
3444 }))
3445}
3446
3447fn eval_datetime_fromepochmillis(args: &[Value]) -> Result<Value> {
3448 let millis = args
3449 .first()
3450 .and_then(|v| v.as_i64())
3451 .ok_or_else(|| anyhow!("datetime.fromepochmillis requires milliseconds argument"))?;
3452
3453 let dt = DateTime::from_timestamp_millis(millis)
3454 .ok_or_else(|| anyhow!("Invalid epoch millis: {}", millis))?;
3455 let epoch_nanos = dt.timestamp_nanos_opt().unwrap_or(0);
3456 Ok(Value::Temporal(TemporalValue::DateTime {
3457 nanos_since_epoch: epoch_nanos,
3458 offset_seconds: 0,
3459 timezone_name: None,
3460 }))
3461}
3462
3463fn eval_truncate(type_name: &str, args: &[Value]) -> Result<Value> {
3468 if args.is_empty() {
3469 return Err(anyhow!(
3470 "{}.truncate requires at least a unit argument",
3471 type_name
3472 ));
3473 }
3474
3475 let unit = args
3476 .first()
3477 .and_then(|v| v.as_str())
3478 .ok_or_else(|| anyhow!("truncate requires unit as first argument"))?;
3479
3480 let temporal = args.get(1);
3481 let adjust_map = args.get(2).and_then(|v| v.as_object());
3482
3483 match type_name {
3484 "date" => truncate_date(unit, temporal, adjust_map),
3485 "time" => truncate_time(unit, temporal, adjust_map, true),
3486 "localtime" => truncate_time(unit, temporal, adjust_map, false),
3487 "datetime" | "localdatetime" => truncate_datetime(unit, temporal, adjust_map, type_name),
3488 _ => Err(anyhow!("Unknown truncate type: {}", type_name)),
3489 }
3490}
3491
3492fn truncate_date(
3493 unit: &str,
3494 temporal: Option<&Value>,
3495 adjust_map: Option<&HashMap<String, Value>>,
3496) -> Result<Value> {
3497 let date = match temporal {
3498 Some(Value::Temporal(_)) => temporal_or_string_to_date(temporal.unwrap())?,
3499 Some(Value::String(s)) => parse_date_string(s)?,
3500 Some(Value::Null) | None => Utc::now().date_naive(),
3501 _ => return Err(anyhow!("truncate expects a date string")),
3502 };
3503
3504 let truncated = truncate_date_to_unit(date, unit)?;
3505
3506 if let Some(map) = adjust_map {
3507 apply_date_adjustments(truncated, map)
3508 } else {
3509 Ok(Value::Temporal(TemporalValue::Date {
3510 days_since_epoch: date_to_days_since_epoch(&truncated),
3511 }))
3512 }
3513}
3514
3515fn truncate_date_to_unit(date: NaiveDate, unit: &str) -> Result<NaiveDate> {
3516 let unit_lower = unit.to_lowercase();
3517 match unit_lower.as_str() {
3518 "millennium" => {
3519 let millennium_year = (date.year() / 1000) * 1000;
3521 NaiveDate::from_ymd_opt(millennium_year, 1, 1)
3522 .ok_or_else(|| anyhow!("Invalid millennium truncation"))
3523 }
3524 "century" => {
3525 let century_year = (date.year() / 100) * 100;
3527 NaiveDate::from_ymd_opt(century_year, 1, 1)
3528 .ok_or_else(|| anyhow!("Invalid century truncation"))
3529 }
3530 "decade" => {
3531 let decade_year = (date.year() / 10) * 10;
3532 NaiveDate::from_ymd_opt(decade_year, 1, 1)
3533 .ok_or_else(|| anyhow!("Invalid decade truncation"))
3534 }
3535 "year" => NaiveDate::from_ymd_opt(date.year(), 1, 1)
3536 .ok_or_else(|| anyhow!("Invalid year truncation")),
3537 "weekyear" => {
3538 let iso_week = date.iso_week();
3540 let week_year = iso_week.year();
3541 let jan4 =
3542 NaiveDate::from_ymd_opt(week_year, 1, 4).ok_or_else(|| anyhow!("Invalid date"))?;
3543 let iso_week_day = jan4.weekday().num_days_from_monday();
3544 Ok(jan4 - Duration::days(iso_week_day as i64))
3545 }
3546 "quarter" => {
3547 let quarter = (date.month() - 1) / 3;
3548 let first_month = quarter * 3 + 1;
3549 NaiveDate::from_ymd_opt(date.year(), first_month, 1)
3550 .ok_or_else(|| anyhow!("Invalid quarter truncation"))
3551 }
3552 "month" => NaiveDate::from_ymd_opt(date.year(), date.month(), 1)
3553 .ok_or_else(|| anyhow!("Invalid month truncation")),
3554 "week" => {
3555 let weekday = date.weekday().num_days_from_monday();
3557 Ok(date - Duration::days(weekday as i64))
3558 }
3559 "day" => Ok(date),
3560 _ => Err(anyhow!("Unknown truncation unit for date: {}", unit)),
3561 }
3562}
3563
3564fn apply_date_adjustments(date: NaiveDate, map: &HashMap<String, Value>) -> Result<Value> {
3565 let mut result = date;
3566
3567 if let Some(dow) = map.get("dayOfWeek").and_then(|v| v.as_i64()) {
3569 let current_dow = result.weekday().num_days_from_monday() as i64 + 1;
3572 let diff = dow - current_dow;
3573 result += Duration::days(diff);
3574 }
3575
3576 if let Some(month) = map.get("month").and_then(|v| v.as_i64()) {
3577 result = NaiveDate::from_ymd_opt(result.year(), month as u32, result.day())
3578 .ok_or_else(|| anyhow!("Invalid month adjustment"))?;
3579 }
3580 if let Some(day) = map.get("day").and_then(|v| v.as_i64()) {
3581 result = NaiveDate::from_ymd_opt(result.year(), result.month(), day as u32)
3582 .ok_or_else(|| anyhow!("Invalid day adjustment"))?;
3583 }
3584
3585 Ok(Value::Temporal(TemporalValue::Date {
3586 days_since_epoch: date_to_days_since_epoch(&result),
3587 }))
3588}
3589
3590fn truncate_time(
3591 unit: &str,
3592 temporal: Option<&Value>,
3593 adjust_map: Option<&HashMap<String, Value>>,
3594 with_timezone: bool,
3595) -> Result<Value> {
3596 let (date, time, tz_info) = match temporal {
3597 Some(Value::Temporal(tv)) => {
3598 let t = tv
3599 .to_time()
3600 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
3601 let offset = match tv {
3602 TemporalValue::Time { offset_seconds, .. }
3603 | TemporalValue::DateTime { offset_seconds, .. } => Some(
3604 TimezoneInfo::FixedOffset(FixedOffset::east_opt(*offset_seconds).unwrap()),
3605 ),
3606 _ => None,
3607 };
3608 (Utc::now().date_naive(), t, offset)
3609 }
3610 Some(Value::String(s)) => {
3611 if let Ok((date, time, tz)) = parse_datetime_with_tz(s) {
3613 (date, time, tz)
3614 } else if let Ok(t) = parse_time_string(s) {
3615 (Utc::now().date_naive(), t, None)
3617 } else {
3618 return Err(anyhow!("truncate expects a time string"));
3619 }
3620 }
3621 Some(Value::Null) | None => {
3622 let now = Utc::now();
3623 (now.date_naive(), now.time(), None)
3624 }
3625 _ => return Err(anyhow!("truncate expects a time string")),
3626 };
3627
3628 let effective_tz = if let Some(map) = adjust_map {
3630 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
3631 Some(parse_timezone(tz_str)?)
3632 } else {
3633 tz_info
3634 }
3635 } else {
3636 tz_info
3637 };
3638
3639 let truncated = truncate_time_to_unit(time, unit)?;
3640
3641 let final_time = if let Some(map) = adjust_map {
3642 apply_time_adjustments(truncated, map)?
3643 } else {
3644 truncated
3645 };
3646
3647 let nanos = time_to_nanos(&final_time);
3649 if with_timezone {
3650 let offset = if let Some(ref tz) = effective_tz {
3651 tz.offset_seconds_with_date(&date)
3652 } else {
3653 0
3654 };
3655 Ok(Value::Temporal(TemporalValue::Time {
3656 nanos_since_midnight: nanos,
3657 offset_seconds: offset,
3658 }))
3659 } else {
3660 Ok(Value::Temporal(TemporalValue::LocalTime {
3661 nanos_since_midnight: nanos,
3662 }))
3663 }
3664}
3665
3666fn truncate_time_to_unit(time: NaiveTime, unit: &str) -> Result<NaiveTime> {
3667 let unit_lower = unit.to_lowercase();
3668 match unit_lower.as_str() {
3669 "day" => NaiveTime::from_hms_opt(0, 0, 0).ok_or_else(|| anyhow!("Invalid truncation")),
3670 "hour" => {
3671 NaiveTime::from_hms_opt(time.hour(), 0, 0).ok_or_else(|| anyhow!("Invalid truncation"))
3672 }
3673 "minute" => NaiveTime::from_hms_opt(time.hour(), time.minute(), 0)
3674 .ok_or_else(|| anyhow!("Invalid truncation")),
3675 "second" => NaiveTime::from_hms_opt(time.hour(), time.minute(), time.second())
3676 .ok_or_else(|| anyhow!("Invalid truncation")),
3677 "millisecond" => {
3678 let millis = time.nanosecond() / 1_000_000;
3679 NaiveTime::from_hms_nano_opt(
3680 time.hour(),
3681 time.minute(),
3682 time.second(),
3683 millis * 1_000_000,
3684 )
3685 .ok_or_else(|| anyhow!("Invalid truncation"))
3686 }
3687 "microsecond" => {
3688 let micros = time.nanosecond() / 1_000;
3689 NaiveTime::from_hms_nano_opt(time.hour(), time.minute(), time.second(), micros * 1_000)
3690 .ok_or_else(|| anyhow!("Invalid truncation"))
3691 }
3692 _ => Err(anyhow!("Unknown truncation unit for time: {}", unit)),
3693 }
3694}
3695
3696fn apply_time_adjustments(time: NaiveTime, map: &HashMap<String, Value>) -> Result<NaiveTime> {
3698 let hour = map
3699 .get("hour")
3700 .and_then(|v| v.as_i64())
3701 .unwrap_or(time.hour() as i64) as u32;
3702 let minute = map
3703 .get("minute")
3704 .and_then(|v| v.as_i64())
3705 .unwrap_or(time.minute() as i64) as u32;
3706 let second = map
3707 .get("second")
3708 .and_then(|v| v.as_i64())
3709 .unwrap_or(time.second() as i64) as u32;
3710 let nanos = build_nanoseconds_with_base(map, time.nanosecond());
3711
3712 NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
3713 .ok_or_else(|| anyhow!("Invalid time adjustment"))
3714}
3715
3716fn truncate_datetime(
3717 unit: &str,
3718 temporal: Option<&Value>,
3719 adjust_map: Option<&HashMap<String, Value>>,
3720 type_name: &str,
3721) -> Result<Value> {
3722 let (date, time, tz_info) = match temporal {
3723 Some(Value::Temporal(_)) => temporal_or_string_to_components(temporal.unwrap())?,
3724 Some(Value::String(s)) => {
3725 parse_datetime_with_tz(s)?
3727 }
3728 Some(Value::Null) | None => {
3729 let now = Utc::now();
3730 (
3731 now.date_naive(),
3732 now.time(),
3733 Some(TimezoneInfo::FixedOffset(FixedOffset::east_opt(0).unwrap())),
3734 )
3735 }
3736 _ => return Err(anyhow!("truncate expects a datetime string")),
3737 };
3738
3739 let effective_tz = if let Some(map) = adjust_map {
3741 if let Some(tz_str) = map.get("timezone").and_then(|v| v.as_str()) {
3742 Some(parse_timezone(tz_str)?)
3743 } else {
3744 tz_info
3745 }
3746 } else {
3747 tz_info
3748 };
3749
3750 let (truncated_date, truncated_time) = truncate_datetime_to_unit(date, time, unit)?;
3752
3753 if let Some(map) = adjust_map {
3754 apply_datetime_adjustments(
3755 truncated_date,
3756 truncated_time,
3757 map,
3758 type_name,
3759 effective_tz.as_ref(),
3760 )
3761 } else {
3762 let ndt = NaiveDateTime::new(truncated_date, truncated_time);
3763 if type_name == "localdatetime" {
3764 Ok(localdatetime_value_from_naive(&ndt))
3765 } else if let Some(ref tz) = effective_tz {
3766 let offset = tz.offset_for_local(&ndt)?;
3767 let offset_secs = offset.local_minus_utc();
3768 Ok(datetime_value_from_local_and_offset(
3769 &ndt,
3770 offset_secs,
3771 tz.name().map(|s| s.to_string()),
3772 ))
3773 } else {
3774 Ok(datetime_value_from_local_and_offset(&ndt, 0, None))
3775 }
3776 }
3777}
3778
3779fn truncate_datetime_to_unit(
3780 date: NaiveDate,
3781 time: NaiveTime,
3782 unit: &str,
3783) -> Result<(NaiveDate, NaiveTime)> {
3784 let unit_lower = unit.to_lowercase();
3785 let midnight =
3786 NaiveTime::from_hms_opt(0, 0, 0).ok_or_else(|| anyhow!("Failed to create midnight"))?;
3787
3788 match unit_lower.as_str() {
3789 "millennium" | "century" | "decade" | "year" | "weekyear" | "quarter" | "month"
3791 | "week" | "day" => {
3792 let truncated_date = truncate_date_to_unit(date, unit)?;
3793 Ok((truncated_date, midnight))
3794 }
3795 "hour" | "minute" | "second" | "millisecond" | "microsecond" => {
3797 let truncated_time = truncate_time_to_unit(time, unit)?;
3798 Ok((date, truncated_time))
3799 }
3800 _ => Err(anyhow!("Unknown truncation unit: {}", unit)),
3801 }
3802}
3803
3804fn apply_datetime_adjustments(
3805 date: NaiveDate,
3806 time: NaiveTime,
3807 map: &HashMap<String, Value>,
3808 type_name: &str,
3809 tz_info: Option<&TimezoneInfo>,
3810) -> Result<Value> {
3811 let year = map
3813 .get("year")
3814 .and_then(|v| v.as_i64())
3815 .unwrap_or(date.year() as i64) as i32;
3816 let month = map
3817 .get("month")
3818 .and_then(|v| v.as_i64())
3819 .unwrap_or(date.month() as i64) as u32;
3820 let day = map
3821 .get("day")
3822 .and_then(|v| v.as_i64())
3823 .unwrap_or(date.day() as i64) as u32;
3824
3825 let hour = map
3827 .get("hour")
3828 .and_then(|v| v.as_i64())
3829 .unwrap_or(time.hour() as i64) as u32;
3830 let minute = map
3831 .get("minute")
3832 .and_then(|v| v.as_i64())
3833 .unwrap_or(time.minute() as i64) as u32;
3834 let second = map
3835 .get("second")
3836 .and_then(|v| v.as_i64())
3837 .unwrap_or(time.second() as i64) as u32;
3838 let nanos = build_nanoseconds_with_base(map, time.nanosecond());
3839
3840 let mut adjusted_date = NaiveDate::from_ymd_opt(year, month, day)
3841 .ok_or_else(|| anyhow!("Invalid date in adjustment"))?;
3842
3843 if let Some(dow) = map.get("dayOfWeek").and_then(|v| v.as_i64()) {
3845 let current_dow = adjusted_date.weekday().num_days_from_monday() as i64 + 1;
3846 let diff = dow - current_dow;
3847 adjusted_date += Duration::days(diff);
3848 }
3849
3850 let adjusted_time = NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)
3851 .ok_or_else(|| anyhow!("Invalid time in adjustment"))?;
3852
3853 let ndt = NaiveDateTime::new(adjusted_date, adjusted_time);
3854
3855 if type_name == "localdatetime" {
3856 Ok(localdatetime_value_from_naive(&ndt))
3857 } else if let Some(tz) = tz_info {
3858 let offset = tz.offset_for_local(&ndt)?;
3859 let offset_secs = offset.local_minus_utc();
3860 Ok(datetime_value_from_local_and_offset(
3861 &ndt,
3862 offset_secs,
3863 tz.name().map(|s| s.to_string()),
3864 ))
3865 } else {
3866 Ok(datetime_value_from_local_and_offset(&ndt, 0, None))
3867 }
3868}
3869
3870#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3875struct ExtendedDate {
3876 year: i64,
3877 month: u32,
3878 day: u32,
3879}
3880
3881#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3882struct ExtendedLocalDateTime {
3883 date: ExtendedDate,
3884 hour: u32,
3885 minute: u32,
3886 second: u32,
3887 nanosecond: u32,
3888}
3889
3890fn is_leap_year_i64(year: i64) -> bool {
3891 year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
3892}
3893
3894fn days_in_month_i64(year: i64, month: u32) -> Option<u32> {
3895 let days = match month {
3896 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
3897 4 | 6 | 9 | 11 => 30,
3898 2 => {
3899 if is_leap_year_i64(year) {
3900 29
3901 } else {
3902 28
3903 }
3904 }
3905 _ => return None,
3906 };
3907 Some(days)
3908}
3909
3910fn parse_extended_date_string(s: &str) -> Option<ExtendedDate> {
3911 let bytes = s.as_bytes();
3912 if bytes.is_empty() {
3913 return None;
3914 }
3915
3916 let mut idx = 0usize;
3917 if matches!(bytes[0], b'+' | b'-') {
3918 idx += 1;
3919 }
3920 if idx >= bytes.len() || !bytes[idx].is_ascii_digit() {
3921 return None;
3922 }
3923
3924 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
3925 idx += 1;
3926 }
3927 if idx >= bytes.len() || bytes[idx] != b'-' {
3928 return None;
3929 }
3930
3931 let year: i64 = s[..idx].parse().ok()?;
3932 let rest = &s[idx + 1..];
3933 let (month_str, day_str) = rest.split_once('-')?;
3934 if month_str.len() != 2 || day_str.len() != 2 {
3935 return None;
3936 }
3937 let month: u32 = month_str.parse().ok()?;
3938 let day: u32 = day_str.parse().ok()?;
3939 let max_day = days_in_month_i64(year, month)?;
3940 if day == 0 || day > max_day {
3941 return None;
3942 }
3943 Some(ExtendedDate { year, month, day })
3944}
3945
3946fn parse_extended_localdatetime_string(s: &str) -> Option<ExtendedLocalDateTime> {
3947 let (date_part, time_part) = if let Some((d, t)) = s.split_once('T') {
3948 (d, Some(t))
3949 } else {
3950 (s, None)
3951 };
3952
3953 let date = parse_extended_date_string(date_part)?;
3954
3955 let Some(time_part) = time_part else {
3956 return Some(ExtendedLocalDateTime {
3957 date,
3958 hour: 0,
3959 minute: 0,
3960 second: 0,
3961 nanosecond: 0,
3962 });
3963 };
3964
3965 if time_part.contains('+') || time_part.contains('Z') || time_part.contains('z') {
3966 return None;
3967 }
3968 let (hms_part, frac_part) = if let Some((hms, frac)) = time_part.split_once('.') {
3969 (hms, Some(frac))
3970 } else {
3971 (time_part, None)
3972 };
3973 let mut parts = hms_part.split(':');
3974 let hour: u32 = parts.next()?.parse().ok()?;
3975 let minute: u32 = parts.next()?.parse().ok()?;
3976 let second: u32 = parts.next().map(|v| v.parse().ok()).unwrap_or(Some(0))?;
3977 if parts.next().is_some() {
3978 return None;
3979 }
3980 if hour > 23 || minute > 59 || second > 59 {
3981 return None;
3982 }
3983
3984 let nanosecond = if let Some(frac) = frac_part {
3985 if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
3986 return None;
3987 }
3988 let mut frac_buf = frac.to_string();
3989 if frac_buf.len() > 9 {
3990 frac_buf.truncate(9);
3991 }
3992 while frac_buf.len() < 9 {
3993 frac_buf.push('0');
3994 }
3995 frac_buf.parse().ok()?
3996 } else {
3997 0
3998 };
3999
4000 Some(ExtendedLocalDateTime {
4001 date,
4002 hour,
4003 minute,
4004 second,
4005 nanosecond,
4006 })
4007}
4008
4009fn days_from_civil(date: ExtendedDate) -> i128 {
4010 let mut y = date.year;
4012 let m = date.month as i64;
4013 let d = date.day as i64;
4014 y -= if m <= 2 { 1 } else { 0 };
4015 let era = y.div_euclid(400);
4016 let yoe = y - era * 400;
4017 let mp = m + if m > 2 { -3 } else { 9 };
4018 let doy = (153 * mp + 2) / 5 + d - 1;
4019 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
4020 era as i128 * 146_097 + doe as i128 - 719_468
4021}
4022
4023fn calendar_months_between_extended(start: &ExtendedDate, end: &ExtendedDate) -> i64 {
4024 let year_diff = end.year - start.year;
4025 let month_diff = end.month as i64 - start.month as i64;
4026 let total_months = year_diff * 12 + month_diff;
4027
4028 if total_months > 0 && end.day < start.day {
4029 total_months - 1
4030 } else if total_months < 0 && end.day > start.day {
4031 total_months + 1
4032 } else {
4033 total_months
4034 }
4035}
4036
4037fn add_months_to_extended_date(date: ExtendedDate, months: i64) -> ExtendedDate {
4038 if months == 0 {
4039 return date;
4040 }
4041
4042 let total_months = date.year as i128 * 12 + (date.month as i128 - 1) + months as i128;
4043 let year = total_months.div_euclid(12) as i64;
4044 let month = (total_months.rem_euclid(12) + 1) as u32;
4045 let max_day = days_in_month_i64(year, month).unwrap_or(31);
4046 let day = date.day.min(max_day);
4047
4048 ExtendedDate { year, month, day }
4049}
4050
4051fn remaining_days_after_months_extended(
4052 start: &ExtendedDate,
4053 end: &ExtendedDate,
4054 months: i64,
4055) -> i64 {
4056 let after_months = add_months_to_extended_date(*start, months);
4057 (days_from_civil(*end) - days_from_civil(after_months)) as i64
4058}
4059
4060fn try_extended_date_from_value(val: &Value) -> Option<ExtendedDate> {
4061 match val {
4062 Value::String(s) => parse_extended_date_string(s),
4063 _ => None,
4064 }
4065}
4066
4067fn try_extended_localdatetime_from_value(val: &Value) -> Option<ExtendedLocalDateTime> {
4068 match val {
4069 Value::String(s) => parse_extended_localdatetime_string(s),
4070 _ => None,
4071 }
4072}
4073
4074fn try_eval_duration_between_extended(args: &[Value]) -> Result<Option<Value>> {
4075 let Some(start) = try_extended_date_from_value(&args[0]) else {
4076 return Ok(None);
4077 };
4078 let Some(end) = try_extended_date_from_value(&args[1]) else {
4079 return Ok(None);
4080 };
4081
4082 let months = calendar_months_between_extended(&start, &end);
4083 let remaining_days = remaining_days_after_months_extended(&start, &end, months);
4084 let dur = CypherDuration::new(months, remaining_days, 0);
4085 Ok(Some(Value::String(dur.to_iso8601())))
4086}
4087
4088fn format_time_only_duration_nanos(total_nanos: i128) -> String {
4089 if total_nanos == 0 {
4090 return "PT0S".to_string();
4091 }
4092 let total_secs = total_nanos / NANOS_PER_SECOND as i128;
4093 let rem_nanos = total_nanos % NANOS_PER_SECOND as i128;
4094
4095 let hours = total_secs / 3600;
4096 let rem_after_hours = total_secs % 3600;
4097 let minutes = rem_after_hours / 60;
4098 let seconds = rem_after_hours % 60;
4099
4100 let mut out = String::from("PT");
4101 if hours != 0 {
4102 out.push_str(&format!("{hours}H"));
4103 }
4104 if minutes != 0 {
4105 out.push_str(&format!("{minutes}M"));
4106 }
4107 if seconds != 0 || rem_nanos != 0 {
4108 if rem_nanos == 0 {
4109 out.push_str(&format!("{seconds}S"));
4110 } else {
4111 let sign = if total_nanos < 0 && seconds == 0 {
4112 "-"
4113 } else {
4114 ""
4115 };
4116 let secs_abs = seconds.abs();
4117 let nanos_abs = rem_nanos.abs();
4118 let frac = format!("{nanos_abs:09}");
4119 let trimmed = frac.trim_end_matches('0');
4120 out.push_str(&format!("{sign}{secs_abs}.{trimmed}S"));
4121 }
4122 }
4123 if out == "PT" { "PT0S".to_string() } else { out }
4124}
4125
4126fn try_eval_duration_in_seconds_extended(args: &[Value]) -> Result<Option<Value>> {
4127 let Some(start) = try_extended_localdatetime_from_value(&args[0]) else {
4128 return Ok(None);
4129 };
4130 let Some(end) = try_extended_localdatetime_from_value(&args[1]) else {
4131 return Ok(None);
4132 };
4133
4134 let start_days = days_from_civil(start.date);
4135 let end_days = days_from_civil(end.date);
4136 let start_tod_nanos =
4137 (start.hour as i128 * 3600 + start.minute as i128 * 60 + start.second as i128)
4138 * NANOS_PER_SECOND as i128
4139 + start.nanosecond as i128;
4140 let end_tod_nanos = (end.hour as i128 * 3600 + end.minute as i128 * 60 + end.second as i128)
4141 * NANOS_PER_SECOND as i128
4142 + end.nanosecond as i128;
4143 let total_nanos =
4144 (end_days - start_days) * NANOS_PER_DAY as i128 + (end_tod_nanos - start_tod_nanos);
4145
4146 if total_nanos >= i64::MIN as i128 && total_nanos <= i64::MAX as i128 {
4147 let dur = CypherDuration::new(0, 0, total_nanos as i64);
4148 Ok(Some(dur.to_temporal_value()))
4149 } else {
4150 Ok(Some(Value::String(format_time_only_duration_nanos(
4151 total_nanos,
4152 ))))
4153 }
4154}
4155
4156fn calendar_months_between(start: &NaiveDate, end: &NaiveDate) -> i64 {
4161 let year_diff = end.year() as i64 - start.year() as i64;
4162 let month_diff = end.month() as i64 - start.month() as i64;
4163 let total_months = year_diff * 12 + month_diff;
4164
4165 if total_months > 0 && end.day() < start.day() {
4167 total_months - 1
4168 } else if total_months < 0 && end.day() > start.day() {
4169 total_months + 1
4170 } else {
4171 total_months
4172 }
4173}
4174
4175fn remaining_days_after_months(start: &NaiveDate, end: &NaiveDate, months: i64) -> i64 {
4177 let after_months = add_months_to_date(*start, months);
4178 end.signed_duration_since(after_months).num_days()
4179}
4180
4181fn eval_duration_between(args: &[Value]) -> Result<Value> {
4182 if args.len() < 2 {
4183 return Err(anyhow!("duration.between requires two temporal arguments"));
4184 }
4185 if args[0].is_null() || args[1].is_null() {
4186 return Ok(Value::Null);
4187 }
4188
4189 let start_res = parse_temporal_value_typed(&args[0]);
4190 let end_res = parse_temporal_value_typed(&args[1]);
4191 let (start, end) = match (start_res, end_res) {
4192 (Ok(start), Ok(end)) => (start, end),
4193 (start_res, end_res) => {
4194 if let Some(value) = try_eval_duration_between_extended(args)? {
4195 return Ok(value);
4196 }
4197 return Err(start_res
4198 .err()
4199 .or_else(|| end_res.err())
4200 .unwrap_or_else(|| anyhow!("duration.between requires two temporal arguments")));
4201 }
4202 };
4203
4204 let start_has_date = has_date_component(start.ttype);
4205 let end_has_date = has_date_component(end.ttype);
4206 let start_has_time = has_time_component(start.ttype);
4207 let end_has_time = has_time_component(end.ttype);
4208
4209 if start.ttype == TemporalType::Date && end.ttype == TemporalType::Date {
4211 let months = calendar_months_between(&start.local_date, &end.local_date);
4212 let remaining_days =
4213 remaining_days_after_months(&start.local_date, &end.local_date, months);
4214 let dur = CypherDuration::new(months, remaining_days, 0);
4215 return Ok(dur.to_temporal_value());
4216 }
4217
4218 if start_has_date && end_has_date && start_has_time && end_has_time {
4221 let tz_aware = both_tz_aware(&start, &end);
4222 let (s_date, s_time, e_date, e_time) = if tz_aware {
4223 (
4224 start.utc_datetime.date(),
4225 start.utc_datetime.time(),
4226 end.utc_datetime.date(),
4227 end.utc_datetime.time(),
4228 )
4229 } else {
4230 (
4231 start.local_date,
4232 start.local_time,
4233 end.local_date,
4234 end.local_time,
4235 )
4236 };
4237
4238 let months = calendar_months_between(&s_date, &e_date);
4239 let date_after_months = add_months_to_date(s_date, months);
4240 let start_dt = NaiveDateTime::new(date_after_months, s_time);
4241 let end_dt = NaiveDateTime::new(e_date, e_time);
4242 let remaining_nanos = end_dt
4243 .signed_duration_since(start_dt)
4244 .num_nanoseconds()
4245 .unwrap_or(0);
4246
4247 let dur = CypherDuration::new(months, 0, remaining_nanos);
4248 return Ok(dur.to_temporal_value());
4249 }
4250
4251 if start_has_date && end_has_date {
4253 let tz_aware = both_tz_aware(&start, &end);
4254 let (s_date, s_time, e_date, e_time) = if tz_aware {
4255 (
4256 start.utc_datetime.date(),
4257 start.utc_datetime.time(),
4258 end.utc_datetime.date(),
4259 end.utc_datetime.time(),
4260 )
4261 } else {
4262 (
4263 start.local_date,
4264 start.local_time,
4265 end.local_date,
4266 end.local_time,
4267 )
4268 };
4269
4270 let months = calendar_months_between(&s_date, &e_date);
4271 let date_after_months = add_months_to_date(s_date, months);
4272 let start_dt = NaiveDateTime::new(date_after_months, s_time);
4273 let end_dt = NaiveDateTime::new(e_date, e_time);
4274 let remaining = end_dt.signed_duration_since(start_dt);
4275 let remaining_days = remaining.num_days();
4276 let remaining_nanos =
4277 remaining.num_nanoseconds().unwrap_or(0) - remaining_days * 86_400_000_000_000;
4278
4279 let dur = CypherDuration::new(months, remaining_days, remaining_nanos);
4280 return Ok(dur.to_temporal_value());
4281 }
4282
4283 let tz_aware = both_tz_aware(&start, &end);
4286 let start_time = if tz_aware {
4287 start.utc_datetime.time()
4288 } else {
4289 start.local_time
4290 };
4291 let end_time = if tz_aware {
4292 end.utc_datetime.time()
4293 } else {
4294 end.local_time
4295 };
4296
4297 let start_nanos = time_to_nanos(&start_time);
4298 let end_nanos = time_to_nanos(&end_time);
4299 let nanos_diff = end_nanos - start_nanos;
4300
4301 let dur = CypherDuration::new(0, 0, nanos_diff);
4302 Ok(dur.to_temporal_value())
4303}
4304
4305fn has_date_component(ttype: TemporalType) -> bool {
4307 matches!(
4308 ttype,
4309 TemporalType::Date | TemporalType::LocalDateTime | TemporalType::DateTime
4310 )
4311}
4312
4313fn has_time_component(ttype: TemporalType) -> bool {
4315 matches!(
4316 ttype,
4317 TemporalType::LocalTime
4318 | TemporalType::Time
4319 | TemporalType::LocalDateTime
4320 | TemporalType::DateTime
4321 )
4322}
4323
4324fn eval_duration_in_months(args: &[Value]) -> Result<Value> {
4325 if args.len() < 2 {
4326 return Err(anyhow!("duration.inMonths requires two temporal arguments"));
4327 }
4328 if args[0].is_null() || args[1].is_null() {
4329 return Ok(Value::Null);
4330 }
4331
4332 let start = parse_temporal_value_typed(&args[0])?;
4333 let end = parse_temporal_value_typed(&args[1])?;
4334
4335 if has_date_component(start.ttype) && has_date_component(end.ttype) {
4336 let tz_aware = both_tz_aware(&start, &end);
4338 let (s_date, s_time, e_date, e_time) = if tz_aware {
4339 (
4340 start.utc_datetime.date(),
4341 start.utc_datetime.time(),
4342 end.utc_datetime.date(),
4343 end.utc_datetime.time(),
4344 )
4345 } else {
4346 (
4347 start.local_date,
4348 start.local_time,
4349 end.local_date,
4350 end.local_time,
4351 )
4352 };
4353 let mut months = calendar_months_between(&s_date, &e_date);
4354 if s_date.day() == e_date.day() {
4359 if months > 0 && e_time < s_time {
4360 months -= 1;
4361 } else if months < 0 && e_time > s_time {
4362 months += 1;
4363 }
4364 }
4365 let dur = CypherDuration::new(months, 0, 0);
4366 Ok(dur.to_temporal_value())
4367 } else {
4368 Ok(Value::Temporal(TemporalValue::Duration {
4369 months: 0,
4370 days: 0,
4371 nanos: 0,
4372 }))
4373 }
4374}
4375
4376fn eval_duration_in_days(args: &[Value]) -> Result<Value> {
4377 if args.len() < 2 {
4378 return Err(anyhow!("duration.inDays requires two temporal arguments"));
4379 }
4380 if args[0].is_null() || args[1].is_null() {
4381 return Ok(Value::Null);
4382 }
4383
4384 let start = parse_temporal_value_typed(&args[0])?;
4385 let end = parse_temporal_value_typed(&args[1])?;
4386
4387 if has_date_component(start.ttype) && has_date_component(end.ttype) {
4388 let tz_aware = both_tz_aware(&start, &end);
4390 let (s_dt, e_dt) = if tz_aware {
4391 (start.utc_datetime, end.utc_datetime)
4392 } else {
4393 (
4394 NaiveDateTime::new(start.local_date, start.local_time),
4395 NaiveDateTime::new(end.local_date, end.local_time),
4396 )
4397 };
4398 let total_nanos = e_dt
4400 .signed_duration_since(s_dt)
4401 .num_nanoseconds()
4402 .ok_or_else(|| anyhow!("Duration overflow in inDays"))?;
4403 let days = total_nanos / 86_400_000_000_000;
4404 let dur = CypherDuration::new(0, days, 0);
4405 Ok(dur.to_temporal_value())
4406 } else {
4407 Ok(Value::Temporal(TemporalValue::Duration {
4408 months: 0,
4409 days: 0,
4410 nanos: 0,
4411 }))
4412 }
4413}
4414
4415fn normalize_local_to_utc(ndt: NaiveDateTime, tz: Tz) -> Result<NaiveDateTime> {
4421 use chrono::TimeZone;
4422 match tz.from_local_datetime(&ndt) {
4423 chrono::LocalResult::Single(dt) => Ok(dt.naive_utc()),
4424 chrono::LocalResult::Ambiguous(earliest, _) => Ok(earliest.naive_utc()),
4425 chrono::LocalResult::None => {
4426 let shifted = ndt + chrono::Duration::hours(1);
4428 match tz.from_local_datetime(&shifted) {
4429 chrono::LocalResult::Single(dt) => Ok(dt.naive_utc()),
4430 chrono::LocalResult::Ambiguous(earliest, _) => Ok(earliest.naive_utc()),
4431 _ => Err(anyhow!("Cannot resolve local time in timezone")),
4432 }
4433 }
4434 }
4435}
4436
4437fn eval_duration_in_seconds(args: &[Value]) -> Result<Value> {
4438 if args.len() < 2 {
4439 return Err(anyhow!(
4440 "duration.inSeconds requires two temporal arguments"
4441 ));
4442 }
4443 if args[0].is_null() || args[1].is_null() {
4444 return Ok(Value::Null);
4445 }
4446
4447 let start_res = parse_temporal_value_typed(&args[0]);
4448 let end_res = parse_temporal_value_typed(&args[1]);
4449 let (start, end) = match (start_res, end_res) {
4450 (Ok(start), Ok(end)) => (start, end),
4451 (start_res, end_res) => {
4452 if let Some(value) = try_eval_duration_in_seconds_extended(args)? {
4453 return Ok(value);
4454 }
4455 return Err(start_res
4456 .err()
4457 .or_else(|| end_res.err())
4458 .unwrap_or_else(|| anyhow!("duration.inSeconds requires two temporal arguments")));
4459 }
4460 };
4461
4462 let start_has_date = has_date_component(start.ttype);
4463 let end_has_date = has_date_component(end.ttype);
4464
4465 let shared_named_tz = start.named_tz.or(end.named_tz);
4469
4470 let have_tz = both_tz_aware(&start, &end);
4479
4480 let resolve =
4481 |pt: &ParsedTemporal, date_override: Option<NaiveDate>| -> Result<NaiveDateTime> {
4482 let local_date = date_override.unwrap_or(pt.local_date);
4483 let local_ndt = NaiveDateTime::new(local_date, pt.local_time);
4484
4485 if let Some(tz) = shared_named_tz {
4486 if pt.named_tz.is_some() && date_override.is_none() {
4488 Ok(pt.utc_datetime)
4490 } else {
4491 normalize_local_to_utc(local_ndt, tz)
4493 }
4494 } else if have_tz {
4495 if date_override.is_some() {
4497 let offset = pt.utc_offset_secs.unwrap_or(0);
4498 Ok(local_ndt - chrono::Duration::seconds(offset as i64))
4499 } else {
4500 Ok(pt.utc_datetime)
4501 }
4502 } else {
4503 Ok(local_ndt)
4505 }
4506 };
4507
4508 if !start_has_date || !end_has_date {
4510 if shared_named_tz.is_some() {
4511 let ref_date = if start_has_date {
4514 start.local_date
4515 } else if end_has_date {
4516 end.local_date
4517 } else {
4518 NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()
4519 };
4520 let s_dt = resolve(&start, Some(ref_date))?;
4521 let e_dt = resolve(&end, Some(ref_date))?;
4522 let total_nanos = e_dt
4523 .signed_duration_since(s_dt)
4524 .num_nanoseconds()
4525 .ok_or_else(|| anyhow!("Duration overflow in inSeconds"))?;
4526 let dur = CypherDuration::new(0, 0, total_nanos);
4527 return Ok(dur.to_temporal_value());
4528 }
4529
4530 let s_time = if have_tz {
4532 start.utc_datetime.time()
4533 } else {
4534 start.local_time
4535 };
4536 let e_time = if have_tz {
4537 end.utc_datetime.time()
4538 } else {
4539 end.local_time
4540 };
4541 let s_nanos = time_to_nanos(&s_time);
4542 let e_nanos = time_to_nanos(&e_time);
4543 let dur = CypherDuration::new(0, 0, e_nanos - s_nanos);
4544 return Ok(dur.to_temporal_value());
4545 }
4546
4547 let s_dt = resolve(&start, None)?;
4549 let e_dt = resolve(&end, None)?;
4550 let total_nanos = e_dt
4551 .signed_duration_since(s_dt)
4552 .num_nanoseconds()
4553 .ok_or_else(|| anyhow!("Duration overflow in inSeconds"))?;
4554
4555 let dur = CypherDuration::new(0, 0, total_nanos);
4556 Ok(dur.to_temporal_value())
4557}
4558
4559struct ParsedTemporal {
4561 local_date: NaiveDate,
4563 local_time: NaiveTime,
4565 utc_datetime: NaiveDateTime,
4567 ttype: TemporalType,
4569 utc_offset_secs: Option<i32>,
4571 named_tz: Option<Tz>,
4573}
4574
4575fn both_tz_aware(a: &ParsedTemporal, b: &ParsedTemporal) -> bool {
4577 a.utc_offset_secs.is_some() && b.utc_offset_secs.is_some()
4578}
4579
4580fn parse_temporal_value_typed(val: &Value) -> Result<ParsedTemporal> {
4582 let midnight =
4583 NaiveTime::from_hms_opt(0, 0, 0).ok_or_else(|| anyhow!("Failed to create midnight"))?;
4584 let epoch_date = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
4585
4586 match val {
4587 Value::String(s) => {
4588 let ttype = classify_temporal(s)
4589 .ok_or_else(|| anyhow!("Cannot classify temporal value: {}", s))?;
4590
4591 match ttype {
4592 TemporalType::DateTime => {
4593 let (date, time, tz_info) = parse_datetime_with_tz(s)?;
4594 let local_ndt = NaiveDateTime::new(date, time);
4595 let iana_tz = tz_info.as_ref().and_then(|info| match info {
4596 TimezoneInfo::Named(tz) => Some(*tz),
4597 _ => None,
4598 });
4599 let offset_secs = if let Some(ref info) = tz_info {
4600 info.offset_for_local(&local_ndt)?.local_minus_utc()
4601 } else {
4602 0
4603 };
4604 let utc_ndt = local_ndt - chrono::Duration::seconds(offset_secs as i64);
4605 Ok(ParsedTemporal {
4606 local_date: date,
4607 local_time: time,
4608 utc_datetime: utc_ndt,
4609 ttype,
4610 utc_offset_secs: Some(offset_secs),
4611
4612 named_tz: iana_tz,
4613 })
4614 }
4615 TemporalType::LocalDateTime => {
4616 let ndt = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")
4617 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f"))
4618 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M"))
4619 .map_err(|_| anyhow!("Cannot parse localdatetime: {}", s))?;
4620 Ok(ParsedTemporal {
4621 local_date: ndt.date(),
4622 local_time: ndt.time(),
4623 utc_datetime: ndt,
4624 ttype,
4625 utc_offset_secs: None,
4626
4627 named_tz: None,
4628 })
4629 }
4630 TemporalType::Date => {
4631 let d = NaiveDate::parse_from_str(s, "%Y-%m-%d")
4632 .map_err(|_| anyhow!("Cannot parse date: {}", s))?;
4633 let ndt = NaiveDateTime::new(d, midnight);
4634 Ok(ParsedTemporal {
4635 local_date: d,
4636 local_time: midnight,
4637 utc_datetime: ndt,
4638 ttype,
4639 utc_offset_secs: None,
4640
4641 named_tz: None,
4642 })
4643 }
4644 TemporalType::Time => {
4645 let (_, time, tz_info) = parse_datetime_with_tz(s)?;
4646 let offset_secs = if let Some(ref info) = tz_info {
4647 let dummy_ndt = NaiveDateTime::new(epoch_date, time);
4648 info.offset_for_local(&dummy_ndt)?.local_minus_utc()
4649 } else {
4650 0
4651 };
4652 let local_ndt = NaiveDateTime::new(epoch_date, time);
4653 let utc_ndt = local_ndt - chrono::Duration::seconds(offset_secs as i64);
4654 Ok(ParsedTemporal {
4655 local_date: epoch_date,
4656 local_time: time,
4657 utc_datetime: utc_ndt,
4658 ttype,
4659 utc_offset_secs: Some(offset_secs),
4660
4661 named_tz: None,
4662 })
4663 }
4664 TemporalType::LocalTime => {
4665 let time = parse_time_string(s)?;
4666 let ndt = NaiveDateTime::new(epoch_date, time);
4667 Ok(ParsedTemporal {
4668 local_date: epoch_date,
4669 local_time: time,
4670 utc_datetime: ndt,
4671 ttype,
4672 utc_offset_secs: None,
4673
4674 named_tz: None,
4675 })
4676 }
4677 TemporalType::Duration | TemporalType::Btic => {
4678 Err(anyhow!("Cannot use {:?} as temporal argument", ttype))
4679 }
4680 }
4681 }
4682 Value::Temporal(tv) => {
4683 let ttype = tv.temporal_type();
4684 match tv {
4685 TemporalValue::Date { days_since_epoch } => {
4686 let d = epoch_date + chrono::Duration::days(*days_since_epoch as i64);
4687 let ndt = NaiveDateTime::new(d, midnight);
4688 Ok(ParsedTemporal {
4689 local_date: d,
4690 local_time: midnight,
4691 utc_datetime: ndt,
4692 ttype,
4693 utc_offset_secs: None,
4694 named_tz: None,
4695 })
4696 }
4697 TemporalValue::LocalTime {
4698 nanos_since_midnight,
4699 } => {
4700 let time = nanos_to_time(*nanos_since_midnight);
4701 let ndt = NaiveDateTime::new(epoch_date, time);
4702 Ok(ParsedTemporal {
4703 local_date: epoch_date,
4704 local_time: time,
4705 utc_datetime: ndt,
4706 ttype,
4707 utc_offset_secs: None,
4708 named_tz: None,
4709 })
4710 }
4711 TemporalValue::Time {
4712 nanos_since_midnight,
4713 offset_seconds,
4714 } => {
4715 let time = nanos_to_time(*nanos_since_midnight);
4716 let local_ndt = NaiveDateTime::new(epoch_date, time);
4717 let utc_ndt = local_ndt - chrono::Duration::seconds(*offset_seconds as i64);
4718 Ok(ParsedTemporal {
4719 local_date: epoch_date,
4720 local_time: time,
4721 utc_datetime: utc_ndt,
4722 ttype,
4723 utc_offset_secs: Some(*offset_seconds),
4724 named_tz: None,
4725 })
4726 }
4727 TemporalValue::LocalDateTime { nanos_since_epoch } => {
4728 let ndt =
4729 chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch).naive_utc();
4730 Ok(ParsedTemporal {
4731 local_date: ndt.date(),
4732 local_time: ndt.time(),
4733 utc_datetime: ndt,
4734 ttype,
4735 utc_offset_secs: None,
4736 named_tz: None,
4737 })
4738 }
4739 TemporalValue::DateTime {
4740 nanos_since_epoch,
4741 offset_seconds,
4742 timezone_name,
4743 } => {
4744 let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
4746 let local_ndt = chrono::DateTime::from_timestamp_nanos(local_nanos).naive_utc();
4747 let utc_ndt =
4748 chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch).naive_utc();
4749 let iana_tz = timezone_name
4750 .as_deref()
4751 .and_then(|name| name.parse::<chrono_tz::Tz>().ok());
4752 Ok(ParsedTemporal {
4753 local_date: local_ndt.date(),
4754 local_time: local_ndt.time(),
4755 utc_datetime: utc_ndt,
4756 ttype,
4757 utc_offset_secs: Some(*offset_seconds),
4758 named_tz: iana_tz,
4759 })
4760 }
4761 TemporalValue::Duration { .. } | TemporalValue::Btic { .. } => Err(anyhow!(
4762 "Cannot use {:?} as temporal argument",
4763 tv.temporal_type()
4764 )),
4765 }
4766 }
4767 _ => Err(anyhow!("Expected temporal value, got: {:?}", val)),
4768 }
4769}
4770
4771#[cfg(test)]
4776mod tests {
4777 use super::*;
4778
4779 fn map_val(pairs: Vec<(&str, Value)>) -> Value {
4781 Value::Map(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
4782 }
4783
4784 #[test]
4785 fn test_parse_datetime_utc_accepts_bracketed_timezone_suffix() {
4786 let dt = parse_datetime_utc("2020-01-01T00:00Z[UTC]").unwrap();
4787 assert_eq!(dt.to_rfc3339(), "2020-01-01T00:00:00+00:00");
4788
4789 let dt = parse_datetime_utc("2020-01-01T01:00:00+01:00[Europe/Paris]").unwrap();
4790 assert_eq!(dt.to_rfc3339(), "2020-01-01T00:00:00+00:00");
4791 }
4792
4793 #[test]
4794 fn test_date_from_map_calendar() {
4795 let result = eval_date(&[map_val(vec![
4796 ("year", Value::Int(1984)),
4797 ("month", Value::Int(10)),
4798 ("day", Value::Int(11)),
4799 ])])
4800 .unwrap();
4801 assert_eq!(result.to_string(), "1984-10-11");
4802 }
4803
4804 #[test]
4805 fn test_date_from_map_defaults() {
4806 let result = eval_date(&[map_val(vec![("year", Value::Int(1984))])]).unwrap();
4807 assert_eq!(result.to_string(), "1984-01-01");
4808 }
4809
4810 #[test]
4811 fn test_date_from_week() {
4812 let result = eval_date(&[map_val(vec![
4814 ("year", Value::Int(1984)),
4815 ("week", Value::Int(10)),
4816 ("dayOfWeek", Value::Int(3)),
4817 ])])
4818 .unwrap();
4819 assert!(result.to_string().starts_with("1984-03"));
4820 }
4821
4822 #[test]
4823 fn test_date_from_ordinal() {
4824 let result = eval_date(&[map_val(vec![
4826 ("year", Value::Int(1984)),
4827 ("ordinalDay", Value::Int(202)),
4828 ])])
4829 .unwrap();
4830 assert_eq!(result.to_string(), "1984-07-20");
4831 }
4832
4833 #[test]
4834 fn test_date_from_quarter() {
4835 let result = eval_date(&[map_val(vec![
4837 ("year", Value::Int(1984)),
4838 ("quarter", Value::Int(3)),
4839 ("dayOfQuarter", Value::Int(45)),
4840 ])])
4841 .unwrap();
4842 assert_eq!(result.to_string(), "1984-08-14");
4843 }
4844
4845 #[test]
4846 fn test_time_from_map() {
4847 let result = eval_time(&[map_val(vec![
4848 ("hour", Value::Int(12)),
4849 ("minute", Value::Int(31)),
4850 ("second", Value::Int(14)),
4851 ])])
4852 .unwrap();
4853 assert_eq!(result.to_string(), "12:31:14Z");
4854 }
4855
4856 #[test]
4857 fn test_time_from_map_with_nanos() {
4858 let result = eval_time(&[map_val(vec![
4859 ("hour", Value::Int(12)),
4860 ("minute", Value::Int(31)),
4861 ("second", Value::Int(14)),
4862 ("millisecond", Value::Int(645)),
4863 ("microsecond", Value::Int(876)),
4864 ("nanosecond", Value::Int(123)),
4865 ])])
4866 .unwrap();
4867 assert!(result.to_string().starts_with("12:31:14.645876"));
4869 }
4870
4871 #[test]
4872 fn test_datetime_from_map() {
4873 let result = eval_datetime(&[map_val(vec![
4874 ("year", Value::Int(1984)),
4875 ("month", Value::Int(10)),
4876 ("day", Value::Int(11)),
4877 ("hour", Value::Int(12)),
4878 ])])
4879 .unwrap();
4880 assert!(result.to_string().contains("1984-10-11T12:00"));
4881 }
4882
4883 #[test]
4884 fn test_localdatetime_from_week() {
4885 let result = eval_localdatetime(&[map_val(vec![
4887 ("year", Value::Int(1816)),
4888 ("week", Value::Int(1)),
4889 ])])
4890 .unwrap();
4891 assert_eq!(result.to_string(), "1816-01-01T00:00");
4892
4893 let result = eval_localdatetime(&[map_val(vec![
4895 ("year", Value::Int(1816)),
4896 ("week", Value::Int(52)),
4897 ])])
4898 .unwrap();
4899 assert_eq!(result.to_string(), "1816-12-23T00:00");
4900
4901 let result = eval_localdatetime(&[map_val(vec![
4903 ("year", Value::Int(1817)),
4904 ("week", Value::Int(1)),
4905 ])])
4906 .unwrap();
4907 assert_eq!(result.to_string(), "1816-12-30T00:00");
4908 }
4909
4910 #[test]
4911 fn test_duration_from_map_extended() {
4912 let result = eval_duration(&[map_val(vec![
4913 ("years", Value::Int(1)),
4914 ("months", Value::Int(2)),
4915 ("days", Value::Int(3)),
4916 ])])
4917 .unwrap();
4918 let dur_str = result.to_string();
4920 assert!(dur_str.starts_with('P'));
4921 assert!(dur_str.contains('Y')); assert!(dur_str.contains('D')); }
4924
4925 #[test]
4926 fn test_datetime_fromepoch() {
4927 let result = eval_datetime_fromepoch(&[Value::Int(0)]).unwrap();
4928 assert_eq!(result.to_string(), "1970-01-01T00:00Z");
4929 }
4930
4931 #[test]
4932 fn test_datetime_fromepochmillis() {
4933 let result = eval_datetime_fromepochmillis(&[Value::Int(0)]).unwrap();
4934 assert_eq!(result.to_string(), "1970-01-01T00:00Z");
4935 }
4936
4937 #[test]
4938 fn test_truncate_date_year() {
4939 let result = eval_truncate(
4940 "date",
4941 &[
4942 Value::String("year".to_string()),
4943 Value::String("1984-10-11".to_string()),
4944 ],
4945 )
4946 .unwrap();
4947 assert_eq!(result.to_string(), "1984-01-01");
4948 }
4949
4950 #[test]
4951 fn test_truncate_date_month() {
4952 let result = eval_truncate(
4953 "date",
4954 &[
4955 Value::String("month".to_string()),
4956 Value::String("1984-10-11".to_string()),
4957 ],
4958 )
4959 .unwrap();
4960 assert_eq!(result.to_string(), "1984-10-01");
4961 }
4962
4963 #[test]
4964 fn test_truncate_datetime_hour() {
4965 let result = eval_truncate(
4966 "datetime",
4967 &[
4968 Value::String("hour".to_string()),
4969 Value::String("1984-10-11T12:31:14Z".to_string()),
4970 ],
4971 )
4972 .unwrap();
4973 assert!(result.to_string().contains("1984-10-11T12:00"));
4974 }
4975
4976 #[test]
4977 fn test_duration_between() {
4978 let result = eval_duration_between(&[
4979 Value::String("1984-10-11".to_string()),
4980 Value::String("1984-10-12".to_string()),
4981 ])
4982 .unwrap();
4983 assert_eq!(result.to_string(), "P1D");
4984 }
4985
4986 #[test]
4987 fn test_duration_in_days() {
4988 let result = eval_duration_in_days(&[
4989 Value::String("1984-10-11".to_string()),
4990 Value::String("1984-10-21".to_string()),
4991 ])
4992 .unwrap();
4993 assert_eq!(result.to_string(), "P10D");
4994 }
4995
4996 #[test]
4997 fn test_duration_in_months() {
4998 let result = eval_duration_in_months(&[
4999 Value::String("1984-10-11".to_string()),
5000 Value::String("1985-01-11".to_string()),
5001 ])
5002 .unwrap();
5003 assert_eq!(result.to_string(), "P3M");
5004 }
5005
5006 #[test]
5007 fn test_duration_in_seconds() {
5008 let result = eval_duration_in_seconds(&[
5009 Value::String("1984-10-11T12:00:00".to_string()),
5010 Value::String("1984-10-11T13:00:00".to_string()),
5011 ])
5012 .unwrap();
5013 assert_eq!(result.to_string(), "PT1H");
5014 }
5015
5016 #[test]
5017 fn test_classify_temporal() {
5018 assert_eq!(classify_temporal("1984-10-11"), Some(TemporalType::Date));
5019 assert_eq!(classify_temporal("12:31:14"), Some(TemporalType::LocalTime));
5020 assert_eq!(
5021 classify_temporal("12:31:14+01:00"),
5022 Some(TemporalType::Time)
5023 );
5024 assert_eq!(
5025 classify_temporal("1984-10-11T12:31:14"),
5026 Some(TemporalType::LocalDateTime)
5027 );
5028 assert_eq!(
5029 classify_temporal("1984-10-11T12:31:14Z"),
5030 Some(TemporalType::DateTime)
5031 );
5032 assert_eq!(
5033 classify_temporal("1984-10-11T12:31:14+01:00"),
5034 Some(TemporalType::DateTime)
5035 );
5036 assert_eq!(classify_temporal("P1Y2M3D"), Some(TemporalType::Duration));
5037 }
5038
5039 #[test]
5040 fn test_add_months_to_date_clamping() {
5041 let date = NaiveDate::from_ymd_opt(2023, 1, 31).unwrap();
5043 let result = add_months_to_date(date, 1);
5044 assert_eq!(result, NaiveDate::from_ymd_opt(2023, 2, 28).unwrap());
5045
5046 let date = NaiveDate::from_ymd_opt(2024, 1, 31).unwrap();
5048 let result = add_months_to_date(date, 1);
5049 assert_eq!(result, NaiveDate::from_ymd_opt(2024, 2, 29).unwrap());
5050 }
5051
5052 #[test]
5053 fn test_cypher_duration_multiply() {
5054 let dur = CypherDuration::new(1, 1, 0);
5055 let result = dur.multiply(2.0);
5056 assert_eq!(result.months, 2);
5057 assert_eq!(result.days, 2);
5058 }
5059
5060 #[test]
5061 fn test_fractional_cascading_in_map() {
5062 let result = eval_duration(&[map_val(vec![
5065 ("months", Value::Float(5.5)),
5066 ("days", Value::Int(0)),
5067 ])])
5068 .unwrap();
5069 let s = result.to_string();
5070 assert_eq!(s, "P5M15DT5H14M33S");
5071 }
5072
5073 #[test]
5074 fn test_fractional_cascading_full() {
5075 let result = eval_duration(&[map_val(vec![
5076 ("years", Value::Float(12.5)),
5077 ("months", Value::Float(5.5)),
5078 ("days", Value::Float(14.5)),
5079 ("hours", Value::Float(16.5)),
5080 ("minutes", Value::Float(12.5)),
5081 ("seconds", Value::Float(70.5)),
5082 ("nanoseconds", Value::Int(3)),
5083 ])])
5084 .unwrap();
5085 let s = result.to_string();
5086 let dur = parse_duration_to_cypher(&s).unwrap();
5088 assert_eq!(dur.months, 155);
5089 assert_eq!(dur.days, 29);
5090 }
5091
5092 #[test]
5093 fn test_parse_iso8601_duration_with_weeks() {
5094 let micros = parse_duration_to_micros("P1W").unwrap();
5095 assert_eq!(micros, 7 * MICROS_PER_DAY);
5096 }
5097
5098 #[test]
5099 fn test_parse_iso8601_duration_complex() {
5100 let micros = parse_duration_to_micros("P1DT2H30M").unwrap();
5101 let expected = MICROS_PER_DAY + 2 * MICROS_PER_HOUR + 30 * MICROS_PER_MINUTE;
5102 assert_eq!(micros, expected);
5103 }
5104}