1use chrono::{
20 Datelike, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike,
21};
22
23#[derive(Debug, Clone, PartialEq)]
28pub enum TzId {
29 Offset(i32),
30 Named(String),
31}
32
33const SECONDS_PER_DAY: i64 = 86_400;
34
35const AVG_MONTH_DAYS: f64 = 365.2425 / 12.0;
45
46const NANOS_PER_SEC: i128 = 1_000_000_000;
47
48fn epoch() -> NaiveDate {
52 NaiveDate::from_ymd_opt(1970, 1, 1).expect("1970-01-01 is a valid date")
53}
54
55pub fn epoch_day_from_ymd(year: i32, month: u32, day: u32) -> Option<i32> {
56 let d = NaiveDate::from_ymd_opt(year, month, day)?;
57 Some(d.signed_duration_since(epoch()).num_days() as i32)
58}
59
60fn date_from_epoch_day(epoch_day: i32) -> NaiveDate {
61 epoch() + chrono::Duration::days(epoch_day as i64)
62}
63
64#[derive(Clone, Copy)]
73pub struct NowSnapshot {
74 pub epoch_day: i32,
75 pub nanos_of_day: i64,
76 pub epoch_seconds: i64,
77 pub nanos: i32,
78}
79
80pub fn capture_now() -> NowSnapshot {
81 let now = chrono::Utc::now();
82 let epoch_seconds = now.timestamp();
83 let nanos = now.nanosecond() as i32;
84 let (epoch_day, nanos_of_day) = split_epoch_seconds(epoch_seconds);
85 NowSnapshot {
86 epoch_day,
87 nanos_of_day: nanos_of_day + nanos as i64,
88 epoch_seconds,
89 nanos,
90 }
91}
92
93pub fn format_date(epoch_day: i32) -> String {
94 let d = date_from_epoch_day(epoch_day);
95 format!("{:04}-{:02}-{:02}", d.year(), d.month(), d.day())
102}
103
104pub fn parse_date(s: &str) -> Option<i32> {
110 let s = s.trim();
111 if !s.is_ascii() {
115 return None;
116 }
117 if let Some(epoch_day) = parse_week_or_ordinal_date(s) {
123 return Some(epoch_day);
124 }
125 let (year, month, day) = if let Some((y, rest)) = s.split_once('-') {
126 let year: i32 = y.parse().ok()?;
127 match rest.split_once('-') {
128 Some((m, d)) => (year, m.parse().ok()?, d.parse().ok()?),
129 None => (year, rest.parse().ok()?, 1),
130 }
131 } else {
132 match s.len() {
133 8 => (
134 s[0..4].parse().ok()?,
135 s[4..6].parse().ok()?,
136 s[6..8].parse().ok()?,
137 ),
138 6 => (s[0..4].parse().ok()?, s[4..6].parse().ok()?, 1),
139 4 => (s[0..4].parse().ok()?, 1, 1),
140 _ => return None,
141 }
142 };
143 epoch_day_from_ymd(year, month, day)
144}
145
146fn parse_week_or_ordinal_date(s: &str) -> Option<i32> {
151 if let Some((y, rest)) = s.split_once('-') {
152 if let Some(w) = rest.strip_prefix('W') {
153 let week_year: i32 = y.parse().ok()?;
154 let (week, day) = match w.split_once('-') {
155 Some((w, d)) => (w.parse().ok()?, d.parse().ok()?),
156 None => (w.parse().ok()?, 1),
157 };
158 return epoch_day_from_week_fields(week_year, week, day);
159 }
160 if rest.len() == 3 && rest.bytes().all(|b| b.is_ascii_digit()) {
163 let year: i32 = y.parse().ok()?;
164 let ordinal: u32 = rest.parse().ok()?;
165 return epoch_day_from_ordinal_fields(year, ordinal);
166 }
167 return None;
168 }
169 if s.len() >= 5 {
170 if let Some(w) = s[4..].strip_prefix('W') {
171 let week_year: i32 = s[0..4].parse().ok()?;
172 let (week, day) = match w.len() {
173 2 => (w.parse().ok()?, 1),
174 3 => (w[0..2].parse().ok()?, w[2..3].parse().ok()?),
175 _ => return None,
176 };
177 return epoch_day_from_week_fields(week_year, week, day);
178 }
179 }
180 if s.len() == 7 && s.bytes().all(|b| b.is_ascii_digit()) {
181 let year: i32 = s[0..4].parse().ok()?;
182 let ordinal: u32 = s[4..7].parse().ok()?;
183 return epoch_day_from_ordinal_fields(year, ordinal);
184 }
185 None
186}
187
188pub fn date_component(epoch_day: i32, prop: &str) -> Option<i64> {
197 let d = date_from_epoch_day(epoch_day);
198 Some(match prop {
199 "year" => d.year() as i64,
200 "month" => d.month() as i64,
201 "day" => d.day() as i64,
202 "quarter" => ((d.month() - 1) / 3 + 1) as i64,
203 "ordinalDay" => d.ordinal() as i64,
204 "weekDay" | "dayOfWeek" => d.weekday().number_from_monday() as i64,
205 "week" => d.iso_week().week() as i64,
206 "weekYear" => d.iso_week().year() as i64,
207 "dayOfQuarter" => {
208 let quarter_start_month = (d.month() - 1) / 3 * 3 + 1;
209 let quarter_start = NaiveDate::from_ymd_opt(d.year(), quarter_start_month, 1)?;
210 d.signed_duration_since(quarter_start).num_days() + 1
211 }
212 _ => return None,
213 })
214}
215
216fn weekday_from_iso_number(n: i64) -> Option<chrono::Weekday> {
219 use chrono::Weekday::*;
220 Some(match n {
221 1 => Mon,
222 2 => Tue,
223 3 => Wed,
224 4 => Thu,
225 5 => Fri,
226 6 => Sat,
227 7 => Sun,
228 _ => return None,
229 })
230}
231
232pub fn epoch_day_from_week_fields(week_year: i32, week: u32, day_of_week: i64) -> Option<i32> {
239 let weekday = weekday_from_iso_number(day_of_week)?;
240 let d = NaiveDate::from_isoywd_opt(week_year, week, weekday)?;
241 Some(d.signed_duration_since(epoch()).num_days() as i32)
242}
243
244pub fn epoch_day_from_ordinal_fields(year: i32, ordinal_day: u32) -> Option<i32> {
247 let d = NaiveDate::from_yo_opt(year, ordinal_day)?;
248 Some(d.signed_duration_since(epoch()).num_days() as i32)
249}
250
251pub fn epoch_day_from_quarter_fields(year: i32, quarter: u32, day_of_quarter: i64) -> Option<i32> {
255 if !(1..=4).contains(&quarter) {
256 return None;
257 }
258 let quarter_start_month = (quarter - 1) * 3 + 1;
259 let quarter_start = NaiveDate::from_ymd_opt(year, quarter_start_month, 1)?;
260 let d = quarter_start.checked_add_signed(chrono::Duration::days(day_of_quarter - 1))?;
261 Some(d.signed_duration_since(epoch()).num_days() as i32)
262}
263
264pub fn add_duration_to_date(
288 epoch_day: i32,
289 months: i64,
290 days: i64,
291 seconds: i64,
292 nanos: i32,
293 negate: bool,
294) -> Option<i32> {
295 let total_ns: i128 = seconds as i128 * NANOS_PER_SEC + nanos as i128;
296 let extra_days = (total_ns / (86_400 * NANOS_PER_SEC)) as i64;
297 let days = days.checked_add(extra_days)?;
298 let (months, days) = if negate {
299 (months.checked_neg()?, days.checked_neg()?)
300 } else {
301 (months, days)
302 };
303 let d = date_from_epoch_day(epoch_day);
304 let with_months = if months >= 0 {
305 d.checked_add_months(chrono::Months::new(months.try_into().ok()?))?
306 } else {
307 d.checked_sub_months(chrono::Months::new(months.checked_neg()?.try_into().ok()?))?
308 };
309 let result = with_months.checked_add_signed(chrono::Duration::try_days(days)?)?;
310 Some(result.signed_duration_since(epoch()).num_days() as i32)
311}
312
313pub type DurationParts = (i64, i64, i64, i32);
319
320#[derive(Default, Clone, Copy)]
325pub struct DurationFields {
326 pub years: f64,
327 pub months: f64,
328 pub weeks: f64,
329 pub days: f64,
330 pub hours: f64,
331 pub minutes: f64,
332 pub seconds: f64,
333 pub milliseconds: f64,
334 pub microseconds: f64,
335 pub nanoseconds: f64,
336}
337
338pub fn normalize_duration(f: DurationFields) -> DurationParts {
352 let months_f = f.years * 12.0 + f.months;
353 let days_f = f.weeks * 7.0 + f.days;
354 let seconds_f = f.hours * 3600.0 + f.minutes * 60.0 + f.seconds;
355 let extra_nanos =
360 (f.milliseconds * 1_000_000.0 + f.microseconds * 1_000.0 + f.nanoseconds).trunc() as i128;
361 cascade(months_f, days_f, seconds_f, extra_nanos)
362}
363
364fn cascade(months_f: f64, days_f: f64, seconds_f: f64, extra_nanos: i128) -> DurationParts {
369 let whole_months = months_f.trunc();
370 let frac_months = months_f - whole_months;
371 let days_f2 = days_f + frac_months * AVG_MONTH_DAYS;
372 let whole_days = days_f2.trunc();
373 let frac_days = days_f2 - whole_days;
374 let seconds_f2 = seconds_f + frac_days * 86_400.0;
375 let total_ns = (seconds_f2 * NANOS_PER_SEC as f64).round() as i128 + extra_nanos;
383 let seconds = (total_ns / NANOS_PER_SEC) as i64;
384 let nanos = (total_ns % NANOS_PER_SEC) as i32;
385 (whole_months as i64, whole_days as i64, seconds, nanos)
386}
387
388pub fn add_duration(a: DurationParts, b: DurationParts) -> Option<DurationParts> {
397 let months = a.0.checked_add(b.0)?;
398 let days = a.1.checked_add(b.1)?;
399 let total_ns =
400 a.2 as i128 * NANOS_PER_SEC + a.3 as i128 + b.2 as i128 * NANOS_PER_SEC + b.3 as i128;
401 Some((
402 months,
403 days,
404 (total_ns / NANOS_PER_SEC).try_into().ok()?,
405 (total_ns % NANOS_PER_SEC) as i32,
406 ))
407}
408
409pub fn negate_duration(a: DurationParts) -> Option<DurationParts> {
410 Some((
411 a.0.checked_neg()?,
412 a.1.checked_neg()?,
413 a.2.checked_neg()?,
414 a.3.checked_neg()?,
415 ))
416}
417
418pub fn sub_duration(a: DurationParts, b: DurationParts) -> Option<DurationParts> {
419 add_duration(a, negate_duration(b)?)
420}
421
422pub fn scale_duration(a: DurationParts, factor: f64) -> DurationParts {
432 let months_f = a.0 as f64 * factor;
433 let days_f = a.1 as f64 * factor;
434 let total_ns_exact = a.2 as i128 * NANOS_PER_SEC + a.3 as i128;
435 let extra_nanos = (total_ns_exact as f64 * factor).trunc() as i128;
436 cascade(months_f, days_f, 0.0, extra_nanos)
437}
438
439pub fn duration_component(
468 months: i64,
469 days: i64,
470 seconds: i64,
471 nanos: i32,
472 prop: &str,
473) -> Option<i64> {
474 let nanos = nanos as i64;
475 Some(match prop {
476 "years" => months / 12,
477 "quarters" => months / 3,
478 "months" => months,
479 "weeks" => days / 7,
480 "days" => days,
481 "hours" => seconds / 3600,
482 "minutes" => seconds / 60,
483 "seconds" => seconds,
484 "milliseconds" => seconds * 1000 + nanos / 1_000_000,
485 "microseconds" => seconds * 1_000_000 + nanos / 1_000,
486 "nanoseconds" => seconds * NANOS_PER_SEC as i64 + nanos,
487 "quartersOfYear" => (months % 12) / 3,
488 "monthsOfQuarter" => (months % 12) % 3,
489 "monthsOfYear" => months % 12,
490 "daysOfWeek" => days % 7,
491 "minutesOfHour" => (seconds / 60) % 60,
492 "secondsOfMinute" => seconds % 60,
493 "millisecondsOfSecond" => nanos / 1_000_000,
494 "microsecondsOfSecond" => nanos / 1_000,
495 "nanosecondsOfSecond" => nanos,
496 _ => return None,
497 })
498}
499
500pub fn format_duration(months: i64, days: i64, seconds: i64, nanos: i32) -> String {
510 if months == 0 && days == 0 && seconds == 0 && nanos == 0 {
511 return "PT0S".to_string();
512 }
513 let mut out = String::from("P");
514 let years = months / 12;
515 let rem_months = months % 12;
516 if years != 0 {
517 out.push_str(&format!("{years}Y"));
518 }
519 if rem_months != 0 {
520 out.push_str(&format!("{rem_months}M"));
521 }
522 if days != 0 {
523 out.push_str(&format!("{days}D"));
524 }
525 let total_time_ns = seconds as i128 * NANOS_PER_SEC + nanos as i128;
526 if total_time_ns != 0 {
527 out.push('T');
528 if total_time_ns >= 0 {
529 let hours = seconds / 3600;
530 let rem = seconds % 3600;
531 let minutes = rem / 60;
532 let secs = rem % 60;
533 if hours != 0 {
534 out.push_str(&format!("{hours}H"));
535 }
536 if minutes != 0 {
537 out.push_str(&format!("{minutes}M"));
538 }
539 if secs != 0 || nanos != 0 {
540 out.push_str(&format_seconds_fraction(secs, nanos));
541 out.push('S');
542 }
543 } else {
544 let total_ns = total_time_ns;
545 let hours = (total_ns / 3_600_000_000_000) as i64;
546 let rem_h = total_ns % 3_600_000_000_000;
547 let minutes = (rem_h / 60_000_000_000) as i64;
548 let rem_m = rem_h % 60_000_000_000;
549 let secs = (rem_m / 1_000_000_000) as i64;
550 let sub_nanos = (rem_m % 1_000_000_000) as i32;
551 if hours != 0 {
552 out.push_str(&format!("{hours}H"));
553 }
554 if minutes != 0 {
555 out.push_str(&format!("{minutes}M"));
556 }
557 if secs != 0 || sub_nanos != 0 {
558 out.push_str(&format_seconds_fraction(secs, sub_nanos));
559 out.push('S');
560 }
561 }
562 }
563 out
564}
565
566fn format_seconds_fraction(secs: i64, nanos: i32) -> String {
572 if nanos == 0 {
573 return secs.to_string();
574 }
575 let negative = secs < 0 || nanos < 0;
576 let mut frac = format!("{:09}", nanos.unsigned_abs());
577 while frac.ends_with('0') {
578 frac.pop();
579 }
580 format!(
581 "{}{}.{}",
582 if negative { "-" } else { "" },
583 secs.unsigned_abs(),
584 frac
585 )
586}
587
588pub fn parse_duration(s: &str) -> Option<DurationParts> {
601 let s = s.trim();
602 let s = s.strip_prefix('P')?;
603 let (date_part, time_part) = match s.split_once('T') {
604 Some((d, t)) => (d, Some(t)),
605 None => (s, None),
606 };
607 if let Some(fields) = parse_combined_date_time_duration(date_part, time_part) {
608 return Some(normalize_duration(fields));
609 }
610 let date_pairs = scan_number_unit_pairs(date_part)?;
611 let time_pairs = match time_part {
612 Some(part) => scan_number_unit_pairs(part)?,
613 None => Vec::new(),
614 };
615 if date_pairs.is_empty() && time_pairs.is_empty() {
616 return None;
617 }
618
619 let mut f = DurationFields::default();
620 for (value, unit) in date_pairs {
621 match unit {
622 'Y' => f.years = value,
623 'M' => f.months = value,
624 'W' => f.weeks = value,
625 'D' => f.days = value,
626 _ => return None,
627 }
628 }
629 for (value, unit) in time_pairs {
630 match unit {
631 'H' => f.hours = value,
632 'M' => f.minutes = value,
633 'S' => f.seconds = value,
634 _ => return None,
635 }
636 }
637 Some(normalize_duration(f))
638}
639
640fn parse_combined_date_time_duration(
652 date_part: &str,
653 time_part: Option<&str>,
654) -> Option<DurationFields> {
655 let mut date_fields = date_part.splitn(3, '-');
656 let years: f64 = date_fields.next()?.parse().ok()?;
657 let months: f64 = date_fields.next()?.parse().ok()?;
658 let days: f64 = date_fields.next()?.parse().ok()?;
659 if date_fields.next().is_some() {
660 return None;
661 }
662 let mut f = DurationFields {
663 years,
664 months,
665 days,
666 ..Default::default()
667 };
668 if let Some(time_part) = time_part {
669 let mut time_fields = time_part.splitn(3, ':');
670 let hours: f64 = time_fields.next()?.parse().ok()?;
671 let minutes: f64 = time_fields.next()?.parse().ok()?;
672 let seconds: f64 = time_fields.next()?.parse().ok()?;
673 if time_fields.next().is_some() {
674 return None;
675 }
676 f.hours = hours;
677 f.minutes = minutes;
678 f.seconds = seconds;
679 }
680 Some(f)
681}
682
683fn scan_number_unit_pairs(s: &str) -> Option<Vec<(f64, char)>> {
691 let mut out = Vec::new();
692 let chars: Vec<char> = s.chars().collect();
693 let mut i = 0;
694 while i < chars.len() {
695 let start = i;
696 if chars[i] == '-' || chars[i] == '+' {
697 i += 1;
698 }
699 let digits_start = i;
700 while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
701 i += 1;
702 }
703 if i == digits_start {
704 return None;
705 }
706 let &unit = chars.get(i)?;
707 let value = chars[start..i]
708 .iter()
709 .collect::<String>()
710 .parse::<f64>()
711 .ok()?;
712 out.push((value, unit));
713 i += 1;
714 }
715 Some(out)
716}
717
718pub fn local_time_nanos_from_fields(
727 hour: i64,
728 minute: i64,
729 second: i64,
730 nanos: i64,
731) -> Option<i64> {
732 if !(0..24).contains(&hour)
733 || !(0..60).contains(&minute)
734 || !(0..60).contains(&second)
735 || !(0..1_000_000_000).contains(&nanos)
736 {
737 return None;
738 }
739 Some(hour * 3_600_000_000_000 + minute * 60_000_000_000 + second * 1_000_000_000 + nanos)
740}
741
742fn parse_time_of_day(s: &str) -> Option<i64> {
746 if !s.is_ascii() || s.is_empty() {
747 return None;
748 }
749 let (hour, minute, second, nanos) = if s.contains(':') {
750 let mut parts = s.splitn(3, ':');
751 let h: u32 = parts.next()?.parse().ok()?;
752 let m: u32 = match parts.next() {
753 Some(p) => p.parse().ok()?,
754 None => 0,
755 };
756 let (sec, nanos) = match parts.next() {
757 Some(p) => parse_seconds_fraction(p)?,
758 None => (0, 0),
759 };
760 (h, m, sec, nanos)
761 } else {
762 match s.len() {
763 2 => (s.parse().ok()?, 0, 0, 0),
764 4 => (s[0..2].parse().ok()?, s[2..4].parse().ok()?, 0, 0),
765 n if n > 4 => {
766 let (sec, nanos) = parse_seconds_fraction(&s[4..])?;
767 (s[0..2].parse().ok()?, s[2..4].parse().ok()?, sec, nanos)
768 }
769 _ => return None,
770 }
771 };
772 local_time_nanos_from_fields(hour as i64, minute as i64, second as i64, nanos as i64)
773}
774
775fn parse_seconds_fraction(s: &str) -> Option<(u32, u32)> {
780 let (sec_str, frac_str) = match s.split_once('.') {
781 Some((a, b)) => (a, Some(b)),
782 None => (s, None),
783 };
784 let sec: u32 = sec_str.parse().ok()?;
785 if sec >= 60 {
786 return None;
787 }
788 let nanos = match frac_str {
789 None => 0,
790 Some(f) => {
791 if f.is_empty() || !f.bytes().all(|b| b.is_ascii_digit()) {
792 return None;
793 }
794 let mut digits = f.to_string();
795 digits.truncate(9);
796 while digits.len() < 9 {
797 digits.push('0');
798 }
799 digits.parse().ok()?
800 }
801 };
802 Some((sec, nanos))
803}
804
805fn split_time_offset(s: &str) -> (&str, Option<&str>) {
813 if let Some(stripped) = s.strip_suffix('Z') {
814 return (stripped, Some("Z"));
815 }
816 let bytes = s.as_bytes();
817 for i in 1..bytes.len() {
818 if bytes[i] == b'+' || bytes[i] == b'-' {
819 return (&s[..i], Some(&s[i..]));
820 }
821 }
822 (s, None)
823}
824
825pub fn parse_offset_seconds(s: &str) -> Option<i32> {
828 if s == "Z" {
829 return Some(0);
830 }
831 let bytes = s.as_bytes();
832 let sign: i32 = match bytes.first()? {
833 b'+' => 1,
834 b'-' => -1,
835 _ => return None,
836 };
837 let rest = &s[1..];
838 let (h, m, sec): (i32, i32, i32) = if rest.contains(':') {
839 let mut parts = rest.splitn(3, ':');
840 let h = parts.next()?.parse().ok()?;
841 let m = match parts.next() {
842 Some(p) => p.parse().ok()?,
843 None => 0,
844 };
845 let sec = match parts.next() {
846 Some(p) => p.parse().ok()?,
847 None => 0,
848 };
849 (h, m, sec)
850 } else {
851 match rest.len() {
852 2 => (rest.parse().ok()?, 0, 0),
853 4 => (rest[0..2].parse().ok()?, rest[2..4].parse().ok()?, 0),
854 6 => (
855 rest[0..2].parse().ok()?,
856 rest[2..4].parse().ok()?,
857 rest[4..6].parse().ok()?,
858 ),
859 _ => return None,
860 }
861 };
862 if !(0..24).contains(&h) || !(0..60).contains(&m) || !(0..60).contains(&sec) {
863 return None;
864 }
865 Some(sign * (h * 3600 + m * 60 + sec))
866}
867
868pub fn parse_local_time(s: &str) -> Option<i64> {
874 parse_time_of_day(s.trim())
875}
876
877pub fn parse_time(s: &str) -> Option<(i64, i32)> {
886 let s = s.trim();
887 if s.contains('[') {
888 return None;
889 }
890 let (time_part, offset_part) = split_time_offset(s);
891 let offset_seconds = match offset_part {
896 Some(part) => parse_offset_seconds(part)?,
897 None => 0,
898 };
899 Some((parse_time_of_day(time_part)?, offset_seconds))
900}
901
902pub fn local_time_component(nanos_of_day: i64, prop: &str) -> Option<i64> {
905 Some(match prop {
906 "hour" => nanos_of_day / 3_600_000_000_000,
907 "minute" => (nanos_of_day / 60_000_000_000) % 60,
908 "second" => (nanos_of_day / 1_000_000_000) % 60,
909 "millisecond" => (nanos_of_day / 1_000_000) % 1000,
910 "microsecond" => (nanos_of_day / 1_000) % 1_000_000,
911 "nanosecond" => nanos_of_day % 1_000_000_000,
912 _ => return None,
913 })
914}
915
916pub fn format_offset(offset_seconds: i32) -> String {
921 if offset_seconds == 0 {
922 return "Z".to_string();
923 }
924 let sign = if offset_seconds < 0 { "-" } else { "+" };
925 let abs = offset_seconds.unsigned_abs();
926 let h = abs / 3600;
927 let m = (abs / 60) % 60;
928 let sec = abs % 60;
929 if sec != 0 {
930 format!("{sign}{h:02}:{m:02}:{sec:02}")
931 } else {
932 format!("{sign}{h:02}:{m:02}")
933 }
934}
935
936fn format_time_of_day(nanos_of_day: i64) -> String {
943 let hour = nanos_of_day / 3_600_000_000_000;
944 let minute = (nanos_of_day / 60_000_000_000) % 60;
945 let second = (nanos_of_day / 1_000_000_000) % 60;
946 let nanos = (nanos_of_day % 1_000_000_000) as u32;
947 let mut out = format!("{hour:02}:{minute:02}");
948 if second != 0 || nanos != 0 {
949 out.push_str(&format!(":{second:02}"));
950 if nanos != 0 {
951 let mut frac = format!("{nanos:09}");
952 while frac.ends_with('0') {
953 frac.pop();
954 }
955 out.push('.');
956 out.push_str(&frac);
957 }
958 }
959 out
960}
961
962pub fn format_local_time(nanos_of_day: i64) -> String {
963 format_time_of_day(nanos_of_day)
964}
965
966pub fn format_time(nanos_of_day: i64, offset_seconds: i32) -> String {
967 format!(
968 "{}{}",
969 format_time_of_day(nanos_of_day),
970 format_offset(offset_seconds)
971 )
972}
973
974pub fn split_epoch_seconds(epoch_seconds: i64) -> (i32, i64) {
985 let epoch_day = epoch_seconds.div_euclid(SECONDS_PER_DAY) as i32;
986 let secs_of_day = epoch_seconds.rem_euclid(SECONDS_PER_DAY);
987 (epoch_day, secs_of_day * 1_000_000_000)
988}
989
990pub fn combine_epoch_day_and_nanos_of_day(epoch_day: i32, nanos_of_day: i64) -> i64 {
991 epoch_day as i64 * SECONDS_PER_DAY + nanos_of_day / 1_000_000_000
992}
993
994pub fn combine_date_and_time(epoch_day: i32, nanos_of_day: i64) -> (i64, i32) {
998 (
999 combine_epoch_day_and_nanos_of_day(epoch_day, nanos_of_day),
1000 (nanos_of_day % 1_000_000_000) as i32,
1001 )
1002}
1003
1004pub struct CalendarDateTime {
1010 pub year: i32,
1011 pub month: u32,
1012 pub day: u32,
1013 pub hour: i64,
1014 pub minute: i64,
1015 pub second: i64,
1016 pub nanos: i64,
1017}
1018
1019pub fn local_date_time_from_fields(f: CalendarDateTime) -> Option<(i64, i32)> {
1023 let epoch_day = epoch_day_from_ymd(f.year, f.month, f.day)?;
1024 let nanos_of_day = local_time_nanos_from_fields(f.hour, f.minute, f.second, f.nanos)?;
1025 Some((
1026 combine_epoch_day_and_nanos_of_day(epoch_day, nanos_of_day),
1027 (nanos_of_day % 1_000_000_000) as i32,
1028 ))
1029}
1030
1031pub fn date_time_from_fields(f: CalendarDateTime, zone: &TzId) -> Option<(i64, i32)> {
1039 match zone {
1040 TzId::Offset(offset_seconds) => {
1041 let (local_epoch_seconds, nanos) = local_date_time_from_fields(f)?;
1042 Some((local_epoch_seconds - *offset_seconds as i64, nanos))
1043 }
1044 TzId::Named(name) => {
1045 let tz = parse_timezone_name(name)?;
1046 let epoch_day = epoch_day_from_ymd(f.year, f.month, f.day)?;
1047 let nanos_of_day = local_time_nanos_from_fields(f.hour, f.minute, f.second, f.nanos)?;
1048 let naive = naive_datetime_from(epoch_day, nanos_of_day);
1049 let (epoch_seconds, _offset) = utc_from_local_and_named_zone(naive, tz)?;
1050 Some((epoch_seconds, (nanos_of_day % 1_000_000_000) as i32))
1051 }
1052 }
1053}
1054
1055pub fn parse_local_date_time(s: &str) -> Option<(i64, i32)> {
1059 let s = s.trim();
1060 let (date_part, time_part) = s.split_once('T')?;
1061 let epoch_day = parse_date(date_part)?;
1062 let nanos_of_day = parse_time_of_day(time_part)?;
1063 Some((
1064 combine_epoch_day_and_nanos_of_day(epoch_day, nanos_of_day),
1065 (nanos_of_day % 1_000_000_000) as i32,
1066 ))
1067}
1068
1069pub fn parse_date_time(s: &str) -> Option<(i64, i32, TzId)> {
1077 let s = s.trim();
1078 let (date_part, time_part) = s.split_once('T')?;
1079 let epoch_day = parse_date(date_part)?;
1080 let (time_part, zone_name) = match time_part.split_once('[') {
1081 Some((t, rest)) => (t, Some(rest.strip_suffix(']')?)),
1082 None => (time_part, None),
1083 };
1084 let (time_only, offset_part) = split_time_offset(time_part);
1085 let nanos_of_day = parse_time_of_day(time_only)?;
1086 match (offset_part, zone_name) {
1087 (Some(offset_str), zone_name) => {
1088 let offset_seconds = parse_offset_seconds(offset_str)?;
1089 let local_epoch_seconds = combine_epoch_day_and_nanos_of_day(epoch_day, nanos_of_day);
1090 let zone = match zone_name {
1091 Some(zone_str) => {
1092 parse_timezone_name(zone_str)?;
1093 TzId::Named(zone_str.to_string())
1094 }
1095 None => TzId::Offset(offset_seconds),
1096 };
1097 Some((
1098 local_epoch_seconds - offset_seconds as i64,
1099 (nanos_of_day % 1_000_000_000) as i32,
1100 zone,
1101 ))
1102 }
1103 (None, Some(zone_str)) => {
1104 let tz = parse_timezone_name(zone_str)?;
1105 let naive = naive_datetime_from(epoch_day, nanos_of_day);
1106 let (epoch_seconds, _offset) = utc_from_local_and_named_zone(naive, tz)?;
1107 Some((
1108 epoch_seconds,
1109 (nanos_of_day % 1_000_000_000) as i32,
1110 TzId::Named(zone_str.to_string()),
1111 ))
1112 }
1113 (None, None) => None,
1114 }
1115}
1116
1117pub fn date_time_calendar_component(epoch_seconds: i64, prop: &str) -> Option<i64> {
1122 let (epoch_day, _) = split_epoch_seconds(epoch_seconds);
1123 date_component(epoch_day, prop)
1124}
1125
1126pub fn date_time_clock_component(epoch_seconds: i64, nanos: i32, prop: &str) -> Option<i64> {
1132 let (_, nanos_of_day) = split_epoch_seconds(epoch_seconds);
1133 local_time_component(nanos_of_day + nanos as i64, prop)
1134}
1135
1136pub fn epoch_seconds_and_millis(epoch_seconds: i64, nanos: i32) -> (i64, i64) {
1137 (
1138 epoch_seconds,
1139 epoch_seconds * 1000 + (nanos as i64) / 1_000_000,
1140 )
1141}
1142
1143pub fn format_local_date_time(epoch_seconds: i64, nanos: i32) -> String {
1147 let (epoch_day, nanos_of_day) = split_epoch_seconds(epoch_seconds);
1148 format!(
1149 "{}T{}",
1150 format_date(epoch_day),
1151 format_time_of_day(nanos_of_day + nanos as i64)
1152 )
1153}
1154
1155pub fn add_duration_to_time(nanos_of_day: i64, seconds: i64, nanos: i32, negate: bool) -> i64 {
1163 let (seconds, nanos) = if negate {
1164 (-seconds, -nanos)
1165 } else {
1166 (seconds, nanos)
1167 };
1168 let total: i128 = nanos_of_day as i128 + seconds as i128 * NANOS_PER_SEC + nanos as i128;
1169 total.rem_euclid(NANOS_PER_DAY as i128) as i64
1170}
1171
1172const NANOS_PER_DAY: i64 = SECONDS_PER_DAY * 1_000_000_000;
1173
1174pub fn add_duration_to_local_date_time(
1188 epoch_seconds: i64,
1189 existing_nanos: i32,
1190 months: i64,
1191 days: i64,
1192 seconds: i64,
1193 nanos: i32,
1194 negate: bool,
1195) -> Option<(i64, i32)> {
1196 let (months, days, seconds, nanos) = if negate {
1197 (
1198 months.checked_neg()?,
1199 days.checked_neg()?,
1200 seconds.checked_neg()?,
1201 nanos.checked_neg()?,
1202 )
1203 } else {
1204 (months, days, seconds, nanos)
1205 };
1206 let (epoch_day, nanos_of_day) = split_epoch_seconds(epoch_seconds);
1207 let d = date_from_epoch_day(epoch_day);
1208 let with_months = if months >= 0 {
1209 d.checked_add_months(chrono::Months::new(months.try_into().ok()?))?
1210 } else {
1211 d.checked_sub_months(chrono::Months::new(months.checked_neg()?.try_into().ok()?))?
1212 };
1213 let new_epoch_day = with_months.signed_duration_since(epoch()).num_days();
1214
1215 let total_ns: i128 = nanos_of_day as i128
1216 + existing_nanos as i128
1217 + days as i128 * NANOS_PER_DAY as i128
1218 + seconds as i128 * NANOS_PER_SEC
1219 + nanos as i128;
1220 let day_ns = NANOS_PER_DAY as i128;
1221 let extra_days = total_ns.div_euclid(day_ns) as i64;
1222 let final_nanos_of_day = total_ns.rem_euclid(day_ns) as i64;
1223
1224 let final_epoch_day = new_epoch_day.checked_add(extra_days)?;
1225 let final_epoch_seconds = final_epoch_day
1226 .checked_mul(SECONDS_PER_DAY)?
1227 .checked_add(final_nanos_of_day / 1_000_000_000)?;
1228 Some((
1229 final_epoch_seconds,
1230 (final_nanos_of_day % 1_000_000_000) as i32,
1231 ))
1232}
1233
1234pub fn format_date_time(epoch_seconds: i64, nanos: i32, zone: &TzId) -> String {
1235 let offset_seconds = resolve_offset(zone, epoch_seconds);
1241 let local_epoch_seconds = epoch_seconds + offset_seconds as i64;
1242 let zone_suffix = match zone {
1243 TzId::Offset(_) => String::new(),
1244 TzId::Named(name) => format!("[{name}]"),
1248 };
1249 format!(
1250 "{}{}{}",
1251 format_local_date_time(local_epoch_seconds, nanos),
1252 format_offset(offset_seconds),
1253 zone_suffix
1254 )
1255}
1256
1257pub fn resolve_offset(zone: &TzId, epoch_seconds: i64) -> i32 {
1270 match zone {
1271 TzId::Offset(o) => *o,
1272 TzId::Named(name) => {
1273 let tz = parse_timezone_name(name).unwrap_or(chrono_tz::Tz::UTC);
1274 let utc = chrono::DateTime::<chrono::Utc>::from_timestamp(epoch_seconds, 0)
1275 .unwrap_or_default();
1276 utc.with_timezone(&tz).offset().fix().local_minus_utc()
1277 }
1278 }
1279}
1280
1281pub fn parse_timezone_name(s: &str) -> Option<chrono_tz::Tz> {
1284 s.parse().ok()
1285}
1286
1287fn utc_from_local_and_named_zone(naive: NaiveDateTime, tz: chrono_tz::Tz) -> Option<(i64, i32)> {
1295 let dt = match tz.from_local_datetime(&naive) {
1296 LocalResult::Single(dt) => dt,
1297 LocalResult::Ambiguous(earlier, _later) => earlier,
1298 LocalResult::None => return None,
1299 };
1300 let offset = dt.offset().fix().local_minus_utc();
1301 Some((dt.timestamp(), offset))
1302}
1303
1304const NANOS_PER_DAY_I64: i64 = SECONDS_PER_DAY * 1_000_000_000;
1309
1310fn naive_datetime_from(epoch_day: i32, nanos_of_day: i64) -> NaiveDateTime {
1311 let secs = (nanos_of_day / 1_000_000_000) as u32;
1312 let nanos = (nanos_of_day % 1_000_000_000) as u32;
1313 NaiveDateTime::new(
1314 date_from_epoch_day(epoch_day),
1315 NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos)
1316 .expect("nanos_of_day is always in 0..NANOS_PER_DAY by construction"),
1317 )
1318}
1319
1320fn proleptic_month(d: NaiveDate) -> i64 {
1327 d.year() as i64 * 12 + d.month() as i64 - 1
1328}
1329
1330fn months_between_dates(a: NaiveDate, b: NaiveDate) -> i64 {
1331 let packed_a = proleptic_month(a) * 32 + a.day() as i64;
1332 let packed_b = proleptic_month(b) * 32 + b.day() as i64;
1333 (packed_b - packed_a) / 32
1334}
1335
1336fn shift_months(dt: NaiveDateTime, months: i64) -> NaiveDateTime {
1340 let d = dt.date();
1341 let shifted = if months >= 0 {
1342 d.checked_add_months(chrono::Months::new(months as u32))
1343 } else {
1344 d.checked_sub_months(chrono::Months::new((-months) as u32))
1345 }
1346 .expect("TCK-scale month shifts stay well within NaiveDate's range");
1347 NaiveDateTime::new(shifted, dt.time())
1348}
1349
1350fn to_utc_instant_tz(dt: NaiveDateTime, zone: &TzId) -> NaiveDateTime {
1376 match zone {
1377 TzId::Offset(o) => dt - chrono::Duration::seconds(*o as i64),
1378 TzId::Named(name) => {
1379 if let Some(tz) = parse_timezone_name(name) {
1380 if let Some((epoch_seconds, _)) = utc_from_local_and_named_zone(dt, tz) {
1381 return chrono::DateTime::<chrono::Utc>::from_timestamp(epoch_seconds, 0)
1382 .map(|utc| utc.naive_utc())
1383 .unwrap_or(dt);
1384 }
1385 }
1386 dt
1387 }
1388 }
1389}
1390
1391fn elapsed_ns(
1392 from: NaiveDateTime,
1393 from_zone: Option<&TzId>,
1394 to: NaiveDateTime,
1395 to_zone: Option<&TzId>,
1396) -> i64 {
1397 let delta = match (from_zone, to_zone) {
1398 (Some(fz), Some(tz)) => to_utc_instant_tz(to, tz) - to_utc_instant_tz(from, fz),
1399 (Some(fz), None) => to_utc_instant_tz(to, fz) - to_utc_instant_tz(from, fz),
1400 (None, Some(tz)) => to_utc_instant_tz(to, tz) - to_utc_instant_tz(from, tz),
1401 (None, None) => to - from,
1402 };
1403 delta
1404 .num_nanoseconds()
1405 .expect("TCK-scale gaps stay well within i64 nanoseconds")
1406}
1407
1408fn months_between_datetimes_offset_aware(
1409 from: NaiveDateTime,
1410 from_zone: Option<&TzId>,
1411 to: NaiveDateTime,
1412 to_zone: Option<&TzId>,
1413) -> i64 {
1414 let mut months = months_between_dates(from.date(), to.date());
1415 let shifted = shift_months(from, months);
1416 let overshot = match (from_zone, to_zone) {
1417 (Some(fz), Some(tz)) => to_utc_instant_tz(shifted, fz) > to_utc_instant_tz(to, tz),
1418 (Some(fz), None) => to_utc_instant_tz(shifted, fz) > to_utc_instant_tz(to, fz),
1419 (None, Some(tz)) => to_utc_instant_tz(shifted, tz) > to_utc_instant_tz(to, tz),
1420 (None, None) => shifted > to,
1421 };
1422 let undershot = match (from_zone, to_zone) {
1423 (Some(fz), Some(tz)) => to_utc_instant_tz(shifted, fz) < to_utc_instant_tz(to, tz),
1424 (Some(fz), None) => to_utc_instant_tz(shifted, fz) < to_utc_instant_tz(to, fz),
1425 (None, Some(tz)) => to_utc_instant_tz(shifted, tz) < to_utc_instant_tz(to, tz),
1426 (None, None) => shifted < to,
1427 };
1428 if months > 0 && overshot {
1429 months -= 1;
1430 } else if months < 0 && undershot {
1431 months += 1;
1432 }
1433 months
1434}
1435
1436fn time_to_utc_nanos(nanos_of_day: i64, zone: &TzId, ref_date: Option<i32>) -> i64 {
1437 let epoch_day = ref_date.unwrap_or(0);
1438 let dt = naive_datetime_from(epoch_day, nanos_of_day);
1439 let utc = to_utc_instant_tz(dt, zone);
1440 (utc.signed_duration_since(epoch().and_hms_opt(0, 0, 0).unwrap()))
1441 .num_nanoseconds()
1442 .unwrap_or(0)
1443}
1444
1445fn between_components(
1446 a_date: Option<i32>,
1447 a_time: Option<i64>,
1448 a_zone: Option<&TzId>,
1449 b_date: Option<i32>,
1450 b_time: Option<i64>,
1451 b_zone: Option<&TzId>,
1452) -> (i64, i64, i64) {
1453 match (a_date, b_date) {
1454 (Some(ad), Some(bd)) => {
1455 let from = naive_datetime_from(ad, a_time.unwrap_or(0));
1456 let to = naive_datetime_from(bd, b_time.unwrap_or(0));
1457 let months = months_between_datetimes_offset_aware(from, a_zone, to, b_zone);
1458 let shifted = shift_months(from, months);
1459 let shifted_remaining_ns = elapsed_ns(shifted, a_zone, to, b_zone);
1460 let raw_total_ns = elapsed_ns(from, a_zone, to, b_zone);
1461 (months, shifted_remaining_ns, raw_total_ns)
1462 }
1463 _ => {
1464 let diff = match (a_zone, b_zone) {
1465 (Some(az), Some(bz)) => {
1466 let ref_date = a_date.or(b_date);
1478 let at = time_to_utc_nanos(a_time.unwrap_or(0), az, ref_date);
1479 let bt = time_to_utc_nanos(b_time.unwrap_or(0), bz, ref_date);
1480 bt - at
1481 }
1482 (Some(az), None) => {
1483 let at = time_to_utc_nanos(a_time.unwrap_or(0), az, a_date);
1484 let bt = time_to_utc_nanos(b_time.unwrap_or(0), az, a_date);
1485 bt - at
1486 }
1487 (None, Some(bz)) => {
1488 let at = time_to_utc_nanos(a_time.unwrap_or(0), bz, b_date);
1489 let bt = time_to_utc_nanos(b_time.unwrap_or(0), bz, b_date);
1490 bt - at
1491 }
1492 (None, None) => b_time.unwrap_or(0) - a_time.unwrap_or(0),
1493 };
1494 (0, diff, diff)
1495 }
1496 }
1497}
1498
1499pub fn duration_between(
1500 a_date: Option<i32>,
1501 a_time: Option<i64>,
1502 a_zone: Option<&TzId>,
1503 b_date: Option<i32>,
1504 b_time: Option<i64>,
1505 b_zone: Option<&TzId>,
1506) -> DurationParts {
1507 let (months, shifted_ns, _) =
1508 between_components(a_date, a_time, a_zone, b_date, b_time, b_zone);
1509 let days = shifted_ns / NANOS_PER_DAY_I64;
1510 let rem = shifted_ns % NANOS_PER_DAY_I64;
1511 let seconds = rem.div_euclid(NANOS_PER_SEC as i64);
1512 let nanos = rem.rem_euclid(NANOS_PER_SEC as i64) as i32;
1513 (months, days, seconds, nanos)
1514}
1515
1516pub fn duration_in_months(
1517 a_date: Option<i32>,
1518 a_time: Option<i64>,
1519 a_zone: Option<&TzId>,
1520 b_date: Option<i32>,
1521 b_time: Option<i64>,
1522 b_zone: Option<&TzId>,
1523) -> DurationParts {
1524 let (months, _, _) = between_components(a_date, a_time, a_zone, b_date, b_time, b_zone);
1525 (months, 0, 0, 0)
1526}
1527
1528pub fn duration_in_days(
1529 a_date: Option<i32>,
1530 a_time: Option<i64>,
1531 a_zone: Option<&TzId>,
1532 b_date: Option<i32>,
1533 b_time: Option<i64>,
1534 b_zone: Option<&TzId>,
1535) -> DurationParts {
1536 let (_, _, raw) = between_components(a_date, a_time, a_zone, b_date, b_time, b_zone);
1537 (0, raw / NANOS_PER_DAY_I64, 0, 0)
1538}
1539
1540pub fn duration_in_seconds(
1541 a_date: Option<i32>,
1542 a_time: Option<i64>,
1543 a_zone: Option<&TzId>,
1544 b_date: Option<i32>,
1545 b_time: Option<i64>,
1546 b_zone: Option<&TzId>,
1547) -> DurationParts {
1548 let (_, _, raw) = between_components(a_date, a_time, a_zone, b_date, b_time, b_zone);
1549 (
1550 0,
1551 0,
1552 raw.div_euclid(NANOS_PER_SEC as i64),
1553 raw.rem_euclid(NANOS_PER_SEC as i64) as i32,
1554 )
1555}
1556
1557pub fn truncate_date_unit(epoch_day: i32, unit: &str) -> Option<i32> {
1572 let d = date_from_epoch_day(epoch_day);
1573 let y = d.year();
1574 let to_epoch_day = |d: NaiveDate| d.signed_duration_since(epoch()).num_days() as i32;
1575 match unit {
1576 "millennium" => epoch_day_from_ymd(y - y.rem_euclid(1000), 1, 1),
1577 "century" => epoch_day_from_ymd(y - y.rem_euclid(100), 1, 1),
1578 "decade" => epoch_day_from_ymd(y - y.rem_euclid(10), 1, 1),
1579 "year" => epoch_day_from_ymd(y, 1, 1),
1580 "quarter" => epoch_day_from_ymd(y, (d.month() - 1) / 3 * 3 + 1, 1),
1581 "month" => epoch_day_from_ymd(y, d.month(), 1),
1582 "week" => {
1583 let iso = d.iso_week();
1584 NaiveDate::from_isoywd_opt(iso.year(), iso.week(), chrono::Weekday::Mon)
1585 .map(to_epoch_day)
1586 }
1587 "weekYear" => {
1588 let iso = d.iso_week();
1589 NaiveDate::from_isoywd_opt(iso.year(), 1, chrono::Weekday::Mon).map(to_epoch_day)
1590 }
1591 "day" => Some(epoch_day),
1592 _ => None,
1593 }
1594}
1595
1596pub fn set_iso_weekday(epoch_day: i32, day_of_week: i64) -> Option<i32> {
1604 if !(1..=7).contains(&day_of_week) {
1605 return None;
1606 }
1607 let d = date_from_epoch_day(epoch_day);
1608 let iso = d.iso_week();
1609 let monday = NaiveDate::from_isoywd_opt(iso.year(), iso.week(), chrono::Weekday::Mon)?;
1610 let result = monday + chrono::Duration::days(day_of_week - 1);
1611 Some(result.signed_duration_since(epoch()).num_days() as i32)
1612}
1613
1614pub fn truncate_time_unit(nanos_of_day: i64, unit: &str) -> Option<i64> {
1618 let floor = |n: i64| (nanos_of_day / n) * n;
1619 match unit {
1620 "hour" => Some(floor(3_600_000_000_000)),
1621 "minute" => Some(floor(60_000_000_000)),
1622 "second" => Some(floor(1_000_000_000)),
1623 "millisecond" => Some(floor(1_000_000)),
1624 "microsecond" => Some(floor(1_000)),
1625 "day" => Some(0),
1626 _ => None,
1627 }
1628}
1629
1630#[cfg(test)]
1631mod tests {
1632 use super::*;
1633
1634 fn du(months: f64, days: f64, hours: f64, minutes: f64, seconds: f64) -> DurationParts {
1635 normalize_duration(DurationFields {
1636 months,
1637 days,
1638 hours,
1639 minutes,
1640 seconds,
1641 ..Default::default()
1642 })
1643 }
1644
1645 #[test]
1646 fn construct_basic() {
1647 assert_eq!(
1648 format_duration_parts(du(0.0, 14.0, 16.0, 12.0, 0.0)),
1649 "P14DT16H12M"
1650 );
1651 }
1652
1653 #[test]
1654 fn construct_fractional_months() {
1655 let d = normalize_duration(DurationFields {
1656 months: 0.75,
1657 ..Default::default()
1658 });
1659 assert_eq!(format_duration_parts(d), "P22DT19H51M49.5S");
1660 }
1661
1662 #[test]
1663 fn construct_fractional_weeks() {
1664 let d = normalize_duration(DurationFields {
1665 weeks: 2.5,
1666 ..Default::default()
1667 });
1668 assert_eq!(format_duration_parts(d), "P17DT12H");
1669 }
1670
1671 #[test]
1672 fn construct_years_months_days_seconds_overflow() {
1673 let d = normalize_duration(DurationFields {
1674 years: 12.0,
1675 months: 5.0,
1676 days: 14.0,
1677 hours: 16.0,
1678 minutes: 12.0,
1679 seconds: 70.0,
1680 ..Default::default()
1681 });
1682 assert_eq!(format_duration_parts(d), "P12Y5M14DT16H13M10S");
1683 }
1684
1685 #[test]
1686 fn construct_sub_second() {
1687 let d = normalize_duration(DurationFields {
1688 days: 14.0,
1689 seconds: 70.0,
1690 milliseconds: 1.0,
1691 ..Default::default()
1692 });
1693 assert_eq!(format_duration_parts(d), "P14DT1M10.001S");
1694 }
1695
1696 #[test]
1697 fn construct_minutes_fraction() {
1698 let d = normalize_duration(DurationFields {
1699 minutes: 1.5,
1700 seconds: 1.0,
1701 ..Default::default()
1702 });
1703 assert_eq!(format_duration_parts(d), "PT1M31S");
1704 }
1705
1706 #[test]
1707 fn parse_string_p14dt16h12m() {
1708 assert_eq!(
1709 format_duration_parts(parse_duration("P14DT16H12M").unwrap()),
1710 "P14DT16H12M"
1711 );
1712 }
1713
1714 #[test]
1715 fn parse_string_p0_75m() {
1716 assert_eq!(
1717 format_duration_parts(parse_duration("P0.75M").unwrap()),
1718 "P22DT19H51M49.5S"
1719 );
1720 }
1721
1722 #[test]
1723 fn parse_string_pt0_75m() {
1724 assert_eq!(
1725 format_duration_parts(parse_duration("PT0.75M").unwrap()),
1726 "PT45S"
1727 );
1728 }
1729
1730 #[test]
1731 fn malformed_temporal_strings_are_rejected_without_panicking() {
1732 assert_eq!(parse_date("123é4"), None);
1733 for malformed in ["P", "PT", "Pgarbage", "P1Ygarbage", "P1Y2", "P1.2.3Y"] {
1734 assert_eq!(
1735 parse_duration(malformed),
1736 None,
1737 "{malformed} must be rejected"
1738 );
1739 }
1740 }
1741
1742 #[test]
1743 fn add_durations() {
1744 let a = du(149.0, 14.0, 16.0, 12.0, 70.0);
1745 let a = (a.0, a.1, a.2, 1);
1746 let sum = add_duration(a, a).unwrap();
1747 assert_eq!(format_duration_parts(sum), "P24Y10M28DT32H26M20.000000002S");
1748 }
1749
1750 #[test]
1751 fn scale_duration_by_half() {
1752 let base = (149, 14, 58390, 1);
1753 assert_eq!(
1754 format_duration_parts(scale_duration(base, 0.5)),
1755 "P6Y2M22DT13H21M8S"
1756 );
1757 assert_eq!(
1758 format_duration_parts(scale_duration(base, 2.0)),
1759 "P24Y10M28DT32H26M20.000000002S"
1760 );
1761 }
1762
1763 #[test]
1764 fn negative_seconds_fraction() {
1765 let d = normalize_duration(DurationFields {
1766 seconds: 2.0,
1767 milliseconds: -1.0,
1768 ..Default::default()
1769 });
1770 assert_eq!(format_duration_parts(d), "PT1.999S");
1771 let d = normalize_duration(DurationFields {
1772 seconds: -2.0,
1773 milliseconds: 1.0,
1774 ..Default::default()
1775 });
1776 assert_eq!(format_duration_parts(d), "PT-1.999S");
1777 let d = normalize_duration(DurationFields {
1778 seconds: -2.0,
1779 milliseconds: -1.0,
1780 ..Default::default()
1781 });
1782 assert_eq!(format_duration_parts(d), "PT-2.001S");
1783 let d = normalize_duration(DurationFields {
1784 seconds: 60.0,
1785 milliseconds: -1.0,
1786 ..Default::default()
1787 });
1788 assert_eq!(format_duration_parts(d), "PT59.999S");
1789 let d = normalize_duration(DurationFields {
1790 minutes: 12.0,
1791 seconds: -60.0,
1792 ..Default::default()
1793 });
1794 assert_eq!(format_duration_parts(d), "PT11M");
1795 }
1796
1797 #[test]
1798 fn date_roundtrip() {
1799 let d = epoch_day_from_ymd(1984, 10, 11).unwrap();
1800 assert_eq!(format_date(d), "1984-10-11");
1801 assert_eq!(parse_date("1984-10-11"), Some(d));
1802 assert_eq!(parse_date("19841011"), Some(d));
1803 }
1804
1805 #[test]
1806 fn date_components() {
1807 let d = epoch_day_from_ymd(1984, 10, 11).unwrap();
1808 assert_eq!(date_component(d, "year"), Some(1984));
1809 assert_eq!(date_component(d, "quarter"), Some(4));
1810 assert_eq!(date_component(d, "month"), Some(10));
1811 assert_eq!(date_component(d, "week"), Some(41));
1812 assert_eq!(date_component(d, "weekYear"), Some(1984));
1813 assert_eq!(date_component(d, "day"), Some(11));
1814 assert_eq!(date_component(d, "ordinalDay"), Some(285));
1815 assert_eq!(date_component(d, "weekDay"), Some(4));
1816 assert_eq!(date_component(d, "dayOfQuarter"), Some(11));
1817 }
1818
1819 #[test]
1820 fn date_plus_duration() {
1821 let x = epoch_day_from_ymd(1984, 10, 11).unwrap();
1822 let d = du(149.0, 14.0, 16.0, 12.0, 70.0);
1823 let sum = add_duration_to_date(x, d.0, d.1, d.2, d.3, false).unwrap();
1824 assert_eq!(format_date(sum), "1997-03-25");
1825 let diff = add_duration_to_date(x, d.0, d.1, d.2, d.3, true).unwrap();
1826 assert_eq!(format_date(diff), "1972-04-27");
1827 }
1828
1829 #[test]
1833 fn date_plus_fractional_duration_carries_extra_day_from_seconds() {
1834 let x = epoch_day_from_ymd(1984, 10, 11).unwrap();
1835 let d = normalize_duration(DurationFields {
1836 years: 12.5,
1837 months: 5.5,
1838 days: 14.5,
1839 hours: 16.5,
1840 minutes: 12.5,
1841 seconds: 70.5,
1842 nanoseconds: 3.0,
1843 ..Default::default()
1844 });
1845 let sum = add_duration_to_date(x, d.0, d.1, d.2, d.3, false).unwrap();
1846 assert_eq!(format_date(sum), "1997-10-11");
1847 let diff = add_duration_to_date(x, d.0, d.1, d.2, d.3, true).unwrap();
1848 assert_eq!(format_date(diff), "1971-10-12");
1849 }
1850
1851 #[test]
1852 fn duration_accessors() {
1853 let d = normalize_duration(DurationFields {
1854 years: 1.0,
1855 months: 4.0,
1856 days: 10.0,
1857 hours: 1.0,
1858 minutes: 1.0,
1859 seconds: 1.0,
1860 nanoseconds: 111_111_111.0,
1861 ..Default::default()
1862 });
1863 let get = |prop: &str| duration_component(d.0, d.1, d.2, d.3, prop).unwrap();
1864 assert_eq!(get("years"), 1);
1865 assert_eq!(get("quarters"), 5);
1866 assert_eq!(get("months"), 16);
1867 assert_eq!(get("weeks"), 1);
1868 assert_eq!(get("days"), 10);
1869 assert_eq!(get("hours"), 1);
1870 assert_eq!(get("minutes"), 61);
1871 assert_eq!(get("seconds"), 3661);
1872 assert_eq!(get("milliseconds"), 3_661_111);
1873 assert_eq!(get("microseconds"), 3_661_111_111);
1874 assert_eq!(get("nanoseconds"), 3_661_111_111_111);
1875 assert_eq!(get("quartersOfYear"), 1);
1876 assert_eq!(get("monthsOfQuarter"), 1);
1877 assert_eq!(get("monthsOfYear"), 4);
1878 assert_eq!(get("daysOfWeek"), 3);
1879 assert_eq!(get("minutesOfHour"), 1);
1880 assert_eq!(get("secondsOfMinute"), 1);
1881 assert_eq!(get("millisecondsOfSecond"), 111);
1882 assert_eq!(get("microsecondsOfSecond"), 111_111);
1883 assert_eq!(get("nanosecondsOfSecond"), 111_111_111);
1884 }
1885
1886 fn format_duration_parts(p: DurationParts) -> String {
1887 format_duration(p.0, p.1, p.2, p.3)
1888 }
1889}