1use chrono::{LocalResult, NaiveDateTime, Offset, TimeZone, Timelike};
20
21#[derive(Debug, Clone, PartialEq)]
26pub enum TzId {
27 Offset(i32),
28 Named(String),
29}
30
31const SECONDS_PER_DAY: i64 = 86_400;
32
33const AVG_MONTH_DAYS: f64 = 365.2425 / 12.0;
43
44const NANOS_PER_SEC: i128 = 1_000_000_000;
45
46pub const MIN_YEAR: i64 = -999_999_999;
62pub const MAX_YEAR: i64 = 999_999_999;
63
64fn is_leap_year(y: i64) -> bool {
65 y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
66}
67
68fn last_day_of_month(y: i64, m: u32) -> u32 {
69 match m {
70 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
71 4 | 6 | 9 | 11 => 30,
72 2 => {
73 if is_leap_year(y) {
74 29
75 } else {
76 28
77 }
78 }
79 _ => 0,
80 }
81}
82
83fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
85 let y = y - (m <= 2) as i64;
86 let era = if y >= 0 { y } else { y - 399 } / 400;
87 let yoe = y - era * 400; let mp = (m as i64 + 9) % 12; let doy = (153 * mp + 2) / 5 + d as i64 - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146_097 + doe - 719_468
92}
93
94fn civil_from_days(z: i64) -> (i64, u32, u32) {
96 let z = z + 719_468;
97 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
98 let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe + era * 400;
101 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32; (y + (m <= 2) as i64, m, d)
106}
107
108fn iso_weekday_from_days(z: i64) -> i64 {
110 (z + 3).rem_euclid(7) + 1
111}
112
113fn ordinal_day_of(y: i64, m: u32, d: u32) -> i64 {
114 days_from_civil(y, m, d) - days_from_civil(y, 1, 1) + 1
115}
116
117fn days_in_year(y: i64) -> i64 {
118 if is_leap_year(y) {
119 366
120 } else {
121 365
122 }
123}
124
125fn iso_weeks_in_year(y: i64) -> i64 {
128 let jan1 = iso_weekday_from_days(days_from_civil(y, 1, 1));
129 if jan1 == 4 || (is_leap_year(y) && jan1 == 3) {
130 53
131 } else {
132 52
133 }
134}
135
136fn iso_week_of(z: i64) -> (i64, i64) {
139 let (y, m, d) = civil_from_days(z);
140 let doy = ordinal_day_of(y, m, d);
141 let wd = iso_weekday_from_days(z);
142 let week = (doy - wd + 10) / 7;
143 if week < 1 {
144 (y - 1, iso_weeks_in_year(y - 1))
145 } else if week > iso_weeks_in_year(y) {
146 (y + 1, 1)
147 } else {
148 (y, week)
149 }
150}
151
152fn iso_week1_monday(week_year: i64) -> i64 {
155 let jan4 = days_from_civil(week_year, 1, 4);
156 jan4 - (iso_weekday_from_days(jan4) - 1)
157}
158
159fn add_months_to_epoch_day(z: i64, months: i64) -> Option<i64> {
163 let (y, m, d) = civil_from_days(z);
164 let total = y
165 .checked_mul(12)?
166 .checked_add(m as i64 - 1)?
167 .checked_add(months)?;
168 let ny = total.div_euclid(12);
169 let nm = total.rem_euclid(12) as u32 + 1;
170 if !(MIN_YEAR..=MAX_YEAR).contains(&ny) {
171 return None;
172 }
173 let nd = d.min(last_day_of_month(ny, nm));
174 Some(days_from_civil(ny, nm, nd))
175}
176
177pub fn epoch_day_from_ymd(year: i64, month: u32, day: u32) -> Option<i64> {
178 if !(MIN_YEAR..=MAX_YEAR).contains(&year) || !(1..=12).contains(&month) {
179 return None;
180 }
181 if day < 1 || day > last_day_of_month(year, month) {
182 return None;
183 }
184 Some(days_from_civil(year, month, day))
185}
186
187#[derive(Clone, Copy)]
196pub struct NowSnapshot {
197 pub epoch_day: i64,
198 pub nanos_of_day: i64,
199 pub epoch_seconds: i64,
200 pub nanos: i32,
201}
202
203pub fn capture_now() -> NowSnapshot {
204 let now = chrono::Utc::now();
205 let epoch_seconds = now.timestamp();
206 let nanos = now.nanosecond() as i32;
207 let (epoch_day, nanos_of_day) = split_epoch_seconds(epoch_seconds);
208 NowSnapshot {
209 epoch_day,
210 nanos_of_day: nanos_of_day + nanos as i64,
211 epoch_seconds,
212 nanos,
213 }
214}
215
216pub fn format_date(epoch_day: i64) -> String {
217 let (y, m, d) = civil_from_days(epoch_day);
218 if (0..=9999).contains(&y) {
219 format!("{y:04}-{m:02}-{d:02}")
220 } else {
221 format!("{y:+}-{m:02}-{d:02}")
225 }
226}
227
228pub fn parse_date(s: &str) -> Option<i64> {
238 let s = s.trim();
239 if !s.is_ascii() {
243 return None;
244 }
245 let (year_sign, s) = match s.as_bytes().first()? {
246 b'+' => (1i64, &s[1..]),
247 b'-' => (-1i64, &s[1..]),
248 _ => (1, s),
249 };
250 if let Some(epoch_day) = parse_week_or_ordinal_date(s, year_sign) {
256 return Some(epoch_day);
257 }
258 let (year, month, day) = if let Some((y, rest)) = s.split_once('-') {
259 let year: i64 = y.parse().ok()?;
260 match rest.split_once('-') {
261 Some((m, d)) => (year, m.parse().ok()?, d.parse().ok()?),
262 None => (year, rest.parse().ok()?, 1),
263 }
264 } else {
265 match s.len() {
266 8 => (
267 s[0..4].parse().ok()?,
268 s[4..6].parse().ok()?,
269 s[6..8].parse().ok()?,
270 ),
271 6 => (s[0..4].parse().ok()?, s[4..6].parse().ok()?, 1),
272 4 => (s[0..4].parse().ok()?, 1, 1),
273 _ => return None,
274 }
275 };
276 epoch_day_from_ymd(year_sign * year, month, day)
277}
278
279fn parse_week_or_ordinal_date(s: &str, year_sign: i64) -> Option<i64> {
284 if let Some((y, rest)) = s.split_once('-') {
285 if let Some(w) = rest.strip_prefix('W') {
286 let week_year: i64 = y.parse().ok()?;
287 let (week, day) = match w.split_once('-') {
288 Some((w, d)) => (w.parse().ok()?, d.parse().ok()?),
289 None => (w.parse().ok()?, 1),
290 };
291 return epoch_day_from_week_fields(year_sign * week_year, week, day);
292 }
293 if rest.len() == 3 && rest.bytes().all(|b| b.is_ascii_digit()) {
296 let year: i64 = y.parse().ok()?;
297 let ordinal: u32 = rest.parse().ok()?;
298 return epoch_day_from_ordinal_fields(year_sign * year, ordinal);
299 }
300 return None;
301 }
302 if s.len() >= 5 {
303 if let Some(w) = s[4..].strip_prefix('W') {
304 let week_year: i64 = s[0..4].parse().ok()?;
305 let (week, day) = match w.len() {
306 2 => (w.parse().ok()?, 1),
307 3 => (w[0..2].parse().ok()?, w[2..3].parse().ok()?),
308 _ => return None,
309 };
310 return epoch_day_from_week_fields(year_sign * week_year, week, day);
311 }
312 }
313 if s.len() == 7 && s.bytes().all(|b| b.is_ascii_digit()) {
314 let year: i64 = s[0..4].parse().ok()?;
315 let ordinal: u32 = s[4..7].parse().ok()?;
316 return epoch_day_from_ordinal_fields(year_sign * year, ordinal);
317 }
318 None
319}
320
321pub fn date_component(epoch_day: i64, prop: &str) -> Option<i64> {
330 let (y, m, d) = civil_from_days(epoch_day);
331 Some(match prop {
332 "year" => y,
333 "month" => m as i64,
334 "day" => d as i64,
335 "quarter" => ((m - 1) / 3 + 1) as i64,
336 "ordinalDay" => ordinal_day_of(y, m, d),
337 "weekDay" | "dayOfWeek" => iso_weekday_from_days(epoch_day),
338 "week" => iso_week_of(epoch_day).1,
339 "weekYear" => iso_week_of(epoch_day).0,
340 "dayOfQuarter" => {
341 let quarter_start_month = (m - 1) / 3 * 3 + 1;
342 epoch_day - days_from_civil(y, quarter_start_month, 1) + 1
343 }
344 _ => return None,
345 })
346}
347
348pub fn epoch_day_from_week_fields(week_year: i64, week: u32, day_of_week: i64) -> Option<i64> {
355 if !(MIN_YEAR..=MAX_YEAR).contains(&week_year)
356 || !(1..=7).contains(&day_of_week)
357 || week < 1
358 || week as i64 > iso_weeks_in_year(week_year)
359 {
360 return None;
361 }
362 Some(iso_week1_monday(week_year) + (week as i64 - 1) * 7 + (day_of_week - 1))
363}
364
365pub fn epoch_day_from_ordinal_fields(year: i64, ordinal_day: u32) -> Option<i64> {
368 if !(MIN_YEAR..=MAX_YEAR).contains(&year)
369 || ordinal_day < 1
370 || ordinal_day as i64 > days_in_year(year)
371 {
372 return None;
373 }
374 Some(days_from_civil(year, 1, 1) + ordinal_day as i64 - 1)
375}
376
377pub fn epoch_day_from_quarter_fields(year: i64, quarter: u32, day_of_quarter: i64) -> Option<i64> {
381 if !(MIN_YEAR..=MAX_YEAR).contains(&year) || !(1..=4).contains(&quarter) {
382 return None;
383 }
384 let quarter_start_month = (quarter - 1) * 3 + 1;
385 Some(days_from_civil(year, quarter_start_month, 1) + (day_of_quarter - 1))
386}
387
388pub fn add_duration_to_date(
412 epoch_day: i64,
413 months: i64,
414 days: i64,
415 seconds: i64,
416 nanos: i32,
417 negate: bool,
418) -> Option<i64> {
419 let total_ns: i128 = seconds as i128 * NANOS_PER_SEC + nanos as i128;
420 let extra_days = (total_ns / (86_400 * NANOS_PER_SEC)) as i64;
421 let days = days.checked_add(extra_days)?;
422 let (months, days) = if negate {
423 (months.checked_neg()?, days.checked_neg()?)
424 } else {
425 (months, days)
426 };
427 let result = add_months_to_epoch_day(epoch_day, months)?.checked_add(days)?;
428 let (y, _, _) = civil_from_days(result);
431 if !(MIN_YEAR..=MAX_YEAR).contains(&y) {
432 return None;
433 }
434 Some(result)
435}
436
437pub type DurationParts = (i64, i64, i64, i32);
443
444#[derive(Default, Clone, Copy)]
449pub struct DurationFields {
450 pub years: f64,
451 pub months: f64,
452 pub weeks: f64,
453 pub days: f64,
454 pub hours: f64,
455 pub minutes: f64,
456 pub seconds: f64,
457 pub milliseconds: f64,
458 pub microseconds: f64,
459 pub nanoseconds: f64,
460}
461
462pub fn normalize_duration(f: DurationFields) -> DurationParts {
476 let months_f = f.years * 12.0 + f.months;
477 let days_f = f.weeks * 7.0 + f.days;
478 let seconds_f = f.hours * 3600.0 + f.minutes * 60.0 + f.seconds;
479 let extra_nanos =
484 (f.milliseconds * 1_000_000.0 + f.microseconds * 1_000.0 + f.nanoseconds).trunc() as i128;
485 cascade(months_f, days_f, seconds_f, extra_nanos)
486}
487
488fn cascade(months_f: f64, days_f: f64, seconds_f: f64, extra_nanos: i128) -> DurationParts {
493 let whole_months = months_f.trunc();
494 let frac_months = months_f - whole_months;
495 let days_f2 = days_f + frac_months * AVG_MONTH_DAYS;
496 let whole_days = days_f2.trunc();
497 let frac_days = days_f2 - whole_days;
498 let seconds_f2 = seconds_f + frac_days * 86_400.0;
499 let total_ns = (seconds_f2 * NANOS_PER_SEC as f64).round() as i128 + extra_nanos;
507 let seconds = (total_ns / NANOS_PER_SEC) as i64;
508 let nanos = (total_ns % NANOS_PER_SEC) as i32;
509 (whole_months as i64, whole_days as i64, seconds, nanos)
510}
511
512pub fn add_duration(a: DurationParts, b: DurationParts) -> Option<DurationParts> {
521 let months = a.0.checked_add(b.0)?;
522 let days = a.1.checked_add(b.1)?;
523 let total_ns =
524 a.2 as i128 * NANOS_PER_SEC + a.3 as i128 + b.2 as i128 * NANOS_PER_SEC + b.3 as i128;
525 Some((
526 months,
527 days,
528 (total_ns / NANOS_PER_SEC).try_into().ok()?,
529 (total_ns % NANOS_PER_SEC) as i32,
530 ))
531}
532
533pub fn negate_duration(a: DurationParts) -> Option<DurationParts> {
534 Some((
535 a.0.checked_neg()?,
536 a.1.checked_neg()?,
537 a.2.checked_neg()?,
538 a.3.checked_neg()?,
539 ))
540}
541
542pub fn sub_duration(a: DurationParts, b: DurationParts) -> Option<DurationParts> {
543 add_duration(a, negate_duration(b)?)
544}
545
546pub fn scale_duration(a: DurationParts, factor: f64) -> DurationParts {
556 let months_f = a.0 as f64 * factor;
557 let days_f = a.1 as f64 * factor;
558 let total_ns_exact = a.2 as i128 * NANOS_PER_SEC + a.3 as i128;
559 let extra_nanos = (total_ns_exact as f64 * factor).trunc() as i128;
560 cascade(months_f, days_f, 0.0, extra_nanos)
561}
562
563pub fn duration_component(
592 months: i64,
593 days: i64,
594 seconds: i64,
595 nanos: i32,
596 prop: &str,
597) -> Option<i64> {
598 let nanos = nanos as i64;
599 Some(match prop {
600 "years" => months / 12,
601 "quarters" => months / 3,
602 "months" => months,
603 "weeks" => days / 7,
604 "days" => days,
605 "hours" => seconds / 3600,
606 "minutes" => seconds / 60,
607 "seconds" => seconds,
608 "milliseconds" => seconds * 1000 + nanos / 1_000_000,
609 "microseconds" => seconds * 1_000_000 + nanos / 1_000,
610 "nanoseconds" => seconds * NANOS_PER_SEC as i64 + nanos,
611 "quartersOfYear" => (months % 12) / 3,
612 "monthsOfQuarter" => (months % 12) % 3,
613 "monthsOfYear" => months % 12,
614 "daysOfWeek" => days % 7,
615 "minutesOfHour" => (seconds / 60) % 60,
616 "secondsOfMinute" => seconds % 60,
617 "millisecondsOfSecond" => nanos / 1_000_000,
618 "microsecondsOfSecond" => nanos / 1_000,
619 "nanosecondsOfSecond" => nanos,
620 _ => return None,
621 })
622}
623
624pub fn format_duration(months: i64, days: i64, seconds: i64, nanos: i32) -> String {
634 if months == 0 && days == 0 && seconds == 0 && nanos == 0 {
635 return "PT0S".to_string();
636 }
637 let mut out = String::from("P");
638 let years = months / 12;
639 let rem_months = months % 12;
640 if years != 0 {
641 out.push_str(&format!("{years}Y"));
642 }
643 if rem_months != 0 {
644 out.push_str(&format!("{rem_months}M"));
645 }
646 if days != 0 {
647 out.push_str(&format!("{days}D"));
648 }
649 let total_time_ns = seconds as i128 * NANOS_PER_SEC + nanos as i128;
650 if total_time_ns != 0 {
651 out.push('T');
652 if total_time_ns >= 0 {
653 let hours = seconds / 3600;
654 let rem = seconds % 3600;
655 let minutes = rem / 60;
656 let secs = rem % 60;
657 if hours != 0 {
658 out.push_str(&format!("{hours}H"));
659 }
660 if minutes != 0 {
661 out.push_str(&format!("{minutes}M"));
662 }
663 if secs != 0 || nanos != 0 {
664 out.push_str(&format_seconds_fraction(secs, nanos));
665 out.push('S');
666 }
667 } else {
668 let total_ns = total_time_ns;
669 let hours = (total_ns / 3_600_000_000_000) as i64;
670 let rem_h = total_ns % 3_600_000_000_000;
671 let minutes = (rem_h / 60_000_000_000) as i64;
672 let rem_m = rem_h % 60_000_000_000;
673 let secs = (rem_m / 1_000_000_000) as i64;
674 let sub_nanos = (rem_m % 1_000_000_000) as i32;
675 if hours != 0 {
676 out.push_str(&format!("{hours}H"));
677 }
678 if minutes != 0 {
679 out.push_str(&format!("{minutes}M"));
680 }
681 if secs != 0 || sub_nanos != 0 {
682 out.push_str(&format_seconds_fraction(secs, sub_nanos));
683 out.push('S');
684 }
685 }
686 }
687 out
688}
689
690fn format_seconds_fraction(secs: i64, nanos: i32) -> String {
696 if nanos == 0 {
697 return secs.to_string();
698 }
699 let negative = secs < 0 || nanos < 0;
700 let mut frac = format!("{:09}", nanos.unsigned_abs());
701 while frac.ends_with('0') {
702 frac.pop();
703 }
704 format!(
705 "{}{}.{}",
706 if negative { "-" } else { "" },
707 secs.unsigned_abs(),
708 frac
709 )
710}
711
712pub fn parse_duration(s: &str) -> Option<DurationParts> {
725 let s = s.trim();
726 let s = s.strip_prefix('P')?;
727 let (date_part, time_part) = match s.split_once('T') {
728 Some((d, t)) => (d, Some(t)),
729 None => (s, None),
730 };
731 if let Some(fields) = parse_combined_date_time_duration(date_part, time_part) {
732 return Some(normalize_duration(fields));
733 }
734 let date_pairs = scan_number_unit_pairs(date_part)?;
735 let time_pairs = match time_part {
736 Some(part) => scan_number_unit_pairs(part)?,
737 None => Vec::new(),
738 };
739 if date_pairs.is_empty() && time_pairs.is_empty() {
740 return None;
741 }
742
743 let mut f = DurationFields::default();
744 for (value, unit) in date_pairs {
745 match unit {
746 'Y' => f.years = value,
747 'M' => f.months = value,
748 'W' => f.weeks = value,
749 'D' => f.days = value,
750 _ => return None,
751 }
752 }
753 for (value, unit) in time_pairs {
754 match unit {
755 'H' => f.hours = value,
756 'M' => f.minutes = value,
757 'S' => f.seconds = value,
758 _ => return None,
759 }
760 }
761 Some(normalize_duration(f))
762}
763
764fn parse_combined_date_time_duration(
776 date_part: &str,
777 time_part: Option<&str>,
778) -> Option<DurationFields> {
779 let mut date_fields = date_part.splitn(3, '-');
780 let years: f64 = date_fields.next()?.parse().ok()?;
781 let months: f64 = date_fields.next()?.parse().ok()?;
782 let days: f64 = date_fields.next()?.parse().ok()?;
783 if date_fields.next().is_some() {
784 return None;
785 }
786 let mut f = DurationFields {
787 years,
788 months,
789 days,
790 ..Default::default()
791 };
792 if let Some(time_part) = time_part {
793 let mut time_fields = time_part.splitn(3, ':');
794 let hours: f64 = time_fields.next()?.parse().ok()?;
795 let minutes: f64 = time_fields.next()?.parse().ok()?;
796 let seconds: f64 = time_fields.next()?.parse().ok()?;
797 if time_fields.next().is_some() {
798 return None;
799 }
800 f.hours = hours;
801 f.minutes = minutes;
802 f.seconds = seconds;
803 }
804 Some(f)
805}
806
807fn scan_number_unit_pairs(s: &str) -> Option<Vec<(f64, char)>> {
815 let mut out = Vec::new();
816 let chars: Vec<char> = s.chars().collect();
817 let mut i = 0;
818 while i < chars.len() {
819 let start = i;
820 if chars[i] == '-' || chars[i] == '+' {
821 i += 1;
822 }
823 let digits_start = i;
824 while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
825 i += 1;
826 }
827 if i == digits_start {
828 return None;
829 }
830 let &unit = chars.get(i)?;
831 let value = chars[start..i]
832 .iter()
833 .collect::<String>()
834 .parse::<f64>()
835 .ok()?;
836 out.push((value, unit));
837 i += 1;
838 }
839 Some(out)
840}
841
842pub fn local_time_nanos_from_fields(
851 hour: i64,
852 minute: i64,
853 second: i64,
854 nanos: i64,
855) -> Option<i64> {
856 if !(0..24).contains(&hour)
857 || !(0..60).contains(&minute)
858 || !(0..60).contains(&second)
859 || !(0..1_000_000_000).contains(&nanos)
860 {
861 return None;
862 }
863 Some(hour * 3_600_000_000_000 + minute * 60_000_000_000 + second * 1_000_000_000 + nanos)
864}
865
866fn parse_time_of_day(s: &str) -> Option<i64> {
870 if !s.is_ascii() || s.is_empty() {
871 return None;
872 }
873 let (hour, minute, second, nanos) = if s.contains(':') {
874 let mut parts = s.splitn(3, ':');
875 let h: u32 = parts.next()?.parse().ok()?;
876 let m: u32 = match parts.next() {
877 Some(p) => p.parse().ok()?,
878 None => 0,
879 };
880 let (sec, nanos) = match parts.next() {
881 Some(p) => parse_seconds_fraction(p)?,
882 None => (0, 0),
883 };
884 (h, m, sec, nanos)
885 } else {
886 match s.len() {
887 2 => (s.parse().ok()?, 0, 0, 0),
888 4 => (s[0..2].parse().ok()?, s[2..4].parse().ok()?, 0, 0),
889 n if n > 4 => {
890 let (sec, nanos) = parse_seconds_fraction(&s[4..])?;
891 (s[0..2].parse().ok()?, s[2..4].parse().ok()?, sec, nanos)
892 }
893 _ => return None,
894 }
895 };
896 local_time_nanos_from_fields(hour as i64, minute as i64, second as i64, nanos as i64)
897}
898
899fn parse_seconds_fraction(s: &str) -> Option<(u32, u32)> {
904 let (sec_str, frac_str) = match s.split_once('.') {
905 Some((a, b)) => (a, Some(b)),
906 None => (s, None),
907 };
908 let sec: u32 = sec_str.parse().ok()?;
909 if sec >= 60 {
910 return None;
911 }
912 let nanos = match frac_str {
913 None => 0,
914 Some(f) => {
915 if f.is_empty() || !f.bytes().all(|b| b.is_ascii_digit()) {
916 return None;
917 }
918 let mut digits = f.to_string();
919 digits.truncate(9);
920 while digits.len() < 9 {
921 digits.push('0');
922 }
923 digits.parse().ok()?
924 }
925 };
926 Some((sec, nanos))
927}
928
929fn split_time_offset(s: &str) -> (&str, Option<&str>) {
937 if let Some(stripped) = s.strip_suffix('Z') {
938 return (stripped, Some("Z"));
939 }
940 let bytes = s.as_bytes();
941 for i in 1..bytes.len() {
942 if bytes[i] == b'+' || bytes[i] == b'-' {
943 return (&s[..i], Some(&s[i..]));
944 }
945 }
946 (s, None)
947}
948
949pub fn parse_offset_seconds(s: &str) -> Option<i32> {
952 if s == "Z" {
953 return Some(0);
954 }
955 let bytes = s.as_bytes();
956 let sign: i32 = match bytes.first()? {
957 b'+' => 1,
958 b'-' => -1,
959 _ => return None,
960 };
961 let rest = &s[1..];
962 let (h, m, sec): (i32, i32, i32) = if rest.contains(':') {
963 let mut parts = rest.splitn(3, ':');
964 let h = parts.next()?.parse().ok()?;
965 let m = match parts.next() {
966 Some(p) => p.parse().ok()?,
967 None => 0,
968 };
969 let sec = match parts.next() {
970 Some(p) => p.parse().ok()?,
971 None => 0,
972 };
973 (h, m, sec)
974 } else {
975 match rest.len() {
976 2 => (rest.parse().ok()?, 0, 0),
977 4 => (rest[0..2].parse().ok()?, rest[2..4].parse().ok()?, 0),
978 6 => (
979 rest[0..2].parse().ok()?,
980 rest[2..4].parse().ok()?,
981 rest[4..6].parse().ok()?,
982 ),
983 _ => return None,
984 }
985 };
986 if !(0..24).contains(&h) || !(0..60).contains(&m) || !(0..60).contains(&sec) {
987 return None;
988 }
989 Some(sign * (h * 3600 + m * 60 + sec))
990}
991
992pub fn parse_local_time(s: &str) -> Option<i64> {
998 parse_time_of_day(s.trim())
999}
1000
1001pub fn parse_time(s: &str) -> Option<(i64, i32)> {
1010 let s = s.trim();
1011 if s.contains('[') {
1012 return None;
1013 }
1014 let (time_part, offset_part) = split_time_offset(s);
1015 let offset_seconds = match offset_part {
1020 Some(part) => parse_offset_seconds(part)?,
1021 None => 0,
1022 };
1023 Some((parse_time_of_day(time_part)?, offset_seconds))
1024}
1025
1026pub fn local_time_component(nanos_of_day: i64, prop: &str) -> Option<i64> {
1029 Some(match prop {
1030 "hour" => nanos_of_day / 3_600_000_000_000,
1031 "minute" => (nanos_of_day / 60_000_000_000) % 60,
1032 "second" => (nanos_of_day / 1_000_000_000) % 60,
1033 "millisecond" => (nanos_of_day / 1_000_000) % 1000,
1034 "microsecond" => (nanos_of_day / 1_000) % 1_000_000,
1035 "nanosecond" => nanos_of_day % 1_000_000_000,
1036 _ => return None,
1037 })
1038}
1039
1040pub fn format_offset(offset_seconds: i32) -> String {
1045 if offset_seconds == 0 {
1046 return "Z".to_string();
1047 }
1048 let sign = if offset_seconds < 0 { "-" } else { "+" };
1049 let abs = offset_seconds.unsigned_abs();
1050 let h = abs / 3600;
1051 let m = (abs / 60) % 60;
1052 let sec = abs % 60;
1053 if sec != 0 {
1054 format!("{sign}{h:02}:{m:02}:{sec:02}")
1055 } else {
1056 format!("{sign}{h:02}:{m:02}")
1057 }
1058}
1059
1060fn format_time_of_day(nanos_of_day: i64) -> String {
1067 let hour = nanos_of_day / 3_600_000_000_000;
1068 let minute = (nanos_of_day / 60_000_000_000) % 60;
1069 let second = (nanos_of_day / 1_000_000_000) % 60;
1070 let nanos = (nanos_of_day % 1_000_000_000) as u32;
1071 let mut out = format!("{hour:02}:{minute:02}");
1072 if second != 0 || nanos != 0 {
1073 out.push_str(&format!(":{second:02}"));
1074 if nanos != 0 {
1075 let mut frac = format!("{nanos:09}");
1076 while frac.ends_with('0') {
1077 frac.pop();
1078 }
1079 out.push('.');
1080 out.push_str(&frac);
1081 }
1082 }
1083 out
1084}
1085
1086pub fn format_local_time(nanos_of_day: i64) -> String {
1087 format_time_of_day(nanos_of_day)
1088}
1089
1090pub fn format_time(nanos_of_day: i64, offset_seconds: i32) -> String {
1091 format!(
1092 "{}{}",
1093 format_time_of_day(nanos_of_day),
1094 format_offset(offset_seconds)
1095 )
1096}
1097
1098pub fn split_epoch_seconds(epoch_seconds: i64) -> (i64, i64) {
1109 let epoch_day = epoch_seconds.div_euclid(SECONDS_PER_DAY);
1110 let secs_of_day = epoch_seconds.rem_euclid(SECONDS_PER_DAY);
1111 (epoch_day, secs_of_day * 1_000_000_000)
1112}
1113
1114pub fn combine_epoch_day_and_nanos_of_day(epoch_day: i64, nanos_of_day: i64) -> i64 {
1115 epoch_day * SECONDS_PER_DAY + nanos_of_day / 1_000_000_000
1116}
1117
1118pub fn combine_date_and_time(epoch_day: i64, nanos_of_day: i64) -> (i64, i32) {
1122 (
1123 combine_epoch_day_and_nanos_of_day(epoch_day, nanos_of_day),
1124 (nanos_of_day % 1_000_000_000) as i32,
1125 )
1126}
1127
1128pub struct CalendarDateTime {
1134 pub year: i64,
1135 pub month: u32,
1136 pub day: u32,
1137 pub hour: i64,
1138 pub minute: i64,
1139 pub second: i64,
1140 pub nanos: i64,
1141}
1142
1143pub fn local_date_time_from_fields(f: CalendarDateTime) -> Option<(i64, i32)> {
1147 let epoch_day = epoch_day_from_ymd(f.year, f.month, f.day)?;
1148 let nanos_of_day = local_time_nanos_from_fields(f.hour, f.minute, f.second, f.nanos)?;
1149 Some((
1150 combine_epoch_day_and_nanos_of_day(epoch_day, nanos_of_day),
1151 (nanos_of_day % 1_000_000_000) as i32,
1152 ))
1153}
1154
1155pub fn date_time_from_fields(f: CalendarDateTime, zone: &TzId) -> Option<(i64, i32)> {
1163 match zone {
1164 TzId::Offset(offset_seconds) => {
1165 let (local_epoch_seconds, nanos) = local_date_time_from_fields(f)?;
1166 Some((local_epoch_seconds - *offset_seconds as i64, nanos))
1167 }
1168 TzId::Named(name) => {
1169 let tz = parse_timezone_name(name)?;
1170 let epoch_day = epoch_day_from_ymd(f.year, f.month, f.day)?;
1171 let nanos_of_day = local_time_nanos_from_fields(f.hour, f.minute, f.second, f.nanos)?;
1172 let naive = chrono_naive_from(epoch_day, nanos_of_day)?;
1173 let (epoch_seconds, _offset) = utc_from_local_and_named_zone(naive, tz)?;
1174 Some((epoch_seconds, (nanos_of_day % 1_000_000_000) as i32))
1175 }
1176 }
1177}
1178
1179pub fn parse_local_date_time(s: &str) -> Option<(i64, i32)> {
1185 let s = s.trim();
1186 let (date_part, time_part) = match s.split_once('T') {
1187 Some(parts) => parts,
1188 None => (s, ""),
1189 };
1190 let epoch_day = parse_date(date_part)?;
1191 let nanos_of_day = if time_part.is_empty() {
1192 0
1193 } else {
1194 parse_time_of_day(time_part)?
1195 };
1196 Some((
1197 combine_epoch_day_and_nanos_of_day(epoch_day, nanos_of_day),
1198 (nanos_of_day % 1_000_000_000) as i32,
1199 ))
1200}
1201
1202pub fn parse_date_time(s: &str) -> Option<(i64, i32, TzId)> {
1210 let s = s.trim();
1211 let (date_part, time_part) = s.split_once('T')?;
1212 let epoch_day = parse_date(date_part)?;
1213 let (time_part, zone_name) = match time_part.split_once('[') {
1214 Some((t, rest)) => (t, Some(rest.strip_suffix(']')?)),
1215 None => (time_part, None),
1216 };
1217 let (time_only, offset_part) = split_time_offset(time_part);
1218 let nanos_of_day = parse_time_of_day(time_only)?;
1219 match (offset_part, zone_name) {
1220 (Some(offset_str), zone_name) => {
1221 let offset_seconds = parse_offset_seconds(offset_str)?;
1222 let local_epoch_seconds = combine_epoch_day_and_nanos_of_day(epoch_day, nanos_of_day);
1223 let zone = match zone_name {
1224 Some(zone_str) => {
1225 parse_timezone_name(zone_str)?;
1226 TzId::Named(zone_str.to_string())
1227 }
1228 None => TzId::Offset(offset_seconds),
1229 };
1230 Some((
1231 local_epoch_seconds - offset_seconds as i64,
1232 (nanos_of_day % 1_000_000_000) as i32,
1233 zone,
1234 ))
1235 }
1236 (None, Some(zone_str)) => {
1237 let tz = parse_timezone_name(zone_str)?;
1238 let naive = chrono_naive_from(epoch_day, nanos_of_day)?;
1239 let (epoch_seconds, _offset) = utc_from_local_and_named_zone(naive, tz)?;
1240 Some((
1241 epoch_seconds,
1242 (nanos_of_day % 1_000_000_000) as i32,
1243 TzId::Named(zone_str.to_string()),
1244 ))
1245 }
1246 (None, None) => None,
1247 }
1248}
1249
1250pub fn date_time_calendar_component(epoch_seconds: i64, prop: &str) -> Option<i64> {
1255 let (epoch_day, _) = split_epoch_seconds(epoch_seconds);
1256 date_component(epoch_day, prop)
1257}
1258
1259pub fn date_time_clock_component(epoch_seconds: i64, nanos: i32, prop: &str) -> Option<i64> {
1265 let (_, nanos_of_day) = split_epoch_seconds(epoch_seconds);
1266 local_time_component(nanos_of_day + nanos as i64, prop)
1267}
1268
1269pub fn epoch_seconds_and_millis(epoch_seconds: i64, nanos: i32) -> (i64, i64) {
1270 (
1271 epoch_seconds,
1272 epoch_seconds * 1000 + (nanos as i64) / 1_000_000,
1273 )
1274}
1275
1276pub fn format_local_date_time(epoch_seconds: i64, nanos: i32) -> String {
1280 let (epoch_day, nanos_of_day) = split_epoch_seconds(epoch_seconds);
1281 format!(
1282 "{}T{}",
1283 format_date(epoch_day),
1284 format_time_of_day(nanos_of_day + nanos as i64)
1285 )
1286}
1287
1288pub fn add_duration_to_time(nanos_of_day: i64, seconds: i64, nanos: i32, negate: bool) -> i64 {
1296 let (seconds, nanos) = if negate {
1297 (-seconds, -nanos)
1298 } else {
1299 (seconds, nanos)
1300 };
1301 let total: i128 = nanos_of_day as i128 + seconds as i128 * NANOS_PER_SEC + nanos as i128;
1302 total.rem_euclid(NANOS_PER_DAY as i128) as i64
1303}
1304
1305const NANOS_PER_DAY: i64 = SECONDS_PER_DAY * 1_000_000_000;
1306
1307pub fn add_duration_to_local_date_time(
1321 epoch_seconds: i64,
1322 existing_nanos: i32,
1323 months: i64,
1324 days: i64,
1325 seconds: i64,
1326 nanos: i32,
1327 negate: bool,
1328) -> Option<(i64, i32)> {
1329 let (months, days, seconds, nanos) = if negate {
1330 (
1331 months.checked_neg()?,
1332 days.checked_neg()?,
1333 seconds.checked_neg()?,
1334 nanos.checked_neg()?,
1335 )
1336 } else {
1337 (months, days, seconds, nanos)
1338 };
1339 let (epoch_day, nanos_of_day) = split_epoch_seconds(epoch_seconds);
1340 let new_epoch_day = add_months_to_epoch_day(epoch_day, months)?;
1341
1342 let total_ns: i128 = nanos_of_day as i128
1343 + existing_nanos as i128
1344 + days as i128 * NANOS_PER_DAY as i128
1345 + seconds as i128 * NANOS_PER_SEC
1346 + nanos as i128;
1347 let day_ns = NANOS_PER_DAY as i128;
1348 let extra_days = total_ns.div_euclid(day_ns) as i64;
1349 let final_nanos_of_day = total_ns.rem_euclid(day_ns) as i64;
1350
1351 let final_epoch_day = new_epoch_day.checked_add(extra_days)?;
1352 let final_epoch_seconds = final_epoch_day
1353 .checked_mul(SECONDS_PER_DAY)?
1354 .checked_add(final_nanos_of_day / 1_000_000_000)?;
1355 Some((
1356 final_epoch_seconds,
1357 (final_nanos_of_day % 1_000_000_000) as i32,
1358 ))
1359}
1360
1361pub fn format_date_time(epoch_seconds: i64, nanos: i32, zone: &TzId) -> String {
1362 let offset_seconds = resolve_offset(zone, epoch_seconds);
1368 let local_epoch_seconds = epoch_seconds + offset_seconds as i64;
1369 let zone_suffix = match zone {
1370 TzId::Offset(_) => String::new(),
1371 TzId::Named(name) => format!("[{name}]"),
1375 };
1376 format!(
1377 "{}{}{}",
1378 format_local_date_time(local_epoch_seconds, nanos),
1379 format_offset(offset_seconds),
1380 zone_suffix
1381 )
1382}
1383
1384pub fn resolve_offset(zone: &TzId, epoch_seconds: i64) -> i32 {
1397 match zone {
1398 TzId::Offset(o) => *o,
1399 TzId::Named(name) => {
1400 let tz = parse_timezone_name(name).unwrap_or(chrono_tz::Tz::UTC);
1401 let utc = chrono::DateTime::<chrono::Utc>::from_timestamp(epoch_seconds, 0)
1402 .unwrap_or_default();
1403 utc.with_timezone(&tz).offset().fix().local_minus_utc()
1404 }
1405 }
1406}
1407
1408pub fn parse_timezone_name(s: &str) -> Option<chrono_tz::Tz> {
1411 s.parse().ok()
1412}
1413
1414fn utc_from_local_and_named_zone(naive: NaiveDateTime, tz: chrono_tz::Tz) -> Option<(i64, i32)> {
1422 let dt = match tz.from_local_datetime(&naive) {
1423 LocalResult::Single(dt) => dt,
1424 LocalResult::Ambiguous(earlier, _later) => earlier,
1425 LocalResult::None => return None,
1426 };
1427 let offset = dt.offset().fix().local_minus_utc();
1428 Some((dt.timestamp(), offset))
1429}
1430
1431const NANOS_PER_DAY_I64: i64 = SECONDS_PER_DAY * 1_000_000_000;
1436
1437#[derive(Clone, Copy)]
1441struct CivilDateTime {
1442 epoch_day: i64,
1443 nanos_of_day: i64,
1444}
1445
1446fn civil_total_ns(dt: CivilDateTime) -> i128 {
1449 dt.epoch_day as i128 * NANOS_PER_DAY_I64 as i128 + dt.nanos_of_day as i128
1450}
1451
1452fn chrono_naive_from(epoch_day: i64, nanos_of_day: i64) -> Option<NaiveDateTime> {
1457 let secs = epoch_day.checked_mul(SECONDS_PER_DAY)? + nanos_of_day / 1_000_000_000;
1458 chrono::DateTime::<chrono::Utc>::from_timestamp(secs, (nanos_of_day % 1_000_000_000) as u32)
1459 .map(|utc| utc.naive_utc())
1460}
1461
1462fn packed_proleptic(epoch_day: i64) -> i64 {
1469 let (y, m, d) = civil_from_days(epoch_day);
1470 (y * 12 + m as i64 - 1) * 32 + d as i64
1471}
1472
1473fn months_between_days(a: i64, b: i64) -> i64 {
1474 (packed_proleptic(b) - packed_proleptic(a)) / 32
1475}
1476
1477fn shift_months(dt: CivilDateTime, months: i64) -> CivilDateTime {
1481 let shifted = add_months_to_epoch_day(dt.epoch_day, months)
1482 .expect("months_between_days never shifts past the endpoint it was computed from");
1483 CivilDateTime {
1484 epoch_day: shifted,
1485 nanos_of_day: dt.nanos_of_day,
1486 }
1487}
1488
1489fn to_utc_instant_ns(dt: CivilDateTime, zone: &TzId) -> i128 {
1515 match zone {
1516 TzId::Offset(o) => civil_total_ns(dt) - *o as i128 * NANOS_PER_SEC,
1517 TzId::Named(name) => {
1518 if let (Some(tz), Some(naive)) = (
1519 parse_timezone_name(name),
1520 chrono_naive_from(dt.epoch_day, dt.nanos_of_day),
1521 ) {
1522 if let Some((epoch_seconds, _)) = utc_from_local_and_named_zone(naive, tz) {
1523 return epoch_seconds as i128 * NANOS_PER_SEC
1524 + (dt.nanos_of_day % 1_000_000_000) as i128;
1525 }
1526 }
1527 civil_total_ns(dt)
1528 }
1529 }
1530}
1531
1532fn elapsed_ns(
1533 from: CivilDateTime,
1534 from_zone: Option<&TzId>,
1535 to: CivilDateTime,
1536 to_zone: Option<&TzId>,
1537) -> i128 {
1538 match (from_zone, to_zone) {
1539 (Some(fz), Some(tz)) => to_utc_instant_ns(to, tz) - to_utc_instant_ns(from, fz),
1540 (Some(fz), None) => to_utc_instant_ns(to, fz) - to_utc_instant_ns(from, fz),
1541 (None, Some(tz)) => to_utc_instant_ns(to, tz) - to_utc_instant_ns(from, tz),
1542 (None, None) => civil_total_ns(to) - civil_total_ns(from),
1543 }
1544}
1545
1546fn months_between_datetimes_offset_aware(
1547 from: CivilDateTime,
1548 from_zone: Option<&TzId>,
1549 to: CivilDateTime,
1550 to_zone: Option<&TzId>,
1551) -> i64 {
1552 let mut months = months_between_days(from.epoch_day, to.epoch_day);
1553 let shifted = shift_months(from, months);
1554 let delta = elapsed_ns(shifted, from_zone, to, to_zone);
1555 if months > 0 && delta < 0 {
1556 months -= 1;
1557 } else if months < 0 && delta > 0 {
1558 months += 1;
1559 }
1560 months
1561}
1562
1563fn time_to_utc_nanos(nanos_of_day: i64, zone: &TzId, ref_date: Option<i64>) -> i128 {
1564 let dt = CivilDateTime {
1565 epoch_day: ref_date.unwrap_or(0),
1566 nanos_of_day,
1567 };
1568 to_utc_instant_ns(dt, zone)
1569}
1570
1571fn between_components(
1572 a_date: Option<i64>,
1573 a_time: Option<i64>,
1574 a_zone: Option<&TzId>,
1575 b_date: Option<i64>,
1576 b_time: Option<i64>,
1577 b_zone: Option<&TzId>,
1578) -> (i64, i128, i128) {
1579 match (a_date, b_date) {
1580 (Some(ad), Some(bd)) => {
1581 let from = CivilDateTime {
1582 epoch_day: ad,
1583 nanos_of_day: a_time.unwrap_or(0),
1584 };
1585 let to = CivilDateTime {
1586 epoch_day: bd,
1587 nanos_of_day: b_time.unwrap_or(0),
1588 };
1589 let months = months_between_datetimes_offset_aware(from, a_zone, to, b_zone);
1590 let shifted = shift_months(from, months);
1591 let shifted_remaining_ns = elapsed_ns(shifted, a_zone, to, b_zone);
1592 let raw_total_ns = elapsed_ns(from, a_zone, to, b_zone);
1593 (months, shifted_remaining_ns, raw_total_ns)
1594 }
1595 _ => {
1596 let diff = match (a_zone, b_zone) {
1597 (Some(az), Some(bz)) => {
1598 let ref_date = a_date.or(b_date);
1610 let at = time_to_utc_nanos(a_time.unwrap_or(0), az, ref_date);
1611 let bt = time_to_utc_nanos(b_time.unwrap_or(0), bz, ref_date);
1612 bt - at
1613 }
1614 (Some(az), None) => {
1615 let at = time_to_utc_nanos(a_time.unwrap_or(0), az, a_date);
1616 let bt = time_to_utc_nanos(b_time.unwrap_or(0), az, a_date);
1617 bt - at
1618 }
1619 (None, Some(bz)) => {
1620 let at = time_to_utc_nanos(a_time.unwrap_or(0), bz, b_date);
1621 let bt = time_to_utc_nanos(b_time.unwrap_or(0), bz, b_date);
1622 bt - at
1623 }
1624 (None, None) => (b_time.unwrap_or(0) - a_time.unwrap_or(0)) as i128,
1625 };
1626 (0, diff, diff)
1627 }
1628 }
1629}
1630
1631pub fn duration_between(
1632 a_date: Option<i64>,
1633 a_time: Option<i64>,
1634 a_zone: Option<&TzId>,
1635 b_date: Option<i64>,
1636 b_time: Option<i64>,
1637 b_zone: Option<&TzId>,
1638) -> DurationParts {
1639 let (months, shifted_ns, _) =
1640 between_components(a_date, a_time, a_zone, b_date, b_time, b_zone);
1641 let days = (shifted_ns / NANOS_PER_DAY_I64 as i128) as i64;
1645 let rem = shifted_ns % NANOS_PER_DAY_I64 as i128;
1646 let seconds = rem.div_euclid(NANOS_PER_SEC) as i64;
1647 let nanos = rem.rem_euclid(NANOS_PER_SEC) as i32;
1648 (months, days, seconds, nanos)
1649}
1650
1651pub fn duration_in_months(
1652 a_date: Option<i64>,
1653 a_time: Option<i64>,
1654 a_zone: Option<&TzId>,
1655 b_date: Option<i64>,
1656 b_time: Option<i64>,
1657 b_zone: Option<&TzId>,
1658) -> DurationParts {
1659 let (months, _, _) = between_components(a_date, a_time, a_zone, b_date, b_time, b_zone);
1660 (months, 0, 0, 0)
1661}
1662
1663pub fn duration_in_days(
1664 a_date: Option<i64>,
1665 a_time: Option<i64>,
1666 a_zone: Option<&TzId>,
1667 b_date: Option<i64>,
1668 b_time: Option<i64>,
1669 b_zone: Option<&TzId>,
1670) -> DurationParts {
1671 let (_, _, raw) = between_components(a_date, a_time, a_zone, b_date, b_time, b_zone);
1672 (0, (raw / NANOS_PER_DAY_I64 as i128) as i64, 0, 0)
1673}
1674
1675pub fn duration_in_seconds(
1676 a_date: Option<i64>,
1677 a_time: Option<i64>,
1678 a_zone: Option<&TzId>,
1679 b_date: Option<i64>,
1680 b_time: Option<i64>,
1681 b_zone: Option<&TzId>,
1682) -> DurationParts {
1683 let (_, _, raw) = between_components(a_date, a_time, a_zone, b_date, b_time, b_zone);
1684 (
1685 0,
1686 0,
1687 raw.div_euclid(NANOS_PER_SEC) as i64,
1690 raw.rem_euclid(NANOS_PER_SEC) as i32,
1691 )
1692}
1693
1694pub fn truncate_date_unit(epoch_day: i64, unit: &str) -> Option<i64> {
1708 let (y, m, _) = civil_from_days(epoch_day);
1709 match unit {
1710 "millennium" => epoch_day_from_ymd(y - y.rem_euclid(1000), 1, 1),
1711 "century" => epoch_day_from_ymd(y - y.rem_euclid(100), 1, 1),
1712 "decade" => epoch_day_from_ymd(y - y.rem_euclid(10), 1, 1),
1713 "year" => epoch_day_from_ymd(y, 1, 1),
1714 "quarter" => epoch_day_from_ymd(y, (m - 1) / 3 * 3 + 1, 1),
1715 "month" => epoch_day_from_ymd(y, m, 1),
1716 "week" => {
1717 let (week_year, week) = iso_week_of(epoch_day);
1718 Some(iso_week1_monday(week_year) + (week - 1) * 7)
1719 }
1720 "weekYear" => {
1721 let (week_year, _) = iso_week_of(epoch_day);
1722 Some(iso_week1_monday(week_year))
1723 }
1724 "day" => Some(epoch_day),
1725 _ => None,
1726 }
1727}
1728
1729pub fn set_iso_weekday(epoch_day: i64, day_of_week: i64) -> Option<i64> {
1737 if !(1..=7).contains(&day_of_week) {
1738 return None;
1739 }
1740 let (week_year, week) = iso_week_of(epoch_day);
1741 let monday = iso_week1_monday(week_year) + (week - 1) * 7;
1742 Some(monday + (day_of_week - 1))
1743}
1744
1745pub fn truncate_time_unit(nanos_of_day: i64, unit: &str) -> Option<i64> {
1749 let floor = |n: i64| (nanos_of_day / n) * n;
1750 match unit {
1751 "hour" => Some(floor(3_600_000_000_000)),
1752 "minute" => Some(floor(60_000_000_000)),
1753 "second" => Some(floor(1_000_000_000)),
1754 "millisecond" => Some(floor(1_000_000)),
1755 "microsecond" => Some(floor(1_000)),
1756 "day" => Some(0),
1757 _ => None,
1758 }
1759}
1760
1761#[cfg(test)]
1762mod tests {
1763 use super::*;
1764
1765 fn du(months: f64, days: f64, hours: f64, minutes: f64, seconds: f64) -> DurationParts {
1766 normalize_duration(DurationFields {
1767 months,
1768 days,
1769 hours,
1770 minutes,
1771 seconds,
1772 ..Default::default()
1773 })
1774 }
1775
1776 #[test]
1777 fn construct_basic() {
1778 assert_eq!(
1779 format_duration_parts(du(0.0, 14.0, 16.0, 12.0, 0.0)),
1780 "P14DT16H12M"
1781 );
1782 }
1783
1784 #[test]
1785 fn construct_fractional_months() {
1786 let d = normalize_duration(DurationFields {
1787 months: 0.75,
1788 ..Default::default()
1789 });
1790 assert_eq!(format_duration_parts(d), "P22DT19H51M49.5S");
1791 }
1792
1793 #[test]
1794 fn construct_fractional_weeks() {
1795 let d = normalize_duration(DurationFields {
1796 weeks: 2.5,
1797 ..Default::default()
1798 });
1799 assert_eq!(format_duration_parts(d), "P17DT12H");
1800 }
1801
1802 #[test]
1803 fn construct_years_months_days_seconds_overflow() {
1804 let d = normalize_duration(DurationFields {
1805 years: 12.0,
1806 months: 5.0,
1807 days: 14.0,
1808 hours: 16.0,
1809 minutes: 12.0,
1810 seconds: 70.0,
1811 ..Default::default()
1812 });
1813 assert_eq!(format_duration_parts(d), "P12Y5M14DT16H13M10S");
1814 }
1815
1816 #[test]
1817 fn construct_sub_second() {
1818 let d = normalize_duration(DurationFields {
1819 days: 14.0,
1820 seconds: 70.0,
1821 milliseconds: 1.0,
1822 ..Default::default()
1823 });
1824 assert_eq!(format_duration_parts(d), "P14DT1M10.001S");
1825 }
1826
1827 #[test]
1828 fn construct_minutes_fraction() {
1829 let d = normalize_duration(DurationFields {
1830 minutes: 1.5,
1831 seconds: 1.0,
1832 ..Default::default()
1833 });
1834 assert_eq!(format_duration_parts(d), "PT1M31S");
1835 }
1836
1837 #[test]
1838 fn parse_string_p14dt16h12m() {
1839 assert_eq!(
1840 format_duration_parts(parse_duration("P14DT16H12M").unwrap()),
1841 "P14DT16H12M"
1842 );
1843 }
1844
1845 #[test]
1846 fn parse_string_p0_75m() {
1847 assert_eq!(
1848 format_duration_parts(parse_duration("P0.75M").unwrap()),
1849 "P22DT19H51M49.5S"
1850 );
1851 }
1852
1853 #[test]
1854 fn parse_string_pt0_75m() {
1855 assert_eq!(
1856 format_duration_parts(parse_duration("PT0.75M").unwrap()),
1857 "PT45S"
1858 );
1859 }
1860
1861 #[test]
1862 fn malformed_temporal_strings_are_rejected_without_panicking() {
1863 assert_eq!(parse_date("123é4"), None);
1864 for malformed in ["P", "PT", "Pgarbage", "P1Ygarbage", "P1Y2", "P1.2.3Y"] {
1865 assert_eq!(
1866 parse_duration(malformed),
1867 None,
1868 "{malformed} must be rejected"
1869 );
1870 }
1871 }
1872
1873 #[test]
1874 fn add_durations() {
1875 let a = du(149.0, 14.0, 16.0, 12.0, 70.0);
1876 let a = (a.0, a.1, a.2, 1);
1877 let sum = add_duration(a, a).unwrap();
1878 assert_eq!(format_duration_parts(sum), "P24Y10M28DT32H26M20.000000002S");
1879 }
1880
1881 #[test]
1882 fn scale_duration_by_half() {
1883 let base = (149, 14, 58390, 1);
1884 assert_eq!(
1885 format_duration_parts(scale_duration(base, 0.5)),
1886 "P6Y2M22DT13H21M8S"
1887 );
1888 assert_eq!(
1889 format_duration_parts(scale_duration(base, 2.0)),
1890 "P24Y10M28DT32H26M20.000000002S"
1891 );
1892 }
1893
1894 #[test]
1895 fn negative_seconds_fraction() {
1896 let d = normalize_duration(DurationFields {
1897 seconds: 2.0,
1898 milliseconds: -1.0,
1899 ..Default::default()
1900 });
1901 assert_eq!(format_duration_parts(d), "PT1.999S");
1902 let d = normalize_duration(DurationFields {
1903 seconds: -2.0,
1904 milliseconds: 1.0,
1905 ..Default::default()
1906 });
1907 assert_eq!(format_duration_parts(d), "PT-1.999S");
1908 let d = normalize_duration(DurationFields {
1909 seconds: -2.0,
1910 milliseconds: -1.0,
1911 ..Default::default()
1912 });
1913 assert_eq!(format_duration_parts(d), "PT-2.001S");
1914 let d = normalize_duration(DurationFields {
1915 seconds: 60.0,
1916 milliseconds: -1.0,
1917 ..Default::default()
1918 });
1919 assert_eq!(format_duration_parts(d), "PT59.999S");
1920 let d = normalize_duration(DurationFields {
1921 minutes: 12.0,
1922 seconds: -60.0,
1923 ..Default::default()
1924 });
1925 assert_eq!(format_duration_parts(d), "PT11M");
1926 }
1927
1928 #[test]
1929 fn date_roundtrip() {
1930 let d = epoch_day_from_ymd(1984, 10, 11).unwrap();
1931 assert_eq!(format_date(d), "1984-10-11");
1932 assert_eq!(parse_date("1984-10-11"), Some(d));
1933 assert_eq!(parse_date("19841011"), Some(d));
1934 }
1935
1936 #[test]
1937 fn date_components() {
1938 let d = epoch_day_from_ymd(1984, 10, 11).unwrap();
1939 assert_eq!(date_component(d, "year"), Some(1984));
1940 assert_eq!(date_component(d, "quarter"), Some(4));
1941 assert_eq!(date_component(d, "month"), Some(10));
1942 assert_eq!(date_component(d, "week"), Some(41));
1943 assert_eq!(date_component(d, "weekYear"), Some(1984));
1944 assert_eq!(date_component(d, "day"), Some(11));
1945 assert_eq!(date_component(d, "ordinalDay"), Some(285));
1946 assert_eq!(date_component(d, "weekDay"), Some(4));
1947 assert_eq!(date_component(d, "dayOfQuarter"), Some(11));
1948 }
1949
1950 #[test]
1951 fn date_plus_duration() {
1952 let x = epoch_day_from_ymd(1984, 10, 11).unwrap();
1953 let d = du(149.0, 14.0, 16.0, 12.0, 70.0);
1954 let sum = add_duration_to_date(x, d.0, d.1, d.2, d.3, false).unwrap();
1955 assert_eq!(format_date(sum), "1997-03-25");
1956 let diff = add_duration_to_date(x, d.0, d.1, d.2, d.3, true).unwrap();
1957 assert_eq!(format_date(diff), "1972-04-27");
1958 }
1959
1960 #[test]
1964 fn date_plus_fractional_duration_carries_extra_day_from_seconds() {
1965 let x = epoch_day_from_ymd(1984, 10, 11).unwrap();
1966 let d = normalize_duration(DurationFields {
1967 years: 12.5,
1968 months: 5.5,
1969 days: 14.5,
1970 hours: 16.5,
1971 minutes: 12.5,
1972 seconds: 70.5,
1973 nanoseconds: 3.0,
1974 ..Default::default()
1975 });
1976 let sum = add_duration_to_date(x, d.0, d.1, d.2, d.3, false).unwrap();
1977 assert_eq!(format_date(sum), "1997-10-11");
1978 let diff = add_duration_to_date(x, d.0, d.1, d.2, d.3, true).unwrap();
1979 assert_eq!(format_date(diff), "1971-10-12");
1980 }
1981
1982 #[test]
1983 fn duration_accessors() {
1984 let d = normalize_duration(DurationFields {
1985 years: 1.0,
1986 months: 4.0,
1987 days: 10.0,
1988 hours: 1.0,
1989 minutes: 1.0,
1990 seconds: 1.0,
1991 nanoseconds: 111_111_111.0,
1992 ..Default::default()
1993 });
1994 let get = |prop: &str| duration_component(d.0, d.1, d.2, d.3, prop).unwrap();
1995 assert_eq!(get("years"), 1);
1996 assert_eq!(get("quarters"), 5);
1997 assert_eq!(get("months"), 16);
1998 assert_eq!(get("weeks"), 1);
1999 assert_eq!(get("days"), 10);
2000 assert_eq!(get("hours"), 1);
2001 assert_eq!(get("minutes"), 61);
2002 assert_eq!(get("seconds"), 3661);
2003 assert_eq!(get("milliseconds"), 3_661_111);
2004 assert_eq!(get("microseconds"), 3_661_111_111);
2005 assert_eq!(get("nanoseconds"), 3_661_111_111_111);
2006 assert_eq!(get("quartersOfYear"), 1);
2007 assert_eq!(get("monthsOfQuarter"), 1);
2008 assert_eq!(get("monthsOfYear"), 4);
2009 assert_eq!(get("daysOfWeek"), 3);
2010 assert_eq!(get("minutesOfHour"), 1);
2011 assert_eq!(get("secondsOfMinute"), 1);
2012 assert_eq!(get("millisecondsOfSecond"), 111);
2013 assert_eq!(get("microsecondsOfSecond"), 111_111);
2014 assert_eq!(get("nanosecondsOfSecond"), 111_111_111);
2015 }
2016
2017 fn format_duration_parts(p: DurationParts) -> String {
2018 format_duration(p.0, p.1, p.2, p.3)
2019 }
2020
2021 #[test]
2025 fn civil_core_matches_chrono_across_its_range() {
2026 use chrono::Datelike;
2027 for epoch_day in (-1_000_000..1_000_000i64).step_by(9973) {
2028 let d = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()
2029 + chrono::Duration::days(epoch_day);
2030 let (y, m, day) = civil_from_days(epoch_day);
2031 assert_eq!((y, m, day), (d.year() as i64, d.month(), d.day()));
2032 assert_eq!(days_from_civil(y, m, day), epoch_day);
2033 assert_eq!(
2034 iso_weekday_from_days(epoch_day),
2035 d.weekday().number_from_monday() as i64
2036 );
2037 let iso = d.iso_week();
2038 assert_eq!(
2039 iso_week_of(epoch_day),
2040 (iso.year() as i64, iso.week() as i64)
2041 );
2042 assert_eq!(ordinal_day_of(y, m, day), d.ordinal() as i64);
2043 }
2044 }
2045
2046 #[test]
2047 fn expanded_year_dates_parse_and_round_trip() {
2048 let min = parse_date("-999999999-01-01").unwrap();
2049 let max = parse_date("+999999999-12-31").unwrap();
2050 assert_eq!(format_date(min), "-999999999-01-01");
2051 assert_eq!(format_date(max), "+999999999-12-31");
2052 assert_eq!(date_component(min, "year"), Some(-999_999_999));
2053 assert_eq!(date_component(max, "year"), Some(999_999_999));
2054 assert_eq!(format_date(parse_date("2020-01-10").unwrap()), "2020-01-10");
2056 assert_eq!(format_date(parse_date("0033-06-01").unwrap()), "0033-06-01");
2057 }
2058
2059 #[test]
2060 fn year_range_and_calendar_validity_are_enforced() {
2061 assert!(parse_date("+1000000000-01-01").is_none());
2062 assert!(parse_date("-1000000000-01-01").is_none());
2063 assert!(epoch_day_from_ymd(2020, 13, 1).is_none());
2064 assert!(epoch_day_from_ymd(2020, 2, 30).is_none());
2065 assert!(epoch_day_from_ymd(1900, 2, 29).is_none());
2067 assert!(epoch_day_from_ymd(2000, 2, 29).is_some());
2068 }
2069
2070 #[test]
2072 fn duration_between_spans_the_full_year_range() {
2073 let a = parse_date("-999999999-01-01").unwrap();
2074 let b = parse_date("+999999999-12-31").unwrap();
2075 let parts = duration_between(Some(a), None, None, Some(b), None, None);
2076 assert_eq!(format_duration_parts(parts), "P1999999998Y11M30D");
2077 }
2078
2079 #[test]
2083 fn duration_in_seconds_spans_the_full_year_range() {
2084 let (a_secs, a_nanos) = parse_local_date_time("-999999999-01-01").unwrap();
2085 let (b_secs, b_nanos) = parse_local_date_time("+999999999-12-31T23:59:59").unwrap();
2086 let (a_day, a_nod) = split_epoch_seconds(a_secs);
2087 let (b_day, b_nod) = split_epoch_seconds(b_secs);
2088 let parts = duration_in_seconds(
2089 Some(a_day),
2090 Some(a_nod + a_nanos as i64),
2091 None,
2092 Some(b_day),
2093 Some(b_nod + b_nanos as i64),
2094 None,
2095 );
2096 assert_eq!(format_duration_parts(parts), "PT17531639991215H59M59S");
2097 }
2098}