1#![expect(
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
70use crate::datetime::{NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND};
71
72pub type DurationNanos = u64;
74
75#[repr(C)]
77#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
78pub struct UnixNanos(u64);
79
80impl UnixNanos {
81 #[must_use]
83 pub const fn new(value: u64) -> Self {
84 Self(value)
85 }
86
87 #[must_use]
89 pub const fn max() -> Self {
90 Self(u64::MAX)
91 }
92
93 #[must_use]
95 pub const fn is_zero(&self) -> bool {
96 self.0 == 0
97 }
98
99 #[must_use]
101 pub const fn as_u64(&self) -> u64 {
102 self.0
103 }
104
105 #[must_use]
111 pub const fn from_millis(millis: u64) -> Self {
112 match millis.checked_mul(NANOSECONDS_IN_MILLISECOND) {
113 Some(nanos) => Self(nanos),
114 None => panic!("UnixNanos overflow in from_millis"),
115 }
116 }
117
118 #[must_use]
124 pub const fn from_micros(micros: u64) -> Self {
125 match micros.checked_mul(NANOSECONDS_IN_MICROSECOND) {
126 Some(nanos) => Self(nanos),
127 None => panic!("UnixNanos overflow in from_micros"),
128 }
129 }
130
131 #[must_use]
137 pub const fn as_i64(&self) -> i64 {
138 assert!(
139 self.0 <= i64::MAX as u64,
140 "UnixNanos value exceeds i64::MAX"
141 );
142 self.0 as i64
143 }
144
145 #[must_use]
147 pub const fn as_f64(&self) -> f64 {
148 self.0 as f64
149 }
150
151 #[must_use]
157 pub const fn to_datetime_utc(&self) -> DateTime<Utc> {
158 DateTime::from_timestamp_nanos(self.as_i64())
159 }
160
161 #[must_use]
163 pub fn to_rfc3339(&self) -> String {
164 self.to_datetime_utc().to_rfc3339()
165 }
166
167 #[must_use]
172 pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
173 self.0.checked_sub(other.0)
174 }
175
176 fn parse_string(s: &str) -> Result<Self, String> {
177 const MAX_NS_F64: f64 = u64::MAX as f64;
178
179 if let Ok(int_value) = s.parse::<u64>() {
181 return Ok(Self(int_value));
182 }
183
184 if s.chars().all(|c| c.is_ascii_digit()) {
190 return Err("Unix timestamp is out of range".into());
191 }
192
193 if let Ok(float_value) = s.parse::<f64>() {
195 if !float_value.is_finite() {
196 return Err("Unix timestamp must be finite".into());
197 }
198
199 if float_value < 0.0 {
200 return Err("Unix timestamp cannot be negative".into());
201 }
202
203 let nanos_f64 = float_value * 1_000_000_000.0;
207
208 if nanos_f64 > MAX_NS_F64 {
209 return Err("Unix timestamp is out of range".into());
210 }
211
212 let nanos = nanos_f64.trunc() as u64;
213 return Ok(Self(nanos));
214 }
215
216 if let Ok(datetime) = DateTime::parse_from_rfc3339(s) {
218 let nanos = datetime
219 .timestamp_nanos_opt()
220 .ok_or_else(|| "Timestamp out of range".to_string())?;
221
222 if nanos < 0 {
223 return Err("Unix timestamp cannot be negative".into());
224 }
225
226 return Ok(Self(nanos as u64));
228 }
229
230 if let Ok(datetime) = NaiveDate::parse_from_str(s, "%Y-%m-%d")
232 .map(|date| date.and_hms_opt(0, 0, 0).unwrap())
235 .map(|naive_dt| DateTime::<Utc>::from_naive_utc_and_offset(naive_dt, Utc))
236 {
237 let nanos = datetime
238 .timestamp_nanos_opt()
239 .ok_or_else(|| "Timestamp out of range".to_string())?;
240
241 if nanos < 0 {
242 return Err("Unix timestamp cannot be negative".into());
243 }
244 return Ok(Self(nanos as u64));
245 }
246
247 Err(format!("Invalid format: {s}"))
248 }
249
250 #[must_use]
252 pub fn checked_add<T: Into<u64>>(self, rhs: T) -> Option<Self> {
253 self.0.checked_add(rhs.into()).map(Self)
254 }
255
256 #[must_use]
258 pub fn checked_sub<T: Into<u64>>(self, rhs: T) -> Option<Self> {
259 self.0.checked_sub(rhs.into()).map(Self)
260 }
261
262 #[must_use]
264 pub fn saturating_add_ns<T: Into<u64>>(self, rhs: T) -> Self {
265 Self(self.0.saturating_add(rhs.into()))
266 }
267
268 #[must_use]
270 pub fn saturating_sub_ns<T: Into<u64>>(self, rhs: T) -> Self {
271 Self(self.0.saturating_sub(rhs.into()))
272 }
273}
274
275impl Deref for UnixNanos {
276 type Target = u64;
277
278 fn deref(&self) -> &Self::Target {
279 &self.0
280 }
281}
282
283impl PartialEq<u64> for UnixNanos {
284 fn eq(&self, other: &u64) -> bool {
285 self.0 == *other
286 }
287}
288
289impl PartialOrd<u64> for UnixNanos {
290 fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
291 self.0.partial_cmp(other)
292 }
293}
294
295impl PartialEq<Option<u64>> for UnixNanos {
296 fn eq(&self, other: &Option<u64>) -> bool {
297 match other {
298 Some(value) => self.0 == *value,
299 None => false,
300 }
301 }
302}
303
304impl PartialOrd<Option<u64>> for UnixNanos {
305 fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
306 match other {
307 Some(value) => self.0.partial_cmp(value),
308 None => Some(Ordering::Greater),
309 }
310 }
311}
312
313impl PartialEq<UnixNanos> for u64 {
314 fn eq(&self, other: &UnixNanos) -> bool {
315 *self == other.0
316 }
317}
318
319impl PartialOrd<UnixNanos> for u64 {
320 fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
321 self.partial_cmp(&other.0)
322 }
323}
324
325impl From<u64> for UnixNanos {
326 fn from(value: u64) -> Self {
327 Self(value)
328 }
329}
330
331impl From<UnixNanos> for u64 {
332 fn from(value: UnixNanos) -> Self {
333 value.0
334 }
335}
336
337impl From<&str> for UnixNanos {
348 fn from(value: &str) -> Self {
349 value
350 .parse()
351 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
352 }
353}
354
355impl From<String> for UnixNanos {
366 fn from(value: String) -> Self {
367 value
368 .parse()
369 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
370 }
371}
372
373impl From<DateTime<Utc>> for UnixNanos {
374 fn from(value: DateTime<Utc>) -> Self {
375 let nanos = value
376 .timestamp_nanos_opt()
377 .expect("DateTime timestamp out of range for UnixNanos");
378
379 assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
380
381 Self::from(nanos as u64)
382 }
383}
384
385impl From<SystemTime> for UnixNanos {
386 fn from(value: SystemTime) -> Self {
387 let duration = value
388 .duration_since(std::time::UNIX_EPOCH)
389 .expect("SystemTime before UNIX EPOCH");
390
391 let nanos = duration.as_nanos();
392 assert!(
393 nanos <= u128::from(u64::MAX),
394 "SystemTime overflowed u64 nanoseconds"
395 );
396
397 Self::from(nanos as u64)
398 }
399}
400
401impl FromStr for UnixNanos {
402 type Err = Box<dyn std::error::Error>;
403
404 fn from_str(s: &str) -> Result<Self, Self::Err> {
405 Self::parse_string(s).map_err(std::convert::Into::into)
406 }
407}
408
409impl Add for UnixNanos {
418 type Output = Self;
419
420 fn add(self, rhs: Self) -> Self::Output {
421 Self(
422 self.0
423 .checked_add(rhs.0)
424 .expect("UnixNanos overflow in addition - invalid timestamp calculation"),
425 )
426 }
427}
428
429impl Sub for UnixNanos {
438 type Output = Self;
439
440 fn sub(self, rhs: Self) -> Self::Output {
441 Self(
442 self.0
443 .checked_sub(rhs.0)
444 .expect("UnixNanos underflow in subtraction - invalid timestamp calculation"),
445 )
446 }
447}
448
449impl Add<u64> for UnixNanos {
456 type Output = Self;
457
458 fn add(self, rhs: u64) -> Self::Output {
459 Self(
460 self.0
461 .checked_add(rhs)
462 .expect("UnixNanos overflow in addition"),
463 )
464 }
465}
466
467impl Sub<u64> for UnixNanos {
474 type Output = Self;
475
476 fn sub(self, rhs: u64) -> Self::Output {
477 Self(
478 self.0
479 .checked_sub(rhs)
480 .expect("UnixNanos underflow in subtraction"),
481 )
482 }
483}
484
485impl<T: Into<u64>> AddAssign<T> for UnixNanos {
491 fn add_assign(&mut self, other: T) {
492 let other_u64 = other.into();
493 self.0 = self
494 .0
495 .checked_add(other_u64)
496 .expect("UnixNanos overflow in add_assign");
497 }
498}
499
500impl<T: Into<u64>> SubAssign<T> for UnixNanos {
506 fn sub_assign(&mut self, other: T) {
507 let other_u64 = other.into();
508 self.0 = self
509 .0
510 .checked_sub(other_u64)
511 .expect("UnixNanos underflow in sub_assign");
512 }
513}
514
515impl Display for UnixNanos {
516 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517 write!(f, "{}", self.0)
518 }
519}
520
521impl From<UnixNanos> for DateTime<Utc> {
522 fn from(value: UnixNanos) -> Self {
523 value.to_datetime_utc()
524 }
525}
526
527impl<'de> Deserialize<'de> for UnixNanos {
528 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
529 where
530 D: Deserializer<'de>,
531 {
532 struct UnixNanosVisitor;
533
534 impl Visitor<'_> for UnixNanosVisitor {
535 type Value = UnixNanos;
536
537 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
538 formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
539 }
540
541 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
542 where
543 E: de::Error,
544 {
545 Ok(UnixNanos(value))
546 }
547
548 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
549 where
550 E: de::Error,
551 {
552 if value < 0 {
553 return Err(E::custom("Unix timestamp cannot be negative"));
554 }
555 Ok(UnixNanos(value as u64))
556 }
557
558 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
559 where
560 E: de::Error,
561 {
562 const MAX_NS_F64: f64 = u64::MAX as f64;
563
564 if !value.is_finite() {
565 return Err(E::custom(format!(
566 "Unix timestamp must be finite, was {value}"
567 )));
568 }
569
570 if value < 0.0 {
571 return Err(E::custom("Unix timestamp cannot be negative"));
572 }
573
574 let nanos_f64 = value * 1_000_000_000.0;
576 if nanos_f64 > MAX_NS_F64 {
577 return Err(E::custom(format!(
578 "Unix timestamp {value} seconds is out of range"
579 )));
580 }
581 let nanos = nanos_f64.trunc() as u64;
582 Ok(UnixNanos(nanos))
583 }
584
585 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
586 where
587 E: de::Error,
588 {
589 UnixNanos::parse_string(value).map_err(E::custom)
590 }
591 }
592
593 deserializer.deserialize_any(UnixNanosVisitor)
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use chrono::{Duration, TimeZone};
600 use rstest::rstest;
601
602 use super::*;
603
604 #[rstest]
605 fn test_new() {
606 let nanos = UnixNanos::new(123);
607 assert_eq!(nanos.as_u64(), 123);
608 assert_eq!(nanos.as_i64(), 123);
609 }
610
611 #[rstest]
612 fn test_max() {
613 let nanos = UnixNanos::max();
614 assert_eq!(nanos.as_u64(), u64::MAX);
615 }
616
617 #[rstest]
618 fn test_is_zero() {
619 assert!(UnixNanos::default().is_zero());
620 assert!(!UnixNanos::max().is_zero());
621 }
622
623 #[rstest]
624 fn test_from_u64() {
625 let nanos = UnixNanos::from(123);
626 assert_eq!(nanos.as_u64(), 123);
627 assert_eq!(nanos.as_i64(), 123);
628 }
629
630 #[rstest]
631 fn test_default() {
632 let nanos = UnixNanos::default();
633 assert_eq!(nanos.as_u64(), 0);
634 assert_eq!(nanos.as_i64(), 0);
635 }
636
637 #[rstest]
638 fn test_into_from() {
639 let nanos: UnixNanos = 456.into();
640 let value: u64 = nanos.into();
641 assert_eq!(value, 456);
642 }
643
644 #[rstest]
645 #[case(0, "1970-01-01T00:00:00+00:00")]
646 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
647 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
648 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
649 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
650 fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
651 let nanos = UnixNanos::from(nanos);
652 let datetime = nanos.to_datetime_utc();
653 assert_eq!(datetime.to_rfc3339(), expected);
654 }
655
656 #[rstest]
657 #[case(0, "1970-01-01T00:00:00+00:00")]
658 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
659 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
660 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
661 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
662 fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
663 let nanos = UnixNanos::from(nanos);
664 assert_eq!(nanos.to_rfc3339(), expected);
665 }
666
667 #[rstest]
668 fn test_from_str() {
669 let nanos: UnixNanos = "123".parse().unwrap();
670 assert_eq!(nanos.as_u64(), 123);
671 }
672
673 #[rstest]
674 fn test_from_str_invalid() {
675 let result = "abc".parse::<UnixNanos>();
676 assert!(result.is_err());
677 }
678
679 #[rstest]
680 fn test_from_str_pre_epoch_date() {
681 let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
682 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
683 }
684
685 #[rstest]
686 fn test_from_str_pre_epoch_rfc3339() {
687 let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
688 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
689 }
690
691 #[rstest]
692 fn test_try_from_datetime_valid() {
693 use chrono::TimeZone;
694 let datetime = Utc.timestamp_opt(1_000_000_000, 0).unwrap(); let nanos = UnixNanos::from(datetime);
696 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
697 }
698
699 #[rstest]
700 fn test_from_system_time() {
701 let system_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
702 let nanos = UnixNanos::from(system_time);
703 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
704 }
705
706 #[rstest]
707 #[should_panic(expected = "SystemTime before UNIX EPOCH")]
708 fn test_from_system_time_before_epoch() {
709 let system_time = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
710 let _ = UnixNanos::from(system_time);
711 }
712
713 #[rstest]
714 fn test_eq() {
715 let nanos = UnixNanos::from(100);
716 assert_eq!(nanos, 100);
717 assert_eq!(nanos, Some(100));
718 assert_ne!(nanos, 200);
719 assert_ne!(nanos, Some(200));
720 assert_ne!(nanos, None);
721 }
722
723 #[rstest]
724 fn test_partial_cmp() {
725 let nanos = UnixNanos::from(100);
726 assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
727 assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
728 assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
729 assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
730 }
731
732 #[rstest]
733 fn test_edge_case_max_value() {
734 let nanos = UnixNanos::from(u64::MAX);
735 assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
736 }
737
738 #[rstest]
739 fn test_display() {
740 let nanos = UnixNanos::from(123);
741 assert_eq!(format!("{nanos}"), "123");
742 }
743
744 #[rstest]
745 fn test_addition() {
746 let nanos1 = UnixNanos::from(100);
747 let nanos2 = UnixNanos::from(200);
748 let result = nanos1 + nanos2;
749 assert_eq!(result.as_u64(), 300);
750 }
751
752 #[rstest]
753 fn test_add_assign() {
754 let mut nanos = UnixNanos::from(100);
755 nanos += 50_u64;
756 assert_eq!(nanos.as_u64(), 150);
757 }
758
759 #[rstest]
760 fn test_subtraction() {
761 let nanos1 = UnixNanos::from(200);
762 let nanos2 = UnixNanos::from(100);
763 let result = nanos1 - nanos2;
764 assert_eq!(result.as_u64(), 100);
765 }
766
767 #[rstest]
768 fn test_sub_assign() {
769 let mut nanos = UnixNanos::from(200);
770 nanos -= 50_u64;
771 assert_eq!(nanos.as_u64(), 150);
772 }
773
774 #[rstest]
775 #[should_panic(expected = "UnixNanos overflow")]
776 fn test_overflow_add() {
777 let nanos = UnixNanos::from(u64::MAX);
778 let _ = nanos + UnixNanos::from(1); }
780
781 #[rstest]
782 #[should_panic(expected = "UnixNanos overflow")]
783 fn test_overflow_add_u64() {
784 let nanos = UnixNanos::from(u64::MAX);
785 let _ = nanos + 1_u64; }
787
788 #[rstest]
789 #[should_panic(expected = "UnixNanos underflow")]
790 fn test_overflow_sub() {
791 let _ = UnixNanos::default() - UnixNanos::from(1); }
793
794 #[rstest]
795 #[should_panic(expected = "UnixNanos underflow")]
796 fn test_overflow_sub_u64() {
797 let _ = UnixNanos::default() - 1_u64; }
799
800 #[rstest]
801 #[case(100, 50, Some(50))]
802 #[case(1_000_000_000, 500_000_000, Some(500_000_000))]
803 #[case(u64::MAX, u64::MAX - 1, Some(1))]
804 #[case(50, 50, Some(0))]
805 #[case(50, 100, None)]
806 #[case(0, 1, None)]
807 fn test_duration_since(
808 #[case] time1: u64,
809 #[case] time2: u64,
810 #[case] expected: Option<DurationNanos>,
811 ) {
812 let nanos1 = UnixNanos::from(time1);
813 let nanos2 = UnixNanos::from(time2);
814 assert_eq!(nanos1.duration_since(&nanos2), expected);
815 }
816
817 #[rstest]
818 fn test_duration_since_same_moment() {
819 let moment = UnixNanos::from(1_707_577_123_456_789_000);
820 assert_eq!(moment.duration_since(&moment), Some(0));
821 }
822
823 #[rstest]
824 fn test_duration_since_chronological() {
825 let earlier = Utc.with_ymd_and_hms(2024, 2, 10, 12, 0, 0).unwrap();
827
828 let later = earlier
830 + Duration::hours(1)
831 + Duration::minutes(30)
832 + Duration::seconds(45)
833 + Duration::nanoseconds(500_000_000);
834
835 let earlier_nanos = UnixNanos::from(earlier);
836 let later_nanos = UnixNanos::from(later);
837
838 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!(
845 later_nanos.duration_since(&earlier_nanos),
846 Some(expected_duration)
847 );
848 assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
849 }
850
851 #[rstest]
852 fn test_duration_since_with_edge_cases() {
853 let max = UnixNanos::from(u64::MAX);
855 let smaller = UnixNanos::from(u64::MAX - 1000);
856
857 assert_eq!(max.duration_since(&smaller), Some(1000));
858 assert_eq!(smaller.duration_since(&max), None);
859
860 let min = UnixNanos::default(); let larger = UnixNanos::from(1000);
863
864 assert_eq!(min.duration_since(&min), Some(0));
865 assert_eq!(larger.duration_since(&min), Some(1000));
866 assert_eq!(min.duration_since(&larger), None);
867 }
868
869 #[rstest]
870 fn test_serde_json() {
871 let nanos = UnixNanos::from(123);
872 let json = serde_json::to_string(&nanos).unwrap();
873 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
874 assert_eq!(deserialized, nanos);
875 }
876
877 #[rstest]
878 fn test_serde_edge_cases() {
879 let nanos = UnixNanos::from(u64::MAX);
880 let json = serde_json::to_string(&nanos).unwrap();
881 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
882 assert_eq!(deserialized, nanos);
883 }
884
885 #[rstest]
886 #[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) {
892 let parsed: UnixNanos = input.parse().unwrap();
893 assert_eq!(parsed.as_u64(), expected);
894 }
895
896 #[rstest]
897 #[case("abc")] #[case("not a timestamp")] #[case("2024-02-10 14:58:43")] fn test_from_str_invalid_formats(#[case] input: &str) {
901 let result = input.parse::<UnixNanos>();
902 assert!(result.is_err());
903 }
904
905 #[rstest]
906 fn test_from_str_integer_overflow() {
907 let input = "184467440737095516160";
909 let result = input.parse::<UnixNanos>();
910 assert!(result.is_err());
911 }
912
913 #[rstest]
914 fn test_checked_add_overflow_returns_none() {
915 let max = UnixNanos::from(u64::MAX);
916 assert_eq!(max.checked_add(1_u64), None);
917 }
918
919 #[rstest]
920 fn test_checked_sub_underflow_returns_none() {
921 let zero = UnixNanos::default();
922 assert_eq!(zero.checked_sub(1_u64), None);
923 }
924
925 #[rstest]
926 fn test_saturating_add_overflow() {
927 let max = UnixNanos::from(u64::MAX);
928 let result = max.saturating_add_ns(1_u64);
929 assert_eq!(result, UnixNanos::from(u64::MAX));
930 }
931
932 #[rstest]
933 fn test_saturating_sub_underflow() {
934 let zero = UnixNanos::default();
935 let result = zero.saturating_sub_ns(1_u64);
936 assert_eq!(result, UnixNanos::default());
937 }
938
939 #[rstest]
940 fn test_from_str_float_overflow() {
941 let input = "2e10"; let result = input.parse::<UnixNanos>();
944 assert!(result.is_err());
945 }
946
947 #[rstest]
948 fn test_deserialize_u64() {
949 let json = "123456789";
950 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
951 assert_eq!(deserialized.as_u64(), 123_456_789);
952 }
953
954 #[rstest]
955 fn test_deserialize_string_with_int() {
956 let json = "\"123456789\"";
957 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
958 assert_eq!(deserialized.as_u64(), 123_456_789);
959 }
960
961 #[rstest]
962 fn test_deserialize_float() {
963 let json = "1234.567";
964 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
965 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
966 }
967
968 #[rstest]
969 fn test_deserialize_string_with_float() {
970 let json = "\"1234.567\"";
971 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
972 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
973 }
974
975 #[rstest]
976 fn test_deserialize_float_uses_truncation() {
977 let json = "0.9999999999";
979 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
980 assert_eq!(deserialized.as_u64(), 999_999_999); }
982
983 #[rstest]
984 #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
985 #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
986 fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
987 let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
988 assert_eq!(deserialized.as_u64(), expected);
989 }
990
991 #[rstest]
992 fn test_deserialize_negative_int_fails() {
993 let json = "-123456789";
994 let result: Result<UnixNanos, _> = serde_json::from_str(json);
995 assert!(result.is_err());
996 }
997
998 #[rstest]
999 fn test_deserialize_negative_float_fails() {
1000 let json = "-1234.567";
1001 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1002 assert!(result.is_err());
1003 }
1004
1005 #[rstest]
1006 fn test_deserialize_nan_fails() {
1007 use serde::de::{
1009 IntoDeserializer,
1010 value::{Error as ValueError, F64Deserializer},
1011 };
1012 let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
1013 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1014 assert!(result.is_err());
1015 assert!(result.unwrap_err().to_string().contains("must be finite"));
1016 }
1017
1018 #[rstest]
1019 fn test_deserialize_infinity_fails() {
1020 use serde::de::{
1021 IntoDeserializer,
1022 value::{Error as ValueError, F64Deserializer},
1023 };
1024 let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
1025 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1026 assert!(result.is_err());
1027 assert!(result.unwrap_err().to_string().contains("must be finite"));
1028 }
1029
1030 #[rstest]
1031 fn test_deserialize_negative_infinity_fails() {
1032 use serde::de::{
1033 IntoDeserializer,
1034 value::{Error as ValueError, F64Deserializer},
1035 };
1036 let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
1037 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1038 assert!(result.is_err());
1039 assert!(result.unwrap_err().to_string().contains("must be finite"));
1040 }
1041
1042 #[rstest]
1043 fn test_deserialize_overflow_float_fails() {
1044 let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
1047 assert!(result.is_err());
1048 assert!(result.unwrap_err().to_string().contains("out of range"));
1049 }
1050
1051 #[rstest]
1052 fn test_deserialize_invalid_string_fails() {
1053 let json = "\"not a timestamp\"";
1054 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1055 assert!(result.is_err());
1056 }
1057
1058 #[rstest]
1059 fn test_deserialize_edge_cases() {
1060 let json = "0";
1062 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1063 assert_eq!(deserialized.as_u64(), 0);
1064
1065 let json = "18446744073709551615"; let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1068 assert_eq!(deserialized.as_u64(), u64::MAX);
1069 }
1070
1071 #[rstest]
1072 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1073 fn test_as_i64_overflow_panics() {
1074 let nanos = UnixNanos::from(u64::MAX);
1075 let _ = nanos.as_i64(); }
1077
1078 use proptest::prelude::*;
1079
1080 fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
1081 prop_oneof![
1082 0u64..1_000_000u64,
1084 1_000_000u64..1_000_000_000_000u64,
1086 1_000_000_000_000u64..=i64::MAX as u64,
1088 (i64::MAX as u64 + 1)..=u64::MAX,
1090 Just(0u64),
1092 Just(1u64),
1093 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), Just(i64::MAX as u64), Just(u64::MAX), ]
1100 .prop_map(UnixNanos::from)
1101 }
1102
1103 fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
1104 (unix_nanos_strategy(), unix_nanos_strategy())
1105 }
1106
1107 proptest! {
1108 #[rstest]
1109 #[expect(
1110 clippy::float_cmp,
1111 reason = "roundtrip: both sides go through the same u64->f64 cast"
1112 )]
1113 fn prop_unix_nanos_construction_roundtrip(nanos in unix_nanos_strategy()) {
1114 let value = nanos.as_u64();
1115 prop_assert_eq!(UnixNanos::from(value).as_u64(), value);
1116 prop_assert_eq!(nanos.as_f64(), value as f64);
1117
1118 if i64::try_from(value).is_ok() {
1120 prop_assert_eq!(nanos.as_i64(), value as i64);
1121 }
1122 }
1123
1124 #[rstest]
1125 fn prop_unix_nanos_addition_commutative(
1126 (nanos1, nanos2) in unix_nanos_pair_strategy()
1127 ) {
1128 if let (Some(sum1), Some(sum2)) = (
1130 nanos1.checked_add(nanos2.as_u64()),
1131 nanos2.checked_add(nanos1.as_u64())
1132 ) {
1133 prop_assert_eq!(sum1, sum2, "Addition should be commutative");
1134 }
1135 }
1136
1137 #[rstest]
1138 fn prop_unix_nanos_addition_associative(
1139 nanos1 in unix_nanos_strategy(),
1140 nanos2 in unix_nanos_strategy(),
1141 nanos3 in unix_nanos_strategy(),
1142 ) {
1143 if let (Some(sum1), Some(sum2)) = (
1145 nanos1.as_u64().checked_add(nanos2.as_u64()),
1146 nanos2.as_u64().checked_add(nanos3.as_u64())
1147 )
1148 && let (Some(left), Some(right)) = (
1149 sum1.checked_add(nanos3.as_u64()),
1150 nanos1.as_u64().checked_add(sum2)
1151 ) {
1152 let left_result = UnixNanos::from(left);
1153 let right_result = UnixNanos::from(right);
1154 prop_assert_eq!(left_result, right_result, "Addition should be associative");
1155 }
1156 }
1157
1158 #[rstest]
1159 fn prop_unix_nanos_subtraction_inverse(
1160 (nanos1, nanos2) in unix_nanos_pair_strategy()
1161 ) {
1162 if let Some(sum) = nanos1.checked_add(nanos2.as_u64()) {
1164 let diff = sum - nanos2;
1165 prop_assert_eq!(diff, nanos1, "Subtraction should be inverse of addition");
1166 }
1167 }
1168
1169 #[rstest]
1170 fn prop_unix_nanos_zero_identity(nanos in unix_nanos_strategy()) {
1171 let zero = UnixNanos::default();
1173 prop_assert_eq!(nanos + zero, nanos, "Zero should be additive identity");
1174 prop_assert_eq!(zero + nanos, nanos, "Zero should be additive identity (commutative)");
1175 prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
1176 }
1177
1178 #[rstest]
1179 fn prop_unix_nanos_ordering_consistency(
1180 (nanos1, nanos2) in unix_nanos_pair_strategy()
1181 ) {
1182 let eq = nanos1 == nanos2;
1184 let lt = nanos1 < nanos2;
1185 let gt = nanos1 > nanos2;
1186 let le = nanos1 <= nanos2;
1187 let ge = nanos1 >= nanos2;
1188
1189 let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
1191 prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
1192
1193 prop_assert_eq!(le, eq || lt, "<= should equal == || <");
1195 prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
1196 prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
1197 prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
1198 }
1199
1200 #[rstest]
1201 fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
1202 let string_repr = nanos.to_string();
1204 let parsed = UnixNanos::from_str(&string_repr);
1205 prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
1206 if let Ok(parsed_nanos) = parsed {
1207 prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
1208 }
1209 }
1210
1211 #[rstest]
1212 fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
1213 if i64::try_from(nanos.as_u64()).is_ok() {
1215 let datetime = nanos.to_datetime_utc();
1216 let converted_back = UnixNanos::from(datetime);
1217 prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
1218
1219 let rfc3339 = nanos.to_rfc3339();
1221 if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
1222 prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
1223 }
1224 }
1225 }
1226
1227 #[rstest]
1228 fn prop_unix_nanos_duration_since(
1229 (nanos1, nanos2) in unix_nanos_pair_strategy()
1230 ) {
1231 let duration = nanos1.duration_since(&nanos2);
1233
1234 if nanos1 >= nanos2 {
1235 prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
1237 if let Some(dur) = duration {
1238 prop_assert_eq!(dur, nanos1.as_u64() - nanos2.as_u64(),
1239 "Duration should equal the difference");
1240 prop_assert_eq!(nanos2 + dur, nanos1.as_u64(),
1241 "second + duration should equal first");
1242 }
1243 } else {
1244 prop_assert!(duration.is_none(), "Duration should be None when first < second");
1246 }
1247 }
1248
1249 #[rstest]
1250 fn prop_unix_nanos_checked_arithmetic(
1251 (nanos1, nanos2) in unix_nanos_pair_strategy()
1252 ) {
1253 let checked_add = nanos1.checked_add(nanos2.as_u64());
1255 let checked_sub = nanos1.checked_sub(nanos2.as_u64());
1256
1257 if let Some(sum) = checked_add
1259 && nanos1.as_u64().checked_add(nanos2.as_u64()).is_some() {
1260 prop_assert_eq!(sum, nanos1 + nanos2, "Checked add should match regular add when no overflow");
1261 }
1262
1263 if let Some(diff) = checked_sub
1265 && nanos1.as_u64() >= nanos2.as_u64() {
1266 prop_assert_eq!(diff, nanos1 - nanos2, "Checked sub should match regular sub when no underflow");
1267 }
1268 }
1269
1270 #[rstest]
1271 fn prop_unix_nanos_saturating_arithmetic(
1272 (nanos1, nanos2) in unix_nanos_pair_strategy()
1273 ) {
1274 let sat_add = nanos1.saturating_add_ns(nanos2.as_u64());
1276 let sat_sub = nanos1.saturating_sub_ns(nanos2.as_u64());
1277
1278 prop_assert!(sat_add >= nanos1, "Saturating add result should be >= first operand");
1280 prop_assert!(sat_add.as_u64() >= nanos2.as_u64(), "Saturating add result should be >= second operand");
1281
1282 prop_assert!(sat_sub <= nanos1, "Saturating sub result should be <= first operand");
1284
1285 if let Some(checked_sum) = nanos1.checked_add(nanos2.as_u64()) {
1287 prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
1288 } else {
1289 prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
1290 }
1291
1292 if let Some(checked_diff) = nanos1.checked_sub(nanos2.as_u64()) {
1293 prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
1294 } else {
1295 prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
1296 }
1297 }
1298
1299 #[rstest]
1300 fn prop_unix_nanos_assign_mirrors_op(
1301 (nanos1, nanos2) in unix_nanos_pair_strategy()
1302 ) {
1303 if let Some(expected) = nanos1.checked_add(nanos2.as_u64()) {
1305 let mut add_result = nanos1;
1306 add_result += nanos2;
1307 prop_assert_eq!(add_result, expected, "AddAssign should mirror Add");
1308 }
1309
1310 if nanos1.as_u64() >= nanos2.as_u64() {
1312 let expected = nanos1 - nanos2;
1313 let mut sub_result = nanos1;
1314 sub_result -= nanos2;
1315 prop_assert_eq!(sub_result, expected, "SubAssign should mirror Sub");
1316 }
1317 }
1318
1319 #[rstest]
1320 fn prop_unix_nanos_serde_roundtrip(nanos in unix_nanos_strategy()) {
1321 let json = serde_json::to_string(&nanos).unwrap();
1322 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1323 prop_assert_eq!(deserialized, nanos, "Serde JSON should round-trip exactly");
1324 }
1325
1326 #[rstest]
1327 fn prop_unix_nanos_f64_deserialize_never_panics(val: f64) {
1328 use serde::de::{IntoDeserializer, value::{Error as ValueError, F64Deserializer}};
1331 let deserializer: F64Deserializer<ValueError> = val.into_deserializer();
1332 let result = UnixNanos::deserialize(deserializer);
1333
1334 if val.is_finite() && val >= 0.0 && val * 1_000_000_000.0 <= u64::MAX as f64 {
1335 prop_assert!(result.is_ok(), "Should succeed for valid f64: {}", val);
1336 } else {
1337 prop_assert!(result.is_err(), "Should error for invalid f64: {}", val);
1338 }
1339 }
1340 }
1341
1342 #[rstest]
1343 fn test_from_millis_zero() {
1344 let nanos = UnixNanos::from_millis(0);
1345 assert_eq!(nanos.as_u64(), 0);
1346 }
1347
1348 #[rstest]
1349 fn test_from_millis_one() {
1350 let nanos = UnixNanos::from_millis(1);
1351 assert_eq!(nanos.as_u64(), 1_000_000);
1352 }
1353
1354 #[rstest]
1355 fn test_from_millis_one_second() {
1356 let nanos = UnixNanos::from_millis(1_000);
1357 assert_eq!(nanos.as_u64(), 1_000_000_000);
1358 }
1359
1360 #[rstest]
1361 fn test_from_millis_realistic_timestamp() {
1362 let nanos = UnixNanos::from_millis(1_700_000_000_000);
1364 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1365 assert_eq!(
1366 nanos.to_datetime_utc(),
1367 Utc.with_ymd_and_hms(2023, 11, 14, 22, 13, 20).unwrap()
1368 );
1369 }
1370
1371 #[rstest]
1372 fn test_from_millis_max_safe() {
1373 let max_ms = u64::MAX / 1_000_000;
1374 let nanos = UnixNanos::from_millis(max_ms);
1375 assert_eq!(nanos.as_u64(), max_ms * 1_000_000);
1376 }
1377
1378 #[rstest]
1379 fn test_from_millis_matches_manual_conversion() {
1380 let ms = 1_625_474_304_765_u64;
1381 let expected = ms * 1_000_000;
1382 assert_eq!(UnixNanos::from_millis(ms).as_u64(), expected);
1383 }
1384
1385 #[rstest]
1386 fn test_from_micros_zero() {
1387 let nanos = UnixNanos::from_micros(0);
1388 assert_eq!(nanos.as_u64(), 0);
1389 }
1390
1391 #[rstest]
1392 fn test_from_micros_one() {
1393 let nanos = UnixNanos::from_micros(1);
1394 assert_eq!(nanos.as_u64(), 1_000);
1395 }
1396
1397 #[rstest]
1398 fn test_from_micros_one_second() {
1399 let nanos = UnixNanos::from_micros(1_000_000);
1400 assert_eq!(nanos.as_u64(), 1_000_000_000);
1401 }
1402
1403 #[rstest]
1404 fn test_from_micros_one_millisecond() {
1405 let nanos = UnixNanos::from_micros(1_000);
1406 assert_eq!(nanos.as_u64(), 1_000_000);
1407 assert_eq!(UnixNanos::from_micros(1_000), UnixNanos::from_millis(1));
1408 }
1409
1410 #[rstest]
1411 fn test_from_micros_realistic_timestamp() {
1412 let micros = 1_700_000_000_000_000_u64;
1413 let nanos = UnixNanos::from_micros(micros);
1414 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1415 }
1416
1417 #[rstest]
1418 fn test_from_micros_max_safe() {
1419 let max_us = u64::MAX / 1_000;
1420 let nanos = UnixNanos::from_micros(max_us);
1421 assert_eq!(nanos.as_u64(), max_us * 1_000);
1422 }
1423
1424 #[rstest]
1425 fn test_from_micros_matches_manual_conversion() {
1426 let us = 1_000_000_123_456_u64;
1427 let expected = us * 1_000;
1428 assert_eq!(UnixNanos::from_micros(us).as_u64(), expected);
1429 }
1430
1431 #[rstest]
1432 fn test_from_millis_and_micros_consistency() {
1433 assert_eq!(
1434 UnixNanos::from_millis(1_000),
1435 UnixNanos::from_micros(1_000_000)
1436 );
1437 assert_eq!(
1438 UnixNanos::from_millis(60_000),
1439 UnixNanos::from_micros(60_000_000)
1440 );
1441 }
1442
1443 #[rstest]
1444 fn test_from_millis_round_trip_to_datetime() {
1445 let ms = 1_707_577_123_456_u64;
1446 let nanos = UnixNanos::from_millis(ms);
1447 let dt = nanos.to_datetime_utc();
1448 assert_eq!(dt.timestamp_millis() as u64, ms);
1449 }
1450
1451 #[rstest]
1452 fn test_from_micros_preserves_sub_millisecond() {
1453 let micros = 1_700_000_000_000_123_u64;
1454 let nanos = UnixNanos::from_micros(micros);
1455 assert_eq!(nanos.as_u64() % 1_000_000, 123_000);
1456 }
1457
1458 #[rstest]
1459 #[should_panic(expected = "UnixNanos overflow in from_millis")]
1460 fn test_from_millis_overflow_panics() {
1461 let _ = UnixNanos::from_millis(u64::MAX / 1_000_000 + 1);
1462 }
1463
1464 #[rstest]
1465 #[should_panic(expected = "UnixNanos overflow in from_micros")]
1466 fn test_from_micros_overflow_panics() {
1467 let _ = UnixNanos::from_micros(u64::MAX / 1_000 + 1);
1468 }
1469}