1pub const UNIX_EPOCH_JDN: i64 = 2_440_588;
16
17const LEAP_SECONDS: &[(i64, u32, u32, i64)] = &[
22 (1972, 1, 1, 10), (1972, 7, 1, 11), (1973, 1, 1, 12), (1974, 1, 1, 13), (1975, 1, 1, 14),
23 (1976, 1, 1, 15), (1977, 1, 1, 16), (1978, 1, 1, 17), (1979, 1, 1, 18), (1980, 1, 1, 19),
24 (1981, 7, 1, 20), (1982, 7, 1, 21), (1983, 7, 1, 22), (1985, 7, 1, 23), (1988, 1, 1, 24),
25 (1990, 1, 1, 25), (1991, 1, 1, 26), (1992, 7, 1, 27), (1993, 7, 1, 28), (1994, 7, 1, 29),
26 (1996, 1, 1, 30), (1997, 7, 1, 31), (1999, 1, 1, 32), (2006, 1, 1, 33), (2009, 1, 1, 34),
27 (2012, 7, 1, 35), (2015, 7, 1, 36), (2017, 1, 1, 37),
28];
29
30pub const TT_MINUS_TAI_NANOS: i64 = 32_184_000_000;
32
33pub fn tai_minus_utc(unix_seconds: i64) -> i64 {
36 let mut offset = LEAP_SECONDS[0].3; for &(y, m, d, value) in LEAP_SECONDS {
38 let effective = days_from_civil(y, m, d) * SECONDS_PER_DAY;
39 if effective <= unix_seconds {
40 offset = value;
41 } else {
42 break;
43 }
44 }
45 offset
46}
47
48pub fn tai_nanos_from_unix_nanos(unix_ns: i64) -> i64 {
50 let secs = unix_ns.div_euclid(NANOS_PER_SECOND);
51 unix_ns + tai_minus_utc(secs) * NANOS_PER_SECOND
52}
53
54pub fn tt_nanos_from_unix_nanos(unix_ns: i64) -> i64 {
57 tai_nanos_from_unix_nanos(unix_ns) + TT_MINUS_TAI_NANOS
58}
59
60pub fn unix_nanos_from_tt_nanos(tt_ns: i64) -> i64 {
62 let tai_ns = tt_ns - TT_MINUS_TAI_NANOS;
63 let unix_secs = tai_to_unix_seconds(tai_ns.div_euclid(NANOS_PER_SECOND));
65 tai_ns - tai_minus_utc(unix_secs) * NANOS_PER_SECOND
66}
67
68pub fn unix_to_tai_seconds(unix_seconds: i64) -> i64 {
70 unix_seconds + tai_minus_utc(unix_seconds)
71}
72
73pub fn tai_to_unix_seconds(tai_seconds: i64) -> i64 {
76 let mut unix = tai_seconds - tai_minus_utc(tai_seconds);
77 for _ in 0..2 {
78 unix = tai_seconds - tai_minus_utc(unix);
79 }
80 unix
81}
82
83#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
90pub struct Hlc {
91 pub physical_nanos: i64,
92 pub logical: u32,
93}
94
95impl Hlc {
96 pub const ZERO: Hlc = Hlc { physical_nanos: 0, logical: 0 };
98
99 pub fn tick(self, now_nanos: i64) -> Hlc {
103 let physical = self.physical_nanos.max(now_nanos);
104 let logical = if physical == self.physical_nanos { self.logical + 1 } else { 0 };
105 Hlc { physical_nanos: physical, logical }
106 }
107
108 pub fn recv(self, remote: Hlc, now_nanos: i64) -> Hlc {
112 let physical = self.physical_nanos.max(remote.physical_nanos).max(now_nanos);
113 let logical = if physical == self.physical_nanos && physical == remote.physical_nanos {
114 self.logical.max(remote.logical) + 1
115 } else if physical == self.physical_nanos {
116 self.logical + 1
117 } else if physical == remote.physical_nanos {
118 remote.logical + 1
119 } else {
120 0
121 };
122 Hlc { physical_nanos: physical, logical }
123 }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub struct EstimateInterval {
132 pub lower_nanos: i64,
133 pub upper_nanos: i64,
134}
135
136impl EstimateInterval {
137 pub fn new(lower_nanos: i64, upper_nanos: i64) -> Self {
138 EstimateInterval { lower_nanos, upper_nanos }
139 }
140 pub fn width(&self) -> i64 {
142 self.upper_nanos - self.lower_nanos
143 }
144 pub fn contains(&self, t: i64) -> bool {
145 self.lower_nanos <= t && t <= self.upper_nanos
146 }
147 pub fn intersect(&self, other: &EstimateInterval) -> EstimateInterval {
149 EstimateInterval {
150 lower_nanos: self.lower_nanos.max(other.lower_nanos),
151 upper_nanos: self.upper_nanos.min(other.upper_nanos),
152 }
153 }
154}
155
156pub fn knowable_horizon(now_nanos: i64, light_delay_nanos: i64) -> i64 {
160 now_nanos - light_delay_nanos
161}
162
163#[derive(Clone, Debug, PartialEq, Eq)]
170pub struct SyncClock {
171 pub hlc: Hlc,
172 pub knowledge: std::collections::BTreeMap<u64, EstimateInterval>,
173}
174
175impl SyncClock {
176 pub fn new() -> Self {
177 SyncClock { hlc: Hlc::ZERO, knowledge: std::collections::BTreeMap::new() }
178 }
179
180 pub fn tick(&self, now_nanos: i64) -> SyncClock {
182 SyncClock { hlc: self.hlc.tick(now_nanos), knowledge: self.knowledge.clone() }
183 }
184
185 pub fn observe(&self, node: u64, interval: EstimateInterval) -> SyncClock {
188 let mut knowledge = self.knowledge.clone();
189 knowledge
190 .entry(node)
191 .and_modify(|iv| *iv = iv.intersect(&interval))
192 .or_insert(interval);
193 SyncClock { hlc: self.hlc, knowledge }
194 }
195
196 pub fn merge(&self, other: &SyncClock) -> SyncClock {
200 let mut knowledge = self.knowledge.clone();
201 for (&node, &interval) in &other.knowledge {
202 knowledge
203 .entry(node)
204 .and_modify(|iv| *iv = iv.intersect(&interval))
205 .or_insert(interval);
206 }
207 SyncClock { hlc: self.hlc.max(other.hlc), knowledge }
208 }
209}
210
211impl Default for SyncClock {
212 fn default() -> Self {
213 SyncClock::new()
214 }
215}
216
217pub const NANOS_PER_SECOND: i64 = 1_000_000_000;
219pub const SECONDS_PER_DAY: i64 = 86_400;
221pub const NANOS_PER_DAY: i64 = SECONDS_PER_DAY * NANOS_PER_SECOND;
223
224#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
227pub struct CivilDateTime {
228 pub year: i64,
229 pub month: u32,
230 pub day: u32,
231 pub hour: u32,
232 pub minute: u32,
233 pub second: u32,
234 pub nanosecond: u32,
235}
236
237pub fn civil_from_unix_nanos(ns: i64) -> CivilDateTime {
240 let day = ns.div_euclid(NANOS_PER_DAY);
241 let rem = ns.rem_euclid(NANOS_PER_DAY); let (year, month, d) = civil_from_days(day);
243 let secs = rem / NANOS_PER_SECOND;
244 CivilDateTime {
245 year,
246 month,
247 day: d,
248 hour: (secs / 3600) as u32,
249 minute: ((secs % 3600) / 60) as u32,
250 second: (secs % 60) as u32,
251 nanosecond: (rem % NANOS_PER_SECOND) as u32,
252 }
253}
254
255pub fn unix_nanos_from_civil(dt: CivilDateTime) -> i64 {
257 let days = days_from_civil(dt.year, dt.month, dt.day);
258 let secs = dt.hour as i64 * 3600 + dt.minute as i64 * 60 + dt.second as i64;
259 days * NANOS_PER_DAY + secs * NANOS_PER_SECOND + dt.nanosecond as i64
260}
261
262#[derive(Clone, Copy, Debug, PartialEq, Eq)]
266pub struct ZoneTransition {
267 pub at_unix_seconds: i64,
268 pub offset_seconds: i32,
269}
270
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
276pub enum Fold {
277 Earlier,
278 Later,
279}
280
281pub fn offset_at(transitions: &[ZoneTransition], base_offset: i32, instant_ns: i64) -> i32 {
284 let secs = instant_ns.div_euclid(NANOS_PER_SECOND);
285 let mut off = base_offset;
286 for t in transitions {
287 if t.at_unix_seconds <= secs {
288 off = t.offset_seconds;
289 } else {
290 break;
291 }
292 }
293 off
294}
295
296pub fn to_local(transitions: &[ZoneTransition], base_offset: i32, instant_ns: i64) -> CivilDateTime {
298 let off = offset_at(transitions, base_offset, instant_ns);
299 civil_from_unix_nanos(instant_ns + off as i64 * NANOS_PER_SECOND)
300}
301
302pub fn from_local(
307 transitions: &[ZoneTransition],
308 base_offset: i32,
309 civil: CivilDateTime,
310 fold: Fold,
311) -> i64 {
312 let local_ns = unix_nanos_from_civil(civil);
313 let mut offsets: Vec<i32> = vec![base_offset];
315 for t in transitions {
316 if !offsets.contains(&t.offset_seconds) {
317 offsets.push(t.offset_seconds);
318 }
319 }
320 let mut valid: Vec<i64> = Vec::new();
323 for o in &offsets {
324 let utc = local_ns - *o as i64 * NANOS_PER_SECOND;
325 if offset_at(transitions, base_offset, utc) == *o {
326 valid.push(utc);
327 }
328 }
329 valid.sort_unstable();
330 valid.dedup();
331 if valid.len() >= 2 {
332 return match fold {
334 Fold::Earlier => valid[0],
335 Fold::Later => *valid.last().unwrap(),
336 };
337 }
338 if valid.len() == 1 {
339 return valid[0];
340 }
341 let mut cands: Vec<i64> = offsets.iter().map(|o| local_ns - *o as i64 * NANOS_PER_SECOND).collect();
343 cands.sort_unstable();
344 match fold {
345 Fold::Earlier => cands[0],
346 Fold::Later => *cands.last().unwrap(),
347 }
348}
349
350#[derive(Clone, Copy, Debug, PartialEq, Eq)]
355pub struct DstRule {
356 pub month: u32,
357 pub week: u32,
358 pub weekday: u32,
359 pub time_seconds: i64,
360}
361
362pub fn nth_weekday_of_month(year: i64, month: u32, week: u32, weekday: u32) -> u32 {
365 let first = days_from_civil(year, month, 1);
366 let first_weekday = weekday_from_days(first);
367 let to_first = (weekday + 7 - first_weekday) % 7; let mut day = 1 + to_first + (week - 1) * 7;
369 let last = last_day_of_month(year, month);
370 while day > last {
371 day -= 7; }
373 day
374}
375
376pub fn dst_transitions_for_year(
380 std_offset: i32,
381 dst_offset: i32,
382 start: DstRule,
383 end: DstRule,
384 year: i64,
385) -> [ZoneTransition; 2] {
386 let transition = |rule: DstRule, pre_offset: i32, new_offset: i32| {
389 let day = nth_weekday_of_month(year, rule.month, rule.week, rule.weekday);
390 let local = days_from_civil(year, rule.month, day) * SECONDS_PER_DAY + rule.time_seconds;
391 ZoneTransition { at_unix_seconds: local - pre_offset as i64, offset_seconds: new_offset }
392 };
393 [
394 transition(start, std_offset, dst_offset), transition(end, dst_offset, std_offset), ]
397}
398
399#[derive(Clone, Copy, Debug, PartialEq, Eq)]
403pub struct ZoneSpec {
404 pub name: &'static str,
405 pub std_offset: i32,
406 pub dst: Option<(i32, DstRule, DstRule)>,
407}
408
409impl ZoneSpec {
410 pub fn transitions(&self, start_year: i64, end_year: i64) -> Vec<ZoneTransition> {
413 let mut v = Vec::new();
414 if let Some((dst_offset, start, end)) = self.dst {
415 for y in start_year..=end_year {
416 let pair = dst_transitions_for_year(self.std_offset, dst_offset, start, end, y);
417 v.extend_from_slice(&pair);
418 }
419 v.sort_by_key(|t| t.at_unix_seconds);
420 }
421 v
422 }
423
424 pub fn to_local(&self, instant_ns: i64) -> CivilDateTime {
426 if self.dst.is_none() {
427 return civil_from_unix_nanos(instant_ns + self.std_offset as i64 * NANOS_PER_SECOND);
428 }
429 let (year, _, _) = civil_from_days(instant_ns.div_euclid(NANOS_PER_DAY));
432 let tz = self.transitions(year - 1, year + 1);
433 to_local(&tz, self.std_offset, instant_ns)
434 }
435}
436
437pub fn local_instant_nanos(instant_ns: i64, zone_name: &str) -> Option<i64> {
443 let zone = zone_by_name(zone_name)?;
444 Some(unix_nanos_from_civil(zone.to_local(instant_ns)))
445}
446
447pub fn format_zoned(instant_ns: i64, zone_name: &str) -> Option<String> {
451 let zone = zone_by_name(zone_name)?;
452 let local = zone.to_local(instant_ns);
453 let offset_secs = (unix_nanos_from_civil(local) - instant_ns) / NANOS_PER_SECOND;
455 let sign = if offset_secs < 0 { '-' } else { '+' };
456 let abs = offset_secs.abs();
457 Some(format!(
458 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
459 local.year, local.month, local.day, local.hour, local.minute, local.second,
460 sign, abs / 3600, (abs % 3600) / 60,
461 ))
462}
463
464pub fn zone_by_name(name: &str) -> Option<ZoneSpec> {
468 let us = (
470 DstRule { month: 3, week: 2, weekday: 0, time_seconds: 2 * 3600 },
471 DstRule { month: 11, week: 1, weekday: 0, time_seconds: 2 * 3600 },
472 );
473 let eu = |std_h: i64| {
475 (
476 DstRule { month: 3, week: 5, weekday: 0, time_seconds: (std_h + 1) * 3600 },
477 DstRule { month: 10, week: 5, weekday: 0, time_seconds: (std_h + 2) * 3600 },
478 )
479 };
480 let spec = match name {
481 "UTC" | "Etc/UTC" | "Zulu" => ZoneSpec { name: "UTC", std_offset: 0, dst: None },
482 "America/New_York" => ZoneSpec { name: "America/New_York", std_offset: -5 * 3600, dst: Some((-4 * 3600, us.0, us.1)) },
483 "America/Chicago" => ZoneSpec { name: "America/Chicago", std_offset: -6 * 3600, dst: Some((-5 * 3600, us.0, us.1)) },
484 "America/Denver" => ZoneSpec { name: "America/Denver", std_offset: -7 * 3600, dst: Some((-6 * 3600, us.0, us.1)) },
485 "America/Los_Angeles" => ZoneSpec { name: "America/Los_Angeles", std_offset: -8 * 3600, dst: Some((-7 * 3600, us.0, us.1)) },
486 "America/Phoenix" => ZoneSpec { name: "America/Phoenix", std_offset: -7 * 3600, dst: None },
487 "Europe/London" | "Europe/Dublin" | "Europe/Lisbon" => {
488 let (s, e) = eu(0);
489 ZoneSpec { name: "Europe/London", std_offset: 0, dst: Some((3600, s, e)) }
490 }
491 "Europe/Paris" | "Europe/Berlin" | "Europe/Madrid" | "Europe/Rome" | "Europe/Amsterdam" => {
492 let (s, e) = eu(1);
493 ZoneSpec { name: "Europe/Paris", std_offset: 3600, dst: Some((2 * 3600, s, e)) }
494 }
495 "Asia/Kolkata" | "Asia/Calcutta" => ZoneSpec { name: "Asia/Kolkata", std_offset: 5 * 3600 + 1800, dst: None },
496 "Asia/Tokyo" => ZoneSpec { name: "Asia/Tokyo", std_offset: 9 * 3600, dst: None },
497 "Asia/Shanghai" => ZoneSpec { name: "Asia/Shanghai", std_offset: 8 * 3600, dst: None },
498 "Australia/Sydney" => ZoneSpec {
499 name: "Australia/Sydney",
500 std_offset: 10 * 3600,
501 dst: Some((
503 11 * 3600,
504 DstRule { month: 10, week: 1, weekday: 0, time_seconds: 2 * 3600 },
505 DstRule { month: 4, week: 1, weekday: 0, time_seconds: 3 * 3600 },
506 )),
507 },
508 _ => return None,
509 };
510 Some(spec)
511}
512
513pub fn add_span(dt: CivilDateTime, months: i64, days: i64) -> CivilDateTime {
519 let total = (dt.year * 12 + (dt.month as i64 - 1)) + months;
520 let new_year = total.div_euclid(12);
521 let new_month = (total.rem_euclid(12) + 1) as u32;
522 let clamped_day = dt.day.min(last_day_of_month(new_year, new_month));
523 let z = days_from_civil(new_year, new_month, clamped_day) + days;
524 let (year, month, day) = civil_from_days(z);
525 CivilDateTime { year, month, day, ..dt }
526}
527
528pub fn months_between(a_nanos: i64, b_nanos: i64) -> i64 {
533 if b_nanos < a_nanos {
534 return -months_between(b_nanos, a_nanos);
535 }
536 let a = civil_from_unix_nanos(a_nanos);
537 let b = civil_from_unix_nanos(b_nanos);
538 let mut months = (b.year - a.year) * 12 + (b.month as i64 - a.month as i64);
539 if unix_nanos_from_civil(add_span(a, months, 0)) > b_nanos {
542 months -= 1;
543 }
544 months
545}
546
547pub fn years_between(a_nanos: i64, b_nanos: i64) -> i64 {
550 months_between(a_nanos, b_nanos) / 12
551}
552
553pub fn is_weekend(z: i64) -> bool {
555 matches!(weekday_from_days(z), 0 | 6) }
557
558pub fn business_days_between(a: i64, b: i64) -> i64 {
561 if a == b {
562 return 0;
563 }
564 let (lo, hi, sign) = if a < b { (a, b, 1) } else { (b, a, -1) };
565 let span = hi - lo;
566 let full_weeks = span / 7;
567 let mut count = full_weeks * 5;
568 let mut d = lo + full_weeks * 7;
569 while d < hi {
570 if !is_weekend(d) {
571 count += 1;
572 }
573 d += 1;
574 }
575 sign * count
576}
577
578pub fn add_business_days(z: i64, n: i64) -> i64 {
583 let step = if n >= 0 { 1 } else { -1 };
584 let mut remaining = n.abs();
585 let mut d = z;
586 while remaining > 0 {
587 d += step;
588 if !is_weekend(d) {
589 remaining -= 1;
590 }
591 }
592 d
593}
594
595pub fn format_duration(nanos: i64) -> String {
600 if nanos == 0 {
601 return "0s".to_string();
602 }
603 const D: u128 = 86_400_000_000_000;
604 const H: u128 = 3_600_000_000_000;
605 const M: u128 = 60_000_000_000;
606 const S: u128 = 1_000_000_000;
607 const MS: u128 = 1_000_000;
608 const US: u128 = 1_000;
609 let mut out = String::new();
610 if nanos < 0 {
611 out.push('-');
612 }
613 let mut abs = (nanos as i128).unsigned_abs();
614 let frac_str = |frac: u128, width: usize| -> String {
616 let mut f = format!("{:0width$}", frac, width = width);
617 while f.ends_with('0') {
618 f.pop();
619 }
620 f
621 };
622 if abs >= S {
623 let d = abs / D;
624 abs %= D;
625 let h = abs / H;
626 abs %= H;
627 let m = abs / M;
628 abs %= M;
629 if d > 0 {
630 out += &format!("{d}d");
631 }
632 if h > 0 {
633 out += &format!("{h}h");
634 }
635 if m > 0 {
636 out += &format!("{m}m");
637 }
638 let (secs, frac) = (abs / S, abs % S);
639 if frac == 0 {
640 if secs > 0 {
641 out += &format!("{secs}s");
642 }
643 } else {
644 out += &format!("{secs}.{}s", frac_str(frac, 9));
645 }
646 } else if abs >= MS {
647 let (w, frac) = (abs / MS, abs % MS);
648 out += &(if frac == 0 { format!("{w}ms") } else { format!("{w}.{}ms", frac_str(frac, 6)) });
649 } else if abs >= US {
650 let (w, frac) = (abs / US, abs % US);
651 out += &(if frac == 0 { format!("{w}us") } else { format!("{w}.{}us", frac_str(frac, 3)) });
652 } else {
653 out += &format!("{abs}ns");
654 }
655 out
656}
657
658pub fn parse_duration(s: &str) -> Option<i64> {
661 let b = s.as_bytes();
662 let mut i = 0;
663 let neg = match b.first() {
664 Some(&b'-') => { i += 1; true }
665 Some(&b'+') => { i += 1; false }
666 _ => false,
667 };
668 if i >= b.len() {
669 return None;
670 }
671 let mut total: i128 = 0;
672 let mut saw_any = false;
673 while i < b.len() {
674 let int_start = i;
675 while i < b.len() && b[i].is_ascii_digit() {
676 i += 1;
677 }
678 let int_str = &s[int_start..i];
679 let mut frac_str = "";
680 if i < b.len() && b[i] == b'.' {
681 i += 1;
682 let frac_start = i;
683 while i < b.len() && b[i].is_ascii_digit() {
684 i += 1;
685 }
686 frac_str = &s[frac_start..i];
687 }
688 if int_str.is_empty() && frac_str.is_empty() {
689 return None; }
691 let rest = &s[i..];
692 let (unit_nanos, ulen): (i128, usize) = if rest.starts_with("ms") {
693 (1_000_000, 2)
694 } else if rest.starts_with("ns") {
695 (1, 2)
696 } else if rest.starts_with("us") {
697 (1_000, 2)
698 } else if rest.starts_with("µs") {
699 (1_000, "µs".len())
700 } else if rest.starts_with('s') {
701 (1_000_000_000, 1)
702 } else if rest.starts_with('m') {
703 (60_000_000_000, 1)
704 } else if rest.starts_with('h') {
705 (3_600_000_000_000, 1)
706 } else if rest.starts_with('d') {
707 (86_400_000_000_000, 1)
708 } else {
709 return None; };
711 i += ulen;
712 let int_val: i128 = if int_str.is_empty() { 0 } else { int_str.parse().ok()? };
713 total = total.checked_add(int_val.checked_mul(unit_nanos)?)?;
714 if !frac_str.is_empty() {
715 let frac_val: i128 = frac_str.parse().ok()?;
716 let mut denom: i128 = 1;
717 for _ in 0..frac_str.len() {
718 denom *= 10;
719 }
720 total = total.checked_add(frac_val * unit_nanos / denom)?;
721 }
722 saw_any = true;
723 }
724 if !saw_any {
725 return None;
726 }
727 i64::try_from(if neg { -total } else { total }).ok()
728}
729
730pub fn format_rfc3339(instant_ns: i64) -> String {
734 let c = civil_from_unix_nanos(instant_ns);
735 let mut s = format!(
736 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
737 c.year, c.month, c.day, c.hour, c.minute, c.second
738 );
739 if c.nanosecond != 0 {
740 let mut frac = format!("{:09}", c.nanosecond);
741 while frac.ends_with('0') {
742 frac.pop();
743 }
744 s.push('.');
745 s.push_str(&frac);
746 }
747 s.push('Z');
748 s
749}
750
751pub fn format_time_of_day(nanos_from_midnight: i64) -> String {
756 let rem = nanos_from_midnight.rem_euclid(NANOS_PER_DAY);
757 let secs = rem / NANOS_PER_SECOND;
758 let mut s = format!("{:02}:{:02}:{:02}", secs / 3600, (secs % 3600) / 60, secs % 60);
759 let nanos = (rem % NANOS_PER_SECOND) as u32;
760 if nanos != 0 {
761 let mut frac = format!("{:09}", nanos);
762 while frac.ends_with('0') {
763 frac.pop();
764 }
765 s.push('.');
766 s.push_str(&frac);
767 }
768 s
769}
770
771pub fn parse_rfc3339(s: &str) -> Option<i64> {
774 let b = s.as_bytes();
775 if b.len() < 20 {
777 return None;
778 }
779 let digits = |start: usize, len: usize| -> Option<i64> {
780 let mut v: i64 = 0;
781 for i in start..start + len {
782 let c = *b.get(i)?;
783 if !c.is_ascii_digit() {
784 return None;
785 }
786 v = v * 10 + (c - b'0') as i64;
787 }
788 Some(v)
789 };
790 if b[4] != b'-' || b[7] != b'-' || b[10] != b'T' || b[13] != b':' || b[16] != b':' {
791 return None;
792 }
793 let year = digits(0, 4)?;
794 let month = digits(5, 2)? as u32;
795 let day = digits(8, 2)? as u32;
796 let hour = digits(11, 2)? as u32;
797 let minute = digits(14, 2)? as u32;
798 let second = digits(17, 2)? as u32;
799 if !(1..=12).contains(&month) || !(1..=last_day_of_month(year, month)).contains(&day) {
800 return None;
801 }
802 if hour > 23 || minute > 59 || second > 59 {
803 return None;
804 }
805 let mut pos = 19;
806 let mut nanosecond: u32 = 0;
808 if b.get(pos) == Some(&b'.') {
809 pos += 1;
810 let (mut frac, mut count) = (0u64, 0u32);
811 while let Some(&c) = b.get(pos) {
812 if !c.is_ascii_digit() {
813 break;
814 }
815 if count < 9 {
816 frac = frac * 10 + (c - b'0') as u64;
817 count += 1;
818 }
819 pos += 1;
820 }
821 if count == 0 {
822 return None; }
824 for _ in count..9 {
825 frac *= 10;
826 }
827 nanosecond = frac as u32;
828 }
829 let offset_seconds: i64 = match b.get(pos) {
831 Some(&b'Z') => {
832 pos += 1;
833 0
834 }
835 Some(&c) if c == b'+' || c == b'-' => {
836 let sign = if c == b'+' { 1 } else { -1 };
837 let oh = digits(pos + 1, 2)?;
838 if b.get(pos + 3) != Some(&b':') {
839 return None;
840 }
841 let om = digits(pos + 4, 2)?;
842 if oh > 23 || om > 59 {
843 return None;
844 }
845 pos += 6;
846 sign * (oh * 3600 + om * 60)
847 }
848 _ => return None,
849 };
850 if pos != b.len() {
851 return None; }
853 let civil = CivilDateTime { year, month, day, hour, minute, second, nanosecond };
854 Some(unix_nanos_from_civil(civil) - offset_seconds * NANOS_PER_SECOND)
855}
856
857pub fn is_gregorian_leap(year: i64) -> bool {
860 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
861}
862
863pub fn is_julian_leap(year: i64) -> bool {
866 year.rem_euclid(4) == 0
867}
868
869pub fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
872 let y = if month <= 2 { year - 1 } else { year };
873 let era = (if y >= 0 { y } else { y - 399 }) / 400;
874 let yoe = (y - era * 400) as i64; let m = month as i64;
876 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + day as i64 - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146097 + doe - 719468
879}
880
881pub fn civil_from_days(z: i64) -> (i64, u32, u32) {
883 let z = z + 719468;
884 let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
885 let doe = z - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe + era * 400;
888 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = (doy - (153 * mp + 2) / 5 + 1) as u32; let month = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32; let year = if month <= 2 { y + 1 } else { y };
893 (year, month, day)
894}
895
896pub fn last_day_of_month(year: i64, month: u32) -> u32 {
898 match month {
899 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
900 4 | 6 | 9 | 11 => 30,
901 2 => if is_gregorian_leap(year) { 29 } else { 28 },
902 _ => 0,
903 }
904}
905
906pub fn weekday_from_days(z: i64) -> u32 {
908 (z + 4).rem_euclid(7) as u32
909}
910
911pub fn iso_weekday_from_days(z: i64) -> u32 {
913 (z + 3).rem_euclid(7) as u32 + 1
914}
915
916pub fn day_of_year(year: i64, month: u32, day: u32) -> u32 {
918 (days_from_civil(year, month, day) - days_from_civil(year, 1, 1) + 1) as u32
919}
920
921pub fn iso_week_from_days(z: i64) -> (i64, u32, u32) {
925 let iso_dow = iso_weekday_from_days(z);
926 let thursday = z + (4 - iso_dow as i64);
928 let (iso_year, _, _) = civil_from_days(thursday);
929 let jan4 = days_from_civil(iso_year, 1, 4);
933 let week1_monday = jan4 - (iso_weekday_from_days(jan4) as i64 - 1);
934 let week = ((z - week1_monday) / 7 + 1) as u32;
935 (iso_year, week, iso_dow)
936}
937
938pub fn days_from_iso_week(iso_year: i64, week: u32, iso_weekday: u32) -> i64 {
941 let jan4 = days_from_civil(iso_year, 1, 4);
942 let week1_monday = jan4 - (iso_weekday_from_days(jan4) as i64 - 1);
943 week1_monday + (week as i64 - 1) * 7 + (iso_weekday as i64 - 1)
944}
945
946pub fn jdn_from_julian(year: i64, month: u32, day: u32) -> i64 {
948 let a = (14 - month as i64) / 12;
949 let y = year + 4800 - a;
950 let m = month as i64 + 12 * a - 3;
951 day as i64 + (153 * m + 2) / 5 + 365 * y + y / 4 - 32083
952}
953
954pub fn julian_from_jdn(jdn: i64) -> (i64, u32, u32) {
956 let c = jdn + 32082;
957 let d = (4 * c + 3) / 1461;
958 let e = c - (1461 * d) / 4;
959 let m = (5 * e + 2) / 153;
960 let day = (e - (153 * m + 2) / 5 + 1) as u32;
961 let month = (m + 3 - 12 * (m / 10)) as u32;
962 let year = d - 4800 + m / 10;
963 (year, month, day)
964}
965
966pub fn days_from_julian(year: i64, month: u32, day: u32) -> i64 {
968 jdn_from_julian(year, month, day) - UNIX_EPOCH_JDN
969}
970
971pub fn julian_from_days(z: i64) -> (i64, u32, u32) {
973 julian_from_jdn(z + UNIX_EPOCH_JDN)
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979
980 struct Rng(u64);
982 impl Rng {
983 fn next(&mut self) -> u64 {
984 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
985 let mut z = self.0;
986 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
987 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
988 z ^ (z >> 31)
989 }
990 }
991
992 #[test]
993 fn local_instant_encodes_the_wall_clock_for_zoned_component_reads() {
994 let read = |ts: &str, zone: &str| {
996 let inst = parse_rfc3339(ts).unwrap();
997 let local = local_instant_nanos(inst, zone).unwrap();
998 civil_from_unix_nanos(local)
999 };
1000 let ny = read("2024-07-01T12:00:00Z", "America/New_York");
1002 assert_eq!((ny.hour, ny.day, ny.month), (8, 1, 7));
1003 let ist = read("2024-01-01T00:00:00Z", "Asia/Kolkata");
1005 assert_eq!((ist.hour, ist.minute, ist.day), (5, 30, 1));
1006 let roll = read("2024-07-01T02:00:00Z", "America/New_York");
1008 assert_eq!((roll.hour, roll.day, roll.month), (22, 30, 6));
1009 assert_eq!(local_instant_nanos(0, "Mars/Olympus_Mons"), None);
1011 }
1012
1013 #[test]
1014 fn calendar_months_and_years_between_count_complete_periods() {
1015 let at = |ts: &str| parse_rfc3339(ts).unwrap();
1016 let months = |a: &str, b: &str| months_between(at(a), at(b));
1017 let years = |a: &str, b: &str| years_between(at(a), at(b));
1018
1019 assert_eq!(months("2024-01-15T00:00:00Z", "2024-03-15T00:00:00Z"), 2);
1021 assert_eq!(months("2024-01-15T00:00:00Z", "2024-03-14T00:00:00Z"), 1); assert_eq!(months("2023-01-31T00:00:00Z", "2023-02-28T00:00:00Z"), 1);
1024 assert_eq!(months("2023-01-31T00:00:00Z", "2023-02-27T00:00:00Z"), 0);
1025 assert_eq!(months("2024-01-15T12:00:00Z", "2024-02-15T11:00:00Z"), 0);
1027 assert_eq!(months("2024-01-15T12:00:00Z", "2024-02-15T12:00:00Z"), 1);
1028 assert_eq!(months("2024-03-15T00:00:00Z", "2024-01-15T00:00:00Z"), -2);
1030 assert_eq!(months("2020-01-01T00:00:00Z", "2020-01-01T00:00:00Z"), 0);
1031
1032 assert_eq!(years("2020-06-01T00:00:00Z", "2024-06-01T00:00:00Z"), 4);
1034 assert_eq!(years("2020-06-01T00:00:00Z", "2024-03-01T00:00:00Z"), 3); assert_eq!(years("2020-06-01T00:00:00Z", "2024-05-31T00:00:00Z"), 3);
1036 assert_eq!(years("2024-01-01T00:00:00Z", "2020-01-01T00:00:00Z"), -4);
1037 }
1038
1039 #[test]
1040 fn time_of_day_formats_losslessly_to_the_nanosecond() {
1041 let h = 3_600 * NANOS_PER_SECOND;
1042 let m = 60 * NANOS_PER_SECOND;
1043 let s = NANOS_PER_SECOND;
1044 assert_eq!(format_time_of_day(0), "00:00:00");
1045 assert_eq!(format_time_of_day(16 * h), "16:00:00"); assert_eq!(format_time_of_day(7 * h + 30 * m + 45 * s), "07:30:45");
1047 assert_eq!(format_time_of_day(23 * h + 59 * m + 59 * s), "23:59:59");
1048 assert_eq!(format_time_of_day(7 * h + 30 * m + 45 * s + 500_000_000), "07:30:45.5");
1050 assert_eq!(format_time_of_day(123_456_789), "00:00:00.123456789");
1051 assert_eq!(format_time_of_day(NANOS_PER_DAY), "00:00:00");
1053 assert_eq!(format_time_of_day(-1), "23:59:59.999999999");
1054 }
1055
1056 #[test]
1057 fn gregorian_known_anchors() {
1058 assert_eq!(days_from_civil(1970, 1, 1), 0);
1060 assert_eq!(civil_from_days(0), (1970, 1, 1));
1061 assert_eq!(weekday_from_days(0), 4); assert_eq!(iso_weekday_from_days(0), 4); assert_eq!(days_from_civil(2000, 1, 1), 10957);
1065 assert_eq!(weekday_from_days(10957), 6); assert_eq!(days_from_civil(1969, 12, 31), -1);
1068 assert_eq!(civil_from_days(-1), (1969, 12, 31));
1069 }
1070
1071 #[test]
1072 fn gregorian_leap_years_and_month_lengths() {
1073 assert!(is_gregorian_leap(2000)); assert!(!is_gregorian_leap(1900)); assert!(is_gregorian_leap(2024));
1076 assert!(!is_gregorian_leap(2023));
1077 assert_eq!(last_day_of_month(2024, 2), 29);
1078 assert_eq!(last_day_of_month(2023, 2), 28);
1079 assert_eq!(last_day_of_month(2024, 1), 31);
1080 assert_eq!(last_day_of_month(2024, 4), 30);
1081 assert_eq!(day_of_year(2024, 12, 31), 366);
1083 assert_eq!(day_of_year(2023, 12, 31), 365);
1084 assert_eq!(day_of_year(2024, 3, 1), 61); }
1086
1087 #[test]
1088 fn iso_week_date_edge_cases() {
1089 assert_eq!(iso_week_from_days(days_from_civil(2021, 1, 1)), (2020, 53, 5));
1091 assert_eq!(iso_week_from_days(days_from_civil(2020, 12, 31)), (2020, 53, 4));
1093 assert_eq!(iso_week_from_days(days_from_civil(2023, 1, 1)), (2022, 52, 7));
1095 assert_eq!(iso_week_from_days(days_from_civil(2024, 1, 1)), (2024, 1, 1));
1097 let (y, w, _) = iso_week_from_days(days_from_civil(2020, 12, 28)); assert_eq!((y, w), (2020, 53));
1100 }
1101
1102 #[test]
1103 fn julian_gregorian_divergence() {
1104 let reform = days_from_civil(1582, 10, 15);
1106 assert_eq!(julian_from_days(reform), (1582, 10, 5));
1107 assert_eq!(julian_from_days(days_from_civil(1970, 1, 1)), (1969, 12, 19));
1110 assert_eq!(days_from_julian(1969, 12, 19), 0);
1112 assert!(is_julian_leap(1900));
1114 assert!(!is_gregorian_leap(1900));
1115 }
1116
1117 #[test]
1118 fn gregorian_round_trips_over_a_wide_range_under_fuzz() {
1119 let mut prev = i64::MIN;
1122 let mut rng = Rng(0x_DA7E_C0DE_1234_5678);
1123 for _ in 0..50_000 {
1124 let z = (rng.next() % 800_001) as i64 - 400_000;
1125 let (y, m, d) = civil_from_days(z);
1126 assert!((1..=12).contains(&m), "month in range at z={z}");
1127 assert!((1..=last_day_of_month(y, m)).contains(&d), "day in range at z={z}");
1128 assert_eq!(days_from_civil(y, m, d), z, "round-trip at z={z}");
1129 assert_eq!(weekday_from_days(z + 1), (weekday_from_days(z) + 1) % 7);
1131 let _ = prev;
1132 prev = z;
1133 }
1134 }
1135
1136 #[test]
1137 fn smooth_utc_instant_civil_decomposition() {
1138 let dt = |y, mo, d, h, mi, s, n| CivilDateTime {
1139 year: y, month: mo, day: d, hour: h, minute: mi, second: s, nanosecond: n,
1140 };
1141 assert_eq!(civil_from_unix_nanos(0), dt(1970, 1, 1, 0, 0, 0, 0));
1143 assert_eq!(unix_nanos_from_civil(dt(1970, 1, 1, 0, 0, 0, 0)), 0);
1144 assert_eq!(unix_nanos_from_civil(dt(1970, 1, 2, 0, 0, 0, 0)), NANOS_PER_DAY);
1146 let stamp = dt(2024, 2, 29, 13, 45, 30, 123_456_789); assert_eq!(civil_from_unix_nanos(unix_nanos_from_civil(stamp)), stamp);
1149 assert_eq!(
1151 civil_from_unix_nanos(-1),
1152 dt(1969, 12, 31, 23, 59, 59, 999_999_999)
1153 );
1154 assert_eq!(civil_from_unix_nanos(12 * 3600 * NANOS_PER_SECOND).hour, 12);
1156 }
1157
1158 #[test]
1159 fn calendar_span_arithmetic_clamps_and_rolls_over() {
1160 let dt = |y, mo, d, h, mi, s, n| CivilDateTime {
1161 year: y, month: mo, day: d, hour: h, minute: mi, second: s, nanosecond: n,
1162 };
1163 assert_eq!(add_span(dt(2023, 1, 31, 9, 0, 0, 0), 1, 0), dt(2023, 2, 28, 9, 0, 0, 0));
1165 assert_eq!(add_span(dt(2024, 1, 31, 9, 0, 0, 0), 1, 0), dt(2024, 2, 29, 9, 0, 0, 0));
1166 assert_eq!(add_span(dt(2024, 3, 15, 13, 45, 30, 7), 0, 1), dt(2024, 3, 16, 13, 45, 30, 7));
1168 assert_eq!(add_span(dt(2024, 12, 10, 0, 0, 0, 0), 1, 0), dt(2025, 1, 10, 0, 0, 0, 0));
1170 assert_eq!(add_span(dt(2024, 1, 15, 0, 0, 0, 0), 0, 20), dt(2024, 2, 4, 0, 0, 0, 0));
1172 assert_eq!(add_span(dt(2024, 3, 31, 0, 0, 0, 0), -1, 0), dt(2024, 2, 29, 0, 0, 0, 0));
1174 assert_eq!(add_span(dt(2024, 6, 15, 0, 0, 0, 0), 12, 0), dt(2025, 6, 15, 0, 0, 0, 0));
1176 }
1177
1178 #[test]
1179 fn business_day_math() {
1180 let d = |y, mo, day| days_from_civil(y, mo, day);
1181 assert_eq!(business_days_between(d(2024, 3, 11), d(2024, 3, 18)), 5);
1183 assert_eq!(business_days_between(d(2024, 3, 11), d(2024, 3, 15)), 4);
1185 assert_eq!(business_days_between(d(2024, 3, 15), d(2024, 3, 18)), 1); assert_eq!(business_days_between(d(2024, 3, 18), d(2024, 3, 11)), -5);
1188 assert_eq!(add_business_days(d(2024, 3, 15), 1), d(2024, 3, 18)); assert_eq!(add_business_days(d(2024, 3, 18), -1), d(2024, 3, 15)); assert_eq!(add_business_days(d(2024, 3, 11), 5), d(2024, 3, 18));
1193 assert!(is_weekend(d(2024, 3, 16))); assert!(is_weekend(d(2024, 3, 17))); assert!(!is_weekend(d(2024, 3, 15))); }
1198
1199 #[test]
1200 fn business_day_invariants_under_fuzz() {
1201 let mut rng = Rng(0x_B17A_DA75_C0DE_2468);
1205 for _ in 0..20_000 {
1206 let z0 = (rng.next() % 200_001) as i64 - 100_000;
1207 let z = if is_weekend(z0) { add_business_days(z0, 1) } else { z0 }; let n = (rng.next() % 39) as i64 + 1; let fwd = add_business_days(z, n);
1210 assert!(!is_weekend(fwd), "lands on a business day at z={z}, n={n}");
1211 assert!(fwd > z, "advancing moves strictly forward at z={z}, n={n}");
1212 assert_eq!(business_days_between(z, fwd), n, "count matches at z={z}, n={n}");
1213 }
1214 }
1215
1216 fn eastern_2024() -> (Vec<ZoneTransition>, i32) {
1220 let spring = days_from_civil(2024, 3, 10) * SECONDS_PER_DAY + 7 * 3600;
1221 let fall = days_from_civil(2024, 11, 3) * SECONDS_PER_DAY + 6 * 3600;
1222 (
1223 vec![
1224 ZoneTransition { at_unix_seconds: spring, offset_seconds: -4 * 3600 },
1225 ZoneTransition { at_unix_seconds: fall, offset_seconds: -5 * 3600 },
1226 ],
1227 -5 * 3600, )
1229 }
1230
1231 fn utc(y: i64, mo: u32, d: u32, h: u32, mi: u32) -> i64 {
1232 unix_nanos_from_civil(CivilDateTime { year: y, month: mo, day: d, hour: h, minute: mi, second: 0, nanosecond: 0 })
1233 }
1234
1235 #[test]
1236 fn timezone_utc_to_local_picks_the_offset_in_effect() {
1237 let (tz, base) = eastern_2024();
1238 let local = |ns| {
1239 let c = to_local(&tz, base, ns);
1240 (c.month, c.day, c.hour, c.minute)
1241 };
1242 assert_eq!(local(utc(2024, 1, 1, 12, 0)), (1, 1, 7, 0));
1244 assert_eq!(local(utc(2024, 7, 1, 12, 0)), (7, 1, 8, 0));
1246 assert_eq!(local(utc(2024, 12, 1, 12, 0)), (12, 1, 7, 0));
1248 assert_eq!(offset_at(&tz, base, utc(2024, 7, 1, 0, 0)), -4 * 3600);
1250 assert_eq!(offset_at(&tz, base, utc(2024, 1, 1, 0, 0)), -5 * 3600);
1251 }
1252
1253 #[test]
1254 fn timezone_local_to_utc_round_trip_and_dst_fold() {
1255 let (tz, base) = eastern_2024();
1256 let cdt = |y, mo, d, h, mi| CivilDateTime {
1257 year: y, month: mo, day: d, hour: h, minute: mi, second: 0, nanosecond: 0,
1258 };
1259 let summer_local = cdt(2024, 7, 1, 8, 0);
1261 let summer_utc = from_local(&tz, base, summer_local, Fold::Later);
1262 assert_eq!(summer_utc, utc(2024, 7, 1, 12, 0));
1263 assert_eq!(to_local(&tz, base, summer_utc), summer_local);
1264 let ambiguous = cdt(2024, 11, 3, 1, 30);
1267 assert_eq!(from_local(&tz, base, ambiguous, Fold::Earlier), utc(2024, 11, 3, 5, 30));
1268 assert_eq!(from_local(&tz, base, ambiguous, Fold::Later), utc(2024, 11, 3, 6, 30));
1269 let nonexistent = cdt(2024, 3, 10, 2, 30);
1272 let gap_earlier = from_local(&tz, base, nonexistent, Fold::Earlier);
1273 let gap_later = from_local(&tz, base, nonexistent, Fold::Later);
1274 assert_ne!(gap_earlier, gap_later, "the two gap resolutions differ");
1275 }
1276
1277 #[test]
1278 fn dst_rule_evaluator_generates_correct_transitions() {
1279 assert_eq!(nth_weekday_of_month(2024, 3, 2, 0), 10); assert_eq!(nth_weekday_of_month(2024, 11, 1, 0), 3); assert_eq!(nth_weekday_of_month(2024, 10, 5, 0), 27); assert_eq!(nth_weekday_of_month(2024, 2, 5, 4), 29); let start = DstRule { month: 3, week: 2, weekday: 0, time_seconds: 2 * 3600 };
1287 let end = DstRule { month: 11, week: 1, weekday: 0, time_seconds: 2 * 3600 };
1288 let gen = dst_transitions_for_year(-5 * 3600, -4 * 3600, start, end, 2024);
1289 let (hand, _) = eastern_2024();
1290 assert_eq!(gen.to_vec(), hand, "generated transitions match the hand-built zone");
1291 let tz = gen.to_vec();
1293 let hour = |y, mo, d| to_local(&tz, -5 * 3600, utc(y, mo, d, 12, 0)).hour;
1294 assert_eq!(hour(2024, 7, 1), 8); assert_eq!(hour(2024, 1, 1), 7); }
1297
1298 #[test]
1299 fn nth_weekday_invariants_under_fuzz() {
1300 let mut rng = Rng(0x_4EE7_DA42_C0DE_9001);
1303 for _ in 0..20_000 {
1304 let year = 1900 + (rng.next() % 300) as i64;
1305 let month = (rng.next() % 12) as u32 + 1;
1306 let weekday = (rng.next() % 7) as u32;
1307 let week = (rng.next() % 5) as u32 + 1; let day = nth_weekday_of_month(year, month, week, weekday);
1309 assert!((1..=last_day_of_month(year, month)).contains(&day), "valid day {year}-{month} w{week}");
1310 assert_eq!(weekday_from_days(days_from_civil(year, month, day)), weekday, "weekday matches");
1311 if week < 5 {
1314 assert_eq!((day - 1) / 7 + 1, week, "in the requested week band");
1315 } else {
1316 assert!(day + 7 > last_day_of_month(year, month), "week 5 is the last occurrence");
1317 }
1318 }
1319 }
1320
1321 #[test]
1322 fn format_zoned_shows_local_time_with_offset() {
1323 let u = |y, mo, d, h| {
1324 unix_nanos_from_civil(CivilDateTime { year: y, month: mo, day: d, hour: h, minute: 0, second: 0, nanosecond: 0 })
1325 };
1326 assert_eq!(format_zoned(u(2024, 7, 1, 12), "America/New_York").as_deref(), Some("2024-07-01T08:00:00-04:00"));
1328 assert_eq!(format_zoned(u(2024, 1, 1, 12), "America/New_York").as_deref(), Some("2024-01-01T07:00:00-05:00"));
1329 assert_eq!(format_zoned(u(2024, 1, 1, 12), "UTC").as_deref(), Some("2024-01-01T12:00:00+00:00"));
1331 assert_eq!(format_zoned(u(2024, 1, 1, 0), "Asia/Kolkata").as_deref(), Some("2024-01-01T05:30:00+05:30"));
1332 assert_eq!(format_zoned(0, "Mars/Base"), None);
1334 }
1335
1336 #[test]
1337 fn named_zone_registry_localizes_with_dst() {
1338 let h = |z: &ZoneSpec, y, mo, d, hh| z.to_local(utc(y, mo, d, hh, 0)).hour;
1339 let ny = zone_by_name("America/New_York").unwrap();
1341 assert_eq!(h(&ny, 2024, 7, 1, 12), 8); assert_eq!(h(&ny, 2024, 1, 1, 12), 7); let u = zone_by_name("UTC").unwrap();
1345 assert!(u.dst.is_none());
1346 assert_eq!(h(&u, 2024, 7, 1, 12), 12);
1347 let lon = zone_by_name("Europe/London").unwrap();
1349 assert_eq!(h(&lon, 2024, 7, 1, 12), 13); assert_eq!(h(&lon, 2024, 1, 1, 12), 12); let ist = zone_by_name("Asia/Kolkata").unwrap();
1353 let c = ist.to_local(utc(2024, 1, 1, 0, 0));
1354 assert_eq!((c.hour, c.minute), (5, 30));
1355 let syd = zone_by_name("Australia/Sydney").unwrap();
1357 assert_eq!(h(&syd, 2024, 1, 1, 0), 11); assert_eq!(h(&syd, 2024, 7, 1, 0), 10); assert!(zone_by_name("Mars/Olympus_Mons").is_none());
1361 }
1362
1363 #[test]
1364 fn timezone_round_trips_unambiguous_locals_under_fuzz() {
1365 let (tz, base) = eastern_2024();
1368 let mut rng = Rng(0x_72ED_C0DE_8888_4321);
1369 let year_start = utc(2024, 1, 1, 0, 0);
1370 for _ in 0..20_000 {
1371 let ns = year_start + (rng.next() % (300 * NANOS_PER_DAY as u64)) as i64;
1372 let local = to_local(&tz, base, ns);
1373 let back_e = from_local(&tz, base, local, Fold::Earlier);
1375 let back_l = from_local(&tz, base, local, Fold::Later);
1376 assert!(
1377 back_e == ns || back_l == ns,
1378 "instant recovered from its local time (ns={ns}, e={back_e}, l={back_l})"
1379 );
1380 }
1381 }
1382
1383 #[test]
1384 fn rfc3339_format_and_parse_round_trip() {
1385 let at = |y, mo, d, h, mi, s, n| CivilDateTime {
1386 year: y, month: mo, day: d, hour: h, minute: mi, second: s, nanosecond: n,
1387 };
1388 assert_eq!(format_rfc3339(0), "1970-01-01T00:00:00Z");
1390 assert_eq!(parse_rfc3339("1970-01-01T00:00:00Z"), Some(0));
1391 let ns = unix_nanos_from_civil(at(2024, 3, 10, 7, 30, 0, 123_456_789));
1393 assert_eq!(format_rfc3339(ns), "2024-03-10T07:30:00.123456789Z");
1394 assert_eq!(parse_rfc3339("2024-03-10T07:30:00.123456789Z"), Some(ns));
1395 let half = unix_nanos_from_civil(at(2000, 1, 1, 0, 0, 0, 500_000_000));
1397 assert_eq!(format_rfc3339(half), "2000-01-01T00:00:00.5Z");
1398 assert_eq!(parse_rfc3339("2000-01-01T00:00:00.5Z"), Some(half));
1399 let utc_730 = unix_nanos_from_civil(at(2024, 3, 10, 7, 30, 0, 0));
1401 assert_eq!(parse_rfc3339("2024-03-10T03:30:00-04:00"), Some(utc_730));
1402 assert_eq!(parse_rfc3339("2024-03-10T13:00:00+05:30"), Some(utc_730));
1403 assert_eq!(format_rfc3339(-NANOS_PER_SECOND), "1969-12-31T23:59:59Z");
1405 assert_eq!(parse_rfc3339("1969-12-31T23:59:59Z"), Some(-NANOS_PER_SECOND));
1406 assert_eq!(parse_rfc3339("not a date"), None);
1408 assert_eq!(parse_rfc3339("2024-13-01T00:00:00Z"), None); assert_eq!(parse_rfc3339("2024-02-30T00:00:00Z"), None); assert_eq!(parse_rfc3339("2024-03-10 07:30:00Z"), None); assert_eq!(parse_rfc3339("2024-03-10T07:30:00"), None); }
1413
1414 #[test]
1415 fn duration_format_and_parse() {
1416 let s = NANOS_PER_SECOND;
1417 assert_eq!(format_duration(0), "0s");
1419 assert_eq!(format_duration(90 * 60 * s), "1h30m");
1420 assert_eq!(format_duration(2 * 86_400 * s), "2d");
1421 assert_eq!(format_duration(s + s / 2), "1.5s");
1422 assert_eq!(format_duration(s / 2), "500ms");
1423 assert_eq!(format_duration(-(90 * 60 * s)), "-1h30m");
1424 assert_eq!(format_duration(86_400 * s + 2 * 3600 * s + 3 * 60 * s + 4 * s), "1d2h3m4s");
1425 assert_eq!(format_duration(1500), "1.5us");
1426 assert_eq!(format_duration(42), "42ns");
1427 assert_eq!(parse_duration("1h30m"), Some(90 * 60 * s));
1429 assert_eq!(parse_duration("90m"), Some(90 * 60 * s)); assert_eq!(parse_duration("1.5s"), Some(s + s / 2));
1431 assert_eq!(parse_duration("500ms"), Some(s / 2));
1432 assert_eq!(parse_duration("-1h"), Some(-(3600 * s)));
1433 assert_eq!(parse_duration("2d"), Some(2 * 86_400 * s));
1434 assert_eq!(parse_duration("1d2h3m4s"), Some(86_400 * s + 2 * 3600 * s + 3 * 60 * s + 4 * s));
1435 assert_eq!(parse_duration(".5s"), Some(s / 2)); assert_eq!(parse_duration("1µs"), Some(1_000)); assert_eq!(parse_duration("250us"), Some(250_000));
1438 assert_eq!(parse_duration(""), None);
1440 assert_eq!(parse_duration("abc"), None);
1441 assert_eq!(parse_duration("1x"), None); assert_eq!(parse_duration("h"), None); }
1444
1445 #[test]
1446 fn duration_round_trips_under_fuzz() {
1447 let mut rng = Rng(0x_D012_A710_C0DE_4444);
1449 for _ in 0..20_000 {
1450 let ns = (rng.next() as i64).wrapping_rem(9_000_000_000_000_000_000);
1451 assert_eq!(parse_duration(&format_duration(ns)), Some(ns), "round-trip {ns}");
1452 }
1453 }
1454
1455 #[test]
1456 fn rfc3339_round_trips_under_fuzz() {
1457 let mut rng = Rng(0x_3339_C0DE_ABCD_1234);
1459 for _ in 0..20_000 {
1460 let ns = (rng.next() as i64).wrapping_rem(6_000_000_000_000_000_000);
1461 let s = format_rfc3339(ns);
1462 assert_eq!(parse_rfc3339(&s), Some(ns), "round-trip {s}");
1463 }
1464 }
1465
1466 #[test]
1467 fn sync_clock_merge_is_a_crdt_join() {
1468 use std::collections::BTreeMap;
1469 let mk = |hp: i64, node: u64, l: i64, u: i64| {
1470 let mut k = BTreeMap::new();
1471 k.insert(node, EstimateInterval::new(l, u));
1472 SyncClock { hlc: Hlc { physical_nanos: hp, logical: 0 }, knowledge: k }
1473 };
1474 let a = mk(100, 1, 10, 50);
1475 let b = mk(200, 1, 20, 40);
1476 let c = mk(150, 2, 5, 9);
1477 assert_eq!(a.merge(&a), a, "idempotent");
1479 assert_eq!(a.merge(&b), b.merge(&a), "commutative");
1480 assert_eq!(a.merge(&b).merge(&c), a.merge(&b.merge(&c)), "associative");
1481 let m = a.merge(&b);
1483 assert_eq!(m.hlc, Hlc { physical_nanos: 200, logical: 0 });
1484 assert_eq!(m.knowledge[&1], EstimateInterval::new(20, 40)); let m2 = a.merge(&c);
1486 assert_eq!(m2.knowledge[&1], EstimateInterval::new(10, 50));
1487 assert_eq!(m2.knowledge[&2], EstimateInterval::new(5, 9));
1488 }
1489
1490 #[test]
1491 fn sync_clock_converges_regardless_of_order_and_intervals_only_tighten() {
1492 use std::collections::BTreeMap;
1493 let make = |hp: i64, node: u64, l: i64, u: i64| {
1494 let mut k = BTreeMap::new();
1495 k.insert(node, EstimateInterval::new(l, u));
1496 SyncClock { hlc: Hlc { physical_nanos: hp, logical: 0 }, knowledge: k }
1497 };
1498 let a = make(100, 1, 0, 100);
1499 let b = make(300, 1, 40, 80);
1500 assert_eq!(a.merge(&b), b.merge(&a));
1502 let merged_iv = a.merge(&b).knowledge[&1];
1504 assert!(merged_iv.width() <= a.knowledge[&1].width());
1505 assert!(merged_iv.width() <= b.knowledge[&1].width());
1506 let iv = EstimateInterval::new(10, 90);
1508 assert_eq!(iv.intersect(&iv), iv);
1509 assert_eq!(iv.intersect(&EstimateInterval::new(30, 60)), EstimateInterval::new(30, 60));
1510 }
1511
1512 #[test]
1513 fn sync_clock_tick_observe_and_light_cone_horizon() {
1514 let c = SyncClock::new();
1515 let c2 = c.tick(500);
1516 assert!(c2.hlc > c.hlc, "tick advances causally");
1517 let c3 = c2.observe(7, EstimateInterval::new(100, 200));
1519 assert_eq!(c3.knowledge[&7], EstimateInterval::new(100, 200));
1520 let c4 = c3.observe(7, EstimateInterval::new(150, 250));
1522 assert_eq!(c4.knowledge[&7], EstimateInterval::new(150, 200));
1523 assert_eq!(knowable_horizon(1_000_000_000, 200_000_000), 800_000_000);
1525 }
1526
1527 #[test]
1528 fn hybrid_logical_clock_tick_and_receive() {
1529 let hlc = |p: i64, l: u32| Hlc { physical_nanos: p, logical: l };
1530 assert_eq!(Hlc::ZERO.tick(100), hlc(100, 0));
1532 assert_eq!(hlc(100, 0).tick(100), hlc(100, 1)); assert_eq!(hlc(100, 1).tick(200), hlc(200, 0)); assert_eq!(hlc(200, 0).tick(150), hlc(200, 1));
1536 assert_eq!(hlc(100, 5).recv(hlc(100, 3), 100), hlc(100, 6));
1538 assert_eq!(hlc(100, 5).recv(hlc(200, 2), 100), hlc(200, 3));
1540 assert_eq!(hlc(200, 7).recv(hlc(100, 9), 150), hlc(200, 8));
1542 assert_eq!(hlc(100, 5).recv(hlc(50, 9), 300), hlc(300, 0));
1544 assert!(hlc(100, 0) < hlc(100, 1) && hlc(100, 1) < hlc(200, 0));
1546 }
1547
1548 #[test]
1549 fn hlc_preserves_causality_under_fuzz() {
1550 let mut rng = Rng(0x_C0DE_C0FF_EE17_AAAA);
1553 let mut local = Hlc::ZERO;
1554 let mut remote = Hlc::ZERO;
1555 for _ in 0..20_000 {
1556 let now = (rng.next() % 1_000_000) as i64;
1557 if rng.next() & 1 == 0 {
1558 let next = local.tick(now);
1559 assert!(next > local, "tick advances");
1560 local = next;
1561 } else {
1562 let before = local;
1563 let next = local.recv(remote, now);
1564 assert!(next > before && next > remote, "recv dominates both inputs");
1565 local = next;
1566 remote = remote.tick((rng.next() % 1_000_000) as i64); }
1568 }
1569 }
1570
1571 #[test]
1572 fn leap_second_tai_utc_offsets() {
1573 let at = |y, mo, d| days_from_civil(y, mo, d) * SECONDS_PER_DAY;
1574 assert_eq!(tai_minus_utc(at(2017, 1, 1)), 37); assert_eq!(tai_minus_utc(at(2024, 6, 1)), 37); assert_eq!(tai_minus_utc(at(1972, 1, 1)), 10); assert_eq!(tai_minus_utc(at(2000, 1, 1)), 32);
1579 assert_eq!(tai_minus_utc(at(1999, 1, 1)), 32); assert_eq!(tai_minus_utc(at(1998, 12, 31)), 31); assert_eq!(tai_minus_utc(at(1970, 1, 1)), 10); assert_eq!(unix_to_tai_seconds(at(2017, 1, 1)), at(2017, 1, 1) + 37);
1584 assert_eq!(unix_to_tai_seconds(at(1972, 1, 1)), at(1972, 1, 1) + 10);
1585 let t = at(2020, 6, 15) + 12_345;
1587 assert_eq!(tai_to_unix_seconds(unix_to_tai_seconds(t)), t);
1588 assert_eq!(TT_MINUS_TAI_NANOS, 32_184_000_000);
1590 }
1591
1592 #[test]
1593 fn tt_scale_is_exact() {
1594 let at_ns = |y, mo, d| days_from_civil(y, mo, d) * NANOS_PER_DAY;
1595 let u = at_ns(2017, 1, 1);
1596 assert_eq!(tai_nanos_from_unix_nanos(u), u + 37 * NANOS_PER_SECOND);
1598 assert_eq!(tt_nanos_from_unix_nanos(u), u + 37 * NANOS_PER_SECOND + TT_MINUS_TAI_NANOS);
1599 let u72 = at_ns(1972, 1, 1);
1601 assert_eq!(tt_nanos_from_unix_nanos(u72), u72 + 10 * NANOS_PER_SECOND + TT_MINUS_TAI_NANOS);
1602 let t = at_ns(2020, 3, 15) + 12_345_678_900;
1604 assert_eq!(unix_nanos_from_tt_nanos(tt_nanos_from_unix_nanos(t)), t);
1605 }
1606
1607 #[test]
1608 fn tt_round_trips_under_fuzz() {
1609 let mut rng = Rng(0x_77_C0DE_5CA1_9999);
1610 let base = days_from_civil(1972, 6, 1) * NANOS_PER_DAY;
1611 for _ in 0..20_000 {
1612 let ns = base + (rng.next() % 1_600_000_000_000_000_000) as i64;
1613 assert_eq!(unix_nanos_from_tt_nanos(tt_nanos_from_unix_nanos(ns)), ns, "TT round-trip at {ns}");
1614 }
1615 }
1616
1617 #[test]
1618 fn tai_round_trips_under_fuzz() {
1619 let mut rng = Rng(0x_7A1_C0DE_5EC0_4242);
1622 let base = days_from_civil(1972, 6, 1) * SECONDS_PER_DAY;
1623 for _ in 0..20_000 {
1624 let secs = base + (rng.next() % 1_700_000_000) as i64; assert_eq!(tai_to_unix_seconds(unix_to_tai_seconds(secs)), secs, "TAI round-trip at {secs}");
1626 }
1627 }
1628
1629 #[test]
1630 fn smooth_utc_round_trips_over_a_wide_range_under_fuzz() {
1631 let mut rng = Rng(0x_5007_8807_C0DE_1357);
1634 for _ in 0..50_000 {
1635 let ns = (rng.next() as i64).wrapping_rem(6_000_000_000_000_000_000);
1637 let c = civil_from_unix_nanos(ns);
1638 assert!((1..=12).contains(&c.month));
1639 assert!((1..=last_day_of_month(c.year, c.month)).contains(&c.day));
1640 assert!(c.hour < 24 && c.minute < 60 && c.second < 60 && c.nanosecond < 1_000_000_000);
1641 assert_eq!(unix_nanos_from_civil(c), ns, "instant round-trip at ns={ns}");
1642 }
1643 }
1644
1645 #[test]
1646 fn iso_week_round_trips_over_a_wide_range_under_fuzz() {
1647 let mut rng = Rng(0x_C0DE_FACE_8601_9999);
1650 for _ in 0..50_000 {
1651 let z = (rng.next() % 800_001) as i64 - 400_000;
1652 let (iy, w, dow) = iso_week_from_days(z);
1653 assert!((1..=53).contains(&w), "ISO week in range at z={z}: {w}");
1654 assert!((1..=7).contains(&dow), "ISO weekday in range at z={z}: {dow}");
1655 let plain = weekday_from_days(z);
1657 assert_eq!(dow % 7, plain, "ISO/plain weekday agree at z={z}");
1658 assert_eq!(days_from_iso_week(iy, w, dow), z, "ISO round-trip at z={z}");
1660 }
1661 }
1662
1663 #[test]
1664 fn julian_round_trips_over_a_wide_range_under_fuzz() {
1665 let mut rng = Rng(0x_1234_ABCD_5678_EF90);
1668 for _ in 0..50_000 {
1669 let z = (rng.next() % 800_001) as i64 - 400_000;
1670 let (y, m, d) = julian_from_days(z);
1671 assert!((1..=12).contains(&m), "julian month in range at z={z}");
1672 assert!((1..=31).contains(&d), "julian day in range at z={z}");
1673 assert_eq!(days_from_julian(y, m, d), z, "julian round-trip at z={z}");
1674 }
1675 }
1676}