1use std::{
51 cmp::Ordering,
52 fmt::Display,
53 ops::{Add, AddAssign, Deref, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
54 str::FromStr,
55 time::{Duration, SystemTime},
56};
57
58use jiff::{SignedDuration, Timestamp, civil::Date, tz::Offset};
59use serde::{
60 Deserialize, Deserializer, Serialize,
61 de::{self, Visitor},
62};
63use thiserror::Error;
64
65use crate::datetime::{
66 NANOSECONDS_IN_DAY, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
67 NANOSECONDS_IN_MINUTE, NANOSECONDS_IN_SECOND, SECONDS_IN_HOUR, U64_UPPER_BOUND_F64,
68};
69
70#[repr(transparent)]
72#[derive(
73 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
74)]
75#[serde(transparent)]
76pub struct DurationNanos(u64);
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
80#[error("duration {value} {unit} exceeds the nanosecond range")]
81pub struct DurationNanosOutOfRangeError {
82 value: u64,
83 unit: &'static str,
84}
85
86impl DurationNanos {
87 pub const ZERO: Self = Self(0);
89
90 pub const MAX: Self = Self(u64::MAX);
92
93 #[must_use]
95 pub const fn new(nanos: u64) -> Self {
96 Self(nanos)
97 }
98
99 #[must_use]
105 pub const fn from_micros(micros: u64) -> Self {
106 match Self::try_from_micros(micros) {
107 Ok(duration) => duration,
108 Err(_) => panic!("DurationNanos overflow in from_micros"),
109 }
110 }
111
112 pub const fn try_from_micros(micros: u64) -> Result<Self, DurationNanosOutOfRangeError> {
118 Self::try_from_units(micros, NANOSECONDS_IN_MICROSECOND, "microseconds")
119 }
120
121 #[must_use]
127 pub const fn from_millis(millis: u64) -> Self {
128 match Self::try_from_millis(millis) {
129 Ok(duration) => duration,
130 Err(_) => panic!("DurationNanos overflow in from_millis"),
131 }
132 }
133
134 pub const fn try_from_millis(millis: u64) -> Result<Self, DurationNanosOutOfRangeError> {
140 Self::try_from_units(millis, NANOSECONDS_IN_MILLISECOND, "milliseconds")
141 }
142
143 #[must_use]
149 pub const fn from_secs(secs: u64) -> Self {
150 match Self::try_from_secs(secs) {
151 Ok(duration) => duration,
152 Err(_) => panic!("DurationNanos overflow in from_secs"),
153 }
154 }
155
156 pub const fn try_from_secs(secs: u64) -> Result<Self, DurationNanosOutOfRangeError> {
162 Self::try_from_units(secs, NANOSECONDS_IN_SECOND, "seconds")
163 }
164
165 #[must_use]
171 pub const fn from_mins(mins: u64) -> Self {
172 match Self::try_from_mins(mins) {
173 Ok(duration) => duration,
174 Err(_) => panic!("DurationNanos overflow in from_mins"),
175 }
176 }
177
178 pub const fn try_from_mins(mins: u64) -> Result<Self, DurationNanosOutOfRangeError> {
184 Self::try_from_units(mins, NANOSECONDS_IN_MINUTE, "minutes")
185 }
186
187 #[must_use]
193 pub const fn from_hours(hours: u64) -> Self {
194 match Self::try_from_hours(hours) {
195 Ok(duration) => duration,
196 Err(_) => panic!("DurationNanos overflow in from_hours"),
197 }
198 }
199
200 pub const fn try_from_hours(hours: u64) -> Result<Self, DurationNanosOutOfRangeError> {
206 Self::try_from_units(hours, SECONDS_IN_HOUR * NANOSECONDS_IN_SECOND, "hours")
207 }
208
209 #[must_use]
215 pub const fn from_days(days: u64) -> Self {
216 match Self::try_from_days(days) {
217 Ok(duration) => duration,
218 Err(_) => panic!("DurationNanos overflow in from_days"),
219 }
220 }
221
222 pub const fn try_from_days(days: u64) -> Result<Self, DurationNanosOutOfRangeError> {
228 Self::try_from_units(days, NANOSECONDS_IN_DAY, "days")
229 }
230
231 const fn try_from_units(
232 value: u64,
233 nanos_per_unit: u64,
234 unit: &'static str,
235 ) -> Result<Self, DurationNanosOutOfRangeError> {
236 match value.checked_mul(nanos_per_unit) {
237 Some(nanos) => Ok(Self(nanos)),
238 None => Err(DurationNanosOutOfRangeError { value, unit }),
239 }
240 }
241
242 #[must_use]
244 pub const fn is_zero(&self) -> bool {
245 self.0 == 0
246 }
247
248 #[must_use]
250 pub const fn as_u64(&self) -> u64 {
251 self.0
252 }
253
254 #[must_use]
256 pub const fn as_micros(&self) -> u64 {
257 self.0 / NANOSECONDS_IN_MICROSECOND
258 }
259
260 #[must_use]
262 pub const fn as_millis(&self) -> u64 {
263 self.0 / NANOSECONDS_IN_MILLISECOND
264 }
265
266 #[must_use]
268 pub const fn as_secs(&self) -> u64 {
269 self.0 / NANOSECONDS_IN_SECOND
270 }
271
272 #[must_use]
274 #[expect(
275 clippy::cast_precision_loss,
276 reason = "subnanosecond precision is unavailable and large durations may lose precision"
277 )]
278 pub const fn as_secs_f64(&self) -> f64 {
279 self.as_secs() as f64 + self.subsec_nanos() as f64 / NANOSECONDS_IN_SECOND as f64
280 }
281
282 #[must_use]
284 pub const fn as_mins(&self) -> u64 {
285 self.0 / NANOSECONDS_IN_MINUTE
286 }
287
288 #[must_use]
290 pub const fn as_hours(&self) -> u64 {
291 self.0 / (SECONDS_IN_HOUR * NANOSECONDS_IN_SECOND)
292 }
293
294 #[must_use]
296 pub const fn as_days(&self) -> u64 {
297 self.0 / NANOSECONDS_IN_DAY
298 }
299
300 #[must_use]
302 pub const fn subsec_millis(&self) -> u64 {
303 (self.0 % NANOSECONDS_IN_SECOND) / NANOSECONDS_IN_MILLISECOND
304 }
305
306 #[must_use]
308 pub const fn subsec_micros(&self) -> u64 {
309 (self.0 % NANOSECONDS_IN_SECOND) / NANOSECONDS_IN_MICROSECOND
310 }
311
312 #[must_use]
314 pub const fn subsec_nanos(&self) -> u64 {
315 self.0 % NANOSECONDS_IN_SECOND
316 }
317
318 #[must_use]
320 pub const fn checked_add(self, rhs: Self) -> Option<Self> {
321 match self.0.checked_add(rhs.0) {
322 Some(value) => Some(Self(value)),
323 None => None,
324 }
325 }
326
327 #[must_use]
329 pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
330 match self.0.checked_sub(rhs.0) {
331 Some(value) => Some(Self(value)),
332 None => None,
333 }
334 }
335
336 #[must_use]
338 pub const fn saturating_add(self, rhs: Self) -> Self {
339 Self(self.0.saturating_add(rhs.0))
340 }
341
342 #[must_use]
344 pub const fn saturating_sub(self, rhs: Self) -> Self {
345 Self(self.0.saturating_sub(rhs.0))
346 }
347
348 #[must_use]
350 pub const fn checked_mul(self, rhs: u64) -> Option<Self> {
351 match self.0.checked_mul(rhs) {
352 Some(value) => Some(Self(value)),
353 None => None,
354 }
355 }
356
357 #[must_use]
359 pub const fn saturating_mul(self, rhs: u64) -> Self {
360 Self(self.0.saturating_mul(rhs))
361 }
362
363 #[must_use]
365 pub const fn checked_div(self, rhs: u64) -> Option<Self> {
366 match self.0.checked_div(rhs) {
367 Some(value) => Some(Self(value)),
368 None => None,
369 }
370 }
371}
372
373#[repr(C)]
375#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
376pub struct UnixNanos(u64);
377
378impl UnixNanos {
379 #[must_use]
381 pub const fn new(value: u64) -> Self {
382 Self(value)
383 }
384
385 #[must_use]
387 pub const fn max() -> Self {
388 Self(u64::MAX)
389 }
390
391 #[must_use]
393 pub const fn is_zero(&self) -> bool {
394 self.0 == 0
395 }
396
397 #[must_use]
399 pub const fn as_u64(&self) -> u64 {
400 self.0
401 }
402
403 #[must_use]
405 pub const fn as_seconds(&self) -> u64 {
406 self.0 / NANOSECONDS_IN_SECOND
407 }
408
409 #[must_use]
411 pub const fn as_millis(&self) -> u64 {
412 self.0 / NANOSECONDS_IN_MILLISECOND
413 }
414
415 #[must_use]
417 pub const fn as_micros(&self) -> u64 {
418 self.0 / NANOSECONDS_IN_MICROSECOND
419 }
420
421 #[must_use]
427 pub const fn from_seconds(seconds: u64) -> Self {
428 match seconds.checked_mul(NANOSECONDS_IN_SECOND) {
429 Some(nanos) => Self(nanos),
430 None => panic!("UnixNanos overflow in from_seconds"),
431 }
432 }
433
434 #[must_use]
440 pub const fn from_millis(millis: u64) -> Self {
441 match millis.checked_mul(NANOSECONDS_IN_MILLISECOND) {
442 Some(nanos) => Self(nanos),
443 None => panic!("UnixNanos overflow in from_millis"),
444 }
445 }
446
447 #[must_use]
451 pub const fn from_millis_checked(millis: i64) -> Option<Self> {
452 Self::from_units_checked(millis, NANOSECONDS_IN_MILLISECOND)
453 }
454
455 #[must_use]
461 pub const fn from_micros(micros: u64) -> Self {
462 match micros.checked_mul(NANOSECONDS_IN_MICROSECOND) {
463 Some(nanos) => Self(nanos),
464 None => panic!("UnixNanos overflow in from_micros"),
465 }
466 }
467
468 #[must_use]
472 pub const fn from_micros_checked(micros: i64) -> Option<Self> {
473 Self::from_units_checked(micros, NANOSECONDS_IN_MICROSECOND)
474 }
475
476 const fn from_units_checked(value: i64, nanos_per_unit: u64) -> Option<Self> {
477 if value < 0 {
478 return None;
479 }
480
481 match value.cast_unsigned().checked_mul(nanos_per_unit) {
482 Some(nanos) => Some(Self(nanos)),
483 None => None,
484 }
485 }
486
487 #[must_use]
493 pub const fn as_i64(&self) -> i64 {
494 assert!(
495 self.0 <= i64::MAX.cast_unsigned(),
496 "UnixNanos value exceeds i64::MAX"
497 );
498 self.0.cast_signed()
499 }
500
501 #[must_use]
503 #[expect(
504 clippy::cast_precision_loss,
505 reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
506 )]
507 pub const fn as_f64(&self) -> f64 {
508 self.0 as f64
509 }
510
511 #[must_use]
517 pub fn to_datetime_utc(&self) -> Timestamp {
518 Timestamp::from_nanosecond(i128::from(self.0))
519 .expect("UnixNanos is within Jiff's timestamp range")
520 }
521
522 #[must_use]
524 pub fn to_rfc3339(&self) -> String {
525 let datetime = self.to_datetime_utc();
526 let display = datetime.display_with_offset(Offset::UTC);
527
528 match datetime.subsec_nanosecond() {
529 0 => format!("{display:.0}"),
530 nanos if nanos % 1_000_000 == 0 => format!("{display:.3}"),
531 nanos if nanos % 1_000 == 0 => format!("{display:.6}"),
532 _ => format!("{display:.9}"),
533 }
534 }
535
536 #[must_use]
541 pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
542 match self.0.checked_sub(other.0) {
543 Some(duration) => Some(DurationNanos(duration)),
544 None => None,
545 }
546 }
547
548 #[must_use]
550 pub const fn saturating_duration_since(&self, earlier: Self) -> DurationNanos {
551 DurationNanos(self.0.saturating_sub(earlier.0))
552 }
553
554 #[must_use]
560 pub const fn floor(self, interval: DurationNanos) -> Self {
561 assert!(
562 !interval.is_zero(),
563 "cannot floor UnixNanos to a zero interval"
564 );
565 Self(self.0 - self.0 % interval.0)
566 }
567
568 fn parse_string(s: &str) -> Result<Self, String> {
569 if (16..=19).contains(&s.len())
571 && let Some(value) = parse_ascii_digits(s.as_bytes())
572 {
573 return Ok(Self(value));
574 }
575
576 if let Ok(int_value) = s.parse::<u64>() {
577 return Ok(Self(int_value));
578 }
579
580 if s.chars().all(|c| c.is_ascii_digit()) {
586 return Err("Unix timestamp is out of range".into());
587 }
588
589 if let Ok(float_value) = s.parse::<f64>() {
591 return f64_seconds_to_nanos(float_value).map(Self);
592 }
593
594 let is_compatible_rfc3339 = matches!(s.as_bytes().get(10), Some(b'T' | b't' | b' '))
597 && !s.as_bytes().contains(&b'[');
598 if is_compatible_rfc3339 && let Ok(datetime) = s.parse::<Timestamp>() {
599 let nanos = datetime.as_nanosecond();
600 let nanos = u64::try_from(nanos)
601 .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
602 return Ok(Self(nanos));
603 }
604
605 if let Ok(date) = Date::strptime("%Y-%m-%d", s) {
607 let datetime = date
608 .at(0, 0, 0, 0)
609 .to_zoned(jiff::tz::TimeZone::UTC)
610 .map_err(|e| e.to_string())?;
611 let nanos = datetime.timestamp().as_nanosecond();
612 let nanos = u64::try_from(nanos)
613 .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
614 return Ok(Self(nanos));
615 }
616
617 Err(format!("Invalid format: {s}"))
618 }
619
620 #[must_use]
622 pub const fn checked_add(self, rhs: DurationNanos) -> Option<Self> {
623 match self.0.checked_add(rhs.0) {
624 Some(value) => Some(Self(value)),
625 None => None,
626 }
627 }
628
629 #[must_use]
631 pub const fn checked_sub(self, rhs: DurationNanos) -> Option<Self> {
632 match self.0.checked_sub(rhs.0) {
633 Some(value) => Some(Self(value)),
634 None => None,
635 }
636 }
637
638 #[must_use]
640 pub const fn saturating_add(self, rhs: DurationNanos) -> Self {
641 Self(self.0.saturating_add(rhs.0))
642 }
643
644 #[must_use]
646 pub const fn saturating_sub(self, rhs: DurationNanos) -> Self {
647 Self(self.0.saturating_sub(rhs.0))
648 }
649}
650
651#[expect(
654 clippy::cast_possible_truncation,
655 clippy::cast_sign_loss,
656 reason = "value is checked finite, non-negative, and within u64 range before the cast"
657)]
658fn f64_seconds_to_nanos(value: f64) -> Result<u64, String> {
659 if !value.is_finite() {
660 return Err(format!("Unix timestamp must be finite, was {value}"));
661 }
662
663 if value < 0.0 {
664 return Err("Unix timestamp cannot be negative".to_string());
665 }
666
667 let nanos_f64 = value * 1_000_000_000.0;
671
672 if nanos_f64 >= U64_UPPER_BOUND_F64 {
673 return Err(format!("Unix timestamp {value} seconds is out of range"));
674 }
675
676 Ok(nanos_f64.trunc() as u64)
677}
678
679impl From<DurationNanos> for Duration {
680 fn from(value: DurationNanos) -> Self {
681 Self::from_nanos(value.0)
682 }
683}
684
685impl From<DurationNanos> for SignedDuration {
686 fn from(value: DurationNanos) -> Self {
687 Self::from_nanos_i128(i128::from(value.0))
688 }
689}
690
691impl TryFrom<SignedDuration> for DurationNanos {
692 type Error = std::num::TryFromIntError;
693
694 fn try_from(value: SignedDuration) -> Result<Self, Self::Error> {
695 u64::try_from(value.as_nanos()).map(Self)
696 }
697}
698
699impl TryFrom<Duration> for DurationNanos {
700 type Error = std::num::TryFromIntError;
701
702 fn try_from(value: Duration) -> Result<Self, Self::Error> {
703 u64::try_from(value.as_nanos()).map(Self)
704 }
705}
706
707impl Add for DurationNanos {
715 type Output = Self;
716
717 fn add(self, rhs: Self) -> Self::Output {
718 self.checked_add(rhs)
719 .expect("DurationNanos overflow in addition")
720 }
721}
722
723impl Sub for DurationNanos {
730 type Output = Self;
731
732 fn sub(self, rhs: Self) -> Self::Output {
733 self.checked_sub(rhs)
734 .expect("DurationNanos underflow in subtraction")
735 }
736}
737
738impl AddAssign for DurationNanos {
744 fn add_assign(&mut self, rhs: Self) {
745 *self = *self + rhs;
746 }
747}
748
749impl SubAssign for DurationNanos {
755 fn sub_assign(&mut self, rhs: Self) {
756 *self = *self - rhs;
757 }
758}
759
760impl Mul<u64> for DurationNanos {
768 type Output = Self;
769
770 fn mul(self, rhs: u64) -> Self::Output {
771 self.checked_mul(rhs)
772 .expect("DurationNanos overflow in multiplication")
773 }
774}
775
776impl MulAssign<u64> for DurationNanos {
782 fn mul_assign(&mut self, rhs: u64) {
783 *self = *self * rhs;
784 }
785}
786
787impl Div<u64> for DurationNanos {
793 type Output = Self;
794
795 fn div(self, rhs: u64) -> Self::Output {
796 self.checked_div(rhs)
797 .expect("DurationNanos division by zero")
798 }
799}
800
801impl DivAssign<u64> for DurationNanos {
807 fn div_assign(&mut self, rhs: u64) {
808 *self = *self / rhs;
809 }
810}
811
812impl Display for DurationNanos {
813 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
814 write!(f, "{}", self.0)
815 }
816}
817
818impl Deref for UnixNanos {
819 type Target = u64;
820
821 fn deref(&self) -> &Self::Target {
822 &self.0
823 }
824}
825
826impl PartialEq<u64> for UnixNanos {
827 fn eq(&self, other: &u64) -> bool {
828 self.0 == *other
829 }
830}
831
832impl PartialOrd<u64> for UnixNanos {
833 fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
834 self.0.partial_cmp(other)
835 }
836}
837
838impl PartialEq<Option<u64>> for UnixNanos {
839 fn eq(&self, other: &Option<u64>) -> bool {
840 match other {
841 Some(value) => self.0 == *value,
842 None => false,
843 }
844 }
845}
846
847impl PartialOrd<Option<u64>> for UnixNanos {
848 fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
849 match other {
850 Some(value) => self.0.partial_cmp(value),
851 None => Some(Ordering::Greater),
852 }
853 }
854}
855
856impl PartialEq<UnixNanos> for u64 {
857 fn eq(&self, other: &UnixNanos) -> bool {
858 *self == other.0
859 }
860}
861
862impl PartialOrd<UnixNanos> for u64 {
863 fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
864 self.partial_cmp(&other.0)
865 }
866}
867
868impl From<u64> for UnixNanos {
869 fn from(value: u64) -> Self {
870 Self(value)
871 }
872}
873
874impl From<UnixNanos> for u64 {
875 fn from(value: UnixNanos) -> Self {
876 value.0
877 }
878}
879
880impl From<&str> for UnixNanos {
891 fn from(value: &str) -> Self {
892 value
893 .parse()
894 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
895 }
896}
897
898impl From<String> for UnixNanos {
909 fn from(value: String) -> Self {
910 value
911 .parse()
912 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
913 }
914}
915
916impl From<Timestamp> for UnixNanos {
917 fn from(value: Timestamp) -> Self {
918 let nanos = value.as_nanosecond();
919
920 assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
921
922 Self::from(u64::try_from(nanos).expect("DateTime timestamp out of range for UnixNanos"))
923 }
924}
925
926impl From<SystemTime> for UnixNanos {
927 fn from(value: SystemTime) -> Self {
928 let duration = value
929 .duration_since(std::time::UNIX_EPOCH)
930 .expect("SystemTime before UNIX EPOCH");
931
932 let nanos =
933 u64::try_from(duration.as_nanos()).expect("SystemTime overflowed u64 nanoseconds");
934
935 Self::from(nanos)
936 }
937}
938
939impl FromStr for UnixNanos {
940 type Err = Box<dyn std::error::Error>;
941
942 fn from_str(s: &str) -> Result<Self, Self::Err> {
943 Self::parse_string(s).map_err(std::convert::Into::into)
944 }
945}
946
947impl Sub for UnixNanos {
954 type Output = DurationNanos;
955
956 fn sub(self, rhs: Self) -> Self::Output {
957 self.duration_since(&rhs)
958 .expect("UnixNanos underflow in timestamp subtraction")
959 }
960}
961
962impl Add<DurationNanos> for UnixNanos {
969 type Output = Self;
970
971 fn add(self, rhs: DurationNanos) -> Self::Output {
972 self.checked_add(rhs)
973 .expect("UnixNanos overflow in duration addition")
974 }
975}
976
977impl Sub<DurationNanos> for UnixNanos {
984 type Output = Self;
985
986 fn sub(self, rhs: DurationNanos) -> Self::Output {
987 self.checked_sub(rhs)
988 .expect("UnixNanos underflow in duration subtraction")
989 }
990}
991
992impl AddAssign<DurationNanos> for UnixNanos {
999 fn add_assign(&mut self, rhs: DurationNanos) {
1000 *self = *self + rhs;
1001 }
1002}
1003
1004impl SubAssign<DurationNanos> for UnixNanos {
1011 fn sub_assign(&mut self, rhs: DurationNanos) {
1012 *self = *self - rhs;
1013 }
1014}
1015
1016impl Display for UnixNanos {
1017 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1018 write!(f, "{}", self.0)
1019 }
1020}
1021
1022impl From<UnixNanos> for Timestamp {
1023 fn from(value: UnixNanos) -> Self {
1024 value.to_datetime_utc()
1025 }
1026}
1027
1028impl<'de> Deserialize<'de> for UnixNanos {
1029 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1030 where
1031 D: Deserializer<'de>,
1032 {
1033 struct UnixNanosVisitor;
1034
1035 impl<'de> Visitor<'de> for UnixNanosVisitor {
1036 type Value = UnixNanos;
1037
1038 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1039 formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
1040 }
1041
1042 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
1043 where
1044 E: de::Error,
1045 {
1046 Ok(UnixNanos(value))
1047 }
1048
1049 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
1050 where
1051 E: de::Error,
1052 {
1053 u64::try_from(value)
1054 .map(UnixNanos)
1055 .map_err(|_| E::custom("Unix timestamp cannot be negative"))
1056 }
1057
1058 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1059 where
1060 E: de::Error,
1061 {
1062 f64_seconds_to_nanos(value)
1063 .map(UnixNanos)
1064 .map_err(E::custom)
1065 }
1066
1067 fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
1068 let number =
1069 serde_json::Number::deserialize(de::value::MapAccessDeserializer::new(map))?;
1070
1071 if let Some(value) = number.as_u64() {
1072 self.visit_u64(value)
1073 } else if let Some(value) = number.as_i64() {
1074 self.visit_i64(value)
1075 } else {
1076 let value = number
1077 .as_f64()
1078 .ok_or_else(|| de::Error::custom("Unix timestamp is outside f64 bounds"))?;
1079 self.visit_f64(value)
1080 }
1081 }
1082
1083 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1084 where
1085 E: de::Error,
1086 {
1087 UnixNanos::parse_string(value).map_err(E::custom)
1088 }
1089 }
1090
1091 deserializer.deserialize_any(UnixNanosVisitor)
1092 }
1093}
1094
1095fn parse_ascii_digits(bytes: &[u8]) -> Option<u64> {
1097 const HIGH_BITS: u64 = 0x8080_8080_8080_8080;
1098 const DIGIT_MASK: u64 = 0x00ff_00ff_00ff_00ff;
1099 const PAIR_MASK: u64 = 0x0000_ffff_0000_ffff;
1100 debug_assert!(
1101 bytes.len() <= 19,
1102 "Invariant: digit inputs have at most 19 bytes"
1103 );
1104 let (chunks, tail) = bytes.as_chunks::<8>();
1105 let mut value = 0;
1106
1107 for chunk in chunks {
1108 let word = u64::from_le_bytes(*chunk);
1109 let ascii = word & !HIGH_BITS;
1110 let lower = (ascii | HIGH_BITS) - 0x3030_3030_3030_3030;
1112 let upper = 0xb9b9_b9b9_b9b9_b9b9 - ascii;
1113 if word & HIGH_BITS != 0 || lower & upper & HIGH_BITS != HIGH_BITS {
1114 return None;
1115 }
1116 let digits = word ^ 0x3030_3030_3030_3030;
1117 let pairs = (digits & DIGIT_MASK) * 10 + ((digits >> 8) & DIGIT_MASK);
1118 let quads = (pairs & PAIR_MASK) * 100 + ((pairs >> 16) & PAIR_MASK);
1119 let eight = (quads & 0xffff_ffff) * 10_000 + (quads >> 32);
1120 value = value * 100_000_000 + eight;
1121 }
1122
1123 for &digit in tail {
1124 if !digit.is_ascii_digit() {
1125 return None;
1126 }
1127 value = value * 10 + u64::from(digit - b'0');
1128 }
1129 Some(value)
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use jiff::SignedDuration;
1135 use proptest::prelude::*;
1136 use rstest::rstest;
1137
1138 use super::*;
1139 use crate::approx_eq;
1140
1141 fn timestamp(value: &str) -> Timestamp {
1142 value.parse().unwrap()
1143 }
1144
1145 #[rstest]
1146 fn test_digit_parser_validates_every_byte_position() {
1147 for len in 1..=19 {
1148 for position in 0..len {
1149 for byte in 0..=u8::MAX {
1150 let mut input = vec![b'1'; len];
1151 input[position] = byte;
1152 let expected = std::str::from_utf8(&input)
1153 .ok()
1154 .filter(|text| text.bytes().all(|digit| digit.is_ascii_digit()))
1155 .and_then(|text| text.parse::<u64>().ok());
1156 assert_eq!(parse_ascii_digits(&input), expected);
1157 }
1158 }
1159 }
1160 }
1161
1162 #[rstest]
1163 fn test_duration_nanos_construction_and_conversion() {
1164 let duration = DurationNanos::new(123);
1165 let standard = Duration::from(duration);
1166 let signed = SignedDuration::from(duration);
1167 let signed_max = SignedDuration::from(DurationNanos::MAX);
1168
1169 assert_eq!(duration.as_u64(), 123);
1170 assert_eq!(standard, Duration::from_nanos(123));
1171 assert_eq!(DurationNanos::try_from(standard), Ok(duration));
1172 assert_eq!(signed, SignedDuration::from_nanos(123));
1173 assert_eq!(DurationNanos::try_from(signed), Ok(duration));
1174 assert_eq!(signed_max.as_nanos(), i128::from(u64::MAX));
1175 assert_eq!(DurationNanos::try_from(signed_max), Ok(DurationNanos::MAX));
1176 assert!(DurationNanos::try_from(SignedDuration::from_nanos(-1)).is_err());
1177 }
1178
1179 #[rstest]
1180 fn test_duration_nanos_unit_construction_and_accessors() {
1181 let duration = DurationNanos::from_hours(1)
1182 + DurationNanos::from_mins(2)
1183 + DurationNanos::from_secs(3)
1184 + DurationNanos::new(456_789_123);
1185
1186 assert_eq!(DurationNanos::from_micros(1), DurationNanos::new(1_000));
1187 assert_eq!(DurationNanos::from_millis(1), DurationNanos::new(1_000_000));
1188 assert_eq!(DurationNanos::from_days(1), DurationNanos::from_hours(24));
1189 assert_eq!(duration.as_micros(), 3_723_456_789);
1190 assert_eq!(duration.as_millis(), 3_723_456);
1191 assert_eq!(duration.as_secs(), 3_723);
1192 assert!(approx_eq!(
1193 f64,
1194 duration.as_secs_f64(),
1195 3_723.456_789_123,
1196 epsilon = 1e-12
1197 ));
1198 let expected_max_secs = Duration::from_nanos(u64::MAX).as_secs_f64();
1199 assert!(approx_eq!(
1200 f64,
1201 DurationNanos::MAX.as_secs_f64(),
1202 expected_max_secs,
1203 epsilon = expected_max_secs * f64::EPSILON
1204 ));
1205 assert_eq!(duration.as_mins(), 62);
1206 assert_eq!(duration.as_hours(), 1);
1207 assert_eq!(DurationNanos::from_hours(49).as_days(), 2);
1208 assert_eq!(duration.subsec_millis(), 456);
1209 assert_eq!(duration.subsec_micros(), 456_789);
1210 assert_eq!(duration.subsec_nanos(), 456_789_123);
1211 }
1212
1213 #[rstest]
1214 fn test_duration_nanos_fallible_unit_construction() {
1215 assert_eq!(
1216 DurationNanos::try_from_secs(1),
1217 Ok(DurationNanos::from_secs(1))
1218 );
1219 assert_eq!(
1220 DurationNanos::try_from_secs(u64::MAX)
1221 .unwrap_err()
1222 .to_string(),
1223 "duration 18446744073709551615 seconds exceeds the nanosecond range"
1224 );
1225 assert!(DurationNanos::try_from_micros(u64::MAX).is_err());
1226 assert!(DurationNanos::try_from_millis(u64::MAX).is_err());
1227 assert!(DurationNanos::try_from_secs(u64::MAX).is_err());
1228 assert!(DurationNanos::try_from_mins(u64::MAX).is_err());
1229 assert!(DurationNanos::try_from_hours(u64::MAX).is_err());
1230 assert!(DurationNanos::try_from_days(u64::MAX).is_err());
1231
1232 let max_hours = DurationNanos::MAX.as_hours();
1233 assert_eq!(
1234 DurationNanos::try_from_hours(max_hours),
1235 Ok(DurationNanos::from_hours(max_hours))
1236 );
1237 assert!(DurationNanos::try_from_hours(max_hours + 1).is_err());
1238
1239 let max_days = DurationNanos::MAX.as_days();
1240 assert_eq!(
1241 DurationNanos::try_from_days(max_days),
1242 Ok(DurationNanos::from_days(max_days))
1243 );
1244 assert!(DurationNanos::try_from_days(max_days + 1).is_err());
1245 }
1246
1247 #[rstest]
1248 fn test_duration_nanos_zero_and_max() {
1249 assert_eq!(DurationNanos::default(), DurationNanos::ZERO);
1250 assert!(DurationNanos::ZERO.is_zero());
1251 assert_eq!(DurationNanos::MAX.as_u64(), u64::MAX);
1252 assert!(!DurationNanos::MAX.is_zero());
1253 }
1254
1255 #[rstest]
1256 fn test_duration_nanos_layout_matches_u64() {
1257 assert_eq!(
1258 std::mem::size_of::<DurationNanos>(),
1259 std::mem::size_of::<u64>()
1260 );
1261 assert_eq!(
1262 std::mem::align_of::<DurationNanos>(),
1263 std::mem::align_of::<u64>()
1264 );
1265 }
1266
1267 #[rstest]
1268 fn test_duration_nanos_format_and_ordering() {
1269 let shorter = DurationNanos::new(123);
1270 let longer = DurationNanos::new(456);
1271
1272 assert_eq!(shorter.to_string(), "123");
1273 assert_eq!(format!("{shorter:?}"), "DurationNanos(123)");
1274 assert!(shorter < longer);
1275 assert_eq!(shorter, DurationNanos::new(123));
1276 }
1277
1278 #[rstest]
1279 fn test_duration_nanos_serde_preserves_u64_format() {
1280 let duration = DurationNanos::MAX;
1281 let json = serde_json::to_string(&duration).unwrap();
1282 let deserialized: DurationNanos = serde_json::from_str(&json).unwrap();
1283
1284 assert_eq!(json, u64::MAX.to_string());
1285 assert_eq!(deserialized, duration);
1286 }
1287
1288 #[rstest]
1289 #[case("-1")]
1290 #[case("1.5")]
1291 #[case("\"1\"")]
1292 fn test_duration_nanos_serde_rejects_non_u64_formats(#[case] json: &str) {
1293 assert!(serde_json::from_str::<DurationNanos>(json).is_err());
1294 }
1295
1296 #[rstest]
1297 fn test_duration_nanos_checked_arithmetic_boundaries() {
1298 let zero = DurationNanos::ZERO;
1299 let one = DurationNanos::new(1);
1300 let max = DurationNanos::MAX;
1301
1302 assert_eq!(zero.checked_sub(one), None);
1303 assert_eq!(max.checked_add(one), None);
1304 assert_eq!(one.checked_sub(one), Some(zero));
1305 assert_eq!(zero.checked_add(max), Some(max));
1306 }
1307
1308 #[rstest]
1309 fn test_duration_nanos_saturating_arithmetic_boundaries() {
1310 let zero = DurationNanos::ZERO;
1311 let one = DurationNanos::new(1);
1312 let max = DurationNanos::MAX;
1313
1314 assert_eq!(zero.saturating_sub(one), zero);
1315 assert_eq!(max.saturating_add(one), max);
1316 assert_eq!(max.saturating_mul(2), max);
1317 }
1318
1319 #[rstest]
1320 fn test_duration_nanos_scalar_arithmetic() {
1321 let duration = DurationNanos::new(12);
1322
1323 assert_eq!(duration.checked_mul(3), Some(DurationNanos::new(36)));
1324 assert_eq!(DurationNanos::MAX.checked_mul(2), None);
1325 assert_eq!(duration.checked_div(3), Some(DurationNanos::new(4)));
1326 assert_eq!(duration.checked_div(0), None);
1327 assert_eq!(duration * 3, DurationNanos::new(36));
1328 assert_eq!(duration / 3, DurationNanos::new(4));
1329 }
1330
1331 #[rstest]
1332 fn test_duration_nanos_assign_operators() {
1333 let mut value = DurationNanos::new(100);
1334 value += DurationNanos::new(23);
1335 assert_eq!(value, DurationNanos::new(123));
1336 value -= DurationNanos::new(23);
1337 assert_eq!(value, DurationNanos::new(100));
1338 value *= 3;
1339 assert_eq!(value, DurationNanos::new(300));
1340 value /= 4;
1341 assert_eq!(value, DurationNanos::new(75));
1342 }
1343
1344 #[rstest]
1345 #[should_panic(expected = "DurationNanos overflow in addition")]
1346 fn test_duration_nanos_addition_panics_on_overflow() {
1347 let _ = DurationNanos::MAX + DurationNanos::new(1);
1348 }
1349
1350 #[rstest]
1351 #[should_panic(expected = "DurationNanos underflow in subtraction")]
1352 fn test_duration_nanos_subtraction_panics_on_underflow() {
1353 let _ = DurationNanos::default() - DurationNanos::new(1);
1354 }
1355
1356 #[rstest]
1357 #[should_panic(expected = "DurationNanos overflow in multiplication")]
1358 fn test_duration_nanos_multiplication_panics_on_overflow() {
1359 let _ = DurationNanos::MAX * 2;
1360 }
1361
1362 #[rstest]
1363 #[should_panic(expected = "DurationNanos division by zero")]
1364 fn test_duration_nanos_division_panics_on_zero() {
1365 let _ = DurationNanos::new(1) / 0;
1366 }
1367
1368 #[rstest]
1369 #[should_panic(expected = "DurationNanos overflow in from_micros")]
1370 fn test_duration_nanos_from_micros_panics_on_overflow() {
1371 let _ = DurationNanos::from_micros(u64::MAX);
1372 }
1373
1374 #[rstest]
1375 #[should_panic(expected = "DurationNanos overflow in from_millis")]
1376 fn test_duration_nanos_from_millis_panics_on_overflow() {
1377 let _ = DurationNanos::from_millis(u64::MAX);
1378 }
1379
1380 #[rstest]
1381 #[should_panic(expected = "DurationNanos overflow in from_secs")]
1382 fn test_duration_nanos_from_secs_panics_on_overflow() {
1383 let _ = DurationNanos::from_secs(u64::MAX);
1384 }
1385
1386 #[rstest]
1387 #[should_panic(expected = "DurationNanos overflow in from_mins")]
1388 fn test_duration_nanos_from_mins_panics_on_overflow() {
1389 let _ = DurationNanos::from_mins(u64::MAX);
1390 }
1391
1392 #[rstest]
1393 #[should_panic(expected = "DurationNanos overflow in from_hours")]
1394 fn test_duration_nanos_from_hours_panics_on_overflow() {
1395 let _ = DurationNanos::from_hours(u64::MAX);
1396 }
1397
1398 #[rstest]
1399 #[should_panic(expected = "DurationNanos overflow in from_days")]
1400 fn test_duration_nanos_from_days_panics_on_overflow() {
1401 let _ = DurationNanos::from_days(u64::MAX);
1402 }
1403
1404 #[rstest]
1405 fn test_new() {
1406 let nanos = UnixNanos::new(123);
1407 assert_eq!(nanos.as_u64(), 123);
1408 assert_eq!(nanos.as_i64(), 123);
1409 }
1410
1411 #[rstest]
1412 fn test_max() {
1413 let nanos = UnixNanos::max();
1414 assert_eq!(nanos.as_u64(), u64::MAX);
1415 }
1416
1417 #[rstest]
1418 fn test_is_zero() {
1419 assert!(UnixNanos::default().is_zero());
1420 assert!(!UnixNanos::max().is_zero());
1421 }
1422
1423 #[rstest]
1424 fn test_from_u64() {
1425 let nanos = UnixNanos::from(123);
1426 assert_eq!(nanos.as_u64(), 123);
1427 assert_eq!(nanos.as_i64(), 123);
1428 }
1429
1430 #[rstest]
1431 fn test_default() {
1432 let nanos = UnixNanos::default();
1433 assert_eq!(nanos.as_u64(), 0);
1434 assert_eq!(nanos.as_i64(), 0);
1435 }
1436
1437 #[rstest]
1438 fn test_into_from() {
1439 let nanos: UnixNanos = 456.into();
1440 let value: u64 = nanos.into();
1441 assert_eq!(value, 456);
1442 }
1443
1444 #[rstest]
1445 #[case(0, "1970-01-01T00:00:00+00:00")]
1446 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
1447 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
1448 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
1449 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
1450 fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
1451 let nanos = UnixNanos::from(nanos);
1452 let datetime = nanos.to_datetime_utc();
1453 assert_eq!(
1454 datetime.display_with_offset(Offset::UTC).to_string(),
1455 expected
1456 );
1457 }
1458
1459 #[rstest]
1460 #[case(0, "1970-01-01T00:00:00+00:00")]
1461 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
1462 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
1463 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
1464 #[case(1_500_000_000_500_000_000, "2017-07-14T02:40:00.500+00:00")]
1465 #[case(1_500_000_000_123_456_000, "2017-07-14T02:40:00.123456+00:00")]
1466 #[case(1_500_000_000_123_456_789, "2017-07-14T02:40:00.123456789+00:00")]
1467 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
1468 fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
1469 let nanos = UnixNanos::from(nanos);
1470 assert_eq!(nanos.to_rfc3339(), expected);
1471 }
1472
1473 #[rstest]
1474 fn test_from_str() {
1475 let nanos: UnixNanos = "123".parse().unwrap();
1476 assert_eq!(nanos.as_u64(), 123);
1477 }
1478
1479 #[rstest]
1480 fn test_from_str_invalid() {
1481 let result = "abc".parse::<UnixNanos>();
1482 assert!(result.is_err());
1483 }
1484
1485 #[rstest]
1486 fn test_from_str_date() {
1487 let nanos: UnixNanos = "2024-02-10".parse().unwrap();
1488 assert_eq!(nanos.as_u64(), 1_707_523_200_000_000_000);
1489 }
1490
1491 #[rstest]
1492 fn test_from_str_pre_epoch_date() {
1493 let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
1494 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
1495 }
1496
1497 #[rstest]
1498 fn test_from_str_pre_epoch_rfc3339() {
1499 let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
1500 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
1501 }
1502
1503 #[rstest]
1504 fn test_from_borrowed_str_and_string() {
1505 assert_eq!(UnixNanos::from("123").as_u64(), 123);
1506 assert_eq!(UnixNanos::from("123".to_string()).as_u64(), 123);
1507 }
1508
1509 #[rstest]
1510 #[should_panic(expected = "Failed to parse string")]
1511 fn test_from_borrowed_str_panics_on_invalid() {
1512 let _ = UnixNanos::from("abc");
1513 }
1514
1515 #[rstest]
1516 #[should_panic(expected = "Failed to parse string")]
1517 fn test_from_string_trait_panics_on_invalid() {
1518 let _ = UnixNanos::from("abc".to_string());
1519 }
1520
1521 #[rstest]
1522 fn test_into_timestamp() {
1523 let nanos = UnixNanos::from(1_000_000_000);
1524 let datetime = Timestamp::from(nanos);
1525 assert_eq!(datetime, timestamp("1970-01-01T00:00:01Z"));
1526 assert_eq!(UnixNanos::from(datetime), nanos);
1527 }
1528
1529 #[rstest]
1530 fn test_try_from_datetime_valid() {
1531 let datetime = Timestamp::from_second(1_000_000_000).unwrap();
1532 let nanos = UnixNanos::from(datetime);
1533 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
1534 }
1535
1536 #[rstest]
1537 fn test_from_system_time() {
1538 let system_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
1539 let nanos = UnixNanos::from(system_time);
1540 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
1541 }
1542
1543 #[rstest]
1544 #[should_panic(expected = "SystemTime before UNIX EPOCH")]
1545 fn test_from_system_time_before_epoch() {
1546 let system_time = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
1547 let _ = UnixNanos::from(system_time);
1548 }
1549
1550 #[rstest]
1551 #[should_panic(expected = "SystemTime overflowed u64 nanoseconds")]
1552 fn test_from_system_time_overflow_panics() {
1553 let system_time =
1555 std::time::UNIX_EPOCH + std::time::Duration::from_secs(u64::MAX / 1_000_000_000 + 1);
1556 let _ = UnixNanos::from(system_time);
1557 }
1558
1559 #[rstest]
1560 fn test_eq() {
1561 let nanos = UnixNanos::from(100);
1562 assert_eq!(nanos, 100);
1563 assert_eq!(nanos, Some(100));
1564 assert_ne!(nanos, 200);
1565 assert_ne!(nanos, Some(200));
1566 assert_ne!(nanos, None);
1567 }
1568
1569 #[rstest]
1570 fn test_partial_cmp() {
1571 let nanos = UnixNanos::from(100);
1572 assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
1573 assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
1574 assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
1575 assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
1576 }
1577
1578 #[rstest]
1579 fn test_u64_comparison_and_deref() {
1580 let nanos = UnixNanos::from(100);
1581 assert_eq!(100u64, nanos);
1582 assert_eq!(99u64.partial_cmp(&nanos), Some(Ordering::Less));
1583 assert_eq!(nanos.partial_cmp(&Some(100u64)), Some(Ordering::Equal));
1584 assert_eq!(*nanos, 100u64);
1585 }
1586
1587 #[rstest]
1588 fn test_edge_case_max_value() {
1589 let nanos = UnixNanos::from(u64::MAX);
1590 assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
1591 }
1592
1593 #[rstest]
1594 fn test_display() {
1595 let nanos = UnixNanos::from(123);
1596 assert_eq!(format!("{nanos}"), "123");
1597 }
1598
1599 #[rstest]
1600 fn test_addition() {
1601 let nanos = UnixNanos::from(100);
1602 let duration = DurationNanos::new(200);
1603 let result = nanos + duration;
1604 assert_eq!(result.as_u64(), 300);
1605 }
1606
1607 #[rstest]
1608 fn test_add_assign() {
1609 let mut nanos = UnixNanos::from(100);
1610 nanos += DurationNanos::new(50);
1611 assert_eq!(nanos.as_u64(), 150);
1612 }
1613
1614 #[rstest]
1615 fn test_subtraction() {
1616 let nanos1 = UnixNanos::from(200);
1617 let nanos2 = UnixNanos::from(100);
1618 let result = nanos1 - nanos2;
1619 assert_eq!(result, DurationNanos::new(100));
1620 }
1621
1622 #[rstest]
1623 fn test_sub_assign() {
1624 let mut nanos = UnixNanos::from(200);
1625 nanos -= DurationNanos::new(50);
1626 assert_eq!(nanos.as_u64(), 150);
1627 }
1628
1629 #[rstest]
1630 #[should_panic(expected = "UnixNanos overflow")]
1631 fn test_overflow_add() {
1632 let nanos = UnixNanos::from(u64::MAX);
1633 let _ = nanos + DurationNanos::new(1);
1634 }
1635
1636 #[rstest]
1637 #[should_panic(expected = "UnixNanos underflow")]
1638 fn test_overflow_sub() {
1639 let _ = UnixNanos::default() - DurationNanos::new(1);
1640 }
1641
1642 #[rstest]
1643 #[case(100, 50, Some(DurationNanos::new(50)))]
1644 #[case(1_000_000_000, 500_000_000, Some(DurationNanos::new(500_000_000)))]
1645 #[case(u64::MAX, u64::MAX - 1, Some(DurationNanos::new(1)))]
1646 #[case(50, 50, Some(DurationNanos::ZERO))]
1647 #[case(50, 100, None)]
1648 #[case(0, 1, None)]
1649 fn test_duration_since(
1650 #[case] time1: u64,
1651 #[case] time2: u64,
1652 #[case] expected: Option<DurationNanos>,
1653 ) {
1654 let nanos1 = UnixNanos::from(time1);
1655 let nanos2 = UnixNanos::from(time2);
1656 assert_eq!(nanos1.duration_since(&nanos2), expected);
1657 }
1658
1659 #[rstest]
1660 fn test_duration_since_same_moment() {
1661 let moment = UnixNanos::from(1_707_577_123_456_789_000);
1662 assert_eq!(
1663 moment.duration_since(&moment),
1664 Some(DurationNanos::default())
1665 );
1666 }
1667
1668 #[rstest]
1669 #[case::later(100, 50, DurationNanos::new(50))]
1670 #[case::same(50, 50, DurationNanos::ZERO)]
1671 #[case::earlier(50, 100, DurationNanos::ZERO)]
1672 #[case::full_range(u64::MAX, 0, DurationNanos::new(u64::MAX))]
1673 fn test_saturating_duration_since(
1674 #[case] time: u64,
1675 #[case] earlier: u64,
1676 #[case] expected: DurationNanos,
1677 ) {
1678 assert_eq!(
1679 UnixNanos::from(time).saturating_duration_since(UnixNanos::from(earlier)),
1680 expected
1681 );
1682 }
1683
1684 #[rstest]
1685 #[case(100, 30, 90)]
1686 #[case(90, 30, 90)]
1687 #[case(100, 101, 0)]
1688 #[case(u64::MAX, u64::MAX, u64::MAX)]
1689 #[case(u64::MAX, 1_000_000_000, 18_446_744_073_000_000_000)]
1690 fn test_floor(#[case] time: u64, #[case] interval: u64, #[case] expected: u64) {
1691 assert_eq!(
1692 UnixNanos::new(time).floor(DurationNanos::new(interval)),
1693 UnixNanos::new(expected)
1694 );
1695 }
1696
1697 #[rstest]
1698 #[should_panic(expected = "cannot floor UnixNanos to a zero interval")]
1699 fn test_floor_panics_for_zero_interval() {
1700 let _ = UnixNanos::default().floor(DurationNanos::ZERO);
1701 }
1702
1703 #[rstest]
1704 fn test_duration_since_chronological() {
1705 let earlier = timestamp("2024-02-10T12:00:00Z");
1707
1708 let later = earlier
1710 + SignedDuration::from_hours(1)
1711 + SignedDuration::from_mins(30)
1712 + SignedDuration::from_secs(45)
1713 + SignedDuration::from_nanos(500_000_000);
1714
1715 let earlier_nanos = UnixNanos::from(earlier);
1716 let later_nanos = UnixNanos::from(later);
1717
1718 let expected_duration =
1720 (60 * 60 + 30 * 60 + 45) * NANOSECONDS_IN_SECOND + 500 * NANOSECONDS_IN_MILLISECOND;
1721
1722 assert_eq!(
1723 later_nanos.duration_since(&earlier_nanos),
1724 Some(DurationNanos::new(expected_duration))
1725 );
1726 assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
1727 }
1728
1729 #[rstest]
1730 fn test_duration_since_with_edge_cases() {
1731 let max = UnixNanos::from(u64::MAX);
1733 let smaller = UnixNanos::from(u64::MAX - 1000);
1734
1735 assert_eq!(max.duration_since(&smaller), Some(DurationNanos::new(1000)));
1736 assert_eq!(smaller.duration_since(&max), None);
1737
1738 let min = UnixNanos::default(); let larger = UnixNanos::from(1000);
1741
1742 assert_eq!(min.duration_since(&min), Some(DurationNanos::default()));
1743 assert_eq!(larger.duration_since(&min), Some(DurationNanos::new(1000)));
1744 assert_eq!(min.duration_since(&larger), None);
1745 }
1746
1747 #[rstest]
1748 fn test_serde_json() {
1749 let nanos = UnixNanos::from(123);
1750 let json = serde_json::to_string(&nanos).unwrap();
1751 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1752 assert_eq!(deserialized, nanos);
1753 }
1754
1755 #[rstest]
1756 fn test_serde_edge_cases() {
1757 let nanos = UnixNanos::from(u64::MAX);
1758 let json = serde_json::to_string(&nanos).unwrap();
1759 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1760 assert_eq!(deserialized, nanos);
1761 }
1762
1763 #[rstest]
1764 #[case("123", 123)] #[case("1234.567", 1_234_567_000_000)] #[case("2024-02-10", 1_707_523_200_000_000_000)] #[case("2024-2-10", 1_707_523_200_000_000_000)] #[case("2024-02-1", 1_706_745_600_000_000_000)] #[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] #[case("2024-02-10t14:58:43Z", 1_707_577_123_000_000_000)] #[case("2024-02-10 14:58:43Z", 1_707_577_123_000_000_000)] #[case("2024-02-10T14:58:43.456789Z", 1_707_577_123_456_789_000)] fn test_from_str_formats(#[case] input: &str, #[case] expected: u64) {
1774 let parsed: UnixNanos = input.parse().unwrap();
1775 assert_eq!(parsed.as_u64(), expected);
1776 }
1777
1778 #[rstest]
1779 #[case("abc")] #[case("not a timestamp")] #[case("2024-02-10 14:58:43")] #[case("2024-02-10T14:58:43Z[UTC]")] fn test_from_str_invalid_formats(#[case] input: &str) {
1784 let result = input.parse::<UnixNanos>();
1785 assert!(result.is_err());
1786 }
1787
1788 #[rstest]
1789 fn test_from_str_integer_overflow() {
1790 let input = "184467440737095516160";
1792 let result = input.parse::<UnixNanos>();
1793 assert!(result.is_err());
1794 }
1795
1796 #[rstest]
1797 fn test_checked_add_overflow_returns_none() {
1798 let max = UnixNanos::from(u64::MAX);
1799 assert_eq!(max.checked_add(DurationNanos::new(1)), None);
1800 }
1801
1802 #[rstest]
1803 fn test_checked_sub_underflow_returns_none() {
1804 let zero = UnixNanos::default();
1805 assert_eq!(zero.checked_sub(DurationNanos::new(1)), None);
1806 }
1807
1808 #[rstest]
1809 fn test_saturating_add_overflow() {
1810 let max = UnixNanos::from(u64::MAX);
1811 let result = max.saturating_add(DurationNanos::new(1));
1812 assert_eq!(result, UnixNanos::from(u64::MAX));
1813 }
1814
1815 #[rstest]
1816 fn test_saturating_sub_underflow() {
1817 let zero = UnixNanos::default();
1818 let result = zero.saturating_sub(DurationNanos::new(1));
1819 assert_eq!(result, UnixNanos::default());
1820 }
1821
1822 #[rstest]
1823 fn test_from_str_float_overflow() {
1824 let input = "2e10"; let err = input.parse::<UnixNanos>().unwrap_err();
1827 assert!(err.to_string().contains("out of range"));
1828 }
1829
1830 #[rstest]
1831 #[case("NaN")]
1832 #[case("nan")]
1833 #[case("inf")]
1834 #[case("-inf")]
1835 fn test_from_str_non_finite_float_errors(#[case] input: &str) {
1836 let err = input.parse::<UnixNanos>().unwrap_err();
1837 assert!(err.to_string().contains("must be finite"));
1838 }
1839
1840 #[rstest]
1841 #[case("-1.5")]
1842 #[case("-0.000001")]
1843 fn test_from_str_negative_float_errors(#[case] input: &str) {
1844 let err = input.parse::<UnixNanos>().unwrap_err();
1845 assert!(err.to_string().contains("cannot be negative"));
1846 }
1847
1848 #[rstest]
1849 #[case("42", Some(42))]
1850 #[case("42.0", Some(42_000_000_000))]
1851 #[case("1e-3", Some(1_000_000))]
1852 #[case("0.9999999999", Some(999_999_999))]
1853 #[case("18446744073709551615", Some(u64::MAX))]
1854 #[case("-1", None)]
1855 #[case("-0.5", None)]
1856 #[case("1e30", None)]
1857 #[case("1e400", None)]
1858 fn test_deserialize_numeric_routes(#[case] token: &str, #[case] expected: Option<u64>) {
1859 #[derive(Deserialize)]
1860 #[serde(tag = "kind")]
1861 enum Tagged {
1862 Time { value: UnixNanos },
1863 }
1864 let direct = serde_json::from_str::<UnixNanos>(token);
1865 let buffered = serde_json::from_str::<serde_json::Value>(token)
1866 .and_then(serde_json::from_value::<UnixNanos>);
1867 let tagged = serde_json::from_str(&format!(r#"{{"kind":"Time","value":{token}}}"#))
1868 .map(|Tagged::Time { value }| value);
1869
1870 assert_eq!(direct.ok().map(|value| value.as_u64()), expected);
1871 assert_eq!(buffered.ok().map(|value| value.as_u64()), expected);
1872 assert_eq!(tagged.ok().map(|value| value.as_u64()), expected);
1873 }
1874
1875 #[rstest]
1876 fn test_deserialize_u64() {
1877 let json = "123456789";
1878 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1879 assert_eq!(deserialized.as_u64(), 123_456_789);
1880 }
1881
1882 #[rstest]
1883 fn test_deserialize_string_with_int() {
1884 let json = "\"123456789\"";
1885 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1886 assert_eq!(deserialized.as_u64(), 123_456_789);
1887 }
1888
1889 #[rstest]
1890 fn test_deserialize_float() {
1891 let json = "1234.567";
1892 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1893 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1894 }
1895
1896 #[rstest]
1897 fn test_deserialize_string_with_float() {
1898 let json = "\"1234.567\"";
1899 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1900 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1901 }
1902
1903 #[rstest]
1904 fn test_deserialize_float_uses_truncation() {
1905 let json = "0.9999999999";
1907 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1908 assert_eq!(deserialized.as_u64(), 999_999_999); }
1910
1911 #[rstest]
1912 #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
1913 #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
1914 fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
1915 let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
1916 assert_eq!(deserialized.as_u64(), expected);
1917 }
1918
1919 #[rstest]
1920 fn test_deserialize_negative_int_fails() {
1921 let json = "-123456789";
1922 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1923 assert!(
1924 result
1925 .unwrap_err()
1926 .to_string()
1927 .contains("cannot be negative")
1928 );
1929 }
1930
1931 #[rstest]
1932 fn test_deserialize_negative_float_fails() {
1933 let json = "-1234.567";
1934 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1935 assert!(
1936 result
1937 .unwrap_err()
1938 .to_string()
1939 .contains("cannot be negative")
1940 );
1941 }
1942
1943 #[rstest]
1944 fn test_deserialize_nan_fails() {
1945 use serde::de::{
1947 IntoDeserializer,
1948 value::{Error as ValueError, F64Deserializer},
1949 };
1950 let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
1951 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1952 assert!(result.is_err());
1953 assert!(result.unwrap_err().to_string().contains("must be finite"));
1954 }
1955
1956 #[rstest]
1957 fn test_deserialize_infinity_fails() {
1958 use serde::de::{
1959 IntoDeserializer,
1960 value::{Error as ValueError, F64Deserializer},
1961 };
1962 let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
1963 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1964 assert!(result.is_err());
1965 assert!(result.unwrap_err().to_string().contains("must be finite"));
1966 }
1967
1968 #[rstest]
1969 fn test_deserialize_negative_infinity_fails() {
1970 use serde::de::{
1971 IntoDeserializer,
1972 value::{Error as ValueError, F64Deserializer},
1973 };
1974 let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
1975 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1976 assert!(result.is_err());
1977 assert!(result.unwrap_err().to_string().contains("must be finite"));
1978 }
1979
1980 #[rstest]
1981 fn test_deserialize_overflow_float_fails() {
1982 let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
1985 assert!(result.is_err());
1986 assert!(result.unwrap_err().to_string().contains("out of range"));
1987 }
1988
1989 #[rstest]
1990 fn test_deserialize_float_u64_boundary_fails() {
1991 let deserializer = serde::de::value::F64Deserializer::<serde::de::value::Error>::new(
1992 18_446_744_073.709_553,
1993 );
1994 let err = UnixNanos::deserialize(deserializer).unwrap_err();
1995 assert!(err.to_string().contains("out of range"));
1996 }
1997
1998 #[rstest]
1999 fn test_deserialize_invalid_string_fails() {
2000 let json = "\"not a timestamp\"";
2001 let result: Result<UnixNanos, _> = serde_json::from_str(json);
2002 assert!(result.is_err());
2003 }
2004
2005 #[rstest]
2006 fn test_deserialize_edge_cases() {
2007 let json = "0";
2009 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
2010 assert_eq!(deserialized.as_u64(), 0);
2011
2012 let json = "18446744073709551615"; let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
2015 assert_eq!(deserialized.as_u64(), u64::MAX);
2016 }
2017
2018 #[rstest]
2019 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
2020 fn test_as_i64_overflow_panics() {
2021 let nanos = UnixNanos::from(u64::MAX);
2022 let _ = nanos.as_i64(); }
2024
2025 #[rstest]
2026 fn test_as_i64_at_i64_max_boundary() {
2027 let nanos = UnixNanos::from(i64::MAX.cast_unsigned());
2028 assert_eq!(nanos.as_i64(), i64::MAX);
2029 }
2030
2031 #[rstest]
2032 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
2033 fn test_as_i64_just_above_i64_max_panics() {
2034 let nanos = UnixNanos::from(i64::MAX.cast_unsigned() + 1);
2035 let _ = nanos.as_i64();
2036 }
2037
2038 fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
2039 prop_oneof![
2040 0u64..1_000_000u64,
2042 1_000_000u64..1_000_000_000_000u64,
2044 1_000_000_000_000u64..=i64::MAX.cast_unsigned(),
2046 (i64::MAX.cast_unsigned() + 1)..=u64::MAX,
2048 Just(0u64),
2050 Just(1u64),
2051 Just(1_000_000_000u64), Just(1_000_000_000_000u64), Just(1_700_000_000_000_000_000u64), Just((i64::MAX / 2).cast_unsigned()), Just(i64::MAX.cast_unsigned()), Just(u64::MAX), ]
2058 .prop_map(UnixNanos::from)
2059 }
2060
2061 fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
2062 (unix_nanos_strategy(), unix_nanos_strategy())
2063 }
2064
2065 fn duration_nanos_strategy() -> impl Strategy<Value = DurationNanos> {
2066 any::<u64>().prop_map(DurationNanos::new)
2067 }
2068
2069 fn duration_nanos_pair_strategy() -> impl Strategy<Value = (DurationNanos, DurationNanos)> {
2070 (duration_nanos_strategy(), duration_nanos_strategy())
2071 }
2072
2073 proptest! {
2074 #[rstest]
2075 #[expect(
2076 clippy::float_cmp,
2077 clippy::cast_precision_loss,
2078 reason = "roundtrip: both sides go through the same u64->f64 cast"
2079 )]
2080 fn prop_unix_nanos_construction_roundtrip(nanos in unix_nanos_strategy()) {
2081 let value = nanos.as_u64();
2082 prop_assert_eq!(UnixNanos::from(value).as_u64(), value);
2083 prop_assert_eq!(nanos.as_f64(), value as f64);
2084
2085 if i64::try_from(value).is_ok() {
2087 prop_assert_eq!(nanos.as_i64(), value.cast_signed());
2088 }
2089 }
2090
2091 #[rstest]
2092 fn prop_duration_nanos_addition_commutative(
2093 (duration1, duration2) in duration_nanos_pair_strategy()
2094 ) {
2095 if let (Some(sum1), Some(sum2)) = (
2096 duration1.checked_add(duration2),
2097 duration2.checked_add(duration1)
2098 ) {
2099 prop_assert_eq!(sum1, sum2, "Addition should be commutative");
2100 }
2101 }
2102
2103 #[rstest]
2104 fn prop_duration_nanos_addition_associative(
2105 duration1 in duration_nanos_strategy(),
2106 duration2 in duration_nanos_strategy(),
2107 duration3 in duration_nanos_strategy(),
2108 ) {
2109 let expected = duration1
2110 .checked_add(duration2)
2111 .and_then(|sum| sum.checked_add(duration3));
2112
2113 if let Some(expected) = expected {
2114 let left = (duration1 + duration2) + duration3;
2115 let right = duration1 + (duration2 + duration3);
2116 prop_assert_eq!(left, expected);
2117 prop_assert_eq!(right, expected);
2118 }
2119 }
2120
2121 #[rstest]
2122 fn prop_unix_nanos_duration_arithmetic_roundtrip(
2123 nanos in unix_nanos_strategy(),
2124 duration in duration_nanos_strategy(),
2125 ) {
2126 if let Some(sum) = nanos.checked_add(duration) {
2127 prop_assert_eq!(sum - duration, nanos);
2128 prop_assert_eq!(sum - nanos, duration);
2129 }
2130 }
2131
2132 #[rstest]
2133 fn prop_duration_nanos_zero_identity(duration in duration_nanos_strategy()) {
2134 let zero = DurationNanos::default();
2135 prop_assert_eq!(duration + zero, duration);
2136 prop_assert_eq!(zero + duration, duration);
2137 prop_assert!(zero.is_zero());
2138 }
2139
2140 #[rstest]
2141 fn prop_unix_nanos_ordering_consistency(
2142 (nanos1, nanos2) in unix_nanos_pair_strategy()
2143 ) {
2144 let eq = nanos1 == nanos2;
2146 let lt = nanos1 < nanos2;
2147 let gt = nanos1 > nanos2;
2148 let le = nanos1 <= nanos2;
2149 let ge = nanos1 >= nanos2;
2150
2151 let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
2153 prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
2154
2155 prop_assert_eq!(le, eq || lt, "<= should equal == || <");
2157 prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
2158 prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
2159 prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
2160 }
2161
2162 #[rstest]
2163 fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
2164 let string_repr = nanos.to_string();
2166 let parsed = UnixNanos::from_str(&string_repr);
2167 prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
2168 if let Ok(parsed_nanos) = parsed {
2169 prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
2170 }
2171 }
2172
2173 #[rstest]
2174 fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
2175 if i64::try_from(nanos.as_u64()).is_ok() {
2177 let datetime = nanos.to_datetime_utc();
2178 let converted_back = UnixNanos::from(datetime);
2179 prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
2180
2181 let rfc3339 = nanos.to_rfc3339();
2183 if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
2184 prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
2185 }
2186 }
2187 }
2188
2189 #[rstest]
2190 fn prop_unix_nanos_duration_since(
2191 (nanos1, nanos2) in unix_nanos_pair_strategy()
2192 ) {
2193 let duration = nanos1.duration_since(&nanos2);
2195 let saturating_duration = nanos1.saturating_duration_since(nanos2);
2196
2197 if nanos1 >= nanos2 {
2198 prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
2200 if let Some(dur) = duration {
2201 prop_assert_eq!(dur.as_u64(), nanos1.as_u64() - nanos2.as_u64(),
2202 "Duration should equal the difference");
2203 prop_assert_eq!(saturating_duration, dur,
2204 "Saturating duration should equal the difference");
2205 prop_assert_eq!(nanos2 + dur, nanos1,
2206 "second + duration should equal first");
2207 }
2208 } else {
2209 prop_assert!(duration.is_none(), "Duration should be None when first < second");
2211 prop_assert_eq!(saturating_duration, DurationNanos::default(),
2212 "Saturating duration should be zero when first < second");
2213 }
2214 }
2215
2216 #[rstest]
2217 fn prop_unix_nanos_checked_arithmetic(
2218 nanos in unix_nanos_strategy(),
2219 duration in duration_nanos_strategy(),
2220 ) {
2221 let checked_add = nanos.checked_add(duration);
2222 let checked_sub = nanos.checked_sub(duration);
2223
2224 if let Some(sum) = checked_add {
2225 prop_assert_eq!(sum, nanos + duration, "Checked add should match regular add when no overflow");
2226 }
2227
2228 if let Some(diff) = checked_sub {
2229 prop_assert_eq!(diff, nanos - duration, "Checked sub should match regular sub when no underflow");
2230 }
2231 }
2232
2233 #[rstest]
2234 fn prop_unix_nanos_saturating_arithmetic(
2235 nanos in unix_nanos_strategy(),
2236 duration in duration_nanos_strategy(),
2237 ) {
2238 let sat_add = nanos.saturating_add(duration);
2239 let sat_sub = nanos.saturating_sub(duration);
2240
2241 prop_assert!(sat_add >= nanos, "Saturating add result should be >= timestamp");
2242 prop_assert!(sat_sub <= nanos, "Saturating sub result should be <= timestamp");
2243
2244 if let Some(checked_sum) = nanos.checked_add(duration) {
2245 prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
2246 } else {
2247 prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
2248 }
2249
2250 if let Some(checked_diff) = nanos.checked_sub(duration) {
2251 prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
2252 } else {
2253 prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
2254 }
2255 }
2256
2257 #[rstest]
2258 fn prop_unix_nanos_assign_mirrors_op(
2259 nanos in unix_nanos_strategy(),
2260 duration in duration_nanos_strategy(),
2261 ) {
2262 if let Some(expected) = nanos.checked_add(duration) {
2263 let mut add_result = nanos;
2264 add_result += duration;
2265 prop_assert_eq!(add_result, expected, "AddAssign should mirror Add");
2266 }
2267
2268 if let Some(expected) = nanos.checked_sub(duration) {
2269 let mut sub_result = nanos;
2270 sub_result -= duration;
2271 prop_assert_eq!(sub_result, expected, "SubAssign should mirror Sub");
2272 }
2273 }
2274
2275 #[rstest]
2276 fn prop_unix_nanos_serde_roundtrip(nanos in unix_nanos_strategy()) {
2277 let json = serde_json::to_string(&nanos).unwrap();
2278 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
2279 prop_assert_eq!(deserialized, nanos, "Serde JSON should round-trip exactly");
2280 }
2281
2282 #[rstest]
2283 fn prop_unix_nanos_f64_deserialize_never_panics(val: f64) {
2284 use serde::de::{IntoDeserializer, value::{Error as ValueError, F64Deserializer}};
2287 let deserializer: F64Deserializer<ValueError> = val.into_deserializer();
2288 let result = UnixNanos::deserialize(deserializer);
2289
2290 let upper_bound = 2.0_f64.powi(64);
2291 if val.is_finite() && val >= 0.0 && val * 1_000_000_000.0 < upper_bound {
2292 prop_assert!(result.is_ok(), "Should succeed for valid f64: {}", val);
2293 } else {
2294 prop_assert!(result.is_err(), "Should error for invalid f64: {}", val);
2295 }
2296 }
2297 }
2298
2299 #[rstest]
2300 fn test_from_seconds_zero() {
2301 let nanos = UnixNanos::from_seconds(0);
2302 assert_eq!(nanos.as_u64(), 0);
2303 }
2304
2305 #[rstest]
2306 fn test_from_seconds_one() {
2307 let nanos = UnixNanos::from_seconds(1);
2308 assert_eq!(nanos.as_u64(), 1_000_000_000);
2309 }
2310
2311 #[rstest]
2312 fn test_from_seconds_realistic_timestamp() {
2313 let nanos = UnixNanos::from_seconds(1_700_000_000);
2314 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2315 assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
2316 }
2317
2318 #[rstest]
2319 fn test_from_seconds_max_safe() {
2320 let max_seconds = u64::MAX / 1_000_000_000;
2321 let nanos = UnixNanos::from_seconds(max_seconds);
2322 assert_eq!(nanos.as_u64(), max_seconds * 1_000_000_000);
2323 }
2324
2325 #[rstest]
2326 fn test_from_millis_zero() {
2327 let nanos = UnixNanos::from_millis(0);
2328 assert_eq!(nanos.as_u64(), 0);
2329 }
2330
2331 #[rstest]
2332 fn test_from_millis_one() {
2333 let nanos = UnixNanos::from_millis(1);
2334 assert_eq!(nanos.as_u64(), 1_000_000);
2335 }
2336
2337 #[rstest]
2338 fn test_from_millis_one_second() {
2339 let nanos = UnixNanos::from_millis(1_000);
2340 assert_eq!(nanos.as_u64(), 1_000_000_000);
2341 }
2342
2343 #[rstest]
2344 fn test_from_millis_realistic_timestamp() {
2345 let nanos = UnixNanos::from_millis(1_700_000_000_000);
2347 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2348 assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
2349 }
2350
2351 #[rstest]
2352 fn test_from_millis_max_safe() {
2353 let max_ms = u64::MAX / 1_000_000;
2354 let nanos = UnixNanos::from_millis(max_ms);
2355 assert_eq!(nanos.as_u64(), max_ms * 1_000_000);
2356 }
2357
2358 #[rstest]
2359 fn test_from_millis_matches_manual_conversion() {
2360 let ms = 1_625_474_304_765_u64;
2361 let expected = ms * 1_000_000;
2362 assert_eq!(UnixNanos::from_millis(ms).as_u64(), expected);
2363 }
2364
2365 #[rstest]
2366 #[case::valid(1_700_000_000_123, Some(1_700_000_000_123_000_000))]
2367 #[case::negative(-1, None)]
2368 #[case::overflow(i64::MAX, None)]
2369 #[case::zero(0, Some(0))]
2370 fn test_from_millis_checked(#[case] millis: i64, #[case] expected: Option<u64>) {
2371 assert_eq!(
2372 UnixNanos::from_millis_checked(millis).map(|value| value.as_u64()),
2373 expected
2374 );
2375 }
2376
2377 #[rstest]
2378 #[case(0, 0)]
2379 #[case(999_999_999, 0)]
2380 #[case(1_000_000_000, 1)]
2381 #[case(1_700_000_000_123_456_789, 1_700_000_000)]
2382 fn test_as_seconds(#[case] nanos: u64, #[case] expected: u64) {
2383 assert_eq!(UnixNanos::from(nanos).as_seconds(), expected);
2384 }
2385
2386 #[rstest]
2387 #[case(0, 0)]
2388 #[case(999_999, 0)]
2389 #[case(1_000_000, 1)]
2390 #[case(1_700_000_000_000_123_456, 1_700_000_000_000)]
2391 fn test_as_millis(#[case] nanos: u64, #[case] expected: u64) {
2392 assert_eq!(UnixNanos::from(nanos).as_millis(), expected);
2393 }
2394
2395 #[rstest]
2396 #[case(0, 0)]
2397 #[case(999, 0)]
2398 #[case(1_000, 1)]
2399 #[case(1_700_000_000_000_123_456, 1_700_000_000_000_123)]
2400 fn test_as_micros(#[case] nanos: u64, #[case] expected: u64) {
2401 assert_eq!(UnixNanos::from(nanos).as_micros(), expected);
2402 }
2403
2404 #[rstest]
2405 fn test_from_micros_zero() {
2406 let nanos = UnixNanos::from_micros(0);
2407 assert_eq!(nanos.as_u64(), 0);
2408 }
2409
2410 #[rstest]
2411 fn test_from_micros_one() {
2412 let nanos = UnixNanos::from_micros(1);
2413 assert_eq!(nanos.as_u64(), 1_000);
2414 }
2415
2416 #[rstest]
2417 fn test_from_micros_one_second() {
2418 let nanos = UnixNanos::from_micros(1_000_000);
2419 assert_eq!(nanos.as_u64(), 1_000_000_000);
2420 }
2421
2422 #[rstest]
2423 fn test_from_micros_one_millisecond() {
2424 let nanos = UnixNanos::from_micros(1_000);
2425 assert_eq!(nanos.as_u64(), 1_000_000);
2426 assert_eq!(UnixNanos::from_micros(1_000), UnixNanos::from_millis(1));
2427 }
2428
2429 #[rstest]
2430 fn test_from_micros_realistic_timestamp() {
2431 let micros = 1_700_000_000_000_000_u64;
2432 let nanos = UnixNanos::from_micros(micros);
2433 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2434 }
2435
2436 #[rstest]
2437 fn test_from_micros_max_safe() {
2438 let max_us = u64::MAX / 1_000;
2439 let nanos = UnixNanos::from_micros(max_us);
2440 assert_eq!(nanos.as_u64(), max_us * 1_000);
2441 }
2442
2443 #[rstest]
2444 fn test_from_micros_matches_manual_conversion() {
2445 let us = 1_000_000_123_456_u64;
2446 let expected = us * 1_000;
2447 assert_eq!(UnixNanos::from_micros(us).as_u64(), expected);
2448 }
2449
2450 #[rstest]
2451 #[case::valid(1_700_000_000_123_456, Some(1_700_000_000_123_456_000))]
2452 #[case::negative(-1, None)]
2453 #[case::overflow(i64::MAX, None)]
2454 #[case::zero(0, Some(0))]
2455 fn test_from_micros_checked(#[case] micros: i64, #[case] expected: Option<u64>) {
2456 assert_eq!(
2457 UnixNanos::from_micros_checked(micros).map(|value| value.as_u64()),
2458 expected
2459 );
2460 }
2461
2462 #[rstest]
2463 fn test_from_seconds_millis_and_micros_consistency() {
2464 assert_eq!(UnixNanos::from_seconds(1), UnixNanos::from_millis(1_000));
2465 assert_eq!(
2466 UnixNanos::from_seconds(60),
2467 UnixNanos::from_micros(60_000_000)
2468 );
2469 assert_eq!(
2470 UnixNanos::from_millis(1_000),
2471 UnixNanos::from_micros(1_000_000)
2472 );
2473 assert_eq!(
2474 UnixNanos::from_millis(60_000),
2475 UnixNanos::from_micros(60_000_000)
2476 );
2477 }
2478
2479 #[rstest]
2480 fn test_from_millis_round_trip_to_datetime() {
2481 let ms = 1_707_577_123_456_u64;
2482 let nanos = UnixNanos::from_millis(ms);
2483 let dt = nanos.to_datetime_utc();
2484 assert_eq!(dt.as_millisecond().cast_unsigned(), ms);
2485 }
2486
2487 #[rstest]
2488 fn test_from_micros_preserves_sub_millisecond() {
2489 let micros = 1_700_000_000_000_123_u64;
2490 let nanos = UnixNanos::from_micros(micros);
2491 assert_eq!(nanos.as_u64() % 1_000_000, 123_000);
2492 }
2493
2494 #[rstest]
2495 #[should_panic(expected = "UnixNanos overflow in from_seconds")]
2496 fn test_from_seconds_overflow_panics() {
2497 let _ = UnixNanos::from_seconds(u64::MAX / 1_000_000_000 + 1);
2498 }
2499
2500 #[rstest]
2501 #[should_panic(expected = "UnixNanos overflow in from_millis")]
2502 fn test_from_millis_overflow_panics() {
2503 let _ = UnixNanos::from_millis(u64::MAX / 1_000_000 + 1);
2504 }
2505
2506 #[rstest]
2507 #[should_panic(expected = "UnixNanos overflow in from_micros")]
2508 fn test_from_micros_overflow_panics() {
2509 let _ = UnixNanos::from_micros(u64::MAX / 1_000 + 1);
2510 }
2511
2512 #[rstest]
2513 #[case("0000000000000000")]
2514 #[case("1234567890123456")]
2515 #[case("01234567890123456")]
2516 #[case("12345678901234567")]
2517 #[case("012345678901234567")]
2518 #[case("123456789012345678")]
2519 #[case("1234567890123456789")]
2520 #[case("9876543210987654321")]
2521 #[case("9999999999999999999")]
2522 fn test_long_numeric_timestamp_matches_integer_parser(#[case] input: &str) {
2523 assert_eq!(
2524 input.parse::<UnixNanos>().unwrap().as_u64(),
2525 input.parse::<u64>().unwrap()
2526 );
2527 }
2528
2529 #[rstest]
2530 #[case("1.500000000000000", 1_500_000_000)]
2531 #[case("00000000000015e-1", 1_500_000_000)]
2532 #[case("+000000000000123", 123)]
2533 #[case("18446744073709551615", u64::MAX)]
2534 #[case("1970-1-2", 86_400_000_000_000)]
2535 #[case("1970-01-01t00:00:01Z", 1_000_000_000)]
2536 fn test_timestamp_parser_preserves_fallback_routes(#[case] input: &str, #[case] expected: u64) {
2537 assert_eq!(input.parse::<UnixNanos>().unwrap().as_u64(), expected);
2538 }
2539
2540 #[rstest]
2541 #[case("18446744073709551616", "Unix timestamp is out of range")]
2542 #[case(
2543 "1970-01-01T00:00:00Z[UTC]",
2544 "Invalid format: 1970-01-01T00:00:00Z[UTC]"
2545 )]
2546 #[case("123456789012345x", "Invalid format: 123456789012345x")]
2547 fn test_timestamp_parser_preserves_errors(#[case] input: &str, #[case] expected: &str) {
2548 assert_eq!(
2549 input.parse::<UnixNanos>().unwrap_err().to_string(),
2550 expected
2551 );
2552 }
2553
2554 proptest! {
2555 #[rstest]
2556 fn prop_long_timestamp_digits_match_integer_parser(input in "[0-9]{16,19}") {
2557 prop_assert_eq!(input.parse::<UnixNanos>().unwrap().as_u64(), input.parse::<u64>().unwrap());
2558 }
2559 }
2560}