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