1use std::{
51 cmp::Ordering,
52 fmt::Display,
53 ops::{Add, AddAssign, Deref, Sub, SubAssign},
54 str::FromStr,
55 time::SystemTime,
56};
57
58use chrono::{DateTime, NaiveDate, Utc};
59use serde::{
60 Deserialize, Deserializer, Serialize,
61 de::{self, Visitor},
62};
63
64use crate::datetime::{
65 NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND, NANOSECONDS_IN_SECOND,
66};
67
68pub type DurationNanos = u64;
70
71#[repr(C)]
73#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
74pub struct UnixNanos(u64);
75
76impl UnixNanos {
77 #[must_use]
79 pub const fn new(value: u64) -> Self {
80 Self(value)
81 }
82
83 #[must_use]
85 pub const fn max() -> Self {
86 Self(u64::MAX)
87 }
88
89 #[must_use]
91 pub const fn is_zero(&self) -> bool {
92 self.0 == 0
93 }
94
95 #[must_use]
97 pub const fn as_u64(&self) -> u64 {
98 self.0
99 }
100
101 #[must_use]
103 pub const fn as_seconds(&self) -> u64 {
104 self.0 / NANOSECONDS_IN_SECOND
105 }
106
107 #[must_use]
109 pub const fn as_millis(&self) -> u64 {
110 self.0 / NANOSECONDS_IN_MILLISECOND
111 }
112
113 #[must_use]
115 pub const fn as_micros(&self) -> u64 {
116 self.0 / NANOSECONDS_IN_MICROSECOND
117 }
118
119 #[must_use]
125 pub const fn from_seconds(seconds: u64) -> Self {
126 match seconds.checked_mul(NANOSECONDS_IN_SECOND) {
127 Some(nanos) => Self(nanos),
128 None => panic!("UnixNanos overflow in from_seconds"),
129 }
130 }
131
132 #[must_use]
138 pub const fn from_millis(millis: u64) -> Self {
139 match millis.checked_mul(NANOSECONDS_IN_MILLISECOND) {
140 Some(nanos) => Self(nanos),
141 None => panic!("UnixNanos overflow in from_millis"),
142 }
143 }
144
145 #[must_use]
151 pub const fn from_micros(micros: u64) -> Self {
152 match micros.checked_mul(NANOSECONDS_IN_MICROSECOND) {
153 Some(nanos) => Self(nanos),
154 None => panic!("UnixNanos overflow in from_micros"),
155 }
156 }
157
158 #[must_use]
164 pub const fn as_i64(&self) -> i64 {
165 assert!(
166 self.0 <= i64::MAX.cast_unsigned(),
167 "UnixNanos value exceeds i64::MAX"
168 );
169 self.0.cast_signed()
170 }
171
172 #[must_use]
174 #[expect(
175 clippy::cast_precision_loss,
176 reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
177 )]
178 pub const fn as_f64(&self) -> f64 {
179 self.0 as f64
180 }
181
182 #[must_use]
188 pub const fn to_datetime_utc(&self) -> DateTime<Utc> {
189 DateTime::from_timestamp_nanos(self.as_i64())
190 }
191
192 #[must_use]
194 pub fn to_rfc3339(&self) -> String {
195 self.to_datetime_utc().to_rfc3339()
196 }
197
198 #[must_use]
203 pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
204 self.0.checked_sub(other.0)
205 }
206
207 fn parse_string(s: &str) -> Result<Self, String> {
208 if let Ok(int_value) = s.parse::<u64>() {
210 return Ok(Self(int_value));
211 }
212
213 if s.chars().all(|c| c.is_ascii_digit()) {
219 return Err("Unix timestamp is out of range".into());
220 }
221
222 if let Ok(float_value) = s.parse::<f64>() {
224 return f64_seconds_to_nanos(float_value).map(Self);
225 }
226
227 if let Ok(datetime) = DateTime::parse_from_rfc3339(s) {
229 let nanos = datetime
230 .timestamp_nanos_opt()
231 .ok_or_else(|| "Timestamp out of range".to_string())?;
232 let nanos = u64::try_from(nanos)
233 .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
234 return Ok(Self(nanos));
235 }
236
237 if let Ok(date) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
239 let datetime = date
240 .and_hms_opt(0, 0, 0)
241 .ok_or_else(|| "Invalid midnight time".to_string())
242 .map(|naive_dt| DateTime::<Utc>::from_naive_utc_and_offset(naive_dt, Utc))?;
243 let nanos = datetime
244 .timestamp_nanos_opt()
245 .ok_or_else(|| "Timestamp out of range".to_string())?;
246 let nanos = u64::try_from(nanos)
247 .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
248 return Ok(Self(nanos));
249 }
250
251 Err(format!("Invalid format: {s}"))
252 }
253
254 #[must_use]
256 pub fn checked_add<T: Into<u64>>(self, rhs: T) -> Option<Self> {
257 self.0.checked_add(rhs.into()).map(Self)
258 }
259
260 #[must_use]
262 pub fn checked_sub<T: Into<u64>>(self, rhs: T) -> Option<Self> {
263 self.0.checked_sub(rhs.into()).map(Self)
264 }
265
266 #[must_use]
268 pub fn saturating_add_ns<T: Into<u64>>(self, rhs: T) -> Self {
269 Self(self.0.saturating_add(rhs.into()))
270 }
271
272 #[must_use]
274 pub fn saturating_sub_ns<T: Into<u64>>(self, rhs: T) -> Self {
275 Self(self.0.saturating_sub(rhs.into()))
276 }
277}
278
279#[expect(
282 clippy::cast_precision_loss,
283 clippy::cast_possible_truncation,
284 clippy::cast_sign_loss,
285 reason = "value is checked finite, non-negative, and within u64 range before the cast"
286)]
287fn f64_seconds_to_nanos(value: f64) -> Result<u64, String> {
288 const MAX_NS_F64: f64 = u64::MAX as f64;
289
290 if !value.is_finite() {
291 return Err(format!("Unix timestamp must be finite, was {value}"));
292 }
293
294 if value < 0.0 {
295 return Err("Unix timestamp cannot be negative".to_string());
296 }
297
298 let nanos_f64 = value * 1_000_000_000.0;
302
303 if nanos_f64 > MAX_NS_F64 {
304 return Err(format!("Unix timestamp {value} seconds is out of range"));
305 }
306
307 Ok(nanos_f64.trunc() as u64)
308}
309
310impl Deref for UnixNanos {
311 type Target = u64;
312
313 fn deref(&self) -> &Self::Target {
314 &self.0
315 }
316}
317
318impl PartialEq<u64> for UnixNanos {
319 fn eq(&self, other: &u64) -> bool {
320 self.0 == *other
321 }
322}
323
324impl PartialOrd<u64> for UnixNanos {
325 fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
326 self.0.partial_cmp(other)
327 }
328}
329
330impl PartialEq<Option<u64>> for UnixNanos {
331 fn eq(&self, other: &Option<u64>) -> bool {
332 match other {
333 Some(value) => self.0 == *value,
334 None => false,
335 }
336 }
337}
338
339impl PartialOrd<Option<u64>> for UnixNanos {
340 fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
341 match other {
342 Some(value) => self.0.partial_cmp(value),
343 None => Some(Ordering::Greater),
344 }
345 }
346}
347
348impl PartialEq<UnixNanos> for u64 {
349 fn eq(&self, other: &UnixNanos) -> bool {
350 *self == other.0
351 }
352}
353
354impl PartialOrd<UnixNanos> for u64 {
355 fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
356 self.partial_cmp(&other.0)
357 }
358}
359
360impl From<u64> for UnixNanos {
361 fn from(value: u64) -> Self {
362 Self(value)
363 }
364}
365
366impl From<UnixNanos> for u64 {
367 fn from(value: UnixNanos) -> Self {
368 value.0
369 }
370}
371
372impl From<&str> for UnixNanos {
383 fn from(value: &str) -> Self {
384 value
385 .parse()
386 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
387 }
388}
389
390impl From<String> for UnixNanos {
401 fn from(value: String) -> Self {
402 value
403 .parse()
404 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
405 }
406}
407
408impl From<DateTime<Utc>> for UnixNanos {
409 fn from(value: DateTime<Utc>) -> Self {
410 let nanos = value
411 .timestamp_nanos_opt()
412 .expect("DateTime timestamp out of range for UnixNanos");
413
414 assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
415
416 Self::from(nanos.cast_unsigned())
417 }
418}
419
420impl From<SystemTime> for UnixNanos {
421 fn from(value: SystemTime) -> Self {
422 let duration = value
423 .duration_since(std::time::UNIX_EPOCH)
424 .expect("SystemTime before UNIX EPOCH");
425
426 let nanos =
427 u64::try_from(duration.as_nanos()).expect("SystemTime overflowed u64 nanoseconds");
428
429 Self::from(nanos)
430 }
431}
432
433impl FromStr for UnixNanos {
434 type Err = Box<dyn std::error::Error>;
435
436 fn from_str(s: &str) -> Result<Self, Self::Err> {
437 Self::parse_string(s).map_err(std::convert::Into::into)
438 }
439}
440
441impl Add for UnixNanos {
450 type Output = Self;
451
452 fn add(self, rhs: Self) -> Self::Output {
453 Self(
454 self.0
455 .checked_add(rhs.0)
456 .expect("UnixNanos overflow in addition - invalid timestamp calculation"),
457 )
458 }
459}
460
461impl Sub for UnixNanos {
470 type Output = Self;
471
472 fn sub(self, rhs: Self) -> Self::Output {
473 Self(
474 self.0
475 .checked_sub(rhs.0)
476 .expect("UnixNanos underflow in subtraction - invalid timestamp calculation"),
477 )
478 }
479}
480
481impl Add<u64> for UnixNanos {
488 type Output = Self;
489
490 fn add(self, rhs: u64) -> Self::Output {
491 Self(
492 self.0
493 .checked_add(rhs)
494 .expect("UnixNanos overflow in addition"),
495 )
496 }
497}
498
499impl Sub<u64> for UnixNanos {
506 type Output = Self;
507
508 fn sub(self, rhs: u64) -> Self::Output {
509 Self(
510 self.0
511 .checked_sub(rhs)
512 .expect("UnixNanos underflow in subtraction"),
513 )
514 }
515}
516
517impl<T: Into<u64>> AddAssign<T> for UnixNanos {
523 fn add_assign(&mut self, other: T) {
524 let other_u64 = other.into();
525 self.0 = self
526 .0
527 .checked_add(other_u64)
528 .expect("UnixNanos overflow in add_assign");
529 }
530}
531
532impl<T: Into<u64>> SubAssign<T> for UnixNanos {
538 fn sub_assign(&mut self, other: T) {
539 let other_u64 = other.into();
540 self.0 = self
541 .0
542 .checked_sub(other_u64)
543 .expect("UnixNanos underflow in sub_assign");
544 }
545}
546
547impl Display for UnixNanos {
548 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549 write!(f, "{}", self.0)
550 }
551}
552
553impl From<UnixNanos> for DateTime<Utc> {
554 fn from(value: UnixNanos) -> Self {
555 value.to_datetime_utc()
556 }
557}
558
559impl<'de> Deserialize<'de> for UnixNanos {
560 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
561 where
562 D: Deserializer<'de>,
563 {
564 struct UnixNanosVisitor;
565
566 impl Visitor<'_> for UnixNanosVisitor {
567 type Value = UnixNanos;
568
569 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
570 formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
571 }
572
573 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
574 where
575 E: de::Error,
576 {
577 Ok(UnixNanos(value))
578 }
579
580 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
581 where
582 E: de::Error,
583 {
584 u64::try_from(value)
585 .map(UnixNanos)
586 .map_err(|_| E::custom("Unix timestamp cannot be negative"))
587 }
588
589 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
590 where
591 E: de::Error,
592 {
593 f64_seconds_to_nanos(value)
594 .map(UnixNanos)
595 .map_err(E::custom)
596 }
597
598 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
599 where
600 E: de::Error,
601 {
602 UnixNanos::parse_string(value).map_err(E::custom)
603 }
604 }
605
606 deserializer.deserialize_any(UnixNanosVisitor)
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use chrono::{Duration, TimeZone};
613 use rstest::rstest;
614
615 use super::*;
616
617 #[rstest]
618 fn test_new() {
619 let nanos = UnixNanos::new(123);
620 assert_eq!(nanos.as_u64(), 123);
621 assert_eq!(nanos.as_i64(), 123);
622 }
623
624 #[rstest]
625 fn test_max() {
626 let nanos = UnixNanos::max();
627 assert_eq!(nanos.as_u64(), u64::MAX);
628 }
629
630 #[rstest]
631 fn test_is_zero() {
632 assert!(UnixNanos::default().is_zero());
633 assert!(!UnixNanos::max().is_zero());
634 }
635
636 #[rstest]
637 fn test_from_u64() {
638 let nanos = UnixNanos::from(123);
639 assert_eq!(nanos.as_u64(), 123);
640 assert_eq!(nanos.as_i64(), 123);
641 }
642
643 #[rstest]
644 fn test_default() {
645 let nanos = UnixNanos::default();
646 assert_eq!(nanos.as_u64(), 0);
647 assert_eq!(nanos.as_i64(), 0);
648 }
649
650 #[rstest]
651 fn test_into_from() {
652 let nanos: UnixNanos = 456.into();
653 let value: u64 = nanos.into();
654 assert_eq!(value, 456);
655 }
656
657 #[rstest]
658 #[case(0, "1970-01-01T00:00:00+00:00")]
659 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
660 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
661 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
662 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
663 fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
664 let nanos = UnixNanos::from(nanos);
665 let datetime = nanos.to_datetime_utc();
666 assert_eq!(datetime.to_rfc3339(), expected);
667 }
668
669 #[rstest]
670 #[case(0, "1970-01-01T00:00:00+00:00")]
671 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
672 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
673 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
674 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
675 fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
676 let nanos = UnixNanos::from(nanos);
677 assert_eq!(nanos.to_rfc3339(), expected);
678 }
679
680 #[rstest]
681 fn test_from_str() {
682 let nanos: UnixNanos = "123".parse().unwrap();
683 assert_eq!(nanos.as_u64(), 123);
684 }
685
686 #[rstest]
687 fn test_from_str_invalid() {
688 let result = "abc".parse::<UnixNanos>();
689 assert!(result.is_err());
690 }
691
692 #[rstest]
693 fn test_from_str_date() {
694 let nanos: UnixNanos = "2024-02-10".parse().unwrap();
695 assert_eq!(nanos.as_u64(), 1_707_523_200_000_000_000);
696 }
697
698 #[rstest]
699 fn test_from_str_pre_epoch_date() {
700 let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
701 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
702 }
703
704 #[rstest]
705 fn test_from_str_pre_epoch_rfc3339() {
706 let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
707 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
708 }
709
710 #[rstest]
711 fn test_try_from_datetime_valid() {
712 use chrono::TimeZone;
713 let datetime = Utc.timestamp_opt(1_000_000_000, 0).unwrap(); let nanos = UnixNanos::from(datetime);
715 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
716 }
717
718 #[rstest]
719 fn test_from_system_time() {
720 let system_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
721 let nanos = UnixNanos::from(system_time);
722 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
723 }
724
725 #[rstest]
726 #[should_panic(expected = "SystemTime before UNIX EPOCH")]
727 fn test_from_system_time_before_epoch() {
728 let system_time = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
729 let _ = UnixNanos::from(system_time);
730 }
731
732 #[rstest]
733 #[should_panic(expected = "SystemTime overflowed u64 nanoseconds")]
734 fn test_from_system_time_overflow_panics() {
735 let system_time =
737 std::time::UNIX_EPOCH + std::time::Duration::from_secs(u64::MAX / 1_000_000_000 + 1);
738 let _ = UnixNanos::from(system_time);
739 }
740
741 #[rstest]
742 fn test_eq() {
743 let nanos = UnixNanos::from(100);
744 assert_eq!(nanos, 100);
745 assert_eq!(nanos, Some(100));
746 assert_ne!(nanos, 200);
747 assert_ne!(nanos, Some(200));
748 assert_ne!(nanos, None);
749 }
750
751 #[rstest]
752 fn test_partial_cmp() {
753 let nanos = UnixNanos::from(100);
754 assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
755 assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
756 assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
757 assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
758 }
759
760 #[rstest]
761 fn test_edge_case_max_value() {
762 let nanos = UnixNanos::from(u64::MAX);
763 assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
764 }
765
766 #[rstest]
767 fn test_display() {
768 let nanos = UnixNanos::from(123);
769 assert_eq!(format!("{nanos}"), "123");
770 }
771
772 #[rstest]
773 fn test_addition() {
774 let nanos1 = UnixNanos::from(100);
775 let nanos2 = UnixNanos::from(200);
776 let result = nanos1 + nanos2;
777 assert_eq!(result.as_u64(), 300);
778 }
779
780 #[rstest]
781 fn test_add_assign() {
782 let mut nanos = UnixNanos::from(100);
783 nanos += 50_u64;
784 assert_eq!(nanos.as_u64(), 150);
785 }
786
787 #[rstest]
788 fn test_subtraction() {
789 let nanos1 = UnixNanos::from(200);
790 let nanos2 = UnixNanos::from(100);
791 let result = nanos1 - nanos2;
792 assert_eq!(result.as_u64(), 100);
793 }
794
795 #[rstest]
796 fn test_sub_assign() {
797 let mut nanos = UnixNanos::from(200);
798 nanos -= 50_u64;
799 assert_eq!(nanos.as_u64(), 150);
800 }
801
802 #[rstest]
803 #[should_panic(expected = "UnixNanos overflow")]
804 fn test_overflow_add() {
805 let nanos = UnixNanos::from(u64::MAX);
806 let _ = nanos + UnixNanos::from(1); }
808
809 #[rstest]
810 #[should_panic(expected = "UnixNanos overflow")]
811 fn test_overflow_add_u64() {
812 let nanos = UnixNanos::from(u64::MAX);
813 let _ = nanos + 1_u64; }
815
816 #[rstest]
817 #[should_panic(expected = "UnixNanos underflow")]
818 fn test_overflow_sub() {
819 let _ = UnixNanos::default() - UnixNanos::from(1); }
821
822 #[rstest]
823 #[should_panic(expected = "UnixNanos underflow")]
824 fn test_overflow_sub_u64() {
825 let _ = UnixNanos::default() - 1_u64; }
827
828 #[rstest]
829 #[case(100, 50, Some(50))]
830 #[case(1_000_000_000, 500_000_000, Some(500_000_000))]
831 #[case(u64::MAX, u64::MAX - 1, Some(1))]
832 #[case(50, 50, Some(0))]
833 #[case(50, 100, None)]
834 #[case(0, 1, None)]
835 fn test_duration_since(
836 #[case] time1: u64,
837 #[case] time2: u64,
838 #[case] expected: Option<DurationNanos>,
839 ) {
840 let nanos1 = UnixNanos::from(time1);
841 let nanos2 = UnixNanos::from(time2);
842 assert_eq!(nanos1.duration_since(&nanos2), expected);
843 }
844
845 #[rstest]
846 fn test_duration_since_same_moment() {
847 let moment = UnixNanos::from(1_707_577_123_456_789_000);
848 assert_eq!(moment.duration_since(&moment), Some(0));
849 }
850
851 #[rstest]
852 fn test_duration_since_chronological() {
853 let earlier = Utc.with_ymd_and_hms(2024, 2, 10, 12, 0, 0).unwrap();
855
856 let later = earlier
858 + Duration::hours(1)
859 + Duration::minutes(30)
860 + Duration::seconds(45)
861 + Duration::nanoseconds(500_000_000);
862
863 let earlier_nanos = UnixNanos::from(earlier);
864 let later_nanos = UnixNanos::from(later);
865
866 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!(
873 later_nanos.duration_since(&earlier_nanos),
874 Some(expected_duration)
875 );
876 assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
877 }
878
879 #[rstest]
880 fn test_duration_since_with_edge_cases() {
881 let max = UnixNanos::from(u64::MAX);
883 let smaller = UnixNanos::from(u64::MAX - 1000);
884
885 assert_eq!(max.duration_since(&smaller), Some(1000));
886 assert_eq!(smaller.duration_since(&max), None);
887
888 let min = UnixNanos::default(); let larger = UnixNanos::from(1000);
891
892 assert_eq!(min.duration_since(&min), Some(0));
893 assert_eq!(larger.duration_since(&min), Some(1000));
894 assert_eq!(min.duration_since(&larger), None);
895 }
896
897 #[rstest]
898 fn test_serde_json() {
899 let nanos = UnixNanos::from(123);
900 let json = serde_json::to_string(&nanos).unwrap();
901 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
902 assert_eq!(deserialized, nanos);
903 }
904
905 #[rstest]
906 fn test_serde_edge_cases() {
907 let nanos = UnixNanos::from(u64::MAX);
908 let json = serde_json::to_string(&nanos).unwrap();
909 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
910 assert_eq!(deserialized, nanos);
911 }
912
913 #[rstest]
914 #[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) {
920 let parsed: UnixNanos = input.parse().unwrap();
921 assert_eq!(parsed.as_u64(), expected);
922 }
923
924 #[rstest]
925 #[case("abc")] #[case("not a timestamp")] #[case("2024-02-10 14:58:43")] fn test_from_str_invalid_formats(#[case] input: &str) {
929 let result = input.parse::<UnixNanos>();
930 assert!(result.is_err());
931 }
932
933 #[rstest]
934 fn test_from_str_integer_overflow() {
935 let input = "184467440737095516160";
937 let result = input.parse::<UnixNanos>();
938 assert!(result.is_err());
939 }
940
941 #[rstest]
942 fn test_checked_add_overflow_returns_none() {
943 let max = UnixNanos::from(u64::MAX);
944 assert_eq!(max.checked_add(1_u64), None);
945 }
946
947 #[rstest]
948 fn test_checked_sub_underflow_returns_none() {
949 let zero = UnixNanos::default();
950 assert_eq!(zero.checked_sub(1_u64), None);
951 }
952
953 #[rstest]
954 fn test_saturating_add_overflow() {
955 let max = UnixNanos::from(u64::MAX);
956 let result = max.saturating_add_ns(1_u64);
957 assert_eq!(result, UnixNanos::from(u64::MAX));
958 }
959
960 #[rstest]
961 fn test_saturating_sub_underflow() {
962 let zero = UnixNanos::default();
963 let result = zero.saturating_sub_ns(1_u64);
964 assert_eq!(result, UnixNanos::default());
965 }
966
967 #[rstest]
968 fn test_from_str_float_overflow() {
969 let input = "2e10"; let err = input.parse::<UnixNanos>().unwrap_err();
972 assert!(err.to_string().contains("out of range"));
973 }
974
975 #[rstest]
976 #[case("NaN")]
977 #[case("nan")]
978 #[case("inf")]
979 #[case("-inf")]
980 fn test_from_str_non_finite_float_errors(#[case] input: &str) {
981 let err = input.parse::<UnixNanos>().unwrap_err();
982 assert!(err.to_string().contains("must be finite"));
983 }
984
985 #[rstest]
986 #[case("-1.5")]
987 #[case("-0.000001")]
988 fn test_from_str_negative_float_errors(#[case] input: &str) {
989 let err = input.parse::<UnixNanos>().unwrap_err();
990 assert!(err.to_string().contains("cannot be negative"));
991 }
992
993 #[rstest]
994 fn test_deserialize_u64() {
995 let json = "123456789";
996 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
997 assert_eq!(deserialized.as_u64(), 123_456_789);
998 }
999
1000 #[rstest]
1001 fn test_deserialize_string_with_int() {
1002 let json = "\"123456789\"";
1003 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1004 assert_eq!(deserialized.as_u64(), 123_456_789);
1005 }
1006
1007 #[rstest]
1008 fn test_deserialize_float() {
1009 let json = "1234.567";
1010 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1011 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1012 }
1013
1014 #[rstest]
1015 fn test_deserialize_string_with_float() {
1016 let json = "\"1234.567\"";
1017 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1018 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1019 }
1020
1021 #[rstest]
1022 fn test_deserialize_float_uses_truncation() {
1023 let json = "0.9999999999";
1025 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1026 assert_eq!(deserialized.as_u64(), 999_999_999); }
1028
1029 #[rstest]
1030 #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
1031 #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
1032 fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
1033 let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
1034 assert_eq!(deserialized.as_u64(), expected);
1035 }
1036
1037 #[rstest]
1038 fn test_deserialize_negative_int_fails() {
1039 let json = "-123456789";
1040 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1041 assert!(
1042 result
1043 .unwrap_err()
1044 .to_string()
1045 .contains("cannot be negative")
1046 );
1047 }
1048
1049 #[rstest]
1050 fn test_deserialize_negative_float_fails() {
1051 let json = "-1234.567";
1052 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1053 assert!(
1054 result
1055 .unwrap_err()
1056 .to_string()
1057 .contains("cannot be negative")
1058 );
1059 }
1060
1061 #[rstest]
1062 fn test_deserialize_nan_fails() {
1063 use serde::de::{
1065 IntoDeserializer,
1066 value::{Error as ValueError, F64Deserializer},
1067 };
1068 let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
1069 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1070 assert!(result.is_err());
1071 assert!(result.unwrap_err().to_string().contains("must be finite"));
1072 }
1073
1074 #[rstest]
1075 fn test_deserialize_infinity_fails() {
1076 use serde::de::{
1077 IntoDeserializer,
1078 value::{Error as ValueError, F64Deserializer},
1079 };
1080 let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
1081 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1082 assert!(result.is_err());
1083 assert!(result.unwrap_err().to_string().contains("must be finite"));
1084 }
1085
1086 #[rstest]
1087 fn test_deserialize_negative_infinity_fails() {
1088 use serde::de::{
1089 IntoDeserializer,
1090 value::{Error as ValueError, F64Deserializer},
1091 };
1092 let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
1093 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1094 assert!(result.is_err());
1095 assert!(result.unwrap_err().to_string().contains("must be finite"));
1096 }
1097
1098 #[rstest]
1099 fn test_deserialize_overflow_float_fails() {
1100 let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
1103 assert!(result.is_err());
1104 assert!(result.unwrap_err().to_string().contains("out of range"));
1105 }
1106
1107 #[rstest]
1108 fn test_deserialize_invalid_string_fails() {
1109 let json = "\"not a timestamp\"";
1110 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1111 assert!(result.is_err());
1112 }
1113
1114 #[rstest]
1115 fn test_deserialize_edge_cases() {
1116 let json = "0";
1118 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1119 assert_eq!(deserialized.as_u64(), 0);
1120
1121 let json = "18446744073709551615"; let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1124 assert_eq!(deserialized.as_u64(), u64::MAX);
1125 }
1126
1127 #[rstest]
1128 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1129 fn test_as_i64_overflow_panics() {
1130 let nanos = UnixNanos::from(u64::MAX);
1131 let _ = nanos.as_i64(); }
1133
1134 #[rstest]
1135 fn test_as_i64_at_i64_max_boundary() {
1136 let nanos = UnixNanos::from(i64::MAX.cast_unsigned());
1137 assert_eq!(nanos.as_i64(), i64::MAX);
1138 }
1139
1140 #[rstest]
1141 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1142 fn test_as_i64_just_above_i64_max_panics() {
1143 let nanos = UnixNanos::from(i64::MAX.cast_unsigned() + 1);
1144 let _ = nanos.as_i64();
1145 }
1146
1147 use proptest::prelude::*;
1148
1149 fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
1150 prop_oneof![
1151 0u64..1_000_000u64,
1153 1_000_000u64..1_000_000_000_000u64,
1155 1_000_000_000_000u64..=i64::MAX.cast_unsigned(),
1157 (i64::MAX.cast_unsigned() + 1)..=u64::MAX,
1159 Just(0u64),
1161 Just(1u64),
1162 Just(1_000_000_000u64), Just(1_000_000_000_000u64), Just(1_700_000_000_000_000_000u64), Just((i64::MAX / 2).cast_unsigned()), Just(i64::MAX.cast_unsigned()), Just(u64::MAX), ]
1169 .prop_map(UnixNanos::from)
1170 }
1171
1172 fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
1173 (unix_nanos_strategy(), unix_nanos_strategy())
1174 }
1175
1176 proptest! {
1177 #[rstest]
1178 #[expect(
1179 clippy::float_cmp,
1180 clippy::cast_precision_loss,
1181 reason = "roundtrip: both sides go through the same u64->f64 cast"
1182 )]
1183 fn prop_unix_nanos_construction_roundtrip(nanos in unix_nanos_strategy()) {
1184 let value = nanos.as_u64();
1185 prop_assert_eq!(UnixNanos::from(value).as_u64(), value);
1186 prop_assert_eq!(nanos.as_f64(), value as f64);
1187
1188 if i64::try_from(value).is_ok() {
1190 prop_assert_eq!(nanos.as_i64(), value.cast_signed());
1191 }
1192 }
1193
1194 #[rstest]
1195 fn prop_unix_nanos_addition_commutative(
1196 (nanos1, nanos2) in unix_nanos_pair_strategy()
1197 ) {
1198 if let (Some(sum1), Some(sum2)) = (
1200 nanos1.checked_add(nanos2.as_u64()),
1201 nanos2.checked_add(nanos1.as_u64())
1202 ) {
1203 prop_assert_eq!(sum1, sum2, "Addition should be commutative");
1204 }
1205 }
1206
1207 #[rstest]
1208 fn prop_unix_nanos_addition_associative(
1209 nanos1 in unix_nanos_strategy(),
1210 nanos2 in unix_nanos_strategy(),
1211 nanos3 in unix_nanos_strategy(),
1212 ) {
1213 if let (Some(sum1), Some(sum2)) = (
1215 nanos1.as_u64().checked_add(nanos2.as_u64()),
1216 nanos2.as_u64().checked_add(nanos3.as_u64())
1217 )
1218 && let (Some(left), Some(right)) = (
1219 sum1.checked_add(nanos3.as_u64()),
1220 nanos1.as_u64().checked_add(sum2)
1221 ) {
1222 let left_result = UnixNanos::from(left);
1223 let right_result = UnixNanos::from(right);
1224 prop_assert_eq!(left_result, right_result, "Addition should be associative");
1225 }
1226 }
1227
1228 #[rstest]
1229 fn prop_unix_nanos_subtraction_inverse(
1230 (nanos1, nanos2) in unix_nanos_pair_strategy()
1231 ) {
1232 if let Some(sum) = nanos1.checked_add(nanos2.as_u64()) {
1234 let diff = sum - nanos2;
1235 prop_assert_eq!(diff, nanos1, "Subtraction should be inverse of addition");
1236 }
1237 }
1238
1239 #[rstest]
1240 fn prop_unix_nanos_zero_identity(nanos in unix_nanos_strategy()) {
1241 let zero = UnixNanos::default();
1243 prop_assert_eq!(nanos + zero, nanos, "Zero should be additive identity");
1244 prop_assert_eq!(zero + nanos, nanos, "Zero should be additive identity (commutative)");
1245 prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
1246 }
1247
1248 #[rstest]
1249 fn prop_unix_nanos_ordering_consistency(
1250 (nanos1, nanos2) in unix_nanos_pair_strategy()
1251 ) {
1252 let eq = nanos1 == nanos2;
1254 let lt = nanos1 < nanos2;
1255 let gt = nanos1 > nanos2;
1256 let le = nanos1 <= nanos2;
1257 let ge = nanos1 >= nanos2;
1258
1259 let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
1261 prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
1262
1263 prop_assert_eq!(le, eq || lt, "<= should equal == || <");
1265 prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
1266 prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
1267 prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
1268 }
1269
1270 #[rstest]
1271 fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
1272 let string_repr = nanos.to_string();
1274 let parsed = UnixNanos::from_str(&string_repr);
1275 prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
1276 if let Ok(parsed_nanos) = parsed {
1277 prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
1278 }
1279 }
1280
1281 #[rstest]
1282 fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
1283 if i64::try_from(nanos.as_u64()).is_ok() {
1285 let datetime = nanos.to_datetime_utc();
1286 let converted_back = UnixNanos::from(datetime);
1287 prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
1288
1289 let rfc3339 = nanos.to_rfc3339();
1291 if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
1292 prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
1293 }
1294 }
1295 }
1296
1297 #[rstest]
1298 fn prop_unix_nanos_duration_since(
1299 (nanos1, nanos2) in unix_nanos_pair_strategy()
1300 ) {
1301 let duration = nanos1.duration_since(&nanos2);
1303
1304 if nanos1 >= nanos2 {
1305 prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
1307 if let Some(dur) = duration {
1308 prop_assert_eq!(dur, nanos1.as_u64() - nanos2.as_u64(),
1309 "Duration should equal the difference");
1310 prop_assert_eq!(nanos2 + dur, nanos1.as_u64(),
1311 "second + duration should equal first");
1312 }
1313 } else {
1314 prop_assert!(duration.is_none(), "Duration should be None when first < second");
1316 }
1317 }
1318
1319 #[rstest]
1320 fn prop_unix_nanos_checked_arithmetic(
1321 (nanos1, nanos2) in unix_nanos_pair_strategy()
1322 ) {
1323 let checked_add = nanos1.checked_add(nanos2.as_u64());
1325 let checked_sub = nanos1.checked_sub(nanos2.as_u64());
1326
1327 if let Some(sum) = checked_add
1329 && nanos1.as_u64().checked_add(nanos2.as_u64()).is_some() {
1330 prop_assert_eq!(sum, nanos1 + nanos2, "Checked add should match regular add when no overflow");
1331 }
1332
1333 if let Some(diff) = checked_sub
1335 && nanos1.as_u64() >= nanos2.as_u64() {
1336 prop_assert_eq!(diff, nanos1 - nanos2, "Checked sub should match regular sub when no underflow");
1337 }
1338 }
1339
1340 #[rstest]
1341 fn prop_unix_nanos_saturating_arithmetic(
1342 (nanos1, nanos2) in unix_nanos_pair_strategy()
1343 ) {
1344 let sat_add = nanos1.saturating_add_ns(nanos2.as_u64());
1346 let sat_sub = nanos1.saturating_sub_ns(nanos2.as_u64());
1347
1348 prop_assert!(sat_add >= nanos1, "Saturating add result should be >= first operand");
1350 prop_assert!(sat_add.as_u64() >= nanos2.as_u64(), "Saturating add result should be >= second operand");
1351
1352 prop_assert!(sat_sub <= nanos1, "Saturating sub result should be <= first operand");
1354
1355 if let Some(checked_sum) = nanos1.checked_add(nanos2.as_u64()) {
1357 prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
1358 } else {
1359 prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
1360 }
1361
1362 if let Some(checked_diff) = nanos1.checked_sub(nanos2.as_u64()) {
1363 prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
1364 } else {
1365 prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
1366 }
1367 }
1368
1369 #[rstest]
1370 fn prop_unix_nanos_assign_mirrors_op(
1371 (nanos1, nanos2) in unix_nanos_pair_strategy()
1372 ) {
1373 if let Some(expected) = nanos1.checked_add(nanos2.as_u64()) {
1375 let mut add_result = nanos1;
1376 add_result += nanos2;
1377 prop_assert_eq!(add_result, expected, "AddAssign should mirror Add");
1378 }
1379
1380 if nanos1.as_u64() >= nanos2.as_u64() {
1382 let expected = nanos1 - nanos2;
1383 let mut sub_result = nanos1;
1384 sub_result -= nanos2;
1385 prop_assert_eq!(sub_result, expected, "SubAssign should mirror Sub");
1386 }
1387 }
1388
1389 #[rstest]
1390 fn prop_unix_nanos_serde_roundtrip(nanos in unix_nanos_strategy()) {
1391 let json = serde_json::to_string(&nanos).unwrap();
1392 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1393 prop_assert_eq!(deserialized, nanos, "Serde JSON should round-trip exactly");
1394 }
1395
1396 #[rstest]
1397 #[expect(
1398 clippy::cast_precision_loss,
1399 reason = "test bound mirrors the production guard in f64_seconds_to_nanos"
1400 )]
1401 fn prop_unix_nanos_f64_deserialize_never_panics(val: f64) {
1402 use serde::de::{IntoDeserializer, value::{Error as ValueError, F64Deserializer}};
1405 let deserializer: F64Deserializer<ValueError> = val.into_deserializer();
1406 let result = UnixNanos::deserialize(deserializer);
1407
1408 if val.is_finite() && val >= 0.0 && val * 1_000_000_000.0 <= u64::MAX as f64 {
1409 prop_assert!(result.is_ok(), "Should succeed for valid f64: {}", val);
1410 } else {
1411 prop_assert!(result.is_err(), "Should error for invalid f64: {}", val);
1412 }
1413 }
1414 }
1415
1416 #[rstest]
1417 fn test_from_seconds_zero() {
1418 let nanos = UnixNanos::from_seconds(0);
1419 assert_eq!(nanos.as_u64(), 0);
1420 }
1421
1422 #[rstest]
1423 fn test_from_seconds_one() {
1424 let nanos = UnixNanos::from_seconds(1);
1425 assert_eq!(nanos.as_u64(), 1_000_000_000);
1426 }
1427
1428 #[rstest]
1429 fn test_from_seconds_realistic_timestamp() {
1430 let nanos = UnixNanos::from_seconds(1_700_000_000);
1431 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1432 assert_eq!(
1433 nanos.to_datetime_utc(),
1434 Utc.with_ymd_and_hms(2023, 11, 14, 22, 13, 20).unwrap()
1435 );
1436 }
1437
1438 #[rstest]
1439 fn test_from_seconds_max_safe() {
1440 let max_seconds = u64::MAX / 1_000_000_000;
1441 let nanos = UnixNanos::from_seconds(max_seconds);
1442 assert_eq!(nanos.as_u64(), max_seconds * 1_000_000_000);
1443 }
1444
1445 #[rstest]
1446 fn test_from_millis_zero() {
1447 let nanos = UnixNanos::from_millis(0);
1448 assert_eq!(nanos.as_u64(), 0);
1449 }
1450
1451 #[rstest]
1452 fn test_from_millis_one() {
1453 let nanos = UnixNanos::from_millis(1);
1454 assert_eq!(nanos.as_u64(), 1_000_000);
1455 }
1456
1457 #[rstest]
1458 fn test_from_millis_one_second() {
1459 let nanos = UnixNanos::from_millis(1_000);
1460 assert_eq!(nanos.as_u64(), 1_000_000_000);
1461 }
1462
1463 #[rstest]
1464 fn test_from_millis_realistic_timestamp() {
1465 let nanos = UnixNanos::from_millis(1_700_000_000_000);
1467 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1468 assert_eq!(
1469 nanos.to_datetime_utc(),
1470 Utc.with_ymd_and_hms(2023, 11, 14, 22, 13, 20).unwrap()
1471 );
1472 }
1473
1474 #[rstest]
1475 fn test_from_millis_max_safe() {
1476 let max_ms = u64::MAX / 1_000_000;
1477 let nanos = UnixNanos::from_millis(max_ms);
1478 assert_eq!(nanos.as_u64(), max_ms * 1_000_000);
1479 }
1480
1481 #[rstest]
1482 fn test_from_millis_matches_manual_conversion() {
1483 let ms = 1_625_474_304_765_u64;
1484 let expected = ms * 1_000_000;
1485 assert_eq!(UnixNanos::from_millis(ms).as_u64(), expected);
1486 }
1487
1488 #[rstest]
1489 #[case(0, 0)]
1490 #[case(999_999_999, 0)]
1491 #[case(1_000_000_000, 1)]
1492 #[case(1_700_000_000_123_456_789, 1_700_000_000)]
1493 fn test_as_seconds(#[case] nanos: u64, #[case] expected: u64) {
1494 assert_eq!(UnixNanos::from(nanos).as_seconds(), expected);
1495 }
1496
1497 #[rstest]
1498 #[case(0, 0)]
1499 #[case(999_999, 0)]
1500 #[case(1_000_000, 1)]
1501 #[case(1_700_000_000_000_123_456, 1_700_000_000_000)]
1502 fn test_as_millis(#[case] nanos: u64, #[case] expected: u64) {
1503 assert_eq!(UnixNanos::from(nanos).as_millis(), expected);
1504 }
1505
1506 #[rstest]
1507 #[case(0, 0)]
1508 #[case(999, 0)]
1509 #[case(1_000, 1)]
1510 #[case(1_700_000_000_000_123_456, 1_700_000_000_000_123)]
1511 fn test_as_micros(#[case] nanos: u64, #[case] expected: u64) {
1512 assert_eq!(UnixNanos::from(nanos).as_micros(), expected);
1513 }
1514
1515 #[rstest]
1516 fn test_from_micros_zero() {
1517 let nanos = UnixNanos::from_micros(0);
1518 assert_eq!(nanos.as_u64(), 0);
1519 }
1520
1521 #[rstest]
1522 fn test_from_micros_one() {
1523 let nanos = UnixNanos::from_micros(1);
1524 assert_eq!(nanos.as_u64(), 1_000);
1525 }
1526
1527 #[rstest]
1528 fn test_from_micros_one_second() {
1529 let nanos = UnixNanos::from_micros(1_000_000);
1530 assert_eq!(nanos.as_u64(), 1_000_000_000);
1531 }
1532
1533 #[rstest]
1534 fn test_from_micros_one_millisecond() {
1535 let nanos = UnixNanos::from_micros(1_000);
1536 assert_eq!(nanos.as_u64(), 1_000_000);
1537 assert_eq!(UnixNanos::from_micros(1_000), UnixNanos::from_millis(1));
1538 }
1539
1540 #[rstest]
1541 fn test_from_micros_realistic_timestamp() {
1542 let micros = 1_700_000_000_000_000_u64;
1543 let nanos = UnixNanos::from_micros(micros);
1544 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1545 }
1546
1547 #[rstest]
1548 fn test_from_micros_max_safe() {
1549 let max_us = u64::MAX / 1_000;
1550 let nanos = UnixNanos::from_micros(max_us);
1551 assert_eq!(nanos.as_u64(), max_us * 1_000);
1552 }
1553
1554 #[rstest]
1555 fn test_from_micros_matches_manual_conversion() {
1556 let us = 1_000_000_123_456_u64;
1557 let expected = us * 1_000;
1558 assert_eq!(UnixNanos::from_micros(us).as_u64(), expected);
1559 }
1560
1561 #[rstest]
1562 fn test_from_seconds_millis_and_micros_consistency() {
1563 assert_eq!(UnixNanos::from_seconds(1), UnixNanos::from_millis(1_000));
1564 assert_eq!(
1565 UnixNanos::from_seconds(60),
1566 UnixNanos::from_micros(60_000_000)
1567 );
1568 assert_eq!(
1569 UnixNanos::from_millis(1_000),
1570 UnixNanos::from_micros(1_000_000)
1571 );
1572 assert_eq!(
1573 UnixNanos::from_millis(60_000),
1574 UnixNanos::from_micros(60_000_000)
1575 );
1576 }
1577
1578 #[rstest]
1579 fn test_from_millis_round_trip_to_datetime() {
1580 let ms = 1_707_577_123_456_u64;
1581 let nanos = UnixNanos::from_millis(ms);
1582 let dt = nanos.to_datetime_utc();
1583 assert_eq!(dt.timestamp_millis().cast_unsigned(), ms);
1584 }
1585
1586 #[rstest]
1587 fn test_from_micros_preserves_sub_millisecond() {
1588 let micros = 1_700_000_000_000_123_u64;
1589 let nanos = UnixNanos::from_micros(micros);
1590 assert_eq!(nanos.as_u64() % 1_000_000, 123_000);
1591 }
1592
1593 #[rstest]
1594 #[should_panic(expected = "UnixNanos overflow in from_seconds")]
1595 fn test_from_seconds_overflow_panics() {
1596 let _ = UnixNanos::from_seconds(u64::MAX / 1_000_000_000 + 1);
1597 }
1598
1599 #[rstest]
1600 #[should_panic(expected = "UnixNanos overflow in from_millis")]
1601 fn test_from_millis_overflow_panics() {
1602 let _ = UnixNanos::from_millis(u64::MAX / 1_000_000 + 1);
1603 }
1604
1605 #[rstest]
1606 #[should_panic(expected = "UnixNanos overflow in from_micros")]
1607 fn test_from_micros_overflow_panics() {
1608 let _ = UnixNanos::from_micros(u64::MAX / 1_000 + 1);
1609 }
1610}