1use std::{
51 cmp::Ordering,
52 fmt::Display,
53 ops::{Add, AddAssign, Deref, Sub, SubAssign},
54 str::FromStr,
55 time::SystemTime,
56};
57
58use jiff::{Timestamp, civil::Date, tz::Offset};
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 U64_UPPER_BOUND_F64,
67};
68
69pub type DurationNanos = u64;
71
72#[repr(C)]
74#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
75pub struct UnixNanos(u64);
76
77impl UnixNanos {
78 #[must_use]
80 pub const fn new(value: u64) -> Self {
81 Self(value)
82 }
83
84 #[must_use]
86 pub const fn max() -> Self {
87 Self(u64::MAX)
88 }
89
90 #[must_use]
92 pub const fn is_zero(&self) -> bool {
93 self.0 == 0
94 }
95
96 #[must_use]
98 pub const fn as_u64(&self) -> u64 {
99 self.0
100 }
101
102 #[must_use]
104 pub const fn as_seconds(&self) -> u64 {
105 self.0 / NANOSECONDS_IN_SECOND
106 }
107
108 #[must_use]
110 pub const fn as_millis(&self) -> u64 {
111 self.0 / NANOSECONDS_IN_MILLISECOND
112 }
113
114 #[must_use]
116 pub const fn as_micros(&self) -> u64 {
117 self.0 / NANOSECONDS_IN_MICROSECOND
118 }
119
120 #[must_use]
126 pub const fn from_seconds(seconds: u64) -> Self {
127 match seconds.checked_mul(NANOSECONDS_IN_SECOND) {
128 Some(nanos) => Self(nanos),
129 None => panic!("UnixNanos overflow in from_seconds"),
130 }
131 }
132
133 #[must_use]
139 pub const fn from_millis(millis: u64) -> Self {
140 match millis.checked_mul(NANOSECONDS_IN_MILLISECOND) {
141 Some(nanos) => Self(nanos),
142 None => panic!("UnixNanos overflow in from_millis"),
143 }
144 }
145
146 #[must_use]
150 pub const fn from_millis_checked(millis: i64) -> Option<Self> {
151 Self::from_units_checked(millis, NANOSECONDS_IN_MILLISECOND)
152 }
153
154 #[must_use]
160 pub const fn from_micros(micros: u64) -> Self {
161 match micros.checked_mul(NANOSECONDS_IN_MICROSECOND) {
162 Some(nanos) => Self(nanos),
163 None => panic!("UnixNanos overflow in from_micros"),
164 }
165 }
166
167 #[must_use]
171 pub const fn from_micros_checked(micros: i64) -> Option<Self> {
172 Self::from_units_checked(micros, NANOSECONDS_IN_MICROSECOND)
173 }
174
175 const fn from_units_checked(value: i64, nanos_per_unit: u64) -> Option<Self> {
176 if value < 0 {
177 return None;
178 }
179
180 match value.cast_unsigned().checked_mul(nanos_per_unit) {
181 Some(nanos) => Some(Self(nanos)),
182 None => None,
183 }
184 }
185
186 #[must_use]
192 pub const fn as_i64(&self) -> i64 {
193 assert!(
194 self.0 <= i64::MAX.cast_unsigned(),
195 "UnixNanos value exceeds i64::MAX"
196 );
197 self.0.cast_signed()
198 }
199
200 #[must_use]
202 #[expect(
203 clippy::cast_precision_loss,
204 reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
205 )]
206 pub const fn as_f64(&self) -> f64 {
207 self.0 as f64
208 }
209
210 #[must_use]
216 pub fn to_datetime_utc(&self) -> Timestamp {
217 Timestamp::from_nanosecond(i128::from(self.0))
218 .expect("UnixNanos is within Jiff's timestamp range")
219 }
220
221 #[must_use]
223 pub fn to_rfc3339(&self) -> String {
224 let datetime = self.to_datetime_utc();
225 let display = datetime.display_with_offset(Offset::UTC);
226
227 match datetime.subsec_nanosecond() {
228 0 => format!("{display:.0}"),
229 nanos if nanos % 1_000_000 == 0 => format!("{display:.3}"),
230 nanos if nanos % 1_000 == 0 => format!("{display:.6}"),
231 _ => format!("{display:.9}"),
232 }
233 }
234
235 #[must_use]
240 pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
241 self.0.checked_sub(other.0)
242 }
243
244 fn parse_string(s: &str) -> Result<Self, String> {
245 if let Ok(int_value) = s.parse::<u64>() {
247 return Ok(Self(int_value));
248 }
249
250 if s.chars().all(|c| c.is_ascii_digit()) {
256 return Err("Unix timestamp is out of range".into());
257 }
258
259 if let Ok(float_value) = s.parse::<f64>() {
261 return f64_seconds_to_nanos(float_value).map(Self);
262 }
263
264 let is_compatible_rfc3339 = matches!(s.as_bytes().get(10), Some(b'T' | b't' | b' '))
267 && !s.as_bytes().contains(&b'[');
268 if is_compatible_rfc3339 && let Ok(datetime) = s.parse::<Timestamp>() {
269 let nanos = datetime.as_nanosecond();
270 let nanos = u64::try_from(nanos)
271 .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
272 return Ok(Self(nanos));
273 }
274
275 if let Ok(date) = Date::strptime("%Y-%m-%d", s) {
277 let datetime = date
278 .at(0, 0, 0, 0)
279 .to_zoned(jiff::tz::TimeZone::UTC)
280 .map_err(|e| e.to_string())?;
281 let nanos = datetime.timestamp().as_nanosecond();
282 let nanos = u64::try_from(nanos)
283 .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
284 return Ok(Self(nanos));
285 }
286
287 Err(format!("Invalid format: {s}"))
288 }
289
290 #[must_use]
292 pub fn checked_add<T: Into<u64>>(self, rhs: T) -> Option<Self> {
293 self.0.checked_add(rhs.into()).map(Self)
294 }
295
296 #[must_use]
298 pub fn checked_sub<T: Into<u64>>(self, rhs: T) -> Option<Self> {
299 self.0.checked_sub(rhs.into()).map(Self)
300 }
301
302 #[must_use]
304 pub fn saturating_add_ns<T: Into<u64>>(self, rhs: T) -> Self {
305 Self(self.0.saturating_add(rhs.into()))
306 }
307
308 #[must_use]
310 pub fn saturating_sub_ns<T: Into<u64>>(self, rhs: T) -> Self {
311 Self(self.0.saturating_sub(rhs.into()))
312 }
313}
314
315#[expect(
318 clippy::cast_possible_truncation,
319 clippy::cast_sign_loss,
320 reason = "value is checked finite, non-negative, and within u64 range before the cast"
321)]
322fn f64_seconds_to_nanos(value: f64) -> Result<u64, String> {
323 if !value.is_finite() {
324 return Err(format!("Unix timestamp must be finite, was {value}"));
325 }
326
327 if value < 0.0 {
328 return Err("Unix timestamp cannot be negative".to_string());
329 }
330
331 let nanos_f64 = value * 1_000_000_000.0;
335
336 if nanos_f64 >= U64_UPPER_BOUND_F64 {
337 return Err(format!("Unix timestamp {value} seconds is out of range"));
338 }
339
340 Ok(nanos_f64.trunc() as u64)
341}
342
343impl Deref for UnixNanos {
344 type Target = u64;
345
346 fn deref(&self) -> &Self::Target {
347 &self.0
348 }
349}
350
351impl PartialEq<u64> for UnixNanos {
352 fn eq(&self, other: &u64) -> bool {
353 self.0 == *other
354 }
355}
356
357impl PartialOrd<u64> for UnixNanos {
358 fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
359 self.0.partial_cmp(other)
360 }
361}
362
363impl PartialEq<Option<u64>> for UnixNanos {
364 fn eq(&self, other: &Option<u64>) -> bool {
365 match other {
366 Some(value) => self.0 == *value,
367 None => false,
368 }
369 }
370}
371
372impl PartialOrd<Option<u64>> for UnixNanos {
373 fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
374 match other {
375 Some(value) => self.0.partial_cmp(value),
376 None => Some(Ordering::Greater),
377 }
378 }
379}
380
381impl PartialEq<UnixNanos> for u64 {
382 fn eq(&self, other: &UnixNanos) -> bool {
383 *self == other.0
384 }
385}
386
387impl PartialOrd<UnixNanos> for u64 {
388 fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
389 self.partial_cmp(&other.0)
390 }
391}
392
393impl From<u64> for UnixNanos {
394 fn from(value: u64) -> Self {
395 Self(value)
396 }
397}
398
399impl From<UnixNanos> for u64 {
400 fn from(value: UnixNanos) -> Self {
401 value.0
402 }
403}
404
405impl From<&str> for UnixNanos {
416 fn from(value: &str) -> Self {
417 value
418 .parse()
419 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
420 }
421}
422
423impl From<String> for UnixNanos {
434 fn from(value: String) -> Self {
435 value
436 .parse()
437 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
438 }
439}
440
441impl From<Timestamp> for UnixNanos {
442 fn from(value: Timestamp) -> Self {
443 let nanos = value.as_nanosecond();
444
445 assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
446
447 Self::from(u64::try_from(nanos).expect("DateTime timestamp out of range for UnixNanos"))
448 }
449}
450
451impl From<SystemTime> for UnixNanos {
452 fn from(value: SystemTime) -> Self {
453 let duration = value
454 .duration_since(std::time::UNIX_EPOCH)
455 .expect("SystemTime before UNIX EPOCH");
456
457 let nanos =
458 u64::try_from(duration.as_nanos()).expect("SystemTime overflowed u64 nanoseconds");
459
460 Self::from(nanos)
461 }
462}
463
464impl FromStr for UnixNanos {
465 type Err = Box<dyn std::error::Error>;
466
467 fn from_str(s: &str) -> Result<Self, Self::Err> {
468 Self::parse_string(s).map_err(std::convert::Into::into)
469 }
470}
471
472impl Add for UnixNanos {
481 type Output = Self;
482
483 fn add(self, rhs: Self) -> Self::Output {
484 Self(
485 self.0
486 .checked_add(rhs.0)
487 .expect("UnixNanos overflow in addition - invalid timestamp calculation"),
488 )
489 }
490}
491
492impl Sub for UnixNanos {
501 type Output = Self;
502
503 fn sub(self, rhs: Self) -> Self::Output {
504 Self(
505 self.0
506 .checked_sub(rhs.0)
507 .expect("UnixNanos underflow in subtraction - invalid timestamp calculation"),
508 )
509 }
510}
511
512impl Add<u64> for UnixNanos {
519 type Output = Self;
520
521 fn add(self, rhs: u64) -> Self::Output {
522 Self(
523 self.0
524 .checked_add(rhs)
525 .expect("UnixNanos overflow in addition"),
526 )
527 }
528}
529
530impl Sub<u64> for UnixNanos {
537 type Output = Self;
538
539 fn sub(self, rhs: u64) -> Self::Output {
540 Self(
541 self.0
542 .checked_sub(rhs)
543 .expect("UnixNanos underflow in subtraction"),
544 )
545 }
546}
547
548impl<T: Into<u64>> AddAssign<T> for UnixNanos {
554 fn add_assign(&mut self, other: T) {
555 let other_u64 = other.into();
556 self.0 = self
557 .0
558 .checked_add(other_u64)
559 .expect("UnixNanos overflow in add_assign");
560 }
561}
562
563impl<T: Into<u64>> SubAssign<T> for UnixNanos {
569 fn sub_assign(&mut self, other: T) {
570 let other_u64 = other.into();
571 self.0 = self
572 .0
573 .checked_sub(other_u64)
574 .expect("UnixNanos underflow in sub_assign");
575 }
576}
577
578impl Display for UnixNanos {
579 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580 write!(f, "{}", self.0)
581 }
582}
583
584impl From<UnixNanos> for Timestamp {
585 fn from(value: UnixNanos) -> Self {
586 value.to_datetime_utc()
587 }
588}
589
590impl<'de> Deserialize<'de> for UnixNanos {
591 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
592 where
593 D: Deserializer<'de>,
594 {
595 struct UnixNanosVisitor;
596
597 impl Visitor<'_> for UnixNanosVisitor {
598 type Value = UnixNanos;
599
600 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
601 formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
602 }
603
604 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
605 where
606 E: de::Error,
607 {
608 Ok(UnixNanos(value))
609 }
610
611 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
612 where
613 E: de::Error,
614 {
615 u64::try_from(value)
616 .map(UnixNanos)
617 .map_err(|_| E::custom("Unix timestamp cannot be negative"))
618 }
619
620 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
621 where
622 E: de::Error,
623 {
624 f64_seconds_to_nanos(value)
625 .map(UnixNanos)
626 .map_err(E::custom)
627 }
628
629 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
630 where
631 E: de::Error,
632 {
633 UnixNanos::parse_string(value).map_err(E::custom)
634 }
635 }
636
637 deserializer.deserialize_any(UnixNanosVisitor)
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use jiff::SignedDuration;
644 use rstest::rstest;
645
646 use super::*;
647
648 fn timestamp(value: &str) -> Timestamp {
649 value.parse().unwrap()
650 }
651
652 #[rstest]
653 fn test_new() {
654 let nanos = UnixNanos::new(123);
655 assert_eq!(nanos.as_u64(), 123);
656 assert_eq!(nanos.as_i64(), 123);
657 }
658
659 #[rstest]
660 fn test_max() {
661 let nanos = UnixNanos::max();
662 assert_eq!(nanos.as_u64(), u64::MAX);
663 }
664
665 #[rstest]
666 fn test_is_zero() {
667 assert!(UnixNanos::default().is_zero());
668 assert!(!UnixNanos::max().is_zero());
669 }
670
671 #[rstest]
672 fn test_from_u64() {
673 let nanos = UnixNanos::from(123);
674 assert_eq!(nanos.as_u64(), 123);
675 assert_eq!(nanos.as_i64(), 123);
676 }
677
678 #[rstest]
679 fn test_default() {
680 let nanos = UnixNanos::default();
681 assert_eq!(nanos.as_u64(), 0);
682 assert_eq!(nanos.as_i64(), 0);
683 }
684
685 #[rstest]
686 fn test_into_from() {
687 let nanos: UnixNanos = 456.into();
688 let value: u64 = nanos.into();
689 assert_eq!(value, 456);
690 }
691
692 #[rstest]
693 #[case(0, "1970-01-01T00:00:00+00:00")]
694 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
695 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
696 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
697 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
698 fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
699 let nanos = UnixNanos::from(nanos);
700 let datetime = nanos.to_datetime_utc();
701 assert_eq!(
702 datetime.display_with_offset(Offset::UTC).to_string(),
703 expected
704 );
705 }
706
707 #[rstest]
708 #[case(0, "1970-01-01T00:00:00+00:00")]
709 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
710 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
711 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
712 #[case(1_500_000_000_500_000_000, "2017-07-14T02:40:00.500+00:00")]
713 #[case(1_500_000_000_123_456_000, "2017-07-14T02:40:00.123456+00:00")]
714 #[case(1_500_000_000_123_456_789, "2017-07-14T02:40:00.123456789+00:00")]
715 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
716 fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
717 let nanos = UnixNanos::from(nanos);
718 assert_eq!(nanos.to_rfc3339(), expected);
719 }
720
721 #[rstest]
722 fn test_from_str() {
723 let nanos: UnixNanos = "123".parse().unwrap();
724 assert_eq!(nanos.as_u64(), 123);
725 }
726
727 #[rstest]
728 fn test_from_str_invalid() {
729 let result = "abc".parse::<UnixNanos>();
730 assert!(result.is_err());
731 }
732
733 #[rstest]
734 fn test_from_str_date() {
735 let nanos: UnixNanos = "2024-02-10".parse().unwrap();
736 assert_eq!(nanos.as_u64(), 1_707_523_200_000_000_000);
737 }
738
739 #[rstest]
740 fn test_from_str_pre_epoch_date() {
741 let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
742 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
743 }
744
745 #[rstest]
746 fn test_from_str_pre_epoch_rfc3339() {
747 let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
748 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
749 }
750
751 #[rstest]
752 fn test_try_from_datetime_valid() {
753 let datetime = Timestamp::from_second(1_000_000_000).unwrap();
754 let nanos = UnixNanos::from(datetime);
755 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
756 }
757
758 #[rstest]
759 fn test_from_system_time() {
760 let system_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
761 let nanos = UnixNanos::from(system_time);
762 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
763 }
764
765 #[rstest]
766 #[should_panic(expected = "SystemTime before UNIX EPOCH")]
767 fn test_from_system_time_before_epoch() {
768 let system_time = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
769 let _ = UnixNanos::from(system_time);
770 }
771
772 #[rstest]
773 #[should_panic(expected = "SystemTime overflowed u64 nanoseconds")]
774 fn test_from_system_time_overflow_panics() {
775 let system_time =
777 std::time::UNIX_EPOCH + std::time::Duration::from_secs(u64::MAX / 1_000_000_000 + 1);
778 let _ = UnixNanos::from(system_time);
779 }
780
781 #[rstest]
782 fn test_eq() {
783 let nanos = UnixNanos::from(100);
784 assert_eq!(nanos, 100);
785 assert_eq!(nanos, Some(100));
786 assert_ne!(nanos, 200);
787 assert_ne!(nanos, Some(200));
788 assert_ne!(nanos, None);
789 }
790
791 #[rstest]
792 fn test_partial_cmp() {
793 let nanos = UnixNanos::from(100);
794 assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
795 assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
796 assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
797 assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
798 }
799
800 #[rstest]
801 fn test_edge_case_max_value() {
802 let nanos = UnixNanos::from(u64::MAX);
803 assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
804 }
805
806 #[rstest]
807 fn test_display() {
808 let nanos = UnixNanos::from(123);
809 assert_eq!(format!("{nanos}"), "123");
810 }
811
812 #[rstest]
813 fn test_addition() {
814 let nanos1 = UnixNanos::from(100);
815 let nanos2 = UnixNanos::from(200);
816 let result = nanos1 + nanos2;
817 assert_eq!(result.as_u64(), 300);
818 }
819
820 #[rstest]
821 fn test_add_assign() {
822 let mut nanos = UnixNanos::from(100);
823 nanos += 50_u64;
824 assert_eq!(nanos.as_u64(), 150);
825 }
826
827 #[rstest]
828 fn test_subtraction() {
829 let nanos1 = UnixNanos::from(200);
830 let nanos2 = UnixNanos::from(100);
831 let result = nanos1 - nanos2;
832 assert_eq!(result.as_u64(), 100);
833 }
834
835 #[rstest]
836 fn test_sub_assign() {
837 let mut nanos = UnixNanos::from(200);
838 nanos -= 50_u64;
839 assert_eq!(nanos.as_u64(), 150);
840 }
841
842 #[rstest]
843 #[should_panic(expected = "UnixNanos overflow")]
844 fn test_overflow_add() {
845 let nanos = UnixNanos::from(u64::MAX);
846 let _ = nanos + UnixNanos::from(1); }
848
849 #[rstest]
850 #[should_panic(expected = "UnixNanos overflow")]
851 fn test_overflow_add_u64() {
852 let nanos = UnixNanos::from(u64::MAX);
853 let _ = nanos + 1_u64; }
855
856 #[rstest]
857 #[should_panic(expected = "UnixNanos underflow")]
858 fn test_overflow_sub() {
859 let _ = UnixNanos::default() - UnixNanos::from(1); }
861
862 #[rstest]
863 #[should_panic(expected = "UnixNanos underflow")]
864 fn test_overflow_sub_u64() {
865 let _ = UnixNanos::default() - 1_u64; }
867
868 #[rstest]
869 #[case(100, 50, Some(50))]
870 #[case(1_000_000_000, 500_000_000, Some(500_000_000))]
871 #[case(u64::MAX, u64::MAX - 1, Some(1))]
872 #[case(50, 50, Some(0))]
873 #[case(50, 100, None)]
874 #[case(0, 1, None)]
875 fn test_duration_since(
876 #[case] time1: u64,
877 #[case] time2: u64,
878 #[case] expected: Option<DurationNanos>,
879 ) {
880 let nanos1 = UnixNanos::from(time1);
881 let nanos2 = UnixNanos::from(time2);
882 assert_eq!(nanos1.duration_since(&nanos2), expected);
883 }
884
885 #[rstest]
886 fn test_duration_since_same_moment() {
887 let moment = UnixNanos::from(1_707_577_123_456_789_000);
888 assert_eq!(moment.duration_since(&moment), Some(0));
889 }
890
891 #[rstest]
892 fn test_duration_since_chronological() {
893 let earlier = timestamp("2024-02-10T12:00:00Z");
895
896 let later = earlier
898 + SignedDuration::from_hours(1)
899 + SignedDuration::from_mins(30)
900 + SignedDuration::from_secs(45)
901 + SignedDuration::from_nanos(500_000_000);
902
903 let earlier_nanos = UnixNanos::from(earlier);
904 let later_nanos = UnixNanos::from(later);
905
906 let expected_duration =
908 (60 * 60 + 30 * 60 + 45) * NANOSECONDS_IN_SECOND + 500 * NANOSECONDS_IN_MILLISECOND;
909
910 assert_eq!(
911 later_nanos.duration_since(&earlier_nanos),
912 Some(expected_duration)
913 );
914 assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
915 }
916
917 #[rstest]
918 fn test_duration_since_with_edge_cases() {
919 let max = UnixNanos::from(u64::MAX);
921 let smaller = UnixNanos::from(u64::MAX - 1000);
922
923 assert_eq!(max.duration_since(&smaller), Some(1000));
924 assert_eq!(smaller.duration_since(&max), None);
925
926 let min = UnixNanos::default(); let larger = UnixNanos::from(1000);
929
930 assert_eq!(min.duration_since(&min), Some(0));
931 assert_eq!(larger.duration_since(&min), Some(1000));
932 assert_eq!(min.duration_since(&larger), None);
933 }
934
935 #[rstest]
936 fn test_serde_json() {
937 let nanos = UnixNanos::from(123);
938 let json = serde_json::to_string(&nanos).unwrap();
939 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
940 assert_eq!(deserialized, nanos);
941 }
942
943 #[rstest]
944 fn test_serde_edge_cases() {
945 let nanos = UnixNanos::from(u64::MAX);
946 let json = serde_json::to_string(&nanos).unwrap();
947 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
948 assert_eq!(deserialized, nanos);
949 }
950
951 #[rstest]
952 #[case("123", 123)] #[case("1234.567", 1_234_567_000_000)] #[case("2024-02-10", 1_707_523_200_000_000_000)] #[case("2024-2-10", 1_707_523_200_000_000_000)] #[case("2024-02-1", 1_706_745_600_000_000_000)] #[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] #[case("2024-02-10t14:58:43Z", 1_707_577_123_000_000_000)] #[case("2024-02-10 14: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) {
962 let parsed: UnixNanos = input.parse().unwrap();
963 assert_eq!(parsed.as_u64(), expected);
964 }
965
966 #[rstest]
967 #[case("abc")] #[case("not a timestamp")] #[case("2024-02-10 14:58:43")] #[case("2024-02-10T14:58:43Z[UTC]")] fn test_from_str_invalid_formats(#[case] input: &str) {
972 let result = input.parse::<UnixNanos>();
973 assert!(result.is_err());
974 }
975
976 #[rstest]
977 fn test_from_str_integer_overflow() {
978 let input = "184467440737095516160";
980 let result = input.parse::<UnixNanos>();
981 assert!(result.is_err());
982 }
983
984 #[rstest]
985 fn test_checked_add_overflow_returns_none() {
986 let max = UnixNanos::from(u64::MAX);
987 assert_eq!(max.checked_add(1_u64), None);
988 }
989
990 #[rstest]
991 fn test_checked_sub_underflow_returns_none() {
992 let zero = UnixNanos::default();
993 assert_eq!(zero.checked_sub(1_u64), None);
994 }
995
996 #[rstest]
997 fn test_saturating_add_overflow() {
998 let max = UnixNanos::from(u64::MAX);
999 let result = max.saturating_add_ns(1_u64);
1000 assert_eq!(result, UnixNanos::from(u64::MAX));
1001 }
1002
1003 #[rstest]
1004 fn test_saturating_sub_underflow() {
1005 let zero = UnixNanos::default();
1006 let result = zero.saturating_sub_ns(1_u64);
1007 assert_eq!(result, UnixNanos::default());
1008 }
1009
1010 #[rstest]
1011 fn test_from_str_float_overflow() {
1012 let input = "2e10"; let err = input.parse::<UnixNanos>().unwrap_err();
1015 assert!(err.to_string().contains("out of range"));
1016 }
1017
1018 #[rstest]
1019 #[case("NaN")]
1020 #[case("nan")]
1021 #[case("inf")]
1022 #[case("-inf")]
1023 fn test_from_str_non_finite_float_errors(#[case] input: &str) {
1024 let err = input.parse::<UnixNanos>().unwrap_err();
1025 assert!(err.to_string().contains("must be finite"));
1026 }
1027
1028 #[rstest]
1029 #[case("-1.5")]
1030 #[case("-0.000001")]
1031 fn test_from_str_negative_float_errors(#[case] input: &str) {
1032 let err = input.parse::<UnixNanos>().unwrap_err();
1033 assert!(err.to_string().contains("cannot be negative"));
1034 }
1035
1036 #[rstest]
1037 fn test_deserialize_u64() {
1038 let json = "123456789";
1039 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1040 assert_eq!(deserialized.as_u64(), 123_456_789);
1041 }
1042
1043 #[rstest]
1044 fn test_deserialize_string_with_int() {
1045 let json = "\"123456789\"";
1046 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1047 assert_eq!(deserialized.as_u64(), 123_456_789);
1048 }
1049
1050 #[rstest]
1051 fn test_deserialize_float() {
1052 let json = "1234.567";
1053 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1054 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1055 }
1056
1057 #[rstest]
1058 fn test_deserialize_string_with_float() {
1059 let json = "\"1234.567\"";
1060 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1061 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1062 }
1063
1064 #[rstest]
1065 fn test_deserialize_float_uses_truncation() {
1066 let json = "0.9999999999";
1068 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1069 assert_eq!(deserialized.as_u64(), 999_999_999); }
1071
1072 #[rstest]
1073 #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
1074 #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
1075 fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
1076 let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
1077 assert_eq!(deserialized.as_u64(), expected);
1078 }
1079
1080 #[rstest]
1081 fn test_deserialize_negative_int_fails() {
1082 let json = "-123456789";
1083 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1084 assert!(
1085 result
1086 .unwrap_err()
1087 .to_string()
1088 .contains("cannot be negative")
1089 );
1090 }
1091
1092 #[rstest]
1093 fn test_deserialize_negative_float_fails() {
1094 let json = "-1234.567";
1095 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1096 assert!(
1097 result
1098 .unwrap_err()
1099 .to_string()
1100 .contains("cannot be negative")
1101 );
1102 }
1103
1104 #[rstest]
1105 fn test_deserialize_nan_fails() {
1106 use serde::de::{
1108 IntoDeserializer,
1109 value::{Error as ValueError, F64Deserializer},
1110 };
1111 let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
1112 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1113 assert!(result.is_err());
1114 assert!(result.unwrap_err().to_string().contains("must be finite"));
1115 }
1116
1117 #[rstest]
1118 fn test_deserialize_infinity_fails() {
1119 use serde::de::{
1120 IntoDeserializer,
1121 value::{Error as ValueError, F64Deserializer},
1122 };
1123 let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
1124 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1125 assert!(result.is_err());
1126 assert!(result.unwrap_err().to_string().contains("must be finite"));
1127 }
1128
1129 #[rstest]
1130 fn test_deserialize_negative_infinity_fails() {
1131 use serde::de::{
1132 IntoDeserializer,
1133 value::{Error as ValueError, F64Deserializer},
1134 };
1135 let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
1136 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1137 assert!(result.is_err());
1138 assert!(result.unwrap_err().to_string().contains("must be finite"));
1139 }
1140
1141 #[rstest]
1142 fn test_deserialize_overflow_float_fails() {
1143 let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
1146 assert!(result.is_err());
1147 assert!(result.unwrap_err().to_string().contains("out of range"));
1148 }
1149
1150 #[rstest]
1151 fn test_deserialize_float_u64_boundary_fails() {
1152 let deserializer = serde::de::value::F64Deserializer::<serde::de::value::Error>::new(
1153 18_446_744_073.709_553,
1154 );
1155 let err = UnixNanos::deserialize(deserializer).unwrap_err();
1156 assert!(err.to_string().contains("out of range"));
1157 }
1158
1159 #[rstest]
1160 fn test_deserialize_invalid_string_fails() {
1161 let json = "\"not a timestamp\"";
1162 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1163 assert!(result.is_err());
1164 }
1165
1166 #[rstest]
1167 fn test_deserialize_edge_cases() {
1168 let json = "0";
1170 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1171 assert_eq!(deserialized.as_u64(), 0);
1172
1173 let json = "18446744073709551615"; let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1176 assert_eq!(deserialized.as_u64(), u64::MAX);
1177 }
1178
1179 #[rstest]
1180 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1181 fn test_as_i64_overflow_panics() {
1182 let nanos = UnixNanos::from(u64::MAX);
1183 let _ = nanos.as_i64(); }
1185
1186 #[rstest]
1187 fn test_as_i64_at_i64_max_boundary() {
1188 let nanos = UnixNanos::from(i64::MAX.cast_unsigned());
1189 assert_eq!(nanos.as_i64(), i64::MAX);
1190 }
1191
1192 #[rstest]
1193 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1194 fn test_as_i64_just_above_i64_max_panics() {
1195 let nanos = UnixNanos::from(i64::MAX.cast_unsigned() + 1);
1196 let _ = nanos.as_i64();
1197 }
1198
1199 use proptest::prelude::*;
1200
1201 fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
1202 prop_oneof![
1203 0u64..1_000_000u64,
1205 1_000_000u64..1_000_000_000_000u64,
1207 1_000_000_000_000u64..=i64::MAX.cast_unsigned(),
1209 (i64::MAX.cast_unsigned() + 1)..=u64::MAX,
1211 Just(0u64),
1213 Just(1u64),
1214 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), ]
1221 .prop_map(UnixNanos::from)
1222 }
1223
1224 fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
1225 (unix_nanos_strategy(), unix_nanos_strategy())
1226 }
1227
1228 proptest! {
1229 #[rstest]
1230 #[expect(
1231 clippy::float_cmp,
1232 clippy::cast_precision_loss,
1233 reason = "roundtrip: both sides go through the same u64->f64 cast"
1234 )]
1235 fn prop_unix_nanos_construction_roundtrip(nanos in unix_nanos_strategy()) {
1236 let value = nanos.as_u64();
1237 prop_assert_eq!(UnixNanos::from(value).as_u64(), value);
1238 prop_assert_eq!(nanos.as_f64(), value as f64);
1239
1240 if i64::try_from(value).is_ok() {
1242 prop_assert_eq!(nanos.as_i64(), value.cast_signed());
1243 }
1244 }
1245
1246 #[rstest]
1247 fn prop_unix_nanos_addition_commutative(
1248 (nanos1, nanos2) in unix_nanos_pair_strategy()
1249 ) {
1250 if let (Some(sum1), Some(sum2)) = (
1252 nanos1.checked_add(nanos2.as_u64()),
1253 nanos2.checked_add(nanos1.as_u64())
1254 ) {
1255 prop_assert_eq!(sum1, sum2, "Addition should be commutative");
1256 }
1257 }
1258
1259 #[rstest]
1260 fn prop_unix_nanos_addition_associative(
1261 nanos1 in unix_nanos_strategy(),
1262 nanos2 in unix_nanos_strategy(),
1263 nanos3 in unix_nanos_strategy(),
1264 ) {
1265 let expected = nanos1
1266 .as_u64()
1267 .checked_add(nanos2.as_u64())
1268 .and_then(|sum| sum.checked_add(nanos3.as_u64()));
1269
1270 if let Some(expected) = expected {
1271 let left = (nanos1 + nanos2) + nanos3;
1272 let right = nanos1 + (nanos2 + nanos3);
1273 prop_assert_eq!(left.as_u64(), expected);
1274 prop_assert_eq!(right.as_u64(), expected);
1275 }
1276 }
1277
1278 #[rstest]
1279 fn prop_unix_nanos_subtraction_inverse(
1280 (nanos1, nanos2) in unix_nanos_pair_strategy()
1281 ) {
1282 if let Some(sum) = nanos1.checked_add(nanos2.as_u64()) {
1284 let diff = sum - nanos2;
1285 prop_assert_eq!(diff, nanos1, "Subtraction should be inverse of addition");
1286 }
1287 }
1288
1289 #[rstest]
1290 fn prop_unix_nanos_zero_identity(nanos in unix_nanos_strategy()) {
1291 let zero = UnixNanos::default();
1293 prop_assert_eq!(nanos + zero, nanos, "Zero should be additive identity");
1294 prop_assert_eq!(zero + nanos, nanos, "Zero should be additive identity (commutative)");
1295 prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
1296 }
1297
1298 #[rstest]
1299 fn prop_unix_nanos_ordering_consistency(
1300 (nanos1, nanos2) in unix_nanos_pair_strategy()
1301 ) {
1302 let eq = nanos1 == nanos2;
1304 let lt = nanos1 < nanos2;
1305 let gt = nanos1 > nanos2;
1306 let le = nanos1 <= nanos2;
1307 let ge = nanos1 >= nanos2;
1308
1309 let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
1311 prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
1312
1313 prop_assert_eq!(le, eq || lt, "<= should equal == || <");
1315 prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
1316 prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
1317 prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
1318 }
1319
1320 #[rstest]
1321 fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
1322 let string_repr = nanos.to_string();
1324 let parsed = UnixNanos::from_str(&string_repr);
1325 prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
1326 if let Ok(parsed_nanos) = parsed {
1327 prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
1328 }
1329 }
1330
1331 #[rstest]
1332 fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
1333 if i64::try_from(nanos.as_u64()).is_ok() {
1335 let datetime = nanos.to_datetime_utc();
1336 let converted_back = UnixNanos::from(datetime);
1337 prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
1338
1339 let rfc3339 = nanos.to_rfc3339();
1341 if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
1342 prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
1343 }
1344 }
1345 }
1346
1347 #[rstest]
1348 fn prop_unix_nanos_duration_since(
1349 (nanos1, nanos2) in unix_nanos_pair_strategy()
1350 ) {
1351 let duration = nanos1.duration_since(&nanos2);
1353
1354 if nanos1 >= nanos2 {
1355 prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
1357 if let Some(dur) = duration {
1358 prop_assert_eq!(dur, nanos1.as_u64() - nanos2.as_u64(),
1359 "Duration should equal the difference");
1360 prop_assert_eq!(nanos2 + dur, nanos1.as_u64(),
1361 "second + duration should equal first");
1362 }
1363 } else {
1364 prop_assert!(duration.is_none(), "Duration should be None when first < second");
1366 }
1367 }
1368
1369 #[rstest]
1370 fn prop_unix_nanos_checked_arithmetic(
1371 (nanos1, nanos2) in unix_nanos_pair_strategy()
1372 ) {
1373 let checked_add = nanos1.checked_add(nanos2.as_u64());
1375 let checked_sub = nanos1.checked_sub(nanos2.as_u64());
1376
1377 if let Some(sum) = checked_add
1379 && nanos1.as_u64().checked_add(nanos2.as_u64()).is_some() {
1380 prop_assert_eq!(sum, nanos1 + nanos2, "Checked add should match regular add when no overflow");
1381 }
1382
1383 if let Some(diff) = checked_sub
1385 && nanos1.as_u64() >= nanos2.as_u64() {
1386 prop_assert_eq!(diff, nanos1 - nanos2, "Checked sub should match regular sub when no underflow");
1387 }
1388 }
1389
1390 #[rstest]
1391 fn prop_unix_nanos_saturating_arithmetic(
1392 (nanos1, nanos2) in unix_nanos_pair_strategy()
1393 ) {
1394 let sat_add = nanos1.saturating_add_ns(nanos2.as_u64());
1396 let sat_sub = nanos1.saturating_sub_ns(nanos2.as_u64());
1397
1398 prop_assert!(sat_add >= nanos1, "Saturating add result should be >= first operand");
1400 prop_assert!(sat_add.as_u64() >= nanos2.as_u64(), "Saturating add result should be >= second operand");
1401
1402 prop_assert!(sat_sub <= nanos1, "Saturating sub result should be <= first operand");
1404
1405 if let Some(checked_sum) = nanos1.checked_add(nanos2.as_u64()) {
1407 prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
1408 } else {
1409 prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
1410 }
1411
1412 if let Some(checked_diff) = nanos1.checked_sub(nanos2.as_u64()) {
1413 prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
1414 } else {
1415 prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
1416 }
1417 }
1418
1419 #[rstest]
1420 fn prop_unix_nanos_assign_mirrors_op(
1421 (nanos1, nanos2) in unix_nanos_pair_strategy()
1422 ) {
1423 if let Some(expected) = nanos1.checked_add(nanos2.as_u64()) {
1425 let mut add_result = nanos1;
1426 add_result += nanos2;
1427 prop_assert_eq!(add_result, expected, "AddAssign should mirror Add");
1428 }
1429
1430 if nanos1.as_u64() >= nanos2.as_u64() {
1432 let expected = nanos1 - nanos2;
1433 let mut sub_result = nanos1;
1434 sub_result -= nanos2;
1435 prop_assert_eq!(sub_result, expected, "SubAssign should mirror Sub");
1436 }
1437 }
1438
1439 #[rstest]
1440 fn prop_unix_nanos_serde_roundtrip(nanos in unix_nanos_strategy()) {
1441 let json = serde_json::to_string(&nanos).unwrap();
1442 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1443 prop_assert_eq!(deserialized, nanos, "Serde JSON should round-trip exactly");
1444 }
1445
1446 #[rstest]
1447 fn prop_unix_nanos_f64_deserialize_never_panics(val: f64) {
1448 use serde::de::{IntoDeserializer, value::{Error as ValueError, F64Deserializer}};
1451 let deserializer: F64Deserializer<ValueError> = val.into_deserializer();
1452 let result = UnixNanos::deserialize(deserializer);
1453
1454 let upper_bound = 2.0_f64.powi(64);
1455 if val.is_finite() && val >= 0.0 && val * 1_000_000_000.0 < upper_bound {
1456 prop_assert!(result.is_ok(), "Should succeed for valid f64: {}", val);
1457 } else {
1458 prop_assert!(result.is_err(), "Should error for invalid f64: {}", val);
1459 }
1460 }
1461 }
1462
1463 #[rstest]
1464 fn test_from_seconds_zero() {
1465 let nanos = UnixNanos::from_seconds(0);
1466 assert_eq!(nanos.as_u64(), 0);
1467 }
1468
1469 #[rstest]
1470 fn test_from_seconds_one() {
1471 let nanos = UnixNanos::from_seconds(1);
1472 assert_eq!(nanos.as_u64(), 1_000_000_000);
1473 }
1474
1475 #[rstest]
1476 fn test_from_seconds_realistic_timestamp() {
1477 let nanos = UnixNanos::from_seconds(1_700_000_000);
1478 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1479 assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
1480 }
1481
1482 #[rstest]
1483 fn test_from_seconds_max_safe() {
1484 let max_seconds = u64::MAX / 1_000_000_000;
1485 let nanos = UnixNanos::from_seconds(max_seconds);
1486 assert_eq!(nanos.as_u64(), max_seconds * 1_000_000_000);
1487 }
1488
1489 #[rstest]
1490 fn test_from_millis_zero() {
1491 let nanos = UnixNanos::from_millis(0);
1492 assert_eq!(nanos.as_u64(), 0);
1493 }
1494
1495 #[rstest]
1496 fn test_from_millis_one() {
1497 let nanos = UnixNanos::from_millis(1);
1498 assert_eq!(nanos.as_u64(), 1_000_000);
1499 }
1500
1501 #[rstest]
1502 fn test_from_millis_one_second() {
1503 let nanos = UnixNanos::from_millis(1_000);
1504 assert_eq!(nanos.as_u64(), 1_000_000_000);
1505 }
1506
1507 #[rstest]
1508 fn test_from_millis_realistic_timestamp() {
1509 let nanos = UnixNanos::from_millis(1_700_000_000_000);
1511 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1512 assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
1513 }
1514
1515 #[rstest]
1516 fn test_from_millis_max_safe() {
1517 let max_ms = u64::MAX / 1_000_000;
1518 let nanos = UnixNanos::from_millis(max_ms);
1519 assert_eq!(nanos.as_u64(), max_ms * 1_000_000);
1520 }
1521
1522 #[rstest]
1523 fn test_from_millis_matches_manual_conversion() {
1524 let ms = 1_625_474_304_765_u64;
1525 let expected = ms * 1_000_000;
1526 assert_eq!(UnixNanos::from_millis(ms).as_u64(), expected);
1527 }
1528
1529 #[rstest]
1530 #[case::valid(1_700_000_000_123, Some(1_700_000_000_123_000_000))]
1531 #[case::negative(-1, None)]
1532 #[case::overflow(i64::MAX, None)]
1533 fn test_from_millis_checked(#[case] millis: i64, #[case] expected: Option<u64>) {
1534 assert_eq!(
1535 UnixNanos::from_millis_checked(millis).map(|value| value.as_u64()),
1536 expected
1537 );
1538 }
1539
1540 #[rstest]
1541 #[case(0, 0)]
1542 #[case(999_999_999, 0)]
1543 #[case(1_000_000_000, 1)]
1544 #[case(1_700_000_000_123_456_789, 1_700_000_000)]
1545 fn test_as_seconds(#[case] nanos: u64, #[case] expected: u64) {
1546 assert_eq!(UnixNanos::from(nanos).as_seconds(), expected);
1547 }
1548
1549 #[rstest]
1550 #[case(0, 0)]
1551 #[case(999_999, 0)]
1552 #[case(1_000_000, 1)]
1553 #[case(1_700_000_000_000_123_456, 1_700_000_000_000)]
1554 fn test_as_millis(#[case] nanos: u64, #[case] expected: u64) {
1555 assert_eq!(UnixNanos::from(nanos).as_millis(), expected);
1556 }
1557
1558 #[rstest]
1559 #[case(0, 0)]
1560 #[case(999, 0)]
1561 #[case(1_000, 1)]
1562 #[case(1_700_000_000_000_123_456, 1_700_000_000_000_123)]
1563 fn test_as_micros(#[case] nanos: u64, #[case] expected: u64) {
1564 assert_eq!(UnixNanos::from(nanos).as_micros(), expected);
1565 }
1566
1567 #[rstest]
1568 fn test_from_micros_zero() {
1569 let nanos = UnixNanos::from_micros(0);
1570 assert_eq!(nanos.as_u64(), 0);
1571 }
1572
1573 #[rstest]
1574 fn test_from_micros_one() {
1575 let nanos = UnixNanos::from_micros(1);
1576 assert_eq!(nanos.as_u64(), 1_000);
1577 }
1578
1579 #[rstest]
1580 fn test_from_micros_one_second() {
1581 let nanos = UnixNanos::from_micros(1_000_000);
1582 assert_eq!(nanos.as_u64(), 1_000_000_000);
1583 }
1584
1585 #[rstest]
1586 fn test_from_micros_one_millisecond() {
1587 let nanos = UnixNanos::from_micros(1_000);
1588 assert_eq!(nanos.as_u64(), 1_000_000);
1589 assert_eq!(UnixNanos::from_micros(1_000), UnixNanos::from_millis(1));
1590 }
1591
1592 #[rstest]
1593 fn test_from_micros_realistic_timestamp() {
1594 let micros = 1_700_000_000_000_000_u64;
1595 let nanos = UnixNanos::from_micros(micros);
1596 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
1597 }
1598
1599 #[rstest]
1600 fn test_from_micros_max_safe() {
1601 let max_us = u64::MAX / 1_000;
1602 let nanos = UnixNanos::from_micros(max_us);
1603 assert_eq!(nanos.as_u64(), max_us * 1_000);
1604 }
1605
1606 #[rstest]
1607 fn test_from_micros_matches_manual_conversion() {
1608 let us = 1_000_000_123_456_u64;
1609 let expected = us * 1_000;
1610 assert_eq!(UnixNanos::from_micros(us).as_u64(), expected);
1611 }
1612
1613 #[rstest]
1614 #[case::valid(1_700_000_000_123_456, Some(1_700_000_000_123_456_000))]
1615 #[case::negative(-1, None)]
1616 #[case::overflow(i64::MAX, None)]
1617 fn test_from_micros_checked(#[case] micros: i64, #[case] expected: Option<u64>) {
1618 assert_eq!(
1619 UnixNanos::from_micros_checked(micros).map(|value| value.as_u64()),
1620 expected
1621 );
1622 }
1623
1624 #[rstest]
1625 fn test_from_seconds_millis_and_micros_consistency() {
1626 assert_eq!(UnixNanos::from_seconds(1), UnixNanos::from_millis(1_000));
1627 assert_eq!(
1628 UnixNanos::from_seconds(60),
1629 UnixNanos::from_micros(60_000_000)
1630 );
1631 assert_eq!(
1632 UnixNanos::from_millis(1_000),
1633 UnixNanos::from_micros(1_000_000)
1634 );
1635 assert_eq!(
1636 UnixNanos::from_millis(60_000),
1637 UnixNanos::from_micros(60_000_000)
1638 );
1639 }
1640
1641 #[rstest]
1642 fn test_from_millis_round_trip_to_datetime() {
1643 let ms = 1_707_577_123_456_u64;
1644 let nanos = UnixNanos::from_millis(ms);
1645 let dt = nanos.to_datetime_utc();
1646 assert_eq!(dt.as_millisecond().cast_unsigned(), ms);
1647 }
1648
1649 #[rstest]
1650 fn test_from_micros_preserves_sub_millisecond() {
1651 let micros = 1_700_000_000_000_123_u64;
1652 let nanos = UnixNanos::from_micros(micros);
1653 assert_eq!(nanos.as_u64() % 1_000_000, 123_000);
1654 }
1655
1656 #[rstest]
1657 #[should_panic(expected = "UnixNanos overflow in from_seconds")]
1658 fn test_from_seconds_overflow_panics() {
1659 let _ = UnixNanos::from_seconds(u64::MAX / 1_000_000_000 + 1);
1660 }
1661
1662 #[rstest]
1663 #[should_panic(expected = "UnixNanos overflow in from_millis")]
1664 fn test_from_millis_overflow_panics() {
1665 let _ = UnixNanos::from_millis(u64::MAX / 1_000_000 + 1);
1666 }
1667
1668 #[rstest]
1669 #[should_panic(expected = "UnixNanos overflow in from_micros")]
1670 fn test_from_micros_overflow_panics() {
1671 let _ = UnixNanos::from_micros(u64::MAX / 1_000 + 1);
1672 }
1673}