1#![allow(
32 clippy::cast_possible_truncation,
33 clippy::cast_sign_loss,
34 clippy::cast_precision_loss,
35 clippy::cast_possible_wrap
36)]
37use std::{
54 cmp::Ordering,
55 fmt::Display,
56 ops::{Add, AddAssign, Deref, Sub, SubAssign},
57 str::FromStr,
58};
59
60use chrono::{DateTime, NaiveDate, Utc};
61use serde::{
62 Deserialize, Deserializer, Serialize,
63 de::{self, Visitor},
64};
65
66pub type DurationNanos = u64;
68
69#[repr(C)]
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
72pub struct UnixNanos(u64);
73
74impl UnixNanos {
75 #[must_use]
77 pub const fn new(value: u64) -> Self {
78 Self(value)
79 }
80
81 #[must_use]
83 pub const fn max() -> Self {
84 Self(u64::MAX)
85 }
86
87 #[must_use]
89 pub const fn is_zero(&self) -> bool {
90 self.0 == 0
91 }
92
93 #[must_use]
95 pub const fn as_u64(&self) -> u64 {
96 self.0
97 }
98
99 #[must_use]
105 pub const fn as_i64(&self) -> i64 {
106 assert!(
107 self.0 <= i64::MAX as u64,
108 "UnixNanos value exceeds i64::MAX"
109 );
110 self.0 as i64
111 }
112
113 #[must_use]
115 pub const fn as_f64(&self) -> f64 {
116 self.0 as f64
117 }
118
119 #[must_use]
125 pub const fn to_datetime_utc(&self) -> DateTime<Utc> {
126 DateTime::from_timestamp_nanos(self.as_i64())
127 }
128
129 #[must_use]
131 pub fn to_rfc3339(&self) -> String {
132 self.to_datetime_utc().to_rfc3339()
133 }
134
135 #[must_use]
140 pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
141 self.0.checked_sub(other.0)
142 }
143
144 fn parse_string(s: &str) -> Result<Self, String> {
145 if let Ok(int_value) = s.parse::<u64>() {
147 return Ok(Self(int_value));
148 }
149
150 if s.chars().all(|c| c.is_ascii_digit()) {
156 return Err("Unix timestamp is out of range".into());
157 }
158
159 if let Ok(float_value) = s.parse::<f64>() {
161 if !float_value.is_finite() {
162 return Err("Unix timestamp must be finite".into());
163 }
164
165 if float_value < 0.0 {
166 return Err("Unix timestamp cannot be negative".into());
167 }
168
169 const MAX_NS_F64: f64 = u64::MAX as f64;
173 let nanos_f64 = float_value * 1_000_000_000.0;
174
175 if nanos_f64 > MAX_NS_F64 {
176 return Err("Unix timestamp is out of range".into());
177 }
178
179 let nanos = nanos_f64.round() as u64;
180 return Ok(Self(nanos));
181 }
182
183 if let Ok(datetime) = DateTime::parse_from_rfc3339(s) {
185 let nanos = datetime
186 .timestamp_nanos_opt()
187 .ok_or_else(|| "Timestamp out of range".to_string())?;
188
189 if nanos < 0 {
190 return Err("Unix timestamp cannot be negative".into());
191 }
192
193 return Ok(Self(nanos as u64));
195 }
196
197 if let Ok(datetime) = NaiveDate::parse_from_str(s, "%Y-%m-%d")
199 .map(|date| date.and_hms_opt(0, 0, 0).unwrap())
202 .map(|naive_dt| DateTime::<Utc>::from_naive_utc_and_offset(naive_dt, Utc))
203 {
204 let nanos = datetime
205 .timestamp_nanos_opt()
206 .ok_or_else(|| "Timestamp out of range".to_string())?;
207 if nanos < 0 {
208 return Err("Unix timestamp cannot be negative".into());
209 }
210 return Ok(Self(nanos as u64));
211 }
212
213 Err(format!("Invalid format: {s}"))
214 }
215
216 #[must_use]
218 pub fn checked_add<T: Into<u64>>(self, rhs: T) -> Option<Self> {
219 self.0.checked_add(rhs.into()).map(Self)
220 }
221
222 #[must_use]
224 pub fn checked_sub<T: Into<u64>>(self, rhs: T) -> Option<Self> {
225 self.0.checked_sub(rhs.into()).map(Self)
226 }
227
228 #[must_use]
230 pub fn saturating_add_ns<T: Into<u64>>(self, rhs: T) -> Self {
231 Self(self.0.saturating_add(rhs.into()))
232 }
233
234 #[must_use]
236 pub fn saturating_sub_ns<T: Into<u64>>(self, rhs: T) -> Self {
237 Self(self.0.saturating_sub(rhs.into()))
238 }
239}
240
241impl Deref for UnixNanos {
242 type Target = u64;
243
244 fn deref(&self) -> &Self::Target {
245 &self.0
246 }
247}
248
249impl PartialEq<u64> for UnixNanos {
250 fn eq(&self, other: &u64) -> bool {
251 self.0 == *other
252 }
253}
254
255impl PartialOrd<u64> for UnixNanos {
256 fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
257 self.0.partial_cmp(other)
258 }
259}
260
261impl PartialEq<Option<u64>> for UnixNanos {
262 fn eq(&self, other: &Option<u64>) -> bool {
263 match other {
264 Some(value) => self.0 == *value,
265 None => false,
266 }
267 }
268}
269
270impl PartialOrd<Option<u64>> for UnixNanos {
271 fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
272 match other {
273 Some(value) => self.0.partial_cmp(value),
274 None => Some(Ordering::Greater),
275 }
276 }
277}
278
279impl PartialEq<UnixNanos> for u64 {
280 fn eq(&self, other: &UnixNanos) -> bool {
281 *self == other.0
282 }
283}
284
285impl PartialOrd<UnixNanos> for u64 {
286 fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
287 self.partial_cmp(&other.0)
288 }
289}
290
291impl From<u64> for UnixNanos {
292 fn from(value: u64) -> Self {
293 Self(value)
294 }
295}
296
297impl From<UnixNanos> for u64 {
298 fn from(value: UnixNanos) -> Self {
299 value.0
300 }
301}
302
303impl From<&str> for UnixNanos {
323 fn from(value: &str) -> Self {
324 value
325 .parse()
326 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
327 }
328}
329
330impl From<String> for UnixNanos {
341 fn from(value: String) -> Self {
342 value
343 .parse()
344 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
345 }
346}
347
348impl From<DateTime<Utc>> for UnixNanos {
349 fn from(value: DateTime<Utc>) -> Self {
350 let nanos = value
351 .timestamp_nanos_opt()
352 .expect("DateTime timestamp out of range for UnixNanos");
353
354 assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
355
356 Self::from(nanos as u64)
357 }
358}
359
360impl FromStr for UnixNanos {
361 type Err = Box<dyn std::error::Error>;
362
363 fn from_str(s: &str) -> Result<Self, Self::Err> {
364 Self::parse_string(s).map_err(std::convert::Into::into)
365 }
366}
367
368impl Add for UnixNanos {
377 type Output = Self;
378
379 fn add(self, rhs: Self) -> Self::Output {
380 Self(
381 self.0
382 .checked_add(rhs.0)
383 .expect("UnixNanos overflow in addition - invalid timestamp calculation"),
384 )
385 }
386}
387
388impl Sub for UnixNanos {
397 type Output = Self;
398
399 fn sub(self, rhs: Self) -> Self::Output {
400 Self(
401 self.0
402 .checked_sub(rhs.0)
403 .expect("UnixNanos underflow in subtraction - invalid timestamp calculation"),
404 )
405 }
406}
407
408impl Add<u64> for UnixNanos {
415 type Output = Self;
416
417 fn add(self, rhs: u64) -> Self::Output {
418 Self(
419 self.0
420 .checked_add(rhs)
421 .expect("UnixNanos overflow in addition"),
422 )
423 }
424}
425
426impl Sub<u64> for UnixNanos {
433 type Output = Self;
434
435 fn sub(self, rhs: u64) -> Self::Output {
436 Self(
437 self.0
438 .checked_sub(rhs)
439 .expect("UnixNanos underflow in subtraction"),
440 )
441 }
442}
443
444impl<T: Into<u64>> AddAssign<T> for UnixNanos {
450 fn add_assign(&mut self, other: T) {
451 let other_u64 = other.into();
452 self.0 = self
453 .0
454 .checked_add(other_u64)
455 .expect("UnixNanos overflow in add_assign");
456 }
457}
458
459impl<T: Into<u64>> SubAssign<T> for UnixNanos {
465 fn sub_assign(&mut self, other: T) {
466 let other_u64 = other.into();
467 self.0 = self
468 .0
469 .checked_sub(other_u64)
470 .expect("UnixNanos underflow in sub_assign");
471 }
472}
473
474impl Display for UnixNanos {
475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476 write!(f, "{}", self.0)
477 }
478}
479
480impl From<UnixNanos> for DateTime<Utc> {
481 fn from(value: UnixNanos) -> Self {
482 value.to_datetime_utc()
483 }
484}
485
486impl<'de> Deserialize<'de> for UnixNanos {
487 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
488 where
489 D: Deserializer<'de>,
490 {
491 struct UnixNanosVisitor;
492
493 impl Visitor<'_> for UnixNanosVisitor {
494 type Value = UnixNanos;
495
496 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
497 formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
498 }
499
500 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
501 where
502 E: de::Error,
503 {
504 Ok(UnixNanos(value))
505 }
506
507 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
508 where
509 E: de::Error,
510 {
511 if value < 0 {
512 return Err(E::custom("Unix timestamp cannot be negative"));
513 }
514 Ok(UnixNanos(value as u64))
515 }
516
517 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
518 where
519 E: de::Error,
520 {
521 if !value.is_finite() {
522 return Err(E::custom(format!(
523 "Unix timestamp must be finite, got {}",
524 value
525 )));
526 }
527 if value < 0.0 {
528 return Err(E::custom("Unix timestamp cannot be negative"));
529 }
530 const MAX_NS_F64: f64 = u64::MAX as f64;
532 let nanos_f64 = value * 1_000_000_000.0;
533 if nanos_f64 > MAX_NS_F64 {
534 return Err(E::custom(format!(
535 "Unix timestamp {} seconds is out of range",
536 value
537 )));
538 }
539 let nanos = nanos_f64.round() as u64;
540 Ok(UnixNanos(nanos))
541 }
542
543 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
544 where
545 E: de::Error,
546 {
547 UnixNanos::parse_string(value).map_err(E::custom)
548 }
549 }
550
551 deserializer.deserialize_any(UnixNanosVisitor)
552 }
553}
554
555#[cfg(test)]
559mod tests {
560 use chrono::{Duration, TimeZone};
561 use rstest::rstest;
562
563 use super::*;
564
565 #[rstest]
566 fn test_new() {
567 let nanos = UnixNanos::new(123);
568 assert_eq!(nanos.as_u64(), 123);
569 assert_eq!(nanos.as_i64(), 123);
570 }
571
572 #[rstest]
573 fn test_max() {
574 let nanos = UnixNanos::max();
575 assert_eq!(nanos.as_u64(), u64::MAX);
576 }
577
578 #[rstest]
579 fn test_is_zero() {
580 assert!(UnixNanos::default().is_zero());
581 assert!(!UnixNanos::max().is_zero());
582 }
583
584 #[rstest]
585 fn test_from_u64() {
586 let nanos = UnixNanos::from(123);
587 assert_eq!(nanos.as_u64(), 123);
588 assert_eq!(nanos.as_i64(), 123);
589 }
590
591 #[rstest]
592 fn test_default() {
593 let nanos = UnixNanos::default();
594 assert_eq!(nanos.as_u64(), 0);
595 assert_eq!(nanos.as_i64(), 0);
596 }
597
598 #[rstest]
599 fn test_into_from() {
600 let nanos: UnixNanos = 456.into();
601 let value: u64 = nanos.into();
602 assert_eq!(value, 456);
603 }
604
605 #[rstest]
606 #[case(0, "1970-01-01T00:00:00+00:00")]
607 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
608 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
609 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
610 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
611 fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
612 let nanos = UnixNanos::from(nanos);
613 let datetime = nanos.to_datetime_utc();
614 assert_eq!(datetime.to_rfc3339(), expected);
615 }
616
617 #[rstest]
618 #[case(0, "1970-01-01T00:00:00+00:00")]
619 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
620 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
621 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
622 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
623 fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
624 let nanos = UnixNanos::from(nanos);
625 assert_eq!(nanos.to_rfc3339(), expected);
626 }
627
628 #[rstest]
629 fn test_from_str() {
630 let nanos: UnixNanos = "123".parse().unwrap();
631 assert_eq!(nanos.as_u64(), 123);
632 }
633
634 #[rstest]
635 fn test_from_str_invalid() {
636 let result = "abc".parse::<UnixNanos>();
637 assert!(result.is_err());
638 }
639
640 #[rstest]
641 fn test_from_str_pre_epoch_date() {
642 let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
643 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
644 }
645
646 #[rstest]
647 fn test_from_str_pre_epoch_rfc3339() {
648 let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
649 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
650 }
651
652 #[rstest]
653 fn test_try_from_datetime_valid() {
654 use chrono::TimeZone;
655 let datetime = Utc.timestamp_opt(1_000_000_000, 0).unwrap(); let nanos = UnixNanos::from(datetime);
657 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
658 }
659
660 #[rstest]
661 fn test_eq() {
662 let nanos = UnixNanos::from(100);
663 assert_eq!(nanos, 100);
664 assert_eq!(nanos, Some(100));
665 assert_ne!(nanos, 200);
666 assert_ne!(nanos, Some(200));
667 assert_ne!(nanos, None);
668 }
669
670 #[rstest]
671 fn test_partial_cmp() {
672 let nanos = UnixNanos::from(100);
673 assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
674 assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
675 assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
676 assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
677 }
678
679 #[rstest]
680 fn test_edge_case_max_value() {
681 let nanos = UnixNanos::from(u64::MAX);
682 assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
683 }
684
685 #[rstest]
686 fn test_display() {
687 let nanos = UnixNanos::from(123);
688 assert_eq!(format!("{nanos}"), "123");
689 }
690
691 #[rstest]
692 fn test_addition() {
693 let nanos1 = UnixNanos::from(100);
694 let nanos2 = UnixNanos::from(200);
695 let result = nanos1 + nanos2;
696 assert_eq!(result.as_u64(), 300);
697 }
698
699 #[rstest]
700 fn test_add_assign() {
701 let mut nanos = UnixNanos::from(100);
702 nanos += 50_u64;
703 assert_eq!(nanos.as_u64(), 150);
704 }
705
706 #[rstest]
707 fn test_subtraction() {
708 let nanos1 = UnixNanos::from(200);
709 let nanos2 = UnixNanos::from(100);
710 let result = nanos1 - nanos2;
711 assert_eq!(result.as_u64(), 100);
712 }
713
714 #[rstest]
715 fn test_sub_assign() {
716 let mut nanos = UnixNanos::from(200);
717 nanos -= 50_u64;
718 assert_eq!(nanos.as_u64(), 150);
719 }
720
721 #[rstest]
722 #[should_panic(expected = "UnixNanos overflow")]
723 fn test_overflow_add() {
724 let nanos = UnixNanos::from(u64::MAX);
725 let _ = nanos + UnixNanos::from(1); }
727
728 #[rstest]
729 #[should_panic(expected = "UnixNanos overflow")]
730 fn test_overflow_add_u64() {
731 let nanos = UnixNanos::from(u64::MAX);
732 let _ = nanos + 1_u64; }
734
735 #[rstest]
736 #[should_panic(expected = "UnixNanos underflow")]
737 fn test_overflow_sub() {
738 let _ = UnixNanos::default() - UnixNanos::from(1); }
740
741 #[rstest]
742 #[should_panic(expected = "UnixNanos underflow")]
743 fn test_overflow_sub_u64() {
744 let _ = UnixNanos::default() - 1_u64; }
746
747 #[rstest]
748 #[case(100, 50, Some(50))]
749 #[case(1_000_000_000, 500_000_000, Some(500_000_000))]
750 #[case(u64::MAX, u64::MAX - 1, Some(1))]
751 #[case(50, 50, Some(0))]
752 #[case(50, 100, None)]
753 #[case(0, 1, None)]
754 fn test_duration_since(
755 #[case] time1: u64,
756 #[case] time2: u64,
757 #[case] expected: Option<DurationNanos>,
758 ) {
759 let nanos1 = UnixNanos::from(time1);
760 let nanos2 = UnixNanos::from(time2);
761 assert_eq!(nanos1.duration_since(&nanos2), expected);
762 }
763
764 #[rstest]
765 fn test_duration_since_same_moment() {
766 let moment = UnixNanos::from(1_707_577_123_456_789_000);
767 assert_eq!(moment.duration_since(&moment), Some(0));
768 }
769
770 #[rstest]
771 fn test_duration_since_chronological() {
772 let earlier = Utc.with_ymd_and_hms(2024, 2, 10, 12, 0, 0).unwrap();
774
775 let later = earlier
777 + Duration::hours(1)
778 + Duration::minutes(30)
779 + Duration::seconds(45)
780 + Duration::nanoseconds(500_000_000);
781
782 let earlier_nanos = UnixNanos::from(earlier);
783 let later_nanos = UnixNanos::from(later);
784
785 let expected_duration = 60 * 60 * 1_000_000_000 + 30 * 60 * 1_000_000_000 + 45 * 1_000_000_000 + 500_000_000; assert_eq!(
792 later_nanos.duration_since(&earlier_nanos),
793 Some(expected_duration)
794 );
795 assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
796 }
797
798 #[rstest]
799 fn test_duration_since_with_edge_cases() {
800 let max = UnixNanos::from(u64::MAX);
802 let smaller = UnixNanos::from(u64::MAX - 1000);
803
804 assert_eq!(max.duration_since(&smaller), Some(1000));
805 assert_eq!(smaller.duration_since(&max), None);
806
807 let min = UnixNanos::default(); let larger = UnixNanos::from(1000);
810
811 assert_eq!(min.duration_since(&min), Some(0));
812 assert_eq!(larger.duration_since(&min), Some(1000));
813 assert_eq!(min.duration_since(&larger), None);
814 }
815
816 #[rstest]
817 fn test_serde_json() {
818 let nanos = UnixNanos::from(123);
819 let json = serde_json::to_string(&nanos).unwrap();
820 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
821 assert_eq!(deserialized, nanos);
822 }
823
824 #[rstest]
825 fn test_serde_edge_cases() {
826 let nanos = UnixNanos::from(u64::MAX);
827 let json = serde_json::to_string(&nanos).unwrap();
828 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
829 assert_eq!(deserialized, nanos);
830 }
831
832 #[rstest]
833 #[case("123", 123)] #[case("1234.567", 1_234_567_000_000)] #[case("2024-02-10", 1_707_523_200_000_000_000)] #[case("2024-02-10T14: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) {
839 let parsed: UnixNanos = input.parse().unwrap();
840 assert_eq!(parsed.as_u64(), expected);
841 }
842
843 #[rstest]
844 #[case("abc")] #[case("not a timestamp")] #[case("2024-02-10 14:58:43")] fn test_from_str_invalid_formats(#[case] input: &str) {
848 let result = input.parse::<UnixNanos>();
849 assert!(result.is_err());
850 }
851
852 #[rstest]
853 fn test_from_str_integer_overflow() {
854 let input = "184467440737095516160";
856 let result = input.parse::<UnixNanos>();
857 assert!(result.is_err());
858 }
859
860 #[rstest]
863 fn test_checked_add_overflow_returns_none() {
864 let max = UnixNanos::from(u64::MAX);
865 assert_eq!(max.checked_add(1_u64), None);
866 }
867
868 #[rstest]
869 fn test_checked_sub_underflow_returns_none() {
870 let zero = UnixNanos::default();
871 assert_eq!(zero.checked_sub(1_u64), None);
872 }
873
874 #[rstest]
875 fn test_saturating_add_overflow() {
876 let max = UnixNanos::from(u64::MAX);
877 let result = max.saturating_add_ns(1_u64);
878 assert_eq!(result, UnixNanos::from(u64::MAX));
879 }
880
881 #[rstest]
882 fn test_saturating_sub_underflow() {
883 let zero = UnixNanos::default();
884 let result = zero.saturating_sub_ns(1_u64);
885 assert_eq!(result, UnixNanos::default());
886 }
887
888 #[rstest]
889 fn test_from_str_float_overflow() {
890 let input = "2e10"; let result = input.parse::<UnixNanos>();
893 assert!(result.is_err());
894 }
895
896 #[rstest]
897 fn test_deserialize_u64() {
898 let json = "123456789";
899 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
900 assert_eq!(deserialized.as_u64(), 123_456_789);
901 }
902
903 #[rstest]
904 fn test_deserialize_string_with_int() {
905 let json = "\"123456789\"";
906 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
907 assert_eq!(deserialized.as_u64(), 123_456_789);
908 }
909
910 #[rstest]
911 fn test_deserialize_float() {
912 let json = "1234.567";
913 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
914 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
915 }
916
917 #[rstest]
918 fn test_deserialize_string_with_float() {
919 let json = "\"1234.567\"";
920 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
921 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
922 }
923
924 #[rstest]
925 #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
926 #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
927 fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
928 let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
929 assert_eq!(deserialized.as_u64(), expected);
930 }
931
932 #[rstest]
933 fn test_deserialize_negative_int_fails() {
934 let json = "-123456789";
935 let result: Result<UnixNanos, _> = serde_json::from_str(json);
936 assert!(result.is_err());
937 }
938
939 #[rstest]
940 fn test_deserialize_negative_float_fails() {
941 let json = "-1234.567";
942 let result: Result<UnixNanos, _> = serde_json::from_str(json);
943 assert!(result.is_err());
944 }
945
946 #[rstest]
947 fn test_deserialize_nan_fails() {
948 use serde::de::{
950 IntoDeserializer,
951 value::{Error as ValueError, F64Deserializer},
952 };
953 let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
954 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
955 assert!(result.is_err());
956 assert!(result.unwrap_err().to_string().contains("must be finite"));
957 }
958
959 #[rstest]
960 fn test_deserialize_infinity_fails() {
961 use serde::de::{
962 IntoDeserializer,
963 value::{Error as ValueError, F64Deserializer},
964 };
965 let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
966 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
967 assert!(result.is_err());
968 assert!(result.unwrap_err().to_string().contains("must be finite"));
969 }
970
971 #[rstest]
972 fn test_deserialize_negative_infinity_fails() {
973 use serde::de::{
974 IntoDeserializer,
975 value::{Error as ValueError, F64Deserializer},
976 };
977 let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
978 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
979 assert!(result.is_err());
980 assert!(result.unwrap_err().to_string().contains("must be finite"));
981 }
982
983 #[rstest]
984 fn test_deserialize_overflow_float_fails() {
985 let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
988 assert!(result.is_err());
989 assert!(result.unwrap_err().to_string().contains("out of range"));
990 }
991
992 #[rstest]
993 fn test_deserialize_invalid_string_fails() {
994 let json = "\"not a timestamp\"";
995 let result: Result<UnixNanos, _> = serde_json::from_str(json);
996 assert!(result.is_err());
997 }
998
999 #[rstest]
1000 fn test_deserialize_edge_cases() {
1001 let json = "0";
1003 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1004 assert_eq!(deserialized.as_u64(), 0);
1005
1006 let json = "18446744073709551615"; let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1009 assert_eq!(deserialized.as_u64(), u64::MAX);
1010 }
1011
1012 #[rstest]
1013 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1014 fn test_as_i64_overflow_panics() {
1015 let nanos = UnixNanos::from(u64::MAX);
1016 let _ = nanos.as_i64(); }
1018
1019 use proptest::prelude::*;
1024
1025 fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
1026 prop_oneof![
1027 0u64..1_000_000u64,
1029 1_000_000u64..1_000_000_000_000u64,
1031 1_000_000_000_000u64..=i64::MAX as u64,
1033 Just(0u64),
1035 Just(1u64),
1036 Just(1_000_000_000u64), Just(1_000_000_000_000u64), Just(1_700_000_000_000_000_000u64), Just((i64::MAX / 2) as u64), ]
1041 .prop_map(UnixNanos::from)
1042 }
1043
1044 fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
1045 (unix_nanos_strategy(), unix_nanos_strategy())
1046 }
1047
1048 proptest! {
1049 #[rstest]
1050 fn prop_unix_nanos_construction_roundtrip(value in 0u64..=i64::MAX as u64) {
1051 let nanos = UnixNanos::from(value);
1052 prop_assert_eq!(nanos.as_u64(), value);
1053 prop_assert_eq!(nanos.as_f64(), value as f64);
1054
1055 if i64::try_from(value).is_ok() {
1057 prop_assert_eq!(nanos.as_i64(), value as i64);
1058 }
1059 }
1060
1061 #[rstest]
1062 fn prop_unix_nanos_addition_commutative(
1063 (nanos1, nanos2) in unix_nanos_pair_strategy()
1064 ) {
1065 if let (Some(sum1), Some(sum2)) = (
1067 nanos1.checked_add(nanos2.as_u64()),
1068 nanos2.checked_add(nanos1.as_u64())
1069 ) {
1070 prop_assert_eq!(sum1, sum2, "Addition should be commutative");
1071 }
1072 }
1073
1074 #[rstest]
1075 fn prop_unix_nanos_addition_associative(
1076 nanos1 in unix_nanos_strategy(),
1077 nanos2 in unix_nanos_strategy(),
1078 nanos3 in unix_nanos_strategy(),
1079 ) {
1080 if let (Some(sum1), Some(sum2)) = (
1082 nanos1.as_u64().checked_add(nanos2.as_u64()),
1083 nanos2.as_u64().checked_add(nanos3.as_u64())
1084 )
1085 && let (Some(left), Some(right)) = (
1086 sum1.checked_add(nanos3.as_u64()),
1087 nanos1.as_u64().checked_add(sum2)
1088 ) {
1089 let left_result = UnixNanos::from(left);
1090 let right_result = UnixNanos::from(right);
1091 prop_assert_eq!(left_result, right_result, "Addition should be associative");
1092 }
1093 }
1094
1095 #[rstest]
1096 fn prop_unix_nanos_subtraction_inverse(
1097 (nanos1, nanos2) in unix_nanos_pair_strategy()
1098 ) {
1099 if let Some(sum) = nanos1.checked_add(nanos2.as_u64()) {
1101 let diff = sum - nanos2;
1102 prop_assert_eq!(diff, nanos1, "Subtraction should be inverse of addition");
1103 }
1104 }
1105
1106 #[rstest]
1107 fn prop_unix_nanos_zero_identity(nanos in unix_nanos_strategy()) {
1108 let zero = UnixNanos::default();
1110 prop_assert_eq!(nanos + zero, nanos, "Zero should be additive identity");
1111 prop_assert_eq!(zero + nanos, nanos, "Zero should be additive identity (commutative)");
1112 prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
1113 }
1114
1115 #[rstest]
1116 fn prop_unix_nanos_ordering_consistency(
1117 (nanos1, nanos2) in unix_nanos_pair_strategy()
1118 ) {
1119 let eq = nanos1 == nanos2;
1121 let lt = nanos1 < nanos2;
1122 let gt = nanos1 > nanos2;
1123 let le = nanos1 <= nanos2;
1124 let ge = nanos1 >= nanos2;
1125
1126 let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
1128 prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
1129
1130 prop_assert_eq!(le, eq || lt, "<= should equal == || <");
1132 prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
1133 prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
1134 prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
1135 }
1136
1137 #[rstest]
1138 fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
1139 let string_repr = nanos.to_string();
1141 let parsed = UnixNanos::from_str(&string_repr);
1142 prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
1143 if let Ok(parsed_nanos) = parsed {
1144 prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
1145 }
1146 }
1147
1148 #[rstest]
1149 fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
1150 if i64::try_from(nanos.as_u64()).is_ok() {
1152 let datetime = nanos.to_datetime_utc();
1153 let converted_back = UnixNanos::from(datetime);
1154 prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
1155
1156 let rfc3339 = nanos.to_rfc3339();
1158 if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
1159 prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
1160 }
1161 }
1162 }
1163
1164 #[rstest]
1165 fn prop_unix_nanos_duration_since(
1166 (nanos1, nanos2) in unix_nanos_pair_strategy()
1167 ) {
1168 let duration = nanos1.duration_since(&nanos2);
1170
1171 if nanos1 >= nanos2 {
1172 prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
1174 if let Some(dur) = duration {
1175 prop_assert_eq!(dur, nanos1.as_u64() - nanos2.as_u64(),
1176 "Duration should equal the difference");
1177 prop_assert_eq!(nanos2 + dur, nanos1.as_u64(),
1178 "second + duration should equal first");
1179 }
1180 } else {
1181 prop_assert!(duration.is_none(), "Duration should be None when first < second");
1183 }
1184 }
1185
1186 #[rstest]
1187 fn prop_unix_nanos_checked_arithmetic(
1188 (nanos1, nanos2) in unix_nanos_pair_strategy()
1189 ) {
1190 let checked_add = nanos1.checked_add(nanos2.as_u64());
1192 let checked_sub = nanos1.checked_sub(nanos2.as_u64());
1193
1194 if let Some(sum) = checked_add
1196 && nanos1.as_u64().checked_add(nanos2.as_u64()).is_some() {
1197 prop_assert_eq!(sum, nanos1 + nanos2, "Checked add should match regular add when no overflow");
1198 }
1199
1200 if let Some(diff) = checked_sub
1202 && nanos1.as_u64() >= nanos2.as_u64() {
1203 prop_assert_eq!(diff, nanos1 - nanos2, "Checked sub should match regular sub when no underflow");
1204 }
1205 }
1206
1207 #[rstest]
1208 fn prop_unix_nanos_saturating_arithmetic(
1209 (nanos1, nanos2) in unix_nanos_pair_strategy()
1210 ) {
1211 let sat_add = nanos1.saturating_add_ns(nanos2.as_u64());
1213 let sat_sub = nanos1.saturating_sub_ns(nanos2.as_u64());
1214
1215 prop_assert!(sat_add >= nanos1, "Saturating add result should be >= first operand");
1217 prop_assert!(sat_add.as_u64() >= nanos2.as_u64(), "Saturating add result should be >= second operand");
1218
1219 prop_assert!(sat_sub <= nanos1, "Saturating sub result should be <= first operand");
1221
1222 if let Some(checked_sum) = nanos1.checked_add(nanos2.as_u64()) {
1224 prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
1225 } else {
1226 prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
1227 }
1228
1229 if let Some(checked_diff) = nanos1.checked_sub(nanos2.as_u64()) {
1230 prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
1231 } else {
1232 prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
1233 }
1234 }
1235 }
1236}