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