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::{
57 cmp::Ordering,
58 fmt::Display,
59 ops::{Add, AddAssign, Deref, Sub, SubAssign},
60 str::FromStr,
61 time::SystemTime,
62};
63
64use chrono::{DateTime, NaiveDate, Utc};
65use serde::{
66 Deserialize, Deserializer, Serialize,
67 de::{self, Visitor},
68};
69
70pub type DurationNanos = u64;
72
73#[repr(C)]
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
76pub struct UnixNanos(u64);
77
78impl UnixNanos {
79 #[must_use]
81 pub const fn new(value: u64) -> Self {
82 Self(value)
83 }
84
85 #[must_use]
87 pub const fn max() -> Self {
88 Self(u64::MAX)
89 }
90
91 #[must_use]
93 pub const fn is_zero(&self) -> bool {
94 self.0 == 0
95 }
96
97 #[must_use]
99 pub const fn as_u64(&self) -> u64 {
100 self.0
101 }
102
103 #[must_use]
109 pub const fn as_i64(&self) -> i64 {
110 assert!(
111 self.0 <= i64::MAX as u64,
112 "UnixNanos value exceeds i64::MAX"
113 );
114 self.0 as i64
115 }
116
117 #[must_use]
119 pub const fn as_f64(&self) -> f64 {
120 self.0 as f64
121 }
122
123 #[must_use]
129 pub const fn to_datetime_utc(&self) -> DateTime<Utc> {
130 DateTime::from_timestamp_nanos(self.as_i64())
131 }
132
133 #[must_use]
135 pub fn to_rfc3339(&self) -> String {
136 self.to_datetime_utc().to_rfc3339()
137 }
138
139 #[must_use]
144 pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
145 self.0.checked_sub(other.0)
146 }
147
148 fn parse_string(s: &str) -> Result<Self, String> {
149 const MAX_NS_F64: f64 = u64::MAX as f64;
150
151 if let Ok(int_value) = s.parse::<u64>() {
153 return Ok(Self(int_value));
154 }
155
156 if s.chars().all(|c| c.is_ascii_digit()) {
162 return Err("Unix timestamp is out of range".into());
163 }
164
165 if let Ok(float_value) = s.parse::<f64>() {
167 if !float_value.is_finite() {
168 return Err("Unix timestamp must be finite".into());
169 }
170
171 if float_value < 0.0 {
172 return Err("Unix timestamp cannot be negative".into());
173 }
174
175 let nanos_f64 = float_value * 1_000_000_000.0;
179
180 if nanos_f64 > MAX_NS_F64 {
181 return Err("Unix timestamp is out of range".into());
182 }
183
184 let nanos = nanos_f64.trunc() as u64;
185 return Ok(Self(nanos));
186 }
187
188 if let Ok(datetime) = DateTime::parse_from_rfc3339(s) {
190 let nanos = datetime
191 .timestamp_nanos_opt()
192 .ok_or_else(|| "Timestamp out of range".to_string())?;
193
194 if nanos < 0 {
195 return Err("Unix timestamp cannot be negative".into());
196 }
197
198 return Ok(Self(nanos as u64));
200 }
201
202 if let Ok(datetime) = NaiveDate::parse_from_str(s, "%Y-%m-%d")
204 .map(|date| date.and_hms_opt(0, 0, 0).unwrap())
207 .map(|naive_dt| DateTime::<Utc>::from_naive_utc_and_offset(naive_dt, Utc))
208 {
209 let nanos = datetime
210 .timestamp_nanos_opt()
211 .ok_or_else(|| "Timestamp out of range".to_string())?;
212 if nanos < 0 {
213 return Err("Unix timestamp cannot be negative".into());
214 }
215 return Ok(Self(nanos as u64));
216 }
217
218 Err(format!("Invalid format: {s}"))
219 }
220
221 #[must_use]
223 pub fn checked_add<T: Into<u64>>(self, rhs: T) -> Option<Self> {
224 self.0.checked_add(rhs.into()).map(Self)
225 }
226
227 #[must_use]
229 pub fn checked_sub<T: Into<u64>>(self, rhs: T) -> Option<Self> {
230 self.0.checked_sub(rhs.into()).map(Self)
231 }
232
233 #[must_use]
235 pub fn saturating_add_ns<T: Into<u64>>(self, rhs: T) -> Self {
236 Self(self.0.saturating_add(rhs.into()))
237 }
238
239 #[must_use]
241 pub fn saturating_sub_ns<T: Into<u64>>(self, rhs: T) -> Self {
242 Self(self.0.saturating_sub(rhs.into()))
243 }
244}
245
246impl Deref for UnixNanos {
247 type Target = u64;
248
249 fn deref(&self) -> &Self::Target {
250 &self.0
251 }
252}
253
254impl PartialEq<u64> for UnixNanos {
255 fn eq(&self, other: &u64) -> bool {
256 self.0 == *other
257 }
258}
259
260impl PartialOrd<u64> for UnixNanos {
261 fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
262 self.0.partial_cmp(other)
263 }
264}
265
266impl PartialEq<Option<u64>> for UnixNanos {
267 fn eq(&self, other: &Option<u64>) -> bool {
268 match other {
269 Some(value) => self.0 == *value,
270 None => false,
271 }
272 }
273}
274
275impl PartialOrd<Option<u64>> for UnixNanos {
276 fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
277 match other {
278 Some(value) => self.0.partial_cmp(value),
279 None => Some(Ordering::Greater),
280 }
281 }
282}
283
284impl PartialEq<UnixNanos> for u64 {
285 fn eq(&self, other: &UnixNanos) -> bool {
286 *self == other.0
287 }
288}
289
290impl PartialOrd<UnixNanos> for u64 {
291 fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
292 self.partial_cmp(&other.0)
293 }
294}
295
296impl From<u64> for UnixNanos {
297 fn from(value: u64) -> Self {
298 Self(value)
299 }
300}
301
302impl From<UnixNanos> for u64 {
303 fn from(value: UnixNanos) -> Self {
304 value.0
305 }
306}
307
308impl From<&str> for UnixNanos {
319 fn from(value: &str) -> Self {
320 value
321 .parse()
322 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
323 }
324}
325
326impl From<String> for UnixNanos {
337 fn from(value: String) -> Self {
338 value
339 .parse()
340 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
341 }
342}
343
344impl From<DateTime<Utc>> for UnixNanos {
345 fn from(value: DateTime<Utc>) -> Self {
346 let nanos = value
347 .timestamp_nanos_opt()
348 .expect("DateTime timestamp out of range for UnixNanos");
349
350 assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
351
352 Self::from(nanos as u64)
353 }
354}
355
356impl From<SystemTime> for UnixNanos {
357 fn from(value: SystemTime) -> Self {
358 let duration = value
359 .duration_since(std::time::UNIX_EPOCH)
360 .expect("SystemTime before UNIX EPOCH");
361
362 let nanos = duration.as_nanos();
363 assert!(
364 nanos <= u64::MAX as u128,
365 "SystemTime overflowed u64 nanoseconds"
366 );
367
368 Self::from(nanos as u64)
369 }
370}
371
372impl FromStr for UnixNanos {
373 type Err = Box<dyn std::error::Error>;
374
375 fn from_str(s: &str) -> Result<Self, Self::Err> {
376 Self::parse_string(s).map_err(std::convert::Into::into)
377 }
378}
379
380impl Add for UnixNanos {
389 type Output = Self;
390
391 fn add(self, rhs: Self) -> Self::Output {
392 Self(
393 self.0
394 .checked_add(rhs.0)
395 .expect("UnixNanos overflow in addition - invalid timestamp calculation"),
396 )
397 }
398}
399
400impl Sub for UnixNanos {
409 type Output = Self;
410
411 fn sub(self, rhs: Self) -> Self::Output {
412 Self(
413 self.0
414 .checked_sub(rhs.0)
415 .expect("UnixNanos underflow in subtraction - invalid timestamp calculation"),
416 )
417 }
418}
419
420impl Add<u64> for UnixNanos {
427 type Output = Self;
428
429 fn add(self, rhs: u64) -> Self::Output {
430 Self(
431 self.0
432 .checked_add(rhs)
433 .expect("UnixNanos overflow in addition"),
434 )
435 }
436}
437
438impl Sub<u64> for UnixNanos {
445 type Output = Self;
446
447 fn sub(self, rhs: u64) -> Self::Output {
448 Self(
449 self.0
450 .checked_sub(rhs)
451 .expect("UnixNanos underflow in subtraction"),
452 )
453 }
454}
455
456impl<T: Into<u64>> AddAssign<T> for UnixNanos {
462 fn add_assign(&mut self, other: T) {
463 let other_u64 = other.into();
464 self.0 = self
465 .0
466 .checked_add(other_u64)
467 .expect("UnixNanos overflow in add_assign");
468 }
469}
470
471impl<T: Into<u64>> SubAssign<T> for UnixNanos {
477 fn sub_assign(&mut self, other: T) {
478 let other_u64 = other.into();
479 self.0 = self
480 .0
481 .checked_sub(other_u64)
482 .expect("UnixNanos underflow in sub_assign");
483 }
484}
485
486impl Display for UnixNanos {
487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488 write!(f, "{}", self.0)
489 }
490}
491
492impl From<UnixNanos> for DateTime<Utc> {
493 fn from(value: UnixNanos) -> Self {
494 value.to_datetime_utc()
495 }
496}
497
498impl<'de> Deserialize<'de> for UnixNanos {
499 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
500 where
501 D: Deserializer<'de>,
502 {
503 struct UnixNanosVisitor;
504
505 impl Visitor<'_> for UnixNanosVisitor {
506 type Value = UnixNanos;
507
508 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
509 formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
510 }
511
512 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
513 where
514 E: de::Error,
515 {
516 Ok(UnixNanos(value))
517 }
518
519 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
520 where
521 E: de::Error,
522 {
523 if value < 0 {
524 return Err(E::custom("Unix timestamp cannot be negative"));
525 }
526 Ok(UnixNanos(value as u64))
527 }
528
529 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
530 where
531 E: de::Error,
532 {
533 const MAX_NS_F64: f64 = u64::MAX as f64;
534
535 if !value.is_finite() {
536 return Err(E::custom(format!(
537 "Unix timestamp must be finite, was {value}"
538 )));
539 }
540 if value < 0.0 {
541 return Err(E::custom("Unix timestamp cannot be negative"));
542 }
543
544 let nanos_f64 = value * 1_000_000_000.0;
546 if nanos_f64 > MAX_NS_F64 {
547 return Err(E::custom(format!(
548 "Unix timestamp {value} seconds is out of range"
549 )));
550 }
551 let nanos = nanos_f64.trunc() as u64;
552 Ok(UnixNanos(nanos))
553 }
554
555 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
556 where
557 E: de::Error,
558 {
559 UnixNanos::parse_string(value).map_err(E::custom)
560 }
561 }
562
563 deserializer.deserialize_any(UnixNanosVisitor)
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use chrono::{Duration, TimeZone};
570 use rstest::rstest;
571
572 use super::*;
573
574 #[rstest]
575 fn test_new() {
576 let nanos = UnixNanos::new(123);
577 assert_eq!(nanos.as_u64(), 123);
578 assert_eq!(nanos.as_i64(), 123);
579 }
580
581 #[rstest]
582 fn test_max() {
583 let nanos = UnixNanos::max();
584 assert_eq!(nanos.as_u64(), u64::MAX);
585 }
586
587 #[rstest]
588 fn test_is_zero() {
589 assert!(UnixNanos::default().is_zero());
590 assert!(!UnixNanos::max().is_zero());
591 }
592
593 #[rstest]
594 fn test_from_u64() {
595 let nanos = UnixNanos::from(123);
596 assert_eq!(nanos.as_u64(), 123);
597 assert_eq!(nanos.as_i64(), 123);
598 }
599
600 #[rstest]
601 fn test_default() {
602 let nanos = UnixNanos::default();
603 assert_eq!(nanos.as_u64(), 0);
604 assert_eq!(nanos.as_i64(), 0);
605 }
606
607 #[rstest]
608 fn test_into_from() {
609 let nanos: UnixNanos = 456.into();
610 let value: u64 = nanos.into();
611 assert_eq!(value, 456);
612 }
613
614 #[rstest]
615 #[case(0, "1970-01-01T00:00:00+00:00")]
616 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
617 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
618 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
619 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
620 fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
621 let nanos = UnixNanos::from(nanos);
622 let datetime = nanos.to_datetime_utc();
623 assert_eq!(datetime.to_rfc3339(), expected);
624 }
625
626 #[rstest]
627 #[case(0, "1970-01-01T00:00:00+00:00")]
628 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
629 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
630 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
631 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
632 fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
633 let nanos = UnixNanos::from(nanos);
634 assert_eq!(nanos.to_rfc3339(), expected);
635 }
636
637 #[rstest]
638 fn test_from_str() {
639 let nanos: UnixNanos = "123".parse().unwrap();
640 assert_eq!(nanos.as_u64(), 123);
641 }
642
643 #[rstest]
644 fn test_from_str_invalid() {
645 let result = "abc".parse::<UnixNanos>();
646 assert!(result.is_err());
647 }
648
649 #[rstest]
650 fn test_from_str_pre_epoch_date() {
651 let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
652 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
653 }
654
655 #[rstest]
656 fn test_from_str_pre_epoch_rfc3339() {
657 let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
658 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
659 }
660
661 #[rstest]
662 fn test_try_from_datetime_valid() {
663 use chrono::TimeZone;
664 let datetime = Utc.timestamp_opt(1_000_000_000, 0).unwrap(); let nanos = UnixNanos::from(datetime);
666 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
667 }
668
669 #[rstest]
670 fn test_from_system_time() {
671 let system_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
672 let nanos = UnixNanos::from(system_time);
673 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
674 }
675
676 #[rstest]
677 #[should_panic(expected = "SystemTime before UNIX EPOCH")]
678 fn test_from_system_time_before_epoch() {
679 let system_time = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
680 let _ = UnixNanos::from(system_time);
681 }
682
683 #[rstest]
684 fn test_eq() {
685 let nanos = UnixNanos::from(100);
686 assert_eq!(nanos, 100);
687 assert_eq!(nanos, Some(100));
688 assert_ne!(nanos, 200);
689 assert_ne!(nanos, Some(200));
690 assert_ne!(nanos, None);
691 }
692
693 #[rstest]
694 fn test_partial_cmp() {
695 let nanos = UnixNanos::from(100);
696 assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
697 assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
698 assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
699 assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
700 }
701
702 #[rstest]
703 fn test_edge_case_max_value() {
704 let nanos = UnixNanos::from(u64::MAX);
705 assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
706 }
707
708 #[rstest]
709 fn test_display() {
710 let nanos = UnixNanos::from(123);
711 assert_eq!(format!("{nanos}"), "123");
712 }
713
714 #[rstest]
715 fn test_addition() {
716 let nanos1 = UnixNanos::from(100);
717 let nanos2 = UnixNanos::from(200);
718 let result = nanos1 + nanos2;
719 assert_eq!(result.as_u64(), 300);
720 }
721
722 #[rstest]
723 fn test_add_assign() {
724 let mut nanos = UnixNanos::from(100);
725 nanos += 50_u64;
726 assert_eq!(nanos.as_u64(), 150);
727 }
728
729 #[rstest]
730 fn test_subtraction() {
731 let nanos1 = UnixNanos::from(200);
732 let nanos2 = UnixNanos::from(100);
733 let result = nanos1 - nanos2;
734 assert_eq!(result.as_u64(), 100);
735 }
736
737 #[rstest]
738 fn test_sub_assign() {
739 let mut nanos = UnixNanos::from(200);
740 nanos -= 50_u64;
741 assert_eq!(nanos.as_u64(), 150);
742 }
743
744 #[rstest]
745 #[should_panic(expected = "UnixNanos overflow")]
746 fn test_overflow_add() {
747 let nanos = UnixNanos::from(u64::MAX);
748 let _ = nanos + UnixNanos::from(1); }
750
751 #[rstest]
752 #[should_panic(expected = "UnixNanos overflow")]
753 fn test_overflow_add_u64() {
754 let nanos = UnixNanos::from(u64::MAX);
755 let _ = nanos + 1_u64; }
757
758 #[rstest]
759 #[should_panic(expected = "UnixNanos underflow")]
760 fn test_overflow_sub() {
761 let _ = UnixNanos::default() - UnixNanos::from(1); }
763
764 #[rstest]
765 #[should_panic(expected = "UnixNanos underflow")]
766 fn test_overflow_sub_u64() {
767 let _ = UnixNanos::default() - 1_u64; }
769
770 #[rstest]
771 #[case(100, 50, Some(50))]
772 #[case(1_000_000_000, 500_000_000, Some(500_000_000))]
773 #[case(u64::MAX, u64::MAX - 1, Some(1))]
774 #[case(50, 50, Some(0))]
775 #[case(50, 100, None)]
776 #[case(0, 1, None)]
777 fn test_duration_since(
778 #[case] time1: u64,
779 #[case] time2: u64,
780 #[case] expected: Option<DurationNanos>,
781 ) {
782 let nanos1 = UnixNanos::from(time1);
783 let nanos2 = UnixNanos::from(time2);
784 assert_eq!(nanos1.duration_since(&nanos2), expected);
785 }
786
787 #[rstest]
788 fn test_duration_since_same_moment() {
789 let moment = UnixNanos::from(1_707_577_123_456_789_000);
790 assert_eq!(moment.duration_since(&moment), Some(0));
791 }
792
793 #[rstest]
794 fn test_duration_since_chronological() {
795 let earlier = Utc.with_ymd_and_hms(2024, 2, 10, 12, 0, 0).unwrap();
797
798 let later = earlier
800 + Duration::hours(1)
801 + Duration::minutes(30)
802 + Duration::seconds(45)
803 + Duration::nanoseconds(500_000_000);
804
805 let earlier_nanos = UnixNanos::from(earlier);
806 let later_nanos = UnixNanos::from(later);
807
808 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!(
815 later_nanos.duration_since(&earlier_nanos),
816 Some(expected_duration)
817 );
818 assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
819 }
820
821 #[rstest]
822 fn test_duration_since_with_edge_cases() {
823 let max = UnixNanos::from(u64::MAX);
825 let smaller = UnixNanos::from(u64::MAX - 1000);
826
827 assert_eq!(max.duration_since(&smaller), Some(1000));
828 assert_eq!(smaller.duration_since(&max), None);
829
830 let min = UnixNanos::default(); let larger = UnixNanos::from(1000);
833
834 assert_eq!(min.duration_since(&min), Some(0));
835 assert_eq!(larger.duration_since(&min), Some(1000));
836 assert_eq!(min.duration_since(&larger), None);
837 }
838
839 #[rstest]
840 fn test_serde_json() {
841 let nanos = UnixNanos::from(123);
842 let json = serde_json::to_string(&nanos).unwrap();
843 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
844 assert_eq!(deserialized, nanos);
845 }
846
847 #[rstest]
848 fn test_serde_edge_cases() {
849 let nanos = UnixNanos::from(u64::MAX);
850 let json = serde_json::to_string(&nanos).unwrap();
851 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
852 assert_eq!(deserialized, nanos);
853 }
854
855 #[rstest]
856 #[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) {
862 let parsed: UnixNanos = input.parse().unwrap();
863 assert_eq!(parsed.as_u64(), expected);
864 }
865
866 #[rstest]
867 #[case("abc")] #[case("not a timestamp")] #[case("2024-02-10 14:58:43")] fn test_from_str_invalid_formats(#[case] input: &str) {
871 let result = input.parse::<UnixNanos>();
872 assert!(result.is_err());
873 }
874
875 #[rstest]
876 fn test_from_str_integer_overflow() {
877 let input = "184467440737095516160";
879 let result = input.parse::<UnixNanos>();
880 assert!(result.is_err());
881 }
882
883 #[rstest]
886 fn test_checked_add_overflow_returns_none() {
887 let max = UnixNanos::from(u64::MAX);
888 assert_eq!(max.checked_add(1_u64), None);
889 }
890
891 #[rstest]
892 fn test_checked_sub_underflow_returns_none() {
893 let zero = UnixNanos::default();
894 assert_eq!(zero.checked_sub(1_u64), None);
895 }
896
897 #[rstest]
898 fn test_saturating_add_overflow() {
899 let max = UnixNanos::from(u64::MAX);
900 let result = max.saturating_add_ns(1_u64);
901 assert_eq!(result, UnixNanos::from(u64::MAX));
902 }
903
904 #[rstest]
905 fn test_saturating_sub_underflow() {
906 let zero = UnixNanos::default();
907 let result = zero.saturating_sub_ns(1_u64);
908 assert_eq!(result, UnixNanos::default());
909 }
910
911 #[rstest]
912 fn test_from_str_float_overflow() {
913 let input = "2e10"; let result = input.parse::<UnixNanos>();
916 assert!(result.is_err());
917 }
918
919 #[rstest]
920 fn test_deserialize_u64() {
921 let json = "123456789";
922 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
923 assert_eq!(deserialized.as_u64(), 123_456_789);
924 }
925
926 #[rstest]
927 fn test_deserialize_string_with_int() {
928 let json = "\"123456789\"";
929 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
930 assert_eq!(deserialized.as_u64(), 123_456_789);
931 }
932
933 #[rstest]
934 fn test_deserialize_float() {
935 let json = "1234.567";
936 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
937 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
938 }
939
940 #[rstest]
941 fn test_deserialize_string_with_float() {
942 let json = "\"1234.567\"";
943 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
944 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
945 }
946
947 #[rstest]
948 fn test_deserialize_float_uses_truncation() {
949 let json = "0.9999999999";
951 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
952 assert_eq!(deserialized.as_u64(), 999_999_999); }
954
955 #[rstest]
956 #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
957 #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
958 fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
959 let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
960 assert_eq!(deserialized.as_u64(), expected);
961 }
962
963 #[rstest]
964 fn test_deserialize_negative_int_fails() {
965 let json = "-123456789";
966 let result: Result<UnixNanos, _> = serde_json::from_str(json);
967 assert!(result.is_err());
968 }
969
970 #[rstest]
971 fn test_deserialize_negative_float_fails() {
972 let json = "-1234.567";
973 let result: Result<UnixNanos, _> = serde_json::from_str(json);
974 assert!(result.is_err());
975 }
976
977 #[rstest]
978 fn test_deserialize_nan_fails() {
979 use serde::de::{
981 IntoDeserializer,
982 value::{Error as ValueError, F64Deserializer},
983 };
984 let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
985 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
986 assert!(result.is_err());
987 assert!(result.unwrap_err().to_string().contains("must be finite"));
988 }
989
990 #[rstest]
991 fn test_deserialize_infinity_fails() {
992 use serde::de::{
993 IntoDeserializer,
994 value::{Error as ValueError, F64Deserializer},
995 };
996 let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
997 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
998 assert!(result.is_err());
999 assert!(result.unwrap_err().to_string().contains("must be finite"));
1000 }
1001
1002 #[rstest]
1003 fn test_deserialize_negative_infinity_fails() {
1004 use serde::de::{
1005 IntoDeserializer,
1006 value::{Error as ValueError, F64Deserializer},
1007 };
1008 let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
1009 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1010 assert!(result.is_err());
1011 assert!(result.unwrap_err().to_string().contains("must be finite"));
1012 }
1013
1014 #[rstest]
1015 fn test_deserialize_overflow_float_fails() {
1016 let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
1019 assert!(result.is_err());
1020 assert!(result.unwrap_err().to_string().contains("out of range"));
1021 }
1022
1023 #[rstest]
1024 fn test_deserialize_invalid_string_fails() {
1025 let json = "\"not a timestamp\"";
1026 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1027 assert!(result.is_err());
1028 }
1029
1030 #[rstest]
1031 fn test_deserialize_edge_cases() {
1032 let json = "0";
1034 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1035 assert_eq!(deserialized.as_u64(), 0);
1036
1037 let json = "18446744073709551615"; let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1040 assert_eq!(deserialized.as_u64(), u64::MAX);
1041 }
1042
1043 #[rstest]
1044 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1045 fn test_as_i64_overflow_panics() {
1046 let nanos = UnixNanos::from(u64::MAX);
1047 let _ = nanos.as_i64(); }
1049
1050 use proptest::prelude::*;
1055
1056 fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
1057 prop_oneof![
1058 0u64..1_000_000u64,
1060 1_000_000u64..1_000_000_000_000u64,
1062 1_000_000_000_000u64..=i64::MAX as u64,
1064 Just(0u64),
1066 Just(1u64),
1067 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), ]
1072 .prop_map(UnixNanos::from)
1073 }
1074
1075 fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
1076 (unix_nanos_strategy(), unix_nanos_strategy())
1077 }
1078
1079 proptest! {
1080 #[rstest]
1081 fn prop_unix_nanos_construction_roundtrip(value in 0u64..=i64::MAX as u64) {
1082 let nanos = UnixNanos::from(value);
1083 prop_assert_eq!(nanos.as_u64(), value);
1084 prop_assert_eq!(nanos.as_f64(), value as f64);
1085
1086 if i64::try_from(value).is_ok() {
1088 prop_assert_eq!(nanos.as_i64(), value as i64);
1089 }
1090 }
1091
1092 #[rstest]
1093 fn prop_unix_nanos_addition_commutative(
1094 (nanos1, nanos2) in unix_nanos_pair_strategy()
1095 ) {
1096 if let (Some(sum1), Some(sum2)) = (
1098 nanos1.checked_add(nanos2.as_u64()),
1099 nanos2.checked_add(nanos1.as_u64())
1100 ) {
1101 prop_assert_eq!(sum1, sum2, "Addition should be commutative");
1102 }
1103 }
1104
1105 #[rstest]
1106 fn prop_unix_nanos_addition_associative(
1107 nanos1 in unix_nanos_strategy(),
1108 nanos2 in unix_nanos_strategy(),
1109 nanos3 in unix_nanos_strategy(),
1110 ) {
1111 if let (Some(sum1), Some(sum2)) = (
1113 nanos1.as_u64().checked_add(nanos2.as_u64()),
1114 nanos2.as_u64().checked_add(nanos3.as_u64())
1115 )
1116 && let (Some(left), Some(right)) = (
1117 sum1.checked_add(nanos3.as_u64()),
1118 nanos1.as_u64().checked_add(sum2)
1119 ) {
1120 let left_result = UnixNanos::from(left);
1121 let right_result = UnixNanos::from(right);
1122 prop_assert_eq!(left_result, right_result, "Addition should be associative");
1123 }
1124 }
1125
1126 #[rstest]
1127 fn prop_unix_nanos_subtraction_inverse(
1128 (nanos1, nanos2) in unix_nanos_pair_strategy()
1129 ) {
1130 if let Some(sum) = nanos1.checked_add(nanos2.as_u64()) {
1132 let diff = sum - nanos2;
1133 prop_assert_eq!(diff, nanos1, "Subtraction should be inverse of addition");
1134 }
1135 }
1136
1137 #[rstest]
1138 fn prop_unix_nanos_zero_identity(nanos in unix_nanos_strategy()) {
1139 let zero = UnixNanos::default();
1141 prop_assert_eq!(nanos + zero, nanos, "Zero should be additive identity");
1142 prop_assert_eq!(zero + nanos, nanos, "Zero should be additive identity (commutative)");
1143 prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
1144 }
1145
1146 #[rstest]
1147 fn prop_unix_nanos_ordering_consistency(
1148 (nanos1, nanos2) in unix_nanos_pair_strategy()
1149 ) {
1150 let eq = nanos1 == nanos2;
1152 let lt = nanos1 < nanos2;
1153 let gt = nanos1 > nanos2;
1154 let le = nanos1 <= nanos2;
1155 let ge = nanos1 >= nanos2;
1156
1157 let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
1159 prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
1160
1161 prop_assert_eq!(le, eq || lt, "<= should equal == || <");
1163 prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
1164 prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
1165 prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
1166 }
1167
1168 #[rstest]
1169 fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
1170 let string_repr = nanos.to_string();
1172 let parsed = UnixNanos::from_str(&string_repr);
1173 prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
1174 if let Ok(parsed_nanos) = parsed {
1175 prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
1176 }
1177 }
1178
1179 #[rstest]
1180 fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
1181 if i64::try_from(nanos.as_u64()).is_ok() {
1183 let datetime = nanos.to_datetime_utc();
1184 let converted_back = UnixNanos::from(datetime);
1185 prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
1186
1187 let rfc3339 = nanos.to_rfc3339();
1189 if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
1190 prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
1191 }
1192 }
1193 }
1194
1195 #[rstest]
1196 fn prop_unix_nanos_duration_since(
1197 (nanos1, nanos2) in unix_nanos_pair_strategy()
1198 ) {
1199 let duration = nanos1.duration_since(&nanos2);
1201
1202 if nanos1 >= nanos2 {
1203 prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
1205 if let Some(dur) = duration {
1206 prop_assert_eq!(dur, nanos1.as_u64() - nanos2.as_u64(),
1207 "Duration should equal the difference");
1208 prop_assert_eq!(nanos2 + dur, nanos1.as_u64(),
1209 "second + duration should equal first");
1210 }
1211 } else {
1212 prop_assert!(duration.is_none(), "Duration should be None when first < second");
1214 }
1215 }
1216
1217 #[rstest]
1218 fn prop_unix_nanos_checked_arithmetic(
1219 (nanos1, nanos2) in unix_nanos_pair_strategy()
1220 ) {
1221 let checked_add = nanos1.checked_add(nanos2.as_u64());
1223 let checked_sub = nanos1.checked_sub(nanos2.as_u64());
1224
1225 if let Some(sum) = checked_add
1227 && nanos1.as_u64().checked_add(nanos2.as_u64()).is_some() {
1228 prop_assert_eq!(sum, nanos1 + nanos2, "Checked add should match regular add when no overflow");
1229 }
1230
1231 if let Some(diff) = checked_sub
1233 && nanos1.as_u64() >= nanos2.as_u64() {
1234 prop_assert_eq!(diff, nanos1 - nanos2, "Checked sub should match regular sub when no underflow");
1235 }
1236 }
1237
1238 #[rstest]
1239 fn prop_unix_nanos_saturating_arithmetic(
1240 (nanos1, nanos2) in unix_nanos_pair_strategy()
1241 ) {
1242 let sat_add = nanos1.saturating_add_ns(nanos2.as_u64());
1244 let sat_sub = nanos1.saturating_sub_ns(nanos2.as_u64());
1245
1246 prop_assert!(sat_add >= nanos1, "Saturating add result should be >= first operand");
1248 prop_assert!(sat_add.as_u64() >= nanos2.as_u64(), "Saturating add result should be >= second operand");
1249
1250 prop_assert!(sat_sub <= nanos1, "Saturating sub result should be <= first operand");
1252
1253 if let Some(checked_sum) = nanos1.checked_add(nanos2.as_u64()) {
1255 prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
1256 } else {
1257 prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
1258 }
1259
1260 if let Some(checked_diff) = nanos1.checked_sub(nanos2.as_u64()) {
1261 prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
1262 } else {
1263 prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
1264 }
1265 }
1266 }
1267}