1use chrono::{NaiveDate, NaiveTime, Timelike};
16use std::collections::HashMap;
17
18pub enum TemporalField {
23 Int(i64),
25 Float(f64),
27 Str(String),
29 Date(i64),
31}
32
33type Fields = HashMap<String, TemporalField>;
35
36fn f_int(f: &Fields, k: &str) -> Option<i64> {
37 match f.get(k)? {
38 TemporalField::Int(n) => Some(*n),
39 _ => None,
40 }
41}
42
43fn f_num(f: &Fields, k: &str) -> Option<f64> {
44 #[allow(
45 clippy::cast_precision_loss,
46 reason = "duration counts are small; exactness beyond f64 is irrelevant"
47 )]
48 match f.get(k)? {
49 TemporalField::Int(n) => Some(*n as f64),
50 TemporalField::Float(x) => Some(*x),
51 _ => None,
52 }
53}
54
55fn f_str<'a>(f: &'a Fields, k: &str) -> Option<&'a str> {
56 match f.get(k)? {
57 TemporalField::Str(s) => Some(s),
58 _ => None,
59 }
60}
61
62fn f_date(f: &Fields, k: &str) -> Option<i64> {
63 match f.get(k)? {
64 TemporalField::Date(d) => Some(*d),
65 _ => None,
66 }
67}
68
69#[must_use]
73pub fn render_temporal_map(name: &str, fields: &Fields) -> Option<String> {
74 match name {
75 "date" => resolve_date(fields).map(format_date),
76 "localtime" => {
77 if !fields.contains_key("hour") {
78 return None;
79 }
80 Some(format_time(&resolve_time(fields)?))
81 }
82 "time" => build_time_map(fields),
83 "localdatetime" => {
84 let date = resolve_date(fields)?;
85 let time = resolve_time(fields)?;
86 Some(format!("{}T{}", format_date(date), format_time(&time)))
87 }
88 "datetime" => build_date_time_map(fields),
89 "duration" => Some(format_duration(&build_duration_map(fields)?)),
90 _ => None,
91 }
92}
93
94#[must_use]
97pub fn date_from_map(fields: &Fields) -> Option<i64> {
98 resolve_date(fields)
99}
100
101#[must_use]
104pub fn date_to_epoch_days(d: NaiveDate) -> Option<i64> {
105 let epoch = NaiveDate::from_ymd_opt(1970, 1, 1)?;
106 Some((d - epoch).num_days())
107}
108
109#[must_use]
115pub fn epoch_days_to_date(days: i64) -> Option<NaiveDate> {
116 NaiveDate::from_ymd_opt(1970, 1, 1)?.checked_add_signed(chrono::Duration::try_days(days)?)
117}
118
119#[must_use]
123pub fn parse_date_or_datetime_prefix(s: &str) -> Option<i64> {
124 let head = s.split_once('T').map_or(s, |(d, _)| d);
125 parse_date_string(head)
126}
127
128#[derive(Default)]
132pub struct DateOverrides {
133 pub year: Option<i64>,
135 pub month: Option<i64>,
137 pub day: Option<i64>,
139 pub week: Option<i64>,
141 pub day_of_week: Option<i64>,
143 pub ordinal_day: Option<i64>,
145 pub quarter: Option<i64>,
147 pub day_of_quarter: Option<i64>,
149}
150
151#[must_use]
155pub fn project_date(base_days: i64, o: &DateOverrides) -> Option<i64> {
156 use crate::calendar;
157 let (base_year, base_month, base_day) = calendar::civil_from_days(base_days);
158 let year = o.year.unwrap_or(base_year);
159 if o.week.is_some() || o.day_of_week.is_some() {
162 let (base_iso_year, base_week) = calendar::iso_week(base_days);
163 let iso_year = o.year.unwrap_or(base_iso_year);
164 let week = match o.week {
165 Some(w) => u32::try_from(w).ok()?,
166 None => base_week,
167 };
168 let dow = match o.day_of_week {
169 Some(d) => u32::try_from(d).ok()?,
170 None => calendar::iso_weekday(base_days),
171 };
172 return calendar::from_iso_ywd(iso_year, week, dow);
173 }
174 if let Some(ord) = o.ordinal_day {
175 return calendar::from_ordinal(year, u32::try_from(ord).ok()?);
176 }
177 if o.quarter.is_some() || o.day_of_quarter.is_some() {
179 let quarter = match o.quarter {
180 Some(q) => q,
181 None => i64::from((base_month - 1) / 3 + 1),
182 };
183 let day_of_quarter = o
184 .day_of_quarter
185 .unwrap_or_else(|| base_day_of_quarter(base_days));
186 let offset = u64::try_from(day_of_quarter - 1).ok()?;
188 let start_month = u32::try_from((quarter - 1) * 3 + 1).ok()?;
189 let start = calendar::ymd_to_days(year, start_month, 1)?;
190 let result = start + i64::try_from(offset).ok()?;
191 let (result_year, result_month, _) = calendar::civil_from_days(result);
193 let result_quarter = i64::from((result_month - 1) / 3 + 1);
194 if result_year != year || result_quarter != quarter {
195 return None;
196 }
197 return Some(result);
198 }
199 let month = match o.month {
200 Some(m) => u32::try_from(m).ok()?,
201 None => base_month,
202 };
203 let day = match o.day {
204 Some(d) => u32::try_from(d).ok()?,
205 None => base_day,
206 };
207 calendar::ymd_to_days(year, month, day)
208}
209
210#[must_use]
212pub fn is_date_accessor(name: &str) -> bool {
213 matches!(
214 name,
215 "year"
216 | "quarter"
217 | "month"
218 | "week"
219 | "weekYear"
220 | "day"
221 | "ordinalDay"
222 | "weekDay"
223 | "dayOfQuarter"
224 )
225}
226
227#[must_use]
231pub fn date_component(days: i64, name: &str) -> Option<i64> {
232 use crate::calendar;
235 let (year, month, day) = calendar::civil_from_days(days);
236 Some(match name {
237 "year" => year,
238 "quarter" => i64::from((month - 1) / 3 + 1),
239 "month" => i64::from(month),
240 "week" => i64::from(calendar::iso_week(days).1),
241 "weekYear" => calendar::iso_week(days).0,
242 "day" => i64::from(day),
243 "ordinalDay" => i64::from(calendar::ordinal(days)),
244 "weekDay" => i64::from(calendar::iso_weekday(days)),
245 "dayOfQuarter" => base_day_of_quarter(days),
246 _ => return None,
247 })
248}
249
250#[must_use]
253pub fn is_time_accessor(name: &str) -> bool {
254 matches!(
255 name,
256 "hour" | "minute" | "second" | "millisecond" | "microsecond" | "nanosecond"
257 )
258}
259
260#[must_use]
263pub fn is_zone_int_accessor(name: &str) -> bool {
264 matches!(name, "offsetMinutes" | "offsetSeconds")
265}
266
267#[must_use]
270pub fn is_zone_str_accessor(name: &str) -> bool {
271 matches!(name, "timezone" | "offset")
272}
273
274#[must_use]
277pub fn is_epoch_accessor(name: &str) -> bool {
278 matches!(name, "epochSeconds" | "epochMillis")
279}
280
281#[must_use]
286pub fn time_component(nanos: i64, name: &str) -> Option<i64> {
287 let sub = nanos.rem_euclid(1_000_000_000);
288 Some(match name {
289 "hour" => nanos.div_euclid(3_600_000_000_000) % 24,
290 "minute" => nanos.div_euclid(60_000_000_000) % 60,
291 "second" => nanos.div_euclid(1_000_000_000) % 60,
292 "millisecond" => sub / 1_000_000,
293 "microsecond" => sub / 1_000,
294 "nanosecond" => sub,
295 _ => return None,
296 })
297}
298
299#[must_use]
302pub fn zone_int_component(offset_seconds: i32, name: &str) -> Option<i64> {
303 Some(match name {
304 "offsetMinutes" => i64::from(offset_seconds) / 60,
305 "offsetSeconds" => i64::from(offset_seconds),
306 _ => return None,
307 })
308}
309
310#[must_use]
314pub fn zone_str_component(offset_seconds: i32, zone: Option<&str>, name: &str) -> Option<String> {
315 let offset_str = || {
316 format_offset(&Offset {
317 seconds: offset_seconds,
318 has_seconds: offset_seconds % 60 != 0,
319 })
320 };
321 Some(match name {
322 "offset" => offset_str(),
323 "timezone" => zone.map_or_else(offset_str, str::to_string),
324 _ => return None,
325 })
326}
327
328#[must_use]
332pub fn epoch_component(days: i64, nanos: i64, offset_seconds: i32, name: &str) -> Option<i64> {
333 let epoch_secs = days * 86_400 + nanos.div_euclid(1_000_000_000) - i64::from(offset_seconds);
334 Some(match name {
335 "epochSeconds" => epoch_secs,
336 "epochMillis" => epoch_secs * 1_000 + nanos.rem_euclid(1_000_000_000) / 1_000_000,
337 _ => return None,
338 })
339}
340
341#[must_use]
348pub fn truncate_date(days: i64, unit: &str) -> Option<i64> {
349 use crate::calendar;
352 let (year, month, _) = calendar::civil_from_days(days);
353 let jan1 = |y: i64| calendar::ymd_to_days(y, 1, 1);
354 match unit {
355 "millennium" => jan1(year.div_euclid(1000) * 1000),
356 "century" => jan1(year.div_euclid(100) * 100),
357 "decade" => jan1(year.div_euclid(10) * 10),
358 "year" => jan1(year),
359 "weekYear" => calendar::from_iso_ywd(calendar::iso_week(days).0, 1, 1),
360 "quarter" => calendar::ymd_to_days(year, ((month - 1) / 3) * 3 + 1, 1),
361 "month" => calendar::ymd_to_days(year, month, 1),
362 "week" => Some(days - i64::from(calendar::num_days_from_monday(days))),
363 "day" => Some(days),
364 _ => None,
365 }
366}
367
368#[must_use]
374pub fn truncate_time_nanos(nanos: i64, unit: &str) -> Option<i64> {
375 let floor = |step: i64| (nanos / step) * step;
376 match unit {
377 "hour" => Some(floor(3_600_000_000_000)),
378 "minute" => Some(floor(60_000_000_000)),
379 "second" => Some(floor(1_000_000_000)),
380 "millisecond" => Some(floor(1_000_000)),
381 "microsecond" => Some(floor(1_000)),
382 "millennium" | "century" | "decade" | "year" | "weekYear" | "quarter" | "month"
383 | "week" | "day" => Some(0),
384 _ => None,
385 }
386}
387
388fn base_day_of_quarter(days: i64) -> i64 {
392 let (year, month, _) = crate::calendar::civil_from_days(days);
393 let start_month = ((month - 1) / 3) * 3 + 1;
394 let start = crate::calendar::ymd_to_days(year, start_month, 1).unwrap_or(days);
395 days - start + 1
396}
397
398fn resolve_date(f: &Fields) -> Option<i64> {
401 use crate::calendar;
404 if let Some(anchor) = f_date(f, "date") {
405 if f.contains_key("week") {
406 let iso_year = match f_int(f, "year") {
407 Some(y) => y,
408 None => calendar::iso_week(anchor).0,
409 };
410 let week = u32::try_from(f_int(f, "week")?).ok()?;
411 let dow = match f_int(f, "dayOfWeek") {
412 Some(d) => u32::try_from(d).ok()?,
413 None => calendar::iso_weekday(anchor),
414 };
415 return calendar::from_iso_ywd(iso_year, week, dow);
416 }
417 return Some(anchor);
418 }
419
420 let year = f_int(f, "year")?;
421 if f.contains_key("month") || f.contains_key("day") {
422 let month = u32::try_from(f_int(f, "month").unwrap_or(1)).ok()?;
423 let day = u32::try_from(f_int(f, "day").unwrap_or(1)).ok()?;
424 calendar::ymd_to_days(year, month, day)
425 } else if f.contains_key("week") {
426 let week = u32::try_from(f_int(f, "week")?).ok()?;
427 let dow = u32::try_from(f_int(f, "dayOfWeek").unwrap_or(1)).ok()?;
428 calendar::from_iso_ywd(year, week, dow)
429 } else if f.contains_key("ordinalDay") {
430 calendar::from_ordinal(year, u32::try_from(f_int(f, "ordinalDay")?).ok()?)
431 } else if f.contains_key("quarter") {
432 let quarter = f_int(f, "quarter")?;
433 let day_of_quarter = f_int(f, "dayOfQuarter").unwrap_or(1);
434 let start_month = u32::try_from((quarter - 1) * 3 + 1).ok()?;
435 let start = calendar::ymd_to_days(year, start_month, 1)?;
436 let offset = i64::try_from(u64::try_from(day_of_quarter - 1).ok()?).ok()?;
437 Some(start + offset)
438 } else {
439 calendar::ymd_to_days(year, 1, 1)
440 }
441}
442
443fn resolve_time(f: &Fields) -> Option<TimeParts> {
447 let hour = u32::try_from(f_int(f, "hour").unwrap_or(0)).ok()?;
448 let minute = u32::try_from(f_int(f, "minute").unwrap_or(0)).ok()?;
449 let second = u32::try_from(f_int(f, "second").unwrap_or(0)).ok()?;
450 let milli = f_int(f, "millisecond").unwrap_or(0);
451 let micro = f_int(f, "microsecond").unwrap_or(0);
452 let nano = f_int(f, "nanosecond").unwrap_or(0);
453 let nanos = u32::try_from(milli * 1_000_000 + micro * 1_000 + nano).ok()?;
454
455 let precision = if f.contains_key("millisecond")
456 || f.contains_key("microsecond")
457 || f.contains_key("nanosecond")
458 {
459 TimePrecision::SubSecond
460 } else if f.contains_key("second") {
461 TimePrecision::Second
462 } else {
463 TimePrecision::Minute
464 };
465
466 NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)?;
467 Some(TimeParts {
468 hour,
469 minute,
470 second,
471 nanos,
472 precision,
473 })
474}
475
476fn resolve_timezone(
480 f: &Fields,
481 date: Option<i64>,
482 time: &TimeParts,
483) -> Option<(Offset, Option<String>)> {
484 match f_str(f, "timezone") {
485 None => Some((
486 Offset {
487 seconds: 0,
488 has_seconds: false,
489 },
490 None,
491 )),
492 Some(tz) if tz == "Z" || tz.starts_with('+') || tz.starts_with('-') => {
493 Some((parse_offset(tz)?, None))
494 }
495 Some(tz) => Some((resolve_zone_offset(date?, time, tz)?, Some(tz.to_string()))),
496 }
497}
498
499fn build_time_map(f: &Fields) -> Option<String> {
501 if !f.contains_key("hour") {
502 return None;
503 }
504 let time = resolve_time(f)?;
505 let (offset, _) = resolve_timezone(f, None, &time)?;
506 Some(format!("{}{}", format_time(&time), format_offset(&offset)))
507}
508
509fn build_date_time_map(f: &Fields) -> Option<String> {
511 use std::fmt::Write as _;
512 let date = resolve_date(f)?;
513 let time = resolve_time(f)?;
514 let (offset, zone) = resolve_timezone(f, Some(date), &time)?;
515 let mut out = format!(
516 "{}T{}{}",
517 format_date(date),
518 format_time(&time),
519 format_offset(&offset)
520 );
521 if let Some(z) = zone {
522 write!(out, "[{z}]").unwrap();
523 }
524 Some(out)
525}
526
527const AVG_DAYS_PER_MONTH: f64 = MONTH_SECS / DAY_SECS;
531
532#[allow(
537 clippy::cast_possible_truncation,
538 reason = "duration component magnitudes stay within i64 for the corpus"
539)]
540fn approximate_duration(months_f: f64, days_f: f64, seconds_f: f64) -> DurationValue {
541 let months = months_f.trunc();
542 let days_f = days_f + (months_f - months) * AVG_DAYS_PER_MONTH;
543 let days = days_f.trunc();
544 let seconds_total = seconds_f + (days_f - days) * DAY_SECS;
545 let mut whole_secs = seconds_total.floor() as i64;
549 let mut sub_nanos = ((seconds_total - seconds_total.floor()) * 1e9).round() as i64;
550 if sub_nanos >= 1_000_000_000 {
553 whole_secs += 1;
554 sub_nanos -= 1_000_000_000;
555 }
556 DurationValue {
557 months: months as i64,
558 days: days as i64,
559 seconds: whole_secs,
560 nanos: sub_nanos,
561 }
562}
563
564fn build_duration_map(f: &Fields) -> Option<DurationValue> {
569 let (mut months_f, mut days_f, mut seconds_f) = (0.0, 0.0, 0.0);
570 let mut any = false;
571 for (key, factor_secs, into) in [
572 ("years", YEAR_SECS, Unit::Month(12)),
573 ("months", MONTH_SECS, Unit::Month(1)),
574 ("weeks", DAY_SECS * 7.0, Unit::Day(7)),
575 ("days", DAY_SECS, Unit::Day(1)),
576 ("hours", 3600.0, Unit::Sec),
577 ("minutes", 60.0, Unit::Sec),
578 ("seconds", 1.0, Unit::Sec),
579 ("milliseconds", 1e-3, Unit::Sec),
580 ("microseconds", 1e-6, Unit::Sec),
581 ("nanoseconds", 1e-9, Unit::Sec),
582 ] {
583 let Some(val) = f_num(f, key) else { continue };
584 any = true;
585 match into {
586 #[allow(clippy::cast_precision_loss, reason = "mult is 1 or 12")]
587 Unit::Month(mult) => months_f += val * mult as f64,
588 Unit::Day(mult) => days_f += val * f64::from(mult),
589 Unit::Sec => seconds_f += val * factor_secs,
590 }
591 }
592 any.then(|| approximate_duration(months_f, days_f, seconds_f))
593}
594
595enum Unit {
597 Month(i64),
598 Day(i32),
599 Sec,
600}
601
602#[must_use]
605pub fn render_from_epoch(seconds: i64, nanos: i64) -> Option<String> {
606 let dt = chrono::DateTime::from_timestamp(seconds, u32::try_from(nanos).ok()?)?;
607 Some(format_epoch_datetime(dt.naive_utc()))
608}
609
610#[must_use]
613pub fn render_from_epoch_millis(millis: i64) -> Option<String> {
614 let dt = chrono::DateTime::from_timestamp_millis(millis)?;
615 Some(format_epoch_datetime(dt.naive_utc()))
616}
617
618fn format_epoch_datetime(naive: chrono::NaiveDateTime) -> String {
620 let nanos = naive.and_utc().timestamp_subsec_nanos();
621 let time = TimeParts {
622 hour: naive.hour(),
623 minute: naive.minute(),
624 second: naive.second(),
625 nanos,
626 precision: if nanos > 0 {
627 TimePrecision::SubSecond
628 } else {
629 TimePrecision::Second
630 },
631 };
632 let days = date_to_epoch_days(naive.date()).unwrap_or(0);
635 format!("{}T{}Z", format_date(days), format_time(&time))
636}
637
638#[must_use]
641pub fn render_date(s: &str) -> Option<String> {
642 Some(format_date(parse_date_string(s.trim())?))
643}
644
645#[must_use]
648pub fn render_local_time(s: &str) -> Option<String> {
649 let s = s.trim();
650 let (_, offset) = split_time_offset(s);
652 if offset.is_some() {
653 return None;
654 }
655 Some(format_time(&parse_time_of_day(s)?))
656}
657
658fn nanos_of_day(t: &TimeParts) -> i64 {
665 (i64::from(t.hour) * 3600 + i64::from(t.minute) * 60 + i64::from(t.second)) * 1_000_000_000
666 + i64::from(t.nanos)
667}
668
669fn time_parts_from_nanos(nanos: i64) -> TimeParts {
673 let nanos = nanos.rem_euclid(86_400_000_000_000);
674 let secs = nanos / 1_000_000_000;
675 let sub = u32::try_from(nanos % 1_000_000_000).unwrap_or(0);
676 let precision = if sub != 0 {
677 TimePrecision::SubSecond
678 } else if secs % 60 != 0 {
679 TimePrecision::Second
680 } else {
681 TimePrecision::Minute
682 };
683 TimeParts {
684 hour: u32::try_from(secs / 3600).unwrap_or(0),
685 minute: u32::try_from((secs % 3600) / 60).unwrap_or(0),
686 second: u32::try_from(secs % 60).unwrap_or(0),
687 nanos: sub,
688 precision,
689 }
690}
691
692#[must_use]
694pub fn localtime_nanos_from_map(fields: &Fields) -> Option<i64> {
695 resolve_time(fields).as_ref().map(nanos_of_day)
696}
697
698#[must_use]
701pub fn localtime_nanos_from_str(s: &str) -> Option<i64> {
702 let s = s.trim();
703 let (_, offset) = split_time_offset(s);
704 if offset.is_some() {
705 return None;
706 }
707 Some(nanos_of_day(&parse_time_of_day(s)?))
708}
709
710#[must_use]
715pub fn time_of_day_nanos_any(s: &str) -> Option<i64> {
716 let s = s.trim();
717 let s = s.split_once('[').map_or(s, |(head, _)| head); let s = s.split_once('T').map_or(s, |(_, time)| time); let (time, _offset) = split_time_offset(s); Some(nanos_of_day(&parse_time_of_day(time)?))
721}
722
723#[must_use]
726pub fn render_localtime_nanos(nanos: i64) -> String {
727 format_time(&time_parts_from_nanos(nanos))
728}
729
730#[derive(Default)]
734pub struct LocalTimeOverrides {
735 pub hour: Option<i64>,
737 pub minute: Option<i64>,
739 pub second: Option<i64>,
741 pub millisecond: Option<i64>,
743 pub microsecond: Option<i64>,
745 pub nanosecond: Option<i64>,
747}
748
749#[must_use]
753pub fn project_localtime(base_nanos: i64, o: &LocalTimeOverrides) -> Option<i64> {
754 let base = time_parts_from_nanos(base_nanos);
755 let hour = match o.hour {
756 Some(h) => u32::try_from(h).ok()?,
757 None => base.hour,
758 };
759 let minute = match o.minute {
760 Some(m) => u32::try_from(m).ok()?,
761 None => base.minute,
762 };
763 let second = match o.second {
764 Some(s) => u32::try_from(s).ok()?,
765 None => base.second,
766 };
767 let nanos = if o.millisecond.is_some() || o.microsecond.is_some() || o.nanosecond.is_some() {
768 let base_milli = i64::from(base.nanos) / 1_000_000;
773 let base_micro = (i64::from(base.nanos) / 1_000) % 1_000;
774 let base_nano = i64::from(base.nanos) % 1_000;
775 let sub = o.millisecond.unwrap_or(base_milli) * 1_000_000
776 + o.microsecond.unwrap_or(base_micro) * 1_000
777 + o.nanosecond.unwrap_or(base_nano);
778 u32::try_from(sub).ok()?
779 } else {
780 base.nanos
781 };
782 let parts = TimeParts {
783 hour,
784 minute,
785 second,
786 nanos,
787 precision: TimePrecision::Minute, };
789 NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)?; Some(nanos_of_day(&parts))
791}
792
793#[must_use]
804pub fn localdatetime_parts_from_map(fields: &Fields) -> Option<(i64, i64)> {
805 let date = resolve_date(fields)?;
806 let time = resolve_time(fields)?;
807 Some((date, nanos_of_day(&time)))
808}
809
810#[must_use]
814pub fn localdatetime_parts_from_str(s: &str) -> Option<(i64, i64)> {
815 let s = s.trim();
816 let Some((date_str, time_str)) = s.split_once('T') else {
819 return Some((parse_date_string(s)?, 0));
820 };
821 let date = parse_date_string(date_str)?;
822 let (_, offset) = split_time_offset(time_str);
823 if offset.is_some() {
824 return None;
825 }
826 Some((date, nanos_of_day(&parse_time_of_day(time_str)?)))
827}
828
829#[must_use]
832pub fn render_localdatetime(date: i64, nanos_of_day: i64) -> String {
833 format!(
834 "{}T{}",
835 format_date(date),
836 render_localtime_nanos(nanos_of_day)
837 )
838}
839
840#[must_use]
843pub fn render_time(s: &str) -> Option<String> {
844 let (time, offset) = split_time_offset(s.trim());
845 let time = parse_time_of_day(time)?;
846 let offset = parse_offset(offset?)?;
847 Some(format!("{}{}", format_time(&time), format_offset(&offset)))
848}
849
850#[must_use]
859pub fn time_value_from_map(fields: &Fields) -> Option<(i64, i32)> {
860 if !fields.contains_key("hour") {
861 return None;
862 }
863 let time = resolve_time(fields)?;
864 let (offset, _) = resolve_timezone(fields, None, &time)?;
865 Some((nanos_of_day(&time), offset.seconds))
866}
867
868#[must_use]
871pub fn time_value_from_str(s: &str) -> Option<(i64, i32)> {
872 let (time, offset) = split_time_offset(s.trim());
873 let offset_secs = match offset {
876 Some(o) => parse_offset(o)?.seconds,
877 None => 0,
878 };
879 Some((nanos_of_day(&parse_time_of_day(time)?), offset_secs))
880}
881
882#[must_use]
887pub fn time_of_day_with_offset(s: &str) -> Option<(i64, Option<i32>)> {
888 let s = s.trim();
889 let s = s.split_once('[').map_or(s, |(head, _)| head); let s = s.split_once('T').map_or(s, |(_, t)| t); let (time, offset) = split_time_offset(s);
892 let nanos = nanos_of_day(&parse_time_of_day(time)?);
893 let offset = match offset {
894 Some(o) => Some(parse_offset(o)?.seconds),
895 None => None,
896 };
897 Some((nanos, offset))
898}
899
900#[must_use]
905pub fn project_time(
906 base_nanos: i64,
907 base_offset: Option<i32>,
908 new_offset: Option<i32>,
909) -> (i64, i32) {
910 match (new_offset, base_offset) {
911 (Some(new), Some(old)) => {
912 let shifted =
913 (base_nanos + i64::from(new - old) * 1_000_000_000).rem_euclid(86_400_000_000_000);
914 (shifted, new)
915 }
916 (Some(new), None) => (base_nanos, new),
917 (None, Some(old)) => (base_nanos, old),
918 (None, None) => (base_nanos, 0),
919 }
920}
921
922#[must_use]
925pub fn render_time_value(nanos: i64, offset_seconds: i32) -> String {
926 let offset = Offset {
927 seconds: offset_seconds,
928 has_seconds: offset_seconds % 60 != 0,
929 };
930 format!(
931 "{}{}",
932 render_localtime_nanos(nanos),
933 format_offset(&offset)
934 )
935}
936
937type DateTimeParts = (i64, i64, i32, Option<String>);
947
948#[must_use]
950pub fn datetime_value_from_map(f: &Fields) -> Option<DateTimeParts> {
951 let date = resolve_date(f)?;
952 let time = resolve_time(f)?;
953 let (offset, zone) = resolve_timezone(f, Some(date), &time)?;
954 Some((date, nanos_of_day(&time), offset.seconds, zone))
955}
956
957#[must_use]
960pub fn datetime_value_from_str(s: &str) -> Option<DateTimeParts> {
961 let s = s.trim();
962 if let Some(bracket) = s.find('[') {
963 let zone = s.get(bracket + 1..)?.strip_suffix(']')?;
964 let (date_str, time_offset) = s.get(..bracket)?.split_once('T')?;
965 let date = parse_date_string(date_str)?;
966 let (time_str, offset_str) = split_time_offset(time_offset);
967 let time = parse_time_of_day(time_str)?;
968 let offset = match offset_str {
969 Some(o) => parse_offset(o)?,
970 None => resolve_zone_offset(date, &time, zone)?,
971 };
972 return Some((
973 date,
974 nanos_of_day(&time),
975 offset.seconds,
976 Some(zone.to_string()),
977 ));
978 }
979 let (date_str, time_offset) = s.split_once('T')?;
980 let date = parse_date_string(date_str)?;
981 let (time_str, offset_str) = split_time_offset(time_offset);
982 let time = parse_time_of_day(time_str)?;
983 Some((
984 date,
985 nanos_of_day(&time),
986 parse_offset(offset_str?)?.seconds,
987 None,
988 ))
989}
990
991#[must_use]
996pub fn time_offset_zone(s: &str) -> Option<(i64, Option<i32>, Option<String>)> {
997 let s = s.trim();
998 let (head, zone) = match s.split_once('[') {
999 Some((h, z)) => (h, z.strip_suffix(']').map(str::to_string)),
1000 None => (s, None),
1001 };
1002 let body = head.split_once('T').map_or(head, |(_, t)| t);
1003 let (time, offset) = split_time_offset(body);
1004 let nanos = nanos_of_day(&parse_time_of_day(time)?);
1005 let offset = match offset {
1006 Some(o) => Some(parse_offset(o)?.seconds),
1007 None => None,
1008 };
1009 Some((nanos, offset, zone))
1010}
1011
1012#[must_use]
1015pub fn render_datetime_value(
1016 date: i64,
1017 nanos: i64,
1018 offset_seconds: i32,
1019 zone: Option<&str>,
1020) -> String {
1021 use std::fmt::Write as _;
1022 let offset = Offset {
1023 seconds: offset_seconds,
1024 has_seconds: offset_seconds % 60 != 0,
1025 };
1026 let mut out = format!(
1027 "{}T{}{}",
1028 format_date(date),
1029 render_localtime_nanos(nanos),
1030 format_offset(&offset)
1031 );
1032 if let Some(z) = zone {
1033 write!(out, "[{z}]").unwrap();
1034 }
1035 out
1036}
1037
1038#[must_use]
1045pub fn project_datetime(
1046 date: i64,
1047 nanos: i64,
1048 src_offset: Option<i32>,
1049 src_zone: Option<&str>,
1050 new_tz: Option<&str>,
1051) -> Option<DateTimeParts> {
1052 use chrono::offset::{Offset as _, TimeZone};
1053 let base = epoch_days_to_date(date)?;
1056 let local = base.and_time(nanos_to_naive_time(nanos)?);
1057 let src_offset = match (src_offset, src_zone) {
1062 (Some(o), Some(z)) => Some(
1063 resolve_zone_offset(date, &time_parts_from_nanos(nanos), z)
1064 .map_or(o, |off| off.seconds),
1065 ),
1066 (o, _) => o,
1067 };
1068 let Some(tz) = new_tz else {
1069 return Some(match src_offset {
1071 Some(o) => (date, nanos, o, src_zone.map(str::to_string)),
1072 None => (date, nanos, 0, None),
1073 });
1074 };
1075 if let Some(new_off) = parse_offset_seconds(tz) {
1077 return Some(match src_offset {
1078 Some(src) => {
1080 let shifted = local + chrono::Duration::seconds(i64::from(new_off - src));
1081 (
1082 date_to_epoch_days(shifted.date())?,
1083 naive_time_nanos(shifted.time()),
1084 new_off,
1085 None,
1086 )
1087 }
1088 None => (date, nanos, new_off, None),
1090 });
1091 }
1092 let zone: chrono_tz::Tz = tz.parse().ok()?;
1094 if let Some(src) = src_offset {
1095 let utc = local - chrono::Duration::seconds(i64::from(src));
1097 let zoned = zone.from_utc_datetime(&utc);
1098 let off = zoned.offset().fix().local_minus_utc();
1099 let l = zoned.naive_local();
1100 Some((
1101 date_to_epoch_days(l.date())?,
1102 naive_time_nanos(l.time()),
1103 off,
1104 Some(tz.to_string()),
1105 ))
1106 } else {
1107 let off = resolve_zone_offset(date, &time_parts_from_nanos(nanos), tz)?;
1109 Some((date, nanos, off.seconds, Some(tz.to_string())))
1110 }
1111}
1112
1113fn nanos_to_naive_time(nanos: i64) -> Option<NaiveTime> {
1115 let tp = time_parts_from_nanos(nanos);
1116 NaiveTime::from_hms_nano_opt(tp.hour, tp.minute, tp.second, tp.nanos)
1117}
1118
1119fn naive_time_nanos(t: NaiveTime) -> i64 {
1121 i64::from(t.num_seconds_from_midnight()) * 1_000_000_000 + i64::from(t.nanosecond())
1122}
1123
1124#[must_use]
1127pub fn render_local_date_time(s: &str) -> Option<String> {
1128 let (date, time) = s.trim().split_once('T')?;
1129 let date = parse_date_string(date)?;
1130 let (_, offset) = split_time_offset(time);
1131 if offset.is_some() {
1132 return None;
1133 }
1134 let time = parse_time_of_day(time)?;
1135 Some(format!("{}T{}", format_date(date), format_time(&time)))
1136}
1137
1138#[must_use]
1144pub fn render_date_time(s: &str) -> Option<String> {
1145 let s = s.trim();
1146 if let Some(bracket) = s.find('[') {
1147 return render_date_time_named(s, bracket);
1148 }
1149 let (date, time_offset) = s.split_once('T')?;
1150 let date = parse_date_string(date)?;
1151 let (time, offset) = split_time_offset(time_offset);
1152 let time = parse_time_of_day(time)?;
1153 let offset = parse_offset(offset?)?;
1154 Some(format!(
1155 "{}T{}{}",
1156 format_date(date),
1157 format_time(&time),
1158 format_offset(&offset)
1159 ))
1160}
1161
1162fn render_date_time_named(s: &str, bracket: usize) -> Option<String> {
1166 let zone = s.get(bracket + 1..)?.strip_suffix(']')?;
1167 let head = s.get(..bracket)?;
1168 let (date_str, time_offset) = head.split_once('T')?;
1169 let date = parse_date_string(date_str)?;
1170 let (time_str, offset_str) = split_time_offset(time_offset);
1171 let time = parse_time_of_day(time_str)?;
1172
1173 let offset = match offset_str {
1174 Some(o) => parse_offset(o)?,
1175 None => resolve_zone_offset(date, &time, zone)?,
1176 };
1177 Some(format!(
1178 "{}T{}{}[{}]",
1179 format_date(date),
1180 format_time(&time),
1181 format_offset(&offset),
1182 zone
1183 ))
1184}
1185
1186fn resolve_zone_offset(days: i64, time: &TimeParts, zone: &str) -> Option<Offset> {
1191 use chrono::offset::{LocalResult, Offset as _, TimeZone};
1192 let tz: chrono_tz::Tz = zone.parse().ok()?;
1193 let date = epoch_days_to_date(days)?;
1197 let naive = date.and_time(NaiveTime::from_hms_nano_opt(
1198 time.hour,
1199 time.minute,
1200 time.second,
1201 time.nanos,
1202 )?);
1203 let resolved = match tz.from_local_datetime(&naive) {
1204 LocalResult::Single(dt) | LocalResult::Ambiguous(dt, _) => dt,
1205 LocalResult::None => return None,
1206 };
1207 Some(Offset {
1208 seconds: resolved.offset().fix().local_minus_utc(),
1209 has_seconds: true,
1210 })
1211}
1212
1213#[derive(Clone, Copy, PartialEq, Eq)]
1219enum TimePrecision {
1220 Minute,
1221 Second,
1222 SubSecond,
1223}
1224
1225struct TimeParts {
1227 hour: u32,
1228 minute: u32,
1229 second: u32,
1230 nanos: u32,
1231 precision: TimePrecision,
1232}
1233
1234struct Offset {
1237 seconds: i32,
1238 has_seconds: bool,
1239}
1240
1241fn parse_time_of_day(s: &str) -> Option<TimeParts> {
1245 let (main, frac) = match s.split_once('.') {
1246 Some((m, f)) => (m, Some(f)),
1247 None => (s, None),
1248 };
1249
1250 let (hour, minute, second, base) = if main.contains(':') {
1251 let parts: Vec<&str> = main.split(':').collect();
1252 match parts.as_slice() {
1253 [h, m] => (h.parse().ok()?, m.parse().ok()?, 0, TimePrecision::Minute),
1254 [h, m, sec] => (
1255 h.parse().ok()?,
1256 m.parse().ok()?,
1257 sec.parse().ok()?,
1258 TimePrecision::Second,
1259 ),
1260 _ => return None,
1261 }
1262 } else {
1263 if !main.bytes().all(|b| b.is_ascii_digit()) {
1264 return None;
1265 }
1266 match main.len() {
1267 2 => (main.parse().ok()?, 0, 0, TimePrecision::Minute),
1268 4 => (
1269 main[..2].parse().ok()?,
1270 main[2..4].parse().ok()?,
1271 0,
1272 TimePrecision::Minute,
1273 ),
1274 6 => (
1275 main[..2].parse().ok()?,
1276 main[2..4].parse().ok()?,
1277 main[4..6].parse().ok()?,
1278 TimePrecision::Second,
1279 ),
1280 _ => return None,
1281 }
1282 };
1283
1284 let (nanos, precision) = match frac {
1285 Some(f) if !f.is_empty() && f.len() <= 9 && f.bytes().all(|b| b.is_ascii_digit()) => {
1288 let mut digits = f.to_string();
1289 while digits.len() < 9 {
1290 digits.push('0');
1291 }
1292 (digits.parse().ok()?, TimePrecision::SubSecond)
1293 }
1294 Some(_) => return None,
1295 None => (0, base),
1296 };
1297
1298 NaiveTime::from_hms_nano_opt(hour, minute, second, nanos)?;
1300 Some(TimeParts {
1301 hour,
1302 minute,
1303 second,
1304 nanos,
1305 precision,
1306 })
1307}
1308
1309fn format_time(t: &TimeParts) -> String {
1312 match t.precision {
1313 TimePrecision::Minute => format!("{:02}:{:02}", t.hour, t.minute),
1314 TimePrecision::Second => format!("{:02}:{:02}:{:02}", t.hour, t.minute, t.second),
1315 TimePrecision::SubSecond => {
1316 let frac = format!("{:09}", t.nanos);
1317 let frac = frac.trim_end_matches('0');
1318 if frac.is_empty() {
1319 format!("{:02}:{:02}:{:02}", t.hour, t.minute, t.second)
1320 } else {
1321 format!("{:02}:{:02}:{:02}.{frac}", t.hour, t.minute, t.second)
1322 }
1323 }
1324 }
1325}
1326
1327fn split_time_offset(s: &str) -> (&str, Option<&str>) {
1331 for (i, c) in s.char_indices() {
1332 if i > 0 && matches!(c, 'Z' | '+' | '-') {
1333 return (&s[..i], Some(&s[i..]));
1334 }
1335 }
1336 (s, None)
1337}
1338
1339#[must_use]
1342pub fn parse_offset_seconds(s: &str) -> Option<i32> {
1343 parse_offset(s).map(|o| o.seconds)
1344}
1345
1346fn parse_offset(s: &str) -> Option<Offset> {
1348 if s == "Z" {
1349 return Some(Offset {
1350 seconds: 0,
1351 has_seconds: false,
1352 });
1353 }
1354 let (sign, rest) = match s.strip_prefix('+') {
1355 Some(rest) => (1, rest),
1356 None => (-1, s.strip_prefix('-')?),
1357 };
1358 let (hours, minutes, secs, has_seconds): (i32, i32, i32, bool) = if rest.contains(':') {
1359 let parts: Vec<&str> = rest.split(':').collect();
1360 if !parts
1362 .iter()
1363 .all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
1364 {
1365 return None;
1366 }
1367 match parts.as_slice() {
1368 [h, m] => (h.parse().ok()?, m.parse().ok()?, 0, false),
1369 [h, m, sec] => (h.parse().ok()?, m.parse().ok()?, sec.parse().ok()?, true),
1370 _ => return None,
1371 }
1372 } else {
1373 if !rest.bytes().all(|b| b.is_ascii_digit()) {
1374 return None;
1375 }
1376 match rest.len() {
1377 2 => (rest.parse().ok()?, 0, 0, false),
1378 4 => (rest[..2].parse().ok()?, rest[2..4].parse().ok()?, 0, false),
1379 6 => (
1380 rest[..2].parse().ok()?,
1381 rest[2..4].parse().ok()?,
1382 rest[4..6].parse().ok()?,
1383 true,
1384 ),
1385 _ => return None,
1386 }
1387 };
1388 if hours >= 24 || minutes >= 60 || secs >= 60 {
1389 return None;
1390 }
1391 Some(Offset {
1392 seconds: sign * (hours * 3600 + minutes * 60 + secs),
1393 has_seconds,
1394 })
1395}
1396
1397fn format_offset(o: &Offset) -> String {
1400 if o.seconds == 0 {
1401 return "Z".to_string();
1402 }
1403 let sign = if o.seconds < 0 { '-' } else { '+' };
1404 let abs = o.seconds.abs();
1405 let (hours, minutes, secs) = (abs / 3600, (abs % 3600) / 60, abs % 60);
1406 if o.has_seconds && secs != 0 {
1407 format!("{sign}{hours:02}:{minutes:02}:{secs:02}")
1408 } else {
1409 format!("{sign}{hours:02}:{minutes:02}")
1410 }
1411}
1412
1413const DAY_SECS: f64 = 86_400.0;
1415const MONTH_SECS: f64 = 2_629_746.0;
1418const YEAR_SECS: f64 = 31_556_952.0;
1420
1421#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1433pub struct DurationValue {
1434 pub months: i64,
1436 pub days: i64,
1438 pub seconds: i64,
1440 pub nanos: i64,
1443}
1444
1445impl DurationValue {
1446 #[must_use]
1452 pub fn from_total_nanos(months: i64, days: i64, total_nanos: i64) -> Self {
1453 Self {
1454 months,
1455 days,
1456 seconds: total_nanos.div_euclid(1_000_000_000),
1457 nanos: total_nanos.rem_euclid(1_000_000_000),
1458 }
1459 }
1460}
1461
1462#[must_use]
1468pub fn render_duration(s: &str) -> Option<String> {
1469 Some(format_duration(&parse_duration(s.trim())?))
1470}
1471
1472#[must_use]
1474pub fn duration_value_from_str(s: &str) -> Option<DurationValue> {
1475 parse_duration(s.trim())
1476}
1477
1478#[must_use]
1480pub fn duration_value_from_map(fields: &Fields) -> Option<DurationValue> {
1481 build_duration_map(fields)
1482}
1483
1484#[must_use]
1488pub fn render_duration_value(dur: &DurationValue) -> String {
1489 format_duration(dur)
1490}
1491
1492#[must_use]
1498pub fn duration_component(dur: &DurationValue, name: &str) -> Option<i64> {
1499 let (months, days, secs) = (dur.months, dur.days, dur.seconds);
1500 let sub = dur.nanos; let total_nanos = || i128::from(secs) * 1_000_000_000 + i128::from(sub);
1509 let v = match name {
1510 "years" => months / 12,
1511 "quarters" => months / 3,
1512 "months" => months,
1513 "monthsOfYear" => months % 12,
1514 "monthsOfQuarter" => months % 3,
1515 "quartersOfYear" => (months / 3) % 4,
1516 "weeks" => days / 7,
1517 "days" => days,
1518 "daysOfWeek" => days % 7,
1519 "hours" => secs / 3600,
1520 "minutes" => secs / 60,
1521 "seconds" => secs,
1522 "minutesOfHour" => (secs / 60) % 60,
1523 "secondsOfMinute" => secs % 60,
1524 "milliseconds" => return i64::try_from(total_nanos() / 1_000_000).ok(),
1525 "microseconds" => return i64::try_from(total_nanos() / 1_000).ok(),
1526 "nanoseconds" => return i64::try_from(total_nanos()).ok(),
1527 "millisecondsOfSecond" => sub / 1_000_000,
1528 "microsecondsOfSecond" => sub / 1_000,
1529 "nanosecondsOfSecond" => sub,
1530 _ => return None,
1531 };
1532 Some(v)
1533}
1534
1535#[must_use]
1537pub fn is_duration_accessor(name: &str) -> bool {
1538 duration_component(
1539 &DurationValue {
1540 months: 0,
1541 days: 0,
1542 seconds: 0,
1543 nanos: 0,
1544 },
1545 name,
1546 )
1547 .is_some()
1548}
1549
1550#[derive(Clone, Copy)]
1552pub enum BetweenMode {
1553 Between,
1555 Months,
1557 Days,
1559 Seconds,
1561}
1562
1563pub type BetweenOperand = (Option<i64>, i64, Option<i32>, Option<String>);
1569
1570const DAY_NANOS: i64 = 86_400_000_000_000;
1571
1572type Instant = (i64, i64);
1577
1578fn instant_from(days: i64, nanos: i64) -> Instant {
1582 (
1583 days + nanos.div_euclid(DAY_NANOS),
1584 nanos.rem_euclid(DAY_NANOS),
1585 )
1586}
1587
1588fn add_signed_months(dt: Instant, m: i64) -> Instant {
1592 (crate::calendar::add_months_to_days(dt.0, m), dt.1)
1593}
1594
1595#[allow(
1598 clippy::cast_possible_truncation,
1599 reason = "the day quotient fits i64 across the full year range (±~7.3e11 days); \
1600 i128 only guards the nanosecond intermediate"
1601)]
1602fn instant_num_days(dt1: Instant, dt2: Instant) -> i64 {
1603 let total = i128::from(dt2.0 - dt1.0) * i128::from(DAY_NANOS) + i128::from(dt2.1 - dt1.1);
1604 (total / i128::from(DAY_NANOS)) as i64
1605}
1606
1607fn instant_sub_nanos(dt1: Instant, dt2: Instant) -> i64 {
1610 (dt2.0 - dt1.0) * DAY_NANOS + (dt2.1 - dt1.1)
1611}
1612
1613fn elapsed_seconds_between(dt1: Instant, dt2: Instant) -> DurationValue {
1619 let day_secs = (dt2.0 - dt1.0) * 86_400;
1620 let nanos_diff = dt2.1 - dt1.1; DurationValue {
1622 months: 0,
1623 days: 0,
1624 seconds: day_secs + nanos_diff.div_euclid(1_000_000_000),
1625 nanos: nanos_diff.rem_euclid(1_000_000_000),
1626 }
1627}
1628
1629fn between_instant(op: &BetweenOperand, partner: &BetweenOperand) -> Option<Instant> {
1636 let (date, nanos, offset, _) = op;
1637 let (p_date, _, p_offset, p_zone) = partner;
1638 let date = date.or(*p_date)?;
1639 let off = match offset {
1640 Some(o) => *o,
1641 None => match p_zone.as_deref() {
1642 Some(z) => resolve_zone_offset(date, &time_parts_from_nanos(*nanos), z)?.seconds,
1643 None => p_offset.unwrap_or(0),
1644 },
1645 };
1646 Some(instant_from(date, *nanos - i64::from(off) * 1_000_000_000))
1647}
1648
1649fn whole_months(dt1: Instant, dt2: Instant) -> i64 {
1659 let (y1, m1, _) = crate::calendar::civil_from_days(dt1.0);
1660 let (y2, m2, _) = crate::calendar::civil_from_days(dt2.0);
1661 let mut m = (y2 - y1) * 12 + (i64::from(m2) - i64::from(m1));
1662 let cand = add_signed_months(dt1, m);
1663 if dt2 >= dt1 {
1664 if cand > dt2 {
1666 m -= 1;
1667 }
1668 } else if cand < dt2 {
1669 m += 1;
1671 }
1672 m
1673}
1674
1675#[allow(
1681 clippy::single_match_else,
1682 reason = "the both-dated arm is the substantive calendar path; the time-only \
1683 else is the fallthrough — a match reads clearer than nested if-let"
1684)]
1685#[must_use]
1686pub fn duration_between(
1687 a: &BetweenOperand,
1688 b: &BetweenOperand,
1689 mode: BetweenMode,
1690) -> Option<DurationValue> {
1691 let (d1, n1, o1, _) = a;
1692 let (d2, n2, o2, _) = b;
1693 let (d1, n1, o1) = (*d1, *n1, *o1);
1694 let (d2, n2, o2) = (*d2, *n2, *o2);
1695 let both_off = o1.zip(o2);
1696 let shift = |n: i64, o: i32| n - i64::from(o) * 1_000_000_000;
1697 let zero = DurationValue {
1698 months: 0,
1699 days: 0,
1700 seconds: 0,
1701 nanos: 0,
1702 };
1703 if matches!(mode, BetweenMode::Seconds) && (d1.is_some() || d2.is_some()) {
1711 let dt1 = between_instant(a, b)?;
1712 let dt2 = between_instant(b, a)?;
1713 return Some(elapsed_seconds_between(dt1, dt2));
1714 }
1715 match (d1, d2) {
1716 (Some(da), Some(db)) => {
1717 let (dt1, dt2) = if let Some((oa, ob)) = both_off {
1718 (
1719 instant_from(da, shift(n1, oa)),
1720 instant_from(db, shift(n2, ob)),
1721 )
1722 } else {
1723 (instant_from(da, n1), instant_from(db, n2))
1724 };
1725 match mode {
1726 BetweenMode::Months => Some(DurationValue {
1729 months: whole_months(dt1, dt2),
1730 ..zero
1731 }),
1732 BetweenMode::Days => Some(DurationValue {
1733 days: instant_num_days(dt1, dt2),
1734 ..zero
1735 }),
1736 BetweenMode::Seconds => Some(elapsed_seconds_between(dt1, dt2)),
1737 BetweenMode::Between => {
1738 let m = whole_months(dt1, dt2);
1739 let rem = instant_sub_nanos(add_signed_months(dt1, m), dt2);
1743 Some(DurationValue::from_total_nanos(
1744 m,
1745 rem / DAY_NANOS,
1746 rem % DAY_NANOS,
1747 ))
1748 }
1749 }
1750 }
1751 _ => {
1754 let diff = match (mode, both_off) {
1755 (BetweenMode::Months | BetweenMode::Days, _) => 0,
1756 (_, Some((oa, ob))) => shift(n2, ob) - shift(n1, oa),
1757 (_, None) => n2 - n1,
1758 };
1759 Some(DurationValue::from_total_nanos(0, 0, diff))
1760 }
1761 }
1762}
1763
1764#[must_use]
1773#[allow(
1774 clippy::cast_precision_loss,
1775 reason = "scaling is inherently f64; the sub-day total is computed in f64 \
1776 (not i64 seconds*1e9) to avoid an i64 overflow for large durations, \
1777 and corpus durations are small whole counts"
1778)]
1779pub fn scale_duration(dur: &DurationValue, num: f64, divide: bool) -> DurationValue {
1780 const DAY_NANOS_F: f64 = DAY_SECS * 1e9;
1781 let op = |x: f64| if divide { x / num } else { x * num };
1782 let m = op(dur.months as f64);
1783 let d = op(dur.days as f64);
1784 let n = op(dur.seconds as f64 * 1e9 + dur.nanos as f64);
1785
1786 let m_whole = m.trunc();
1787 let d_total = d + (m - m_whole) * AVG_DAYS_PER_MONTH;
1788 let d_whole = d_total.trunc();
1789 let n_total = n + (d_total - d_whole) * DAY_NANOS_F;
1790
1791 #[allow(
1792 clippy::cast_possible_truncation,
1793 reason = "scaled component magnitudes stay within i64 for the corpus"
1794 )]
1795 DurationValue::from_total_nanos(m_whole as i64, d_whole as i64, n_total.trunc() as i64)
1796}
1797
1798#[must_use]
1803#[allow(
1804 clippy::cast_possible_truncation,
1805 reason = "the whole-day quotient fits i64; i128 only guards the nanosecond product"
1806)]
1807pub fn date_plus_duration(date: i64, dur: &DurationValue) -> i64 {
1808 let after_months = crate::calendar::add_months_to_days(date, dur.months);
1809 let total_nanos = i128::from(dur.seconds) * 1_000_000_000 + i128::from(dur.nanos);
1815 let extra_days = (total_nanos / i128::from(DAY_NANOS)) as i64;
1816 after_months + dur.days + extra_days
1817}
1818
1819#[must_use]
1822pub fn localtime_plus_duration(nanos_of_day: i64, dur_nanos: i64) -> i64 {
1823 (nanos_of_day + dur_nanos).rem_euclid(DAY_NANOS)
1824}
1825
1826#[must_use]
1830#[allow(
1831 clippy::cast_possible_truncation,
1832 reason = "the day carry and nanos-of-day both fit i64; i128 only guards the \
1833 nanosecond product"
1834)]
1835pub fn datetime_plus_duration(date: i64, nanos_of_day: i64, dur: &DurationValue) -> (i64, i64) {
1836 let days = crate::calendar::add_months_to_days(date, dur.months) + dur.days;
1838 let total_nanos =
1841 i128::from(nanos_of_day) + i128::from(dur.seconds) * 1_000_000_000 + i128::from(dur.nanos);
1842 let day_ns = i128::from(DAY_NANOS);
1843 let carry = (total_nanos.div_euclid(day_ns)) as i64;
1844 let nod = (total_nanos.rem_euclid(day_ns)) as i64;
1845 (days + carry, nod)
1846}
1847
1848fn parse_duration(s: &str) -> Option<DurationValue> {
1850 let rest = s.strip_prefix('P')?;
1851 if rest.is_empty() {
1852 return None; }
1854 let date_seg = rest.split('T').next().unwrap_or(rest);
1860 let alternative = rest.contains(':')
1861 || (date_seg.contains('-') && !date_seg.bytes().any(|b| b.is_ascii_alphabetic()));
1862 if alternative {
1863 return parse_duration_alternative(rest);
1864 }
1865 let (date_part, time_part) = match rest.split_once('T') {
1866 Some((d, t)) => (d, Some(t)),
1867 None => (rest, None),
1868 };
1869 let mut acc = (0.0_f64, 0.0_f64, 0.0_f64);
1870 parse_designators(date_part, false, &mut acc)?;
1871 if let Some(t) = time_part {
1872 if t.is_empty() {
1873 return None; }
1875 parse_designators(t, true, &mut acc)?;
1876 }
1877 Some(approximate_duration(acc.0, acc.1, acc.2))
1878}
1879
1880fn parse_duration_alternative(rest: &str) -> Option<DurationValue> {
1883 let (date_str, time_str) = rest.split_once('T')?;
1884 let [years, months, days] = date_str.split('-').collect::<Vec<_>>()[..] else {
1885 return None;
1886 };
1887 let [hours, minutes, secs] = time_str.split(':').collect::<Vec<_>>()[..] else {
1888 return None;
1889 };
1890 let years: i64 = years.parse().ok()?;
1891 let months: i64 = months.parse().ok()?;
1892 let days: i64 = days.parse().ok()?;
1893 let hours: f64 = hours.parse().ok()?;
1894 let minutes: f64 = minutes.parse().ok()?;
1895 let secs: f64 = secs.parse().ok()?;
1896 #[allow(
1897 clippy::cast_possible_truncation,
1898 reason = "alternative-form times are small, well within i64 nanos"
1899 )]
1900 Some(DurationValue::from_total_nanos(
1901 years * 12 + months,
1902 days,
1903 ((hours * 3600.0 + minutes * 60.0 + secs) * 1e9).round() as i64,
1904 ))
1905}
1906
1907fn parse_designators(s: &str, in_time: bool, acc: &mut (f64, f64, f64)) -> Option<()> {
1913 let mut chars = s.chars().peekable();
1914 while chars.peek().is_some() {
1915 let mut num = String::new();
1916 if matches!(chars.peek(), Some('-' | '+')) {
1920 num.push(chars.next()?);
1921 }
1922 while let Some(&c) = chars.peek() {
1923 if c.is_ascii_digit() || c == '.' {
1924 num.push(c);
1925 chars.next();
1926 } else {
1927 break;
1928 }
1929 }
1930 if num.is_empty() || num == "-" || num == "+" {
1931 return None;
1932 }
1933 let unit = chars.next()?;
1934 let val: f64 = num.parse().ok()?;
1935 match (in_time, unit) {
1936 (false, 'Y') => acc.0 += val * 12.0,
1937 (false, 'M') => acc.0 += val,
1938 (false, 'W') => acc.1 += val * 7.0,
1939 (false, 'D') => acc.1 += val,
1940 (true, 'H') => acc.2 += val * 3600.0,
1941 (true, 'M') => acc.2 += val * 60.0,
1942 (true, 'S') => acc.2 += val,
1943 _ => return None,
1944 }
1945 }
1946 Some(())
1947}
1948
1949fn format_duration(dur: &DurationValue) -> String {
1953 use std::fmt::Write as _;
1954 let years = dur.months / 12;
1955 let months = dur.months % 12;
1956 let days = dur.days;
1957 let (mut secs, mut sub_ns) = (dur.seconds, dur.nanos);
1963 if secs < 0 && sub_ns > 0 {
1964 secs += 1;
1965 sub_ns -= 1_000_000_000;
1966 }
1967 let hours = secs / 3600;
1973 let rem = secs % 3600;
1974 let minutes = rem / 60;
1975 let whole_secs = rem % 60;
1976
1977 let mut out = String::from("P");
1978 if years != 0 {
1979 write!(out, "{years}Y").unwrap();
1980 }
1981 if months != 0 {
1982 write!(out, "{months}M").unwrap();
1983 }
1984 if days != 0 {
1985 write!(out, "{days}D").unwrap();
1986 }
1987 let mut time = String::new();
1988 if hours != 0 {
1989 write!(time, "{hours}H").unwrap();
1990 }
1991 if minutes != 0 {
1992 write!(time, "{minutes}M").unwrap();
1993 }
1994 if whole_secs != 0 || sub_ns != 0 {
1995 write!(time, "{}S", format_seconds_int(whole_secs, sub_ns)).unwrap();
1996 }
1997 if !time.is_empty() {
1998 out.push('T');
1999 out.push_str(&time);
2000 }
2001 if out == "P" {
2002 out.push_str("T0S");
2003 }
2004 out
2005}
2006
2007fn format_seconds_int(whole_secs: i64, sub_ns: i64) -> String {
2011 if sub_ns == 0 {
2012 return whole_secs.to_string();
2013 }
2014 let neg = whole_secs < 0 || sub_ns < 0;
2015 let mut frac = format!("{:09}", sub_ns.unsigned_abs());
2016 while frac.ends_with('0') {
2017 frac.pop();
2018 }
2019 format!(
2020 "{}{}.{}",
2021 if neg { "-" } else { "" },
2022 whole_secs.unsigned_abs(),
2023 frac
2024 )
2025}
2026
2027#[must_use]
2048pub fn parse_date_string(input: &str) -> Option<i64> {
2049 use crate::calendar;
2050 let s = input.trim();
2051 let (neg, had_sign, s) = if let Some(r) = s.strip_prefix('-') {
2057 (true, true, r)
2058 } else if let Some(r) = s.strip_prefix('+') {
2059 (false, true, r)
2060 } else {
2061 (false, false, s)
2062 };
2063 let signed = |y: i64| if neg { -y } else { y };
2064
2065 if let Some(wpos) = s.find('W') {
2068 let year: i64 = s[..wpos].trim_end_matches('-').parse().ok()?;
2069 let rest: String = s[wpos + 1..].chars().filter(|c| *c != '-').collect();
2070 if rest.len() < 2 || !rest.bytes().all(|b| b.is_ascii_digit()) {
2073 return None;
2074 }
2075 let week: u32 = rest[..2].parse().ok()?;
2076 let weekday: u32 = if rest.len() > 2 {
2077 rest[2..].parse().ok()?
2078 } else {
2079 1
2080 };
2081 return calendar::from_iso_ywd(signed(year), week, weekday);
2082 }
2083
2084 if let Some((head, tail)) = s.split_once('-') {
2085 let year: i64 = head.parse().ok()?;
2087 return match tail.split_once('-') {
2088 Some((month, day)) => {
2089 calendar::ymd_to_days(signed(year), month.parse().ok()?, day.parse().ok()?)
2090 }
2091 None if tail.len() == 3 => calendar::from_ordinal(signed(year), tail.parse().ok()?),
2092 None => calendar::ymd_to_days(signed(year), tail.parse().ok()?, 1),
2093 };
2094 }
2095
2096 if !had_sign && !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {
2101 let year: i64 = s.get(..4)?.parse().ok()?;
2102 return match s.len() {
2103 8 => calendar::ymd_to_days(signed(year), s[4..6].parse().ok()?, s[6..8].parse().ok()?),
2104 7 => calendar::from_ordinal(signed(year), s[4..].parse().ok()?),
2105 6 => calendar::ymd_to_days(signed(year), s[4..6].parse().ok()?, 1),
2106 4 => calendar::ymd_to_days(signed(year), 1, 1),
2107 _ => None,
2108 };
2109 }
2110
2111 None
2112}
2113
2114#[must_use]
2117pub fn format_date(days: i64) -> String {
2118 crate::calendar::format_date(days)
2119}
2120
2121#[cfg(test)]
2122mod tests {
2123 use super::*;
2124
2125 fn d(s: &str) -> String {
2126 format_date(parse_date_string(s).expect("should parse"))
2127 }
2128
2129 #[test]
2130 fn calendar_forms() {
2131 assert_eq!(d("2015-07-21"), "2015-07-21");
2132 assert_eq!(d("20150721"), "2015-07-21");
2133 }
2134
2135 #[test]
2136 fn truncate_date_units() {
2137 let t = |s: &str, unit: &str| {
2139 format_date(
2140 truncate_date(parse_date_string(s).expect("parse"), unit).expect("truncate"),
2141 )
2142 };
2143 assert_eq!(t("2017-10-11", "millennium"), "2000-01-01");
2144 assert_eq!(t("1984-10-11", "century"), "1900-01-01");
2145 assert_eq!(t("1984-10-11", "decade"), "1980-01-01");
2146 assert_eq!(t("1984-10-11", "year"), "1984-01-01");
2147 assert_eq!(t("1984-02-01", "weekYear"), "1984-01-02");
2149 assert_eq!(t("1984-11-11", "quarter"), "1984-10-01");
2150 assert_eq!(t("1984-10-11", "month"), "1984-10-01");
2151 assert_eq!(t("1984-10-11", "week"), "1984-10-08");
2153 assert_eq!(t("1984-10-11", "day"), "1984-10-11");
2154 assert!(truncate_date(parse_date_string("1984-10-11").unwrap(), "bogus").is_none());
2155 }
2156
2157 #[test]
2158 fn extreme_year_date_operations_are_range_complete() {
2159 let hi = parse_date_string("+999999999-06-15").expect("parse extreme +year");
2163 assert_eq!(date_component(hi, "year"), Some(999_999_999));
2164 assert_eq!(date_component(hi, "month"), Some(6));
2165 assert_eq!(date_component(hi, "day"), Some(15));
2166 assert_eq!(date_component(hi, "quarter"), Some(2));
2167 assert!(date_component(hi, "weekDay").is_some());
2169 assert!(date_component(hi, "ordinalDay").is_some());
2170 assert!(date_component(hi, "dayOfQuarter").is_some());
2171 assert_eq!(
2173 format_date(truncate_date(hi, "year").unwrap()),
2174 "+999999999-01-01"
2175 );
2176 assert_eq!(
2177 format_date(truncate_date(hi, "month").unwrap()),
2178 "+999999999-06-01"
2179 );
2180 let projected = project_date(
2182 hi,
2183 &DateOverrides {
2184 day: Some(1),
2185 ..DateOverrides::default()
2186 },
2187 );
2188 assert_eq!(format_date(projected.unwrap()), "+999999999-06-01");
2189 let lo = parse_date_string("-999999999-01-01").expect("parse extreme -year");
2191 assert_eq!(date_component(lo, "year"), Some(-999_999_999));
2192 }
2193
2194 #[test]
2195 fn duration_subsecond_accessors_dont_overflow_on_huge_spans() {
2196 let huge = DurationValue::from_total_nanos(0, 0, 0);
2200 let huge = DurationValue {
2201 seconds: 31_556_951_999_913_600, ..huge
2203 };
2204 assert_eq!(duration_component(&huge, "nanoseconds"), None);
2205 assert_eq!(duration_component(&huge, "milliseconds"), None);
2206 assert_eq!(duration_component(&huge, "microseconds"), None);
2207 assert_eq!(
2209 duration_component(&huge, "seconds"),
2210 Some(31_556_951_999_913_600)
2211 );
2212 let small = DurationValue::from_total_nanos(0, 0, 1_500_000_000);
2214 assert_eq!(duration_component(&small, "milliseconds"), Some(1_500));
2215 }
2216
2217 #[test]
2218 fn signed_basic_dateless_form_is_rejected_not_misparsed() {
2219 assert_eq!(parse_date_string("+10000202"), None);
2223 assert_eq!(parse_date_string("-10000202"), None);
2224 assert_eq!(
2226 format_date(parse_date_string("+10000-202").expect("extended ordinal")),
2227 "+10000-07-20"
2228 );
2229 }
2230
2231 #[test]
2232 fn truncate_time_units() {
2233 let n = 45_074_645_876_123_i64;
2235 let r =
2236 |unit: &str| render_localtime_nanos(truncate_time_nanos(n, unit).expect("truncate"));
2237 assert_eq!(r("hour"), "12:00");
2238 assert_eq!(r("minute"), "12:31");
2239 assert_eq!(r("second"), "12:31:14");
2240 assert_eq!(r("millisecond"), "12:31:14.645");
2241 assert_eq!(r("microsecond"), "12:31:14.645876");
2242 assert_eq!(r("day"), "00:00");
2244 assert_eq!(r("year"), "00:00");
2245 assert!(truncate_time_nanos(n, "bogus").is_none());
2246 }
2247
2248 #[test]
2249 fn project_localtime_keeps_unspecified_subsecond_groups() {
2250 let base = 45_074_645_000_000_i64; let only_ns = LocalTimeOverrides {
2255 hour: None,
2256 minute: None,
2257 second: None,
2258 millisecond: None,
2259 microsecond: None,
2260 nanosecond: Some(2),
2261 };
2262 assert_eq!(
2263 render_localtime_nanos(project_localtime(base, &only_ns).unwrap()),
2264 "12:31:14.645000002"
2265 );
2266 let ms = LocalTimeOverrides {
2268 millisecond: Some(123),
2269 ..only_ns
2270 };
2271 assert_eq!(
2272 render_localtime_nanos(project_localtime(base, &ms).unwrap()),
2273 "12:31:14.123000002"
2274 );
2275 }
2276
2277 #[test]
2278 fn duration_str_parses_negative_components_round_trip() {
2279 let secs = 16 * 3600; let v = DurationValue::from_total_nanos(149, -14, i64::from(secs) * 1_000_000_000);
2283 assert_eq!(duration_value_from_str("P12Y5M-14DT16H"), Some(v));
2284 assert_eq!(duration_value_from_str(&render_duration_value(&v)), Some(v));
2286 }
2287
2288 #[test]
2289 fn duration_construction_carries_rounded_up_subsecond() {
2290 let fields: Fields =
2294 [("seconds".to_string(), TemporalField::Float(0.999_999_999_6))].into();
2295 let dv = build_duration_map(&fields).expect("duration");
2296 assert!(dv.nanos >= 0 && dv.nanos < 1_000_000_000, "nanos canonical");
2297 assert_eq!((dv.seconds, dv.nanos), (1, 0));
2298 assert_eq!(render_duration_value(&dv), "PT1S");
2299 }
2300
2301 #[test]
2302 fn duration_render_preserves_subsecond_for_large_spans() {
2303 let nanos = (278_565_i64 * 3600 + 45 * 60 + 22) * 1_000_000_000 + 142_000_000;
2306 let dv = |n: i64| DurationValue::from_total_nanos(0, 0, n);
2307 assert_eq!(render_duration_value(&dv(nanos)), "PT278565H45M22.142S");
2308 assert_eq!(render_duration_value(&dv(-1_999_000_000)), "PT-1.999S");
2310 assert_eq!(render_duration_value(&dv(-1_000_000)), "PT-0.001S");
2311 }
2312
2313 #[test]
2314 fn duration_scale_matches_opencypher() {
2315 let base = DurationValue::from_total_nanos(149, 14, 58_390_000_000_001);
2317 assert_eq!(
2319 scale_duration(&base, 2.0, false),
2320 DurationValue::from_total_nanos(298, 28, 116_780_000_000_002),
2321 "P24Y10M28DT32H26M20.000000002S"
2322 );
2323 assert_eq!(
2326 scale_duration(&base, 2.0, true),
2327 DurationValue::from_total_nanos(74, 22, 48_068_000_000_000),
2328 "P6Y2M22DT13H21M8S"
2329 );
2330 assert_eq!(
2332 scale_duration(&base, 0.5, false),
2333 DurationValue::from_total_nanos(74, 22, 48_068_000_000_000)
2334 );
2335 }
2336
2337 #[test]
2338 fn duration_arithmetic() {
2339 let d = |s: &str| parse_date_string(s).unwrap();
2340 let nanos = (16 * 3600 + 12 * 60 + 70) * 1_000_000_000 + 2;
2343 let dur = |m, dd, n| DurationValue::from_total_nanos(m, dd, n);
2344 assert_eq!(
2345 date_plus_duration(d("1984-10-11"), &dur(149, 14, nanos)),
2346 d("1997-03-25")
2347 );
2348 assert_eq!(
2350 date_plus_duration(d("1984-10-11"), &dur(-149, -14, -nanos)),
2351 d("1972-04-27")
2352 );
2353 let big = 122_293_500_000_000_i64;
2357 assert_eq!(
2358 date_plus_duration(d("1984-10-11"), &dur(155, 29, big)),
2359 d("1997-10-11")
2360 );
2361 assert_eq!(
2362 date_plus_duration(d("1984-10-11"), &dur(-155, -29, -big)),
2363 d("1971-10-12")
2364 );
2365 let day = 86_400_000_000_000_i64;
2367 assert_eq!(
2368 localtime_plus_duration(23 * 3_600_000_000_000, 2 * 3_600_000_000_000),
2369 3_600_000_000_000
2370 );
2371 assert_eq!(localtime_plus_duration(0, -1), day - 1);
2372 let (date, nod) = datetime_plus_duration(
2374 d("1984-10-11"),
2375 12 * 3_600_000_000_000,
2376 &dur(0, 0, 13 * 3_600_000_000_000),
2377 );
2378 assert_eq!((date, nod), (d("1984-10-12"), 3_600_000_000_000)); }
2380
2381 #[test]
2382 fn time_value_from_str_defaults_offset_to_utc() {
2383 assert_eq!(
2385 time_value_from_str("14:30"),
2386 Some((14 * 3_600_000_000_000 + 30 * 60_000_000_000, 0))
2387 );
2388 assert_eq!(
2390 time_value_from_str("14:30+01:00"),
2391 Some((14 * 3_600_000_000_000 + 30 * 60_000_000_000, 3600))
2392 );
2393 }
2394
2395 #[test]
2396 fn duration_between_units() {
2397 let date = |s: &str| (Some(parse_date_string(s).unwrap()), 0_i64, None, None);
2398 let lt = |h: i64, m: i64| (None, (h * 3600 + m * 60) * 1_000_000_000, None, None);
2399 let dv = |m, dd, n| DurationValue::from_total_nanos(m, dd, n);
2400 assert_eq!(
2402 duration_between(
2403 &date("1984-10-11"),
2404 &date("2015-06-24"),
2405 BetweenMode::Between
2406 ),
2407 Some(dv(368, 13, 0)) );
2409 assert_eq!(
2411 duration_between(
2412 &date("1984-10-11"),
2413 &date("2015-06-24"),
2414 BetweenMode::Months
2415 ),
2416 Some(dv(368, 0, 0))
2417 );
2418 assert_eq!(
2419 duration_between(&date("1984-10-11"), &date("2015-06-24"), BetweenMode::Days),
2420 Some(dv(0, 11213, 0))
2421 );
2422 assert_eq!(
2424 duration_between(<(14, 30), <(16, 30), BetweenMode::Between),
2425 Some(dv(0, 0, 2 * 3_600_000_000_000))
2426 );
2427 let r = duration_between(
2429 &date("2015-06-24"),
2430 &date("1984-10-11"),
2431 BetweenMode::Between,
2432 )
2433 .unwrap();
2434 assert!(r.months <= 0 && r.days <= 0 && r.seconds <= 0 && r.nanos <= 0);
2435
2436 let ldt = |s: &str| {
2439 let (d, t) = s.split_once('T').unwrap();
2440 (
2441 Some(parse_date_string(d).unwrap()),
2442 nanos_of_day(&parse_time_of_day(t).unwrap()),
2443 None,
2444 None,
2445 )
2446 };
2447 assert_eq!(
2448 duration_between(
2449 &ldt("2018-01-02T10:00:00"),
2450 &ldt("2018-01-01T12:00:00"),
2451 BetweenMode::Between,
2452 ),
2453 Some(dv(0, 0, -22 * 3_600_000_000_000)),
2454 "a 22h backward span is PT-22H, not P-1M30DT2H"
2455 );
2456 }
2457
2458 #[test]
2459 fn duration_inseconds_dst_named_zone() {
2460 let d = |y, m, day| crate::calendar::ymd_to_days(y, m, day).unwrap();
2464 let h = |n: i64| n * 3_600_000_000_000;
2465 let zoned = (
2467 Some(d(2017, 10, 29)),
2468 0_i64,
2469 Some(7200_i32),
2470 Some("Europe/Stockholm".to_string()),
2471 );
2472 let unzoned_0429_04 = (Some(d(2017, 10, 29)), h(4), None, None);
2473 assert_eq!(
2474 duration_between(&zoned, &unzoned_0429_04, BetweenMode::Seconds),
2475 Some(DurationValue::from_total_nanos(0, 0, h(5))),
2476 "00:00 (+02) → 04:00 across the fall-back is 5 real hours"
2477 );
2478 let next_date = (Some(d(2017, 10, 30)), 0_i64, None, None);
2480 assert_eq!(
2481 duration_between(&zoned, &next_date, BetweenMode::Seconds),
2482 Some(DurationValue::from_total_nanos(0, 0, h(25)))
2483 );
2484 }
2485
2486 #[test]
2487 fn localtime_str_round_trip() {
2488 let r = |s: &str| render_localtime_nanos(localtime_nanos_from_str(s).expect("parse"));
2490 assert_eq!(r("21:40:32.142"), "21:40:32.142");
2491 assert_eq!(r("214032.142"), "21:40:32.142");
2492 assert_eq!(r("21:40:32"), "21:40:32");
2493 assert_eq!(r("21:40"), "21:40");
2494 assert_eq!(r("21"), "21:00");
2495 assert!(localtime_nanos_from_str("21:40:32+01:00").is_none());
2497 }
2498
2499 #[test]
2500 fn localtime_of_day_from_any_temporal_string() {
2501 let r = |s: &str| render_localtime_nanos(time_of_day_nanos_any(s).expect("parse"));
2502 assert_eq!(r("12:31:14.645876123"), "12:31:14.645876123"); assert_eq!(r("12:31:14.645876+01:00"), "12:31:14.645876"); assert_eq!(r("1984-10-11T12:31:14.645"), "12:31:14.645"); assert_eq!(r("1984-10-11T12:00+01:00[Europe/Stockholm]"), "12:00"); }
2507
2508 #[test]
2509 fn localtime_projection_overrides() {
2510 let base = localtime_nanos_from_str("12:31:14.645876123").unwrap();
2511 let proj =
2512 |o: &LocalTimeOverrides| render_localtime_nanos(project_localtime(base, o).unwrap());
2513 assert_eq!(proj(&LocalTimeOverrides::default()), "12:31:14.645876123");
2514 assert_eq!(
2515 proj(&LocalTimeOverrides {
2516 second: Some(42),
2517 ..Default::default()
2518 }),
2519 "12:31:42.645876123"
2520 );
2521 }
2522
2523 #[test]
2524 fn localdatetime_str_round_trip() {
2525 let r = |s: &str| {
2527 let (d, n) = localdatetime_parts_from_str(s).expect("parse");
2528 render_localdatetime(d, n)
2529 };
2530 assert_eq!(r("2015-07-21T21:40:32.142"), "2015-07-21T21:40:32.142");
2531 assert_eq!(r("2015-W30-2T214032.142"), "2015-07-21T21:40:32.142");
2532 assert_eq!(r("2015-202T21:40:32"), "2015-07-21T21:40:32");
2533 assert_eq!(r("2015T214032"), "2015-01-01T21:40:32");
2534 assert_eq!(r("20150721T21:40"), "2015-07-21T21:40");
2535 assert_eq!(r("2015202T21"), "2015-07-21T21:00");
2536 assert!(localdatetime_parts_from_str("2015-07-21T21:40:32+01:00").is_none());
2538 }
2539
2540 #[test]
2541 fn localdatetime_full_year_range() {
2542 let r = |s: &str| {
2545 let (d, n) = localdatetime_parts_from_str(s).expect("parse");
2546 render_localdatetime(d, n)
2547 };
2548 assert_eq!(
2549 r("0001-01-01T01:01:01.000000001"),
2550 "0001-01-01T01:01:01.000000001"
2551 );
2552 assert_eq!(
2553 r("9999-09-09T09:59:59.999999999"),
2554 "9999-09-09T09:59:59.999999999"
2555 );
2556 }
2557
2558 #[test]
2559 fn time_str_round_trip() {
2560 let r = |s: &str| {
2562 let (n, o) = time_value_from_str(s).expect("parse");
2563 render_time_value(n, o)
2564 };
2565 assert_eq!(r("21:40:32.142+0100"), "21:40:32.142+01:00");
2566 assert_eq!(r("214032.142Z"), "21:40:32.142Z");
2567 assert_eq!(r("214032-0100"), "21:40:32-01:00");
2568 assert_eq!(r("21:40-01:30"), "21:40-01:30");
2569 assert_eq!(r("2140-00:00"), "21:40Z"); assert_eq!(r("2140-02"), "21:40-02:00");
2571 assert_eq!(r("22+18:00"), "22:00+18:00");
2572 assert_eq!(r("21:40:32"), "21:40:32Z");
2574 }
2575
2576 #[test]
2577 fn time_projection_zone_shift_vs_attach() {
2578 let base = |s: &str| time_of_day_with_offset(s).expect("parse");
2579 let ren = |(n, o): (i64, i32)| render_time_value(n, o);
2580 let plus5 = Some(5 * 3600);
2581 let (n, off) = base("12:31:14.645876123");
2583 assert_eq!(ren(project_time(n, off, plus5)), "12:31:14.645876123+05:00");
2584 let (n, off) = base("12:31:14.645876+01:00");
2586 assert_eq!(ren(project_time(n, off, plus5)), "16:31:14.645876+05:00");
2587 let (n, off) = base("12:31:14.645876+01:00");
2589 assert_eq!(ren(project_time(n, off, None)), "12:31:14.645876+01:00");
2590 let (n, off) = base("12:31:14.645876123");
2591 assert_eq!(ren(project_time(n, off, None)), "12:31:14.645876123Z");
2592 }
2593
2594 #[test]
2595 fn datetime_str_round_trip() {
2596 let r = |s: &str| {
2597 let (d, n, o, z) = datetime_value_from_str(s).expect("parse");
2598 render_datetime_value(d, n, o, z.as_deref())
2599 };
2600 assert_eq!(
2601 r("2015-07-21T21:40:32.142+01:00"),
2602 "2015-07-21T21:40:32.142+01:00"
2603 );
2604 assert_eq!(r("2015-07-21T21:40Z"), "2015-07-21T21:40Z");
2605 assert_eq!(
2607 r("2017-08-08T12:31:14.645876123[Europe/Stockholm]"),
2608 "2017-08-08T12:31:14.645876123+02:00[Europe/Stockholm]"
2609 );
2610 }
2611
2612 #[test]
2613 fn datetime_projection_shift_and_attach() {
2614 let render = |p: Option<DateTimeParts>| {
2615 let (d, n, o, z) = p.unwrap();
2616 render_datetime_value(d, n, o, z.as_deref())
2617 };
2618 let date = parse_date_string("1984-10-11").unwrap();
2619 let nanos = nanos_of_day(&parse_time_of_day("12:31:14.645876").unwrap());
2620 assert_eq!(
2622 render(project_datetime(
2623 date,
2624 nanos,
2625 Some(3600),
2626 None,
2627 Some("+05:00")
2628 )),
2629 "1984-10-11T16:31:14.645876+05:00"
2630 );
2631 assert_eq!(
2633 render(project_datetime(
2634 date,
2635 nanos,
2636 Some(3600),
2637 None,
2638 Some("Pacific/Honolulu")
2639 )),
2640 "1984-10-11T01:31:14.645876-10:00[Pacific/Honolulu]"
2641 );
2642 assert_eq!(
2644 render(project_datetime(
2645 date,
2646 nanos,
2647 None,
2648 None,
2649 Some("Pacific/Honolulu")
2650 )),
2651 "1984-10-11T12:31:14.645876-10:00[Pacific/Honolulu]"
2652 );
2653 assert_eq!(
2655 render(project_datetime(
2656 date,
2657 nanos,
2658 Some(3600),
2659 Some("Europe/Stockholm"),
2660 None
2661 )),
2662 "1984-10-11T12:31:14.645876+01:00[Europe/Stockholm]"
2663 );
2664 }
2665
2666 #[test]
2667 fn partial_forms() {
2668 assert_eq!(d("2015-07"), "2015-07-01");
2669 assert_eq!(d("201507"), "2015-07-01");
2670 assert_eq!(d("2015"), "2015-01-01");
2671 }
2672
2673 #[test]
2674 fn week_forms() {
2675 assert_eq!(d("2015-W30-2"), "2015-07-21");
2676 assert_eq!(d("2015W302"), "2015-07-21");
2677 assert_eq!(d("2015-W30"), "2015-07-20");
2678 assert_eq!(d("2015W30"), "2015-07-20");
2679 }
2680
2681 #[test]
2682 fn ordinal_forms() {
2683 assert_eq!(d("2015-202"), "2015-07-21");
2684 assert_eq!(d("2015202"), "2015-07-21");
2685 }
2686
2687 #[test]
2688 fn rejects_garbage() {
2689 assert!(parse_date_string("not-a-date").is_none());
2690 assert!(parse_date_string("2015-13-01").is_none());
2691 assert!(parse_date_string("").is_none());
2692 }
2693
2694 #[test]
2695 fn non_ascii_week_does_not_panic() {
2696 assert!(parse_date_string("2015W€0").is_none());
2698 assert!(parse_date_string("2015W3é").is_none());
2699 }
2700
2701 #[test]
2702 fn local_time_forms() {
2703 let r = |s| render_local_time(s).unwrap();
2704 assert_eq!(r("21:40:32.142"), "21:40:32.142");
2705 assert_eq!(r("214032.142"), "21:40:32.142");
2706 assert_eq!(r("21:40:32"), "21:40:32");
2707 assert_eq!(r("214032"), "21:40:32");
2708 assert_eq!(r("21:40"), "21:40");
2709 assert_eq!(r("2140"), "21:40");
2710 assert_eq!(r("21"), "21:00");
2711 assert!(render_local_time("21:40Z").is_none());
2713 }
2714
2715 #[test]
2716 fn time_forms() {
2717 let r = |s| render_time(s).unwrap();
2718 assert_eq!(r("21:40:32.142+0100"), "21:40:32.142+01:00");
2719 assert_eq!(r("214032.142Z"), "21:40:32.142Z");
2720 assert_eq!(r("21:40:32+01:00"), "21:40:32+01:00");
2721 assert_eq!(r("214032-0100"), "21:40:32-01:00");
2722 assert_eq!(r("21:40-01:30"), "21:40-01:30");
2723 assert_eq!(r("2140-00:00"), "21:40Z"); assert_eq!(r("2140-02"), "21:40-02:00");
2725 assert_eq!(r("22+18:00"), "22:00+18:00");
2726 assert!(render_time("21:40").is_none());
2728 }
2729
2730 #[test]
2731 fn local_date_time_forms() {
2732 let r = |s| render_local_date_time(s).unwrap();
2733 assert_eq!(r("2015-07-21T21:40:32.142"), "2015-07-21T21:40:32.142");
2734 assert_eq!(r("2015-W30-2T214032.142"), "2015-07-21T21:40:32.142");
2735 assert_eq!(r("2015-202T21:40:32"), "2015-07-21T21:40:32");
2736 assert_eq!(r("2015T214032"), "2015-01-01T21:40:32");
2737 assert_eq!(r("20150721T21:40"), "2015-07-21T21:40");
2738 assert_eq!(r("2015-W30T2140"), "2015-07-20T21:40");
2739 assert_eq!(r("2015202T21"), "2015-07-21T21:00");
2740 }
2741
2742 #[test]
2743 fn date_time_forms() {
2744 let r = |s| render_date_time(s).unwrap();
2745 assert_eq!(
2746 r("2015-07-21T21:40:32.142+0100"),
2747 "2015-07-21T21:40:32.142+01:00"
2748 );
2749 assert_eq!(r("2015-W30-2T214032.142Z"), "2015-07-21T21:40:32.142Z");
2750 assert_eq!(r("2015-202T21:40:32+01:00"), "2015-07-21T21:40:32+01:00");
2751 assert_eq!(r("2015T214032-0100"), "2015-01-01T21:40:32-01:00");
2752 assert_eq!(r("20150721T21:40-01:30"), "2015-07-21T21:40-01:30");
2753 assert_eq!(r("2015-W30T2140-00:00"), "2015-07-20T21:40Z");
2754 assert_eq!(r("2015-W30T2140-02"), "2015-07-20T21:40-02:00");
2755 assert_eq!(r("2015202T21+18:00"), "2015-07-21T21:00+18:00");
2756 }
2757
2758 #[test]
2759 fn date_time_named_zone_forms() {
2760 let r = |s| render_date_time(s).unwrap();
2761 assert_eq!(
2763 r("2015-07-21T21:40:32.142+02:00[Europe/Stockholm]"),
2764 "2015-07-21T21:40:32.142+02:00[Europe/Stockholm]"
2765 );
2766 assert_eq!(
2767 r("2015-07-21T21:40:32.142+0845[Australia/Eucla]"),
2768 "2015-07-21T21:40:32.142+08:45[Australia/Eucla]"
2769 );
2770 assert_eq!(
2771 r("2015-07-21T21:40:32.142-04[America/New_York]"),
2772 "2015-07-21T21:40:32.142-04:00[America/New_York]"
2773 );
2774 assert_eq!(
2776 r("2015-07-21T21:40:32.142[Europe/London]"),
2777 "2015-07-21T21:40:32.142+01:00[Europe/London]"
2778 );
2779 assert_eq!(
2781 r("1818-07-21T21:40:32.142[Europe/Stockholm]"),
2782 "1818-07-21T21:40:32.142+00:53:28[Europe/Stockholm]"
2783 );
2784 }
2785
2786 #[test]
2787 fn duration_forms() {
2788 let r = |s| render_duration(s).unwrap();
2789 assert_eq!(r("P14DT16H12M"), "P14DT16H12M");
2790 assert_eq!(r("P5M1.5D"), "P5M1DT12H");
2791 assert_eq!(r("P0.75M"), "P22DT19H51M49.5S");
2792 assert_eq!(r("PT0.75M"), "PT45S");
2793 assert_eq!(r("P2.5W"), "P17DT12H");
2794 assert_eq!(r("P12Y5M14DT16H12M70S"), "P12Y5M14DT16H13M10S");
2795 assert_eq!(r("P2012-02-02T14:37:21.545"), "P2012Y2M2DT14H37M21.545S");
2796 }
2797
2798 fn fields(pairs: &[(&str, TemporalField)]) -> Fields {
2799 pairs
2800 .iter()
2801 .map(|(k, v)| {
2802 (
2803 (*k).to_string(),
2804 match v {
2805 TemporalField::Int(n) => TemporalField::Int(*n),
2806 TemporalField::Float(x) => TemporalField::Float(*x),
2807 TemporalField::Str(s) => TemporalField::Str(s.clone()),
2808 TemporalField::Date(d) => TemporalField::Date(*d),
2809 },
2810 )
2811 })
2812 .collect()
2813 }
2814
2815 fn int(n: i64) -> TemporalField {
2816 TemporalField::Int(n)
2817 }
2818
2819 #[test]
2820 fn date_map_forms() {
2821 let r = |p: &[(&str, TemporalField)]| render_temporal_map("date", &fields(p)).unwrap();
2822 assert_eq!(
2823 r(&[("year", int(1984)), ("month", int(10)), ("day", int(11))]),
2824 "1984-10-11"
2825 );
2826 assert_eq!(r(&[("year", int(1984)), ("month", int(10))]), "1984-10-01");
2827 assert_eq!(
2828 r(&[
2829 ("year", int(1984)),
2830 ("week", int(10)),
2831 ("dayOfWeek", int(3))
2832 ]),
2833 "1984-03-07"
2834 );
2835 assert_eq!(r(&[("year", int(1984)), ("week", int(10))]), "1984-03-05");
2836 assert_eq!(r(&[("year", int(1984))]), "1984-01-01");
2837 assert_eq!(
2838 r(&[("year", int(1984)), ("ordinalDay", int(202))]),
2839 "1984-07-20"
2840 );
2841 assert_eq!(
2842 r(&[
2843 ("year", int(1984)),
2844 ("quarter", int(3)),
2845 ("dayOfQuarter", int(45))
2846 ]),
2847 "1984-08-14"
2848 );
2849 assert_eq!(r(&[("year", int(1817)), ("week", int(1))]), "1816-12-30");
2851 }
2852
2853 #[test]
2854 fn date_map_anchored_week() {
2855 let anchor = TemporalField::Date(parse_date_string("1816-12-31").unwrap());
2856 let out =
2857 render_temporal_map("date", &fields(&[("date", anchor), ("week", int(2))])).unwrap();
2858 assert_eq!(out, "1817-01-07");
2859 }
2860
2861 #[test]
2862 fn time_and_subsecond_maps() {
2863 let lt =
2864 |p: &[(&str, TemporalField)]| render_temporal_map("localtime", &fields(p)).unwrap();
2865 assert_eq!(
2866 lt(&[
2867 ("hour", int(12)),
2868 ("minute", int(31)),
2869 ("second", int(14)),
2870 ("nanosecond", int(789)),
2871 ("millisecond", int(123)),
2872 ("microsecond", int(456)),
2873 ]),
2874 "12:31:14.123456789"
2875 );
2876 assert_eq!(lt(&[("hour", int(12))]), "12:00");
2877 let t = |p: &[(&str, TemporalField)]| render_temporal_map("time", &fields(p)).unwrap();
2879 assert_eq!(t(&[("hour", int(12)), ("minute", int(31))]), "12:31Z");
2880 assert_eq!(
2881 t(&[
2882 ("hour", int(12)),
2883 ("minute", int(34)),
2884 ("second", int(56)),
2885 ("timezone", TemporalField::Str("+02:05:59".into())),
2886 ]),
2887 "12:34:56+02:05:59"
2888 );
2889 }
2890
2891 #[test]
2892 fn datetime_maps_with_zones() {
2893 let dt = |p: &[(&str, TemporalField)]| render_temporal_map("datetime", &fields(p)).unwrap();
2894 assert_eq!(
2896 dt(&[("year", int(1984)), ("month", int(10)), ("day", int(11))]),
2897 "1984-10-11T00:00Z"
2898 );
2899 assert_eq!(
2901 dt(&[
2902 ("year", int(1984)),
2903 ("ordinalDay", int(202)),
2904 ("hour", int(12)),
2905 ("timezone", TemporalField::Str("+01:00".into())),
2906 ]),
2907 "1984-07-20T12:00+01:00"
2908 );
2909 assert_eq!(
2911 dt(&[
2912 ("year", int(1984)),
2913 ("ordinalDay", int(202)),
2914 ("hour", int(12)),
2915 ("timezone", TemporalField::Str("Europe/Stockholm".into())),
2916 ]),
2917 "1984-07-20T12:00+02:00[Europe/Stockholm]"
2918 );
2919 assert_eq!(
2920 dt(&[
2921 ("year", int(1984)),
2922 ("month", int(10)),
2923 ("day", int(11)),
2924 ("hour", int(12)),
2925 ("timezone", TemporalField::Str("Europe/Stockholm".into())),
2926 ]),
2927 "1984-10-11T12:00+01:00[Europe/Stockholm]"
2928 );
2929 }
2930
2931 #[test]
2932 fn duration_maps() {
2933 let d = |p: &[(&str, TemporalField)]| render_temporal_map("duration", &fields(p)).unwrap();
2934 assert_eq!(
2935 d(&[("days", int(14)), ("hours", int(16)), ("minutes", int(12))]),
2936 "P14DT16H12M"
2937 );
2938 assert_eq!(
2939 d(&[("months", int(5)), ("days", TemporalField::Float(1.5))]),
2940 "P5M1DT12H"
2941 );
2942 assert_eq!(
2943 d(&[("months", TemporalField::Float(0.75))]),
2944 "P22DT19H51M49.5S"
2945 );
2946 assert_eq!(d(&[("weeks", TemporalField::Float(2.5))]), "P17DT12H");
2947 assert_eq!(
2948 d(&[
2949 ("days", int(14)),
2950 ("seconds", int(70)),
2951 ("nanoseconds", int(1))
2952 ]),
2953 "P14DT1M10.000000001S"
2954 );
2955 assert_eq!(
2956 d(&[("minutes", TemporalField::Float(1.5)), ("seconds", int(1))]),
2957 "PT1M31S"
2958 );
2959 }
2960
2961 #[test]
2962 fn epoch_constructors() {
2963 assert_eq!(
2964 render_from_epoch(416_779, 999_999_999).unwrap(),
2965 "1970-01-05T19:46:19.999999999Z"
2966 );
2967 assert_eq!(
2968 render_from_epoch_millis(237_821_673_987).unwrap(),
2969 "1977-07-15T13:34:33.987Z"
2970 );
2971 }
2972
2973 #[test]
2974 fn rejects_malformed_temporals() {
2975 assert!(render_local_time("21:40:32.1234567890").is_none());
2977 assert!(render_time("21:40+01:-30").is_none());
2979 assert!(render_time("21:40+24:00").is_none());
2980 assert!(render_duration("P").is_none());
2982 }
2983
2984 #[test]
2985 fn date_components() {
2986 let d = parse_date_string("1984-10-11").unwrap();
2988 let c = |n| date_component(d, n).unwrap();
2989 assert_eq!(c("year"), 1984);
2990 assert_eq!(c("quarter"), 4);
2991 assert_eq!(c("month"), 10);
2992 assert_eq!(c("week"), 41);
2993 assert_eq!(c("weekYear"), 1984);
2994 assert_eq!(c("day"), 11);
2995 assert_eq!(c("ordinalDay"), 285);
2996 assert_eq!(c("weekDay"), 4);
2997 assert_eq!(c("dayOfQuarter"), 11);
2998 let d2 = parse_date_string("1984-01-01").unwrap();
3000 assert_eq!(date_component(d2, "weekYear").unwrap(), 1983);
3001 assert_eq!(date_component(d2, "week").unwrap(), 52);
3002 assert_eq!(date_component(d2, "weekDay").unwrap(), 7);
3003 assert!(date_component(d, "bogus").is_none());
3004 }
3005}