1use std::{
51 cmp::Ordering,
52 fmt::Display,
53 ops::{Add, AddAssign, Deref, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
54 str::FromStr,
55 time::{Duration, SystemTime},
56};
57
58use jiff::{SignedDuration, Timestamp, civil::Date, tz::Offset};
59use serde::{
60 Deserialize, Deserializer, Serialize,
61 de::{self, Visitor},
62};
63use thiserror::Error;
64
65use crate::datetime::{
66 NANOSECONDS_IN_DAY, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
67 NANOSECONDS_IN_MINUTE, NANOSECONDS_IN_SECOND, SECONDS_IN_HOUR, U64_UPPER_BOUND_F64,
68};
69
70#[repr(transparent)]
72#[derive(
73 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
74)]
75#[serde(transparent)]
76pub struct DurationNanos(u64);
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
80#[error("duration {value} {unit} exceeds the nanosecond range")]
81pub struct DurationNanosOutOfRangeError {
82 value: u64,
83 unit: &'static str,
84}
85
86impl DurationNanos {
87 pub const ZERO: Self = Self(0);
89
90 pub const MAX: Self = Self(u64::MAX);
92
93 #[must_use]
95 pub const fn new(nanos: u64) -> Self {
96 Self(nanos)
97 }
98
99 #[must_use]
105 pub const fn from_micros(micros: u64) -> Self {
106 match Self::try_from_micros(micros) {
107 Ok(duration) => duration,
108 Err(_) => panic!("DurationNanos overflow in from_micros"),
109 }
110 }
111
112 pub const fn try_from_micros(micros: u64) -> Result<Self, DurationNanosOutOfRangeError> {
118 Self::try_from_units(micros, NANOSECONDS_IN_MICROSECOND, "microseconds")
119 }
120
121 #[must_use]
127 pub const fn from_millis(millis: u64) -> Self {
128 match Self::try_from_millis(millis) {
129 Ok(duration) => duration,
130 Err(_) => panic!("DurationNanos overflow in from_millis"),
131 }
132 }
133
134 pub const fn try_from_millis(millis: u64) -> Result<Self, DurationNanosOutOfRangeError> {
140 Self::try_from_units(millis, NANOSECONDS_IN_MILLISECOND, "milliseconds")
141 }
142
143 #[must_use]
149 pub const fn from_secs(secs: u64) -> Self {
150 match Self::try_from_secs(secs) {
151 Ok(duration) => duration,
152 Err(_) => panic!("DurationNanos overflow in from_secs"),
153 }
154 }
155
156 pub const fn try_from_secs(secs: u64) -> Result<Self, DurationNanosOutOfRangeError> {
162 Self::try_from_units(secs, NANOSECONDS_IN_SECOND, "seconds")
163 }
164
165 #[must_use]
171 pub const fn from_mins(mins: u64) -> Self {
172 match Self::try_from_mins(mins) {
173 Ok(duration) => duration,
174 Err(_) => panic!("DurationNanos overflow in from_mins"),
175 }
176 }
177
178 pub const fn try_from_mins(mins: u64) -> Result<Self, DurationNanosOutOfRangeError> {
184 Self::try_from_units(mins, NANOSECONDS_IN_MINUTE, "minutes")
185 }
186
187 #[must_use]
193 pub const fn from_hours(hours: u64) -> Self {
194 match Self::try_from_hours(hours) {
195 Ok(duration) => duration,
196 Err(_) => panic!("DurationNanos overflow in from_hours"),
197 }
198 }
199
200 pub const fn try_from_hours(hours: u64) -> Result<Self, DurationNanosOutOfRangeError> {
206 Self::try_from_units(hours, SECONDS_IN_HOUR * NANOSECONDS_IN_SECOND, "hours")
207 }
208
209 #[must_use]
215 pub const fn from_days(days: u64) -> Self {
216 match Self::try_from_days(days) {
217 Ok(duration) => duration,
218 Err(_) => panic!("DurationNanos overflow in from_days"),
219 }
220 }
221
222 pub const fn try_from_days(days: u64) -> Result<Self, DurationNanosOutOfRangeError> {
228 Self::try_from_units(days, NANOSECONDS_IN_DAY, "days")
229 }
230
231 const fn try_from_units(
232 value: u64,
233 nanos_per_unit: u64,
234 unit: &'static str,
235 ) -> Result<Self, DurationNanosOutOfRangeError> {
236 match value.checked_mul(nanos_per_unit) {
237 Some(nanos) => Ok(Self(nanos)),
238 None => Err(DurationNanosOutOfRangeError { value, unit }),
239 }
240 }
241
242 #[must_use]
244 pub const fn is_zero(&self) -> bool {
245 self.0 == 0
246 }
247
248 #[must_use]
250 pub const fn as_u64(&self) -> u64 {
251 self.0
252 }
253
254 #[must_use]
256 pub const fn as_micros(&self) -> u64 {
257 self.0 / NANOSECONDS_IN_MICROSECOND
258 }
259
260 #[must_use]
262 pub const fn as_millis(&self) -> u64 {
263 self.0 / NANOSECONDS_IN_MILLISECOND
264 }
265
266 #[must_use]
268 pub const fn as_secs(&self) -> u64 {
269 self.0 / NANOSECONDS_IN_SECOND
270 }
271
272 #[must_use]
274 #[expect(
275 clippy::cast_precision_loss,
276 reason = "subnanosecond precision is unavailable and large durations may lose precision"
277 )]
278 pub const fn as_secs_f64(&self) -> f64 {
279 self.as_secs() as f64 + self.subsec_nanos() as f64 / NANOSECONDS_IN_SECOND as f64
280 }
281
282 #[must_use]
284 pub const fn as_mins(&self) -> u64 {
285 self.0 / NANOSECONDS_IN_MINUTE
286 }
287
288 #[must_use]
290 pub const fn as_hours(&self) -> u64 {
291 self.0 / (SECONDS_IN_HOUR * NANOSECONDS_IN_SECOND)
292 }
293
294 #[must_use]
296 pub const fn as_days(&self) -> u64 {
297 self.0 / NANOSECONDS_IN_DAY
298 }
299
300 #[must_use]
302 pub const fn subsec_millis(&self) -> u64 {
303 (self.0 % NANOSECONDS_IN_SECOND) / NANOSECONDS_IN_MILLISECOND
304 }
305
306 #[must_use]
308 pub const fn subsec_micros(&self) -> u64 {
309 (self.0 % NANOSECONDS_IN_SECOND) / NANOSECONDS_IN_MICROSECOND
310 }
311
312 #[must_use]
314 pub const fn subsec_nanos(&self) -> u64 {
315 self.0 % NANOSECONDS_IN_SECOND
316 }
317
318 #[must_use]
320 pub const fn checked_add(self, rhs: Self) -> Option<Self> {
321 match self.0.checked_add(rhs.0) {
322 Some(value) => Some(Self(value)),
323 None => None,
324 }
325 }
326
327 #[must_use]
329 pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
330 match self.0.checked_sub(rhs.0) {
331 Some(value) => Some(Self(value)),
332 None => None,
333 }
334 }
335
336 #[must_use]
338 pub const fn saturating_add(self, rhs: Self) -> Self {
339 Self(self.0.saturating_add(rhs.0))
340 }
341
342 #[must_use]
344 pub const fn saturating_sub(self, rhs: Self) -> Self {
345 Self(self.0.saturating_sub(rhs.0))
346 }
347
348 #[must_use]
350 pub const fn checked_mul(self, rhs: u64) -> Option<Self> {
351 match self.0.checked_mul(rhs) {
352 Some(value) => Some(Self(value)),
353 None => None,
354 }
355 }
356
357 #[must_use]
359 pub const fn saturating_mul(self, rhs: u64) -> Self {
360 Self(self.0.saturating_mul(rhs))
361 }
362
363 #[must_use]
365 pub const fn checked_div(self, rhs: u64) -> Option<Self> {
366 match self.0.checked_div(rhs) {
367 Some(value) => Some(Self(value)),
368 None => None,
369 }
370 }
371}
372
373#[repr(C)]
375#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
376pub struct UnixNanos(u64);
377
378impl UnixNanos {
379 #[must_use]
381 pub const fn new(value: u64) -> Self {
382 Self(value)
383 }
384
385 #[must_use]
387 pub const fn max() -> Self {
388 Self(u64::MAX)
389 }
390
391 #[must_use]
393 pub const fn is_zero(&self) -> bool {
394 self.0 == 0
395 }
396
397 #[must_use]
399 pub const fn as_u64(&self) -> u64 {
400 self.0
401 }
402
403 #[must_use]
405 pub const fn as_seconds(&self) -> u64 {
406 self.0 / NANOSECONDS_IN_SECOND
407 }
408
409 #[must_use]
411 pub const fn as_millis(&self) -> u64 {
412 self.0 / NANOSECONDS_IN_MILLISECOND
413 }
414
415 #[must_use]
417 pub const fn as_micros(&self) -> u64 {
418 self.0 / NANOSECONDS_IN_MICROSECOND
419 }
420
421 #[must_use]
427 pub const fn from_seconds(seconds: u64) -> Self {
428 match seconds.checked_mul(NANOSECONDS_IN_SECOND) {
429 Some(nanos) => Self(nanos),
430 None => panic!("UnixNanos overflow in from_seconds"),
431 }
432 }
433
434 #[must_use]
440 pub const fn from_millis(millis: u64) -> Self {
441 match millis.checked_mul(NANOSECONDS_IN_MILLISECOND) {
442 Some(nanos) => Self(nanos),
443 None => panic!("UnixNanos overflow in from_millis"),
444 }
445 }
446
447 #[must_use]
451 pub const fn from_millis_checked(millis: i64) -> Option<Self> {
452 Self::from_units_checked(millis, NANOSECONDS_IN_MILLISECOND)
453 }
454
455 #[must_use]
461 pub const fn from_micros(micros: u64) -> Self {
462 match micros.checked_mul(NANOSECONDS_IN_MICROSECOND) {
463 Some(nanos) => Self(nanos),
464 None => panic!("UnixNanos overflow in from_micros"),
465 }
466 }
467
468 #[must_use]
472 pub const fn from_micros_checked(micros: i64) -> Option<Self> {
473 Self::from_units_checked(micros, NANOSECONDS_IN_MICROSECOND)
474 }
475
476 const fn from_units_checked(value: i64, nanos_per_unit: u64) -> Option<Self> {
477 if value < 0 {
478 return None;
479 }
480
481 match value.cast_unsigned().checked_mul(nanos_per_unit) {
482 Some(nanos) => Some(Self(nanos)),
483 None => None,
484 }
485 }
486
487 #[must_use]
493 pub const fn as_i64(&self) -> i64 {
494 assert!(
495 self.0 <= i64::MAX.cast_unsigned(),
496 "UnixNanos value exceeds i64::MAX"
497 );
498 self.0.cast_signed()
499 }
500
501 #[must_use]
503 #[expect(
504 clippy::cast_precision_loss,
505 reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
506 )]
507 pub const fn as_f64(&self) -> f64 {
508 self.0 as f64
509 }
510
511 #[must_use]
517 pub fn to_datetime_utc(&self) -> Timestamp {
518 Timestamp::from_nanosecond(i128::from(self.0))
519 .expect("UnixNanos is within Jiff's timestamp range")
520 }
521
522 #[must_use]
524 pub fn to_rfc3339(&self) -> String {
525 let datetime = self.to_datetime_utc();
526 let display = datetime.display_with_offset(Offset::UTC);
527
528 match datetime.subsec_nanosecond() {
529 0 => format!("{display:.0}"),
530 nanos if nanos % 1_000_000 == 0 => format!("{display:.3}"),
531 nanos if nanos % 1_000 == 0 => format!("{display:.6}"),
532 _ => format!("{display:.9}"),
533 }
534 }
535
536 #[must_use]
541 pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
542 match self.0.checked_sub(other.0) {
543 Some(duration) => Some(DurationNanos(duration)),
544 None => None,
545 }
546 }
547
548 #[must_use]
550 pub const fn saturating_duration_since(&self, earlier: Self) -> DurationNanos {
551 DurationNanos(self.0.saturating_sub(earlier.0))
552 }
553
554 #[must_use]
560 pub const fn floor(self, interval: DurationNanos) -> Self {
561 assert!(
562 !interval.is_zero(),
563 "cannot floor UnixNanos to a zero interval"
564 );
565 Self(self.0 - self.0 % interval.0)
566 }
567
568 fn parse_string(s: &str) -> Result<Self, String> {
569 if let Ok(int_value) = s.parse::<u64>() {
571 return Ok(Self(int_value));
572 }
573
574 if s.chars().all(|c| c.is_ascii_digit()) {
580 return Err("Unix timestamp is out of range".into());
581 }
582
583 if let Ok(float_value) = s.parse::<f64>() {
585 return f64_seconds_to_nanos(float_value).map(Self);
586 }
587
588 let is_compatible_rfc3339 = matches!(s.as_bytes().get(10), Some(b'T' | b't' | b' '))
591 && !s.as_bytes().contains(&b'[');
592 if is_compatible_rfc3339 && let Ok(datetime) = s.parse::<Timestamp>() {
593 let nanos = datetime.as_nanosecond();
594 let nanos = u64::try_from(nanos)
595 .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
596 return Ok(Self(nanos));
597 }
598
599 if let Ok(date) = Date::strptime("%Y-%m-%d", s) {
601 let datetime = date
602 .at(0, 0, 0, 0)
603 .to_zoned(jiff::tz::TimeZone::UTC)
604 .map_err(|e| e.to_string())?;
605 let nanos = datetime.timestamp().as_nanosecond();
606 let nanos = u64::try_from(nanos)
607 .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
608 return Ok(Self(nanos));
609 }
610
611 Err(format!("Invalid format: {s}"))
612 }
613
614 #[must_use]
616 pub const fn checked_add(self, rhs: DurationNanos) -> Option<Self> {
617 match self.0.checked_add(rhs.0) {
618 Some(value) => Some(Self(value)),
619 None => None,
620 }
621 }
622
623 #[must_use]
625 pub const fn checked_sub(self, rhs: DurationNanos) -> Option<Self> {
626 match self.0.checked_sub(rhs.0) {
627 Some(value) => Some(Self(value)),
628 None => None,
629 }
630 }
631
632 #[must_use]
634 pub const fn saturating_add(self, rhs: DurationNanos) -> Self {
635 Self(self.0.saturating_add(rhs.0))
636 }
637
638 #[must_use]
640 pub const fn saturating_sub(self, rhs: DurationNanos) -> Self {
641 Self(self.0.saturating_sub(rhs.0))
642 }
643}
644
645#[expect(
648 clippy::cast_possible_truncation,
649 clippy::cast_sign_loss,
650 reason = "value is checked finite, non-negative, and within u64 range before the cast"
651)]
652fn f64_seconds_to_nanos(value: f64) -> Result<u64, String> {
653 if !value.is_finite() {
654 return Err(format!("Unix timestamp must be finite, was {value}"));
655 }
656
657 if value < 0.0 {
658 return Err("Unix timestamp cannot be negative".to_string());
659 }
660
661 let nanos_f64 = value * 1_000_000_000.0;
665
666 if nanos_f64 >= U64_UPPER_BOUND_F64 {
667 return Err(format!("Unix timestamp {value} seconds is out of range"));
668 }
669
670 Ok(nanos_f64.trunc() as u64)
671}
672
673impl From<DurationNanos> for Duration {
674 fn from(value: DurationNanos) -> Self {
675 Self::from_nanos(value.0)
676 }
677}
678
679impl From<DurationNanos> for SignedDuration {
680 fn from(value: DurationNanos) -> Self {
681 Self::from_nanos_i128(i128::from(value.0))
682 }
683}
684
685impl TryFrom<SignedDuration> for DurationNanos {
686 type Error = std::num::TryFromIntError;
687
688 fn try_from(value: SignedDuration) -> Result<Self, Self::Error> {
689 u64::try_from(value.as_nanos()).map(Self)
690 }
691}
692
693impl TryFrom<Duration> for DurationNanos {
694 type Error = std::num::TryFromIntError;
695
696 fn try_from(value: Duration) -> Result<Self, Self::Error> {
697 u64::try_from(value.as_nanos()).map(Self)
698 }
699}
700
701impl Add for DurationNanos {
709 type Output = Self;
710
711 fn add(self, rhs: Self) -> Self::Output {
712 self.checked_add(rhs)
713 .expect("DurationNanos overflow in addition")
714 }
715}
716
717impl Sub for DurationNanos {
724 type Output = Self;
725
726 fn sub(self, rhs: Self) -> Self::Output {
727 self.checked_sub(rhs)
728 .expect("DurationNanos underflow in subtraction")
729 }
730}
731
732impl AddAssign for DurationNanos {
738 fn add_assign(&mut self, rhs: Self) {
739 *self = *self + rhs;
740 }
741}
742
743impl SubAssign for DurationNanos {
749 fn sub_assign(&mut self, rhs: Self) {
750 *self = *self - rhs;
751 }
752}
753
754impl Mul<u64> for DurationNanos {
762 type Output = Self;
763
764 fn mul(self, rhs: u64) -> Self::Output {
765 self.checked_mul(rhs)
766 .expect("DurationNanos overflow in multiplication")
767 }
768}
769
770impl MulAssign<u64> for DurationNanos {
776 fn mul_assign(&mut self, rhs: u64) {
777 *self = *self * rhs;
778 }
779}
780
781impl Div<u64> for DurationNanos {
787 type Output = Self;
788
789 fn div(self, rhs: u64) -> Self::Output {
790 self.checked_div(rhs)
791 .expect("DurationNanos division by zero")
792 }
793}
794
795impl DivAssign<u64> for DurationNanos {
801 fn div_assign(&mut self, rhs: u64) {
802 *self = *self / rhs;
803 }
804}
805
806impl Display for DurationNanos {
807 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808 write!(f, "{}", self.0)
809 }
810}
811
812impl Deref for UnixNanos {
813 type Target = u64;
814
815 fn deref(&self) -> &Self::Target {
816 &self.0
817 }
818}
819
820impl PartialEq<u64> for UnixNanos {
821 fn eq(&self, other: &u64) -> bool {
822 self.0 == *other
823 }
824}
825
826impl PartialOrd<u64> for UnixNanos {
827 fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
828 self.0.partial_cmp(other)
829 }
830}
831
832impl PartialEq<Option<u64>> for UnixNanos {
833 fn eq(&self, other: &Option<u64>) -> bool {
834 match other {
835 Some(value) => self.0 == *value,
836 None => false,
837 }
838 }
839}
840
841impl PartialOrd<Option<u64>> for UnixNanos {
842 fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
843 match other {
844 Some(value) => self.0.partial_cmp(value),
845 None => Some(Ordering::Greater),
846 }
847 }
848}
849
850impl PartialEq<UnixNanos> for u64 {
851 fn eq(&self, other: &UnixNanos) -> bool {
852 *self == other.0
853 }
854}
855
856impl PartialOrd<UnixNanos> for u64 {
857 fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
858 self.partial_cmp(&other.0)
859 }
860}
861
862impl From<u64> for UnixNanos {
863 fn from(value: u64) -> Self {
864 Self(value)
865 }
866}
867
868impl From<UnixNanos> for u64 {
869 fn from(value: UnixNanos) -> Self {
870 value.0
871 }
872}
873
874impl From<&str> for UnixNanos {
885 fn from(value: &str) -> Self {
886 value
887 .parse()
888 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
889 }
890}
891
892impl From<String> for UnixNanos {
903 fn from(value: String) -> Self {
904 value
905 .parse()
906 .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
907 }
908}
909
910impl From<Timestamp> for UnixNanos {
911 fn from(value: Timestamp) -> Self {
912 let nanos = value.as_nanosecond();
913
914 assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
915
916 Self::from(u64::try_from(nanos).expect("DateTime timestamp out of range for UnixNanos"))
917 }
918}
919
920impl From<SystemTime> for UnixNanos {
921 fn from(value: SystemTime) -> Self {
922 let duration = value
923 .duration_since(std::time::UNIX_EPOCH)
924 .expect("SystemTime before UNIX EPOCH");
925
926 let nanos =
927 u64::try_from(duration.as_nanos()).expect("SystemTime overflowed u64 nanoseconds");
928
929 Self::from(nanos)
930 }
931}
932
933impl FromStr for UnixNanos {
934 type Err = Box<dyn std::error::Error>;
935
936 fn from_str(s: &str) -> Result<Self, Self::Err> {
937 Self::parse_string(s).map_err(std::convert::Into::into)
938 }
939}
940
941impl Sub for UnixNanos {
948 type Output = DurationNanos;
949
950 fn sub(self, rhs: Self) -> Self::Output {
951 self.duration_since(&rhs)
952 .expect("UnixNanos underflow in timestamp subtraction")
953 }
954}
955
956impl Add<DurationNanos> for UnixNanos {
963 type Output = Self;
964
965 fn add(self, rhs: DurationNanos) -> Self::Output {
966 self.checked_add(rhs)
967 .expect("UnixNanos overflow in duration addition")
968 }
969}
970
971impl Sub<DurationNanos> for UnixNanos {
978 type Output = Self;
979
980 fn sub(self, rhs: DurationNanos) -> Self::Output {
981 self.checked_sub(rhs)
982 .expect("UnixNanos underflow in duration subtraction")
983 }
984}
985
986impl AddAssign<DurationNanos> for UnixNanos {
992 fn add_assign(&mut self, rhs: DurationNanos) {
993 *self = *self + rhs;
994 }
995}
996
997impl SubAssign<DurationNanos> for UnixNanos {
1003 fn sub_assign(&mut self, rhs: DurationNanos) {
1004 *self = *self - rhs;
1005 }
1006}
1007
1008impl Display for UnixNanos {
1009 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1010 write!(f, "{}", self.0)
1011 }
1012}
1013
1014impl From<UnixNanos> for Timestamp {
1015 fn from(value: UnixNanos) -> Self {
1016 value.to_datetime_utc()
1017 }
1018}
1019
1020impl<'de> Deserialize<'de> for UnixNanos {
1021 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1022 where
1023 D: Deserializer<'de>,
1024 {
1025 struct UnixNanosVisitor;
1026
1027 impl Visitor<'_> for UnixNanosVisitor {
1028 type Value = UnixNanos;
1029
1030 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1031 formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
1032 }
1033
1034 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
1035 where
1036 E: de::Error,
1037 {
1038 Ok(UnixNanos(value))
1039 }
1040
1041 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
1042 where
1043 E: de::Error,
1044 {
1045 u64::try_from(value)
1046 .map(UnixNanos)
1047 .map_err(|_| E::custom("Unix timestamp cannot be negative"))
1048 }
1049
1050 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1051 where
1052 E: de::Error,
1053 {
1054 f64_seconds_to_nanos(value)
1055 .map(UnixNanos)
1056 .map_err(E::custom)
1057 }
1058
1059 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1060 where
1061 E: de::Error,
1062 {
1063 UnixNanos::parse_string(value).map_err(E::custom)
1064 }
1065 }
1066
1067 deserializer.deserialize_any(UnixNanosVisitor)
1068 }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073 use jiff::SignedDuration;
1074 use rstest::rstest;
1075
1076 use super::*;
1077 use crate::approx_eq;
1078
1079 fn timestamp(value: &str) -> Timestamp {
1080 value.parse().unwrap()
1081 }
1082
1083 #[rstest]
1084 fn test_duration_nanos_construction_and_conversion() {
1085 let duration = DurationNanos::new(123);
1086 let standard = Duration::from(duration);
1087 let signed = SignedDuration::from(duration);
1088 let signed_max = SignedDuration::from(DurationNanos::MAX);
1089
1090 assert_eq!(duration.as_u64(), 123);
1091 assert_eq!(standard, Duration::from_nanos(123));
1092 assert_eq!(DurationNanos::try_from(standard), Ok(duration));
1093 assert_eq!(signed, SignedDuration::from_nanos(123));
1094 assert_eq!(DurationNanos::try_from(signed), Ok(duration));
1095 assert_eq!(signed_max.as_nanos(), i128::from(u64::MAX));
1096 assert_eq!(DurationNanos::try_from(signed_max), Ok(DurationNanos::MAX));
1097 assert!(DurationNanos::try_from(SignedDuration::from_nanos(-1)).is_err());
1098 }
1099
1100 #[rstest]
1101 fn test_duration_nanos_unit_construction_and_accessors() {
1102 let duration = DurationNanos::from_hours(1)
1103 + DurationNanos::from_mins(2)
1104 + DurationNanos::from_secs(3)
1105 + DurationNanos::new(456_789_123);
1106
1107 assert_eq!(DurationNanos::from_micros(1), DurationNanos::new(1_000));
1108 assert_eq!(DurationNanos::from_millis(1), DurationNanos::new(1_000_000));
1109 assert_eq!(DurationNanos::from_days(1), DurationNanos::from_hours(24));
1110 assert_eq!(duration.as_micros(), 3_723_456_789);
1111 assert_eq!(duration.as_millis(), 3_723_456);
1112 assert_eq!(duration.as_secs(), 3_723);
1113 assert!(approx_eq!(
1114 f64,
1115 duration.as_secs_f64(),
1116 3_723.456_789_123,
1117 epsilon = 1e-12
1118 ));
1119 let expected_max_secs = Duration::from_nanos(u64::MAX).as_secs_f64();
1120 assert!(approx_eq!(
1121 f64,
1122 DurationNanos::MAX.as_secs_f64(),
1123 expected_max_secs,
1124 epsilon = expected_max_secs * f64::EPSILON
1125 ));
1126 assert_eq!(duration.as_mins(), 62);
1127 assert_eq!(duration.as_hours(), 1);
1128 assert_eq!(DurationNanos::from_hours(49).as_days(), 2);
1129 assert_eq!(duration.subsec_millis(), 456);
1130 assert_eq!(duration.subsec_micros(), 456_789);
1131 assert_eq!(duration.subsec_nanos(), 456_789_123);
1132 }
1133
1134 #[rstest]
1135 fn test_duration_nanos_fallible_unit_construction() {
1136 assert_eq!(
1137 DurationNanos::try_from_secs(1),
1138 Ok(DurationNanos::from_secs(1))
1139 );
1140 assert_eq!(
1141 DurationNanos::try_from_secs(u64::MAX)
1142 .unwrap_err()
1143 .to_string(),
1144 "duration 18446744073709551615 seconds exceeds the nanosecond range"
1145 );
1146 assert!(DurationNanos::try_from_micros(u64::MAX).is_err());
1147 assert!(DurationNanos::try_from_millis(u64::MAX).is_err());
1148 assert!(DurationNanos::try_from_secs(u64::MAX).is_err());
1149 assert!(DurationNanos::try_from_mins(u64::MAX).is_err());
1150 assert!(DurationNanos::try_from_hours(u64::MAX).is_err());
1151 assert!(DurationNanos::try_from_days(u64::MAX).is_err());
1152
1153 let max_hours = DurationNanos::MAX.as_hours();
1154 assert_eq!(
1155 DurationNanos::try_from_hours(max_hours),
1156 Ok(DurationNanos::from_hours(max_hours))
1157 );
1158 assert!(DurationNanos::try_from_hours(max_hours + 1).is_err());
1159
1160 let max_days = DurationNanos::MAX.as_days();
1161 assert_eq!(
1162 DurationNanos::try_from_days(max_days),
1163 Ok(DurationNanos::from_days(max_days))
1164 );
1165 assert!(DurationNanos::try_from_days(max_days + 1).is_err());
1166 }
1167
1168 #[rstest]
1169 fn test_duration_nanos_zero_and_max() {
1170 assert_eq!(DurationNanos::default(), DurationNanos::ZERO);
1171 assert!(DurationNanos::ZERO.is_zero());
1172 assert_eq!(DurationNanos::MAX.as_u64(), u64::MAX);
1173 assert!(!DurationNanos::MAX.is_zero());
1174 }
1175
1176 #[rstest]
1177 fn test_duration_nanos_layout_matches_u64() {
1178 assert_eq!(
1179 std::mem::size_of::<DurationNanos>(),
1180 std::mem::size_of::<u64>()
1181 );
1182 assert_eq!(
1183 std::mem::align_of::<DurationNanos>(),
1184 std::mem::align_of::<u64>()
1185 );
1186 }
1187
1188 #[rstest]
1189 fn test_duration_nanos_format_and_ordering() {
1190 let shorter = DurationNanos::new(123);
1191 let longer = DurationNanos::new(456);
1192
1193 assert_eq!(shorter.to_string(), "123");
1194 assert_eq!(format!("{shorter:?}"), "DurationNanos(123)");
1195 assert!(shorter < longer);
1196 assert_eq!(shorter, DurationNanos::new(123));
1197 }
1198
1199 #[rstest]
1200 fn test_duration_nanos_serde_preserves_u64_format() {
1201 let duration = DurationNanos::MAX;
1202 let json = serde_json::to_string(&duration).unwrap();
1203 let deserialized: DurationNanos = serde_json::from_str(&json).unwrap();
1204
1205 assert_eq!(json, u64::MAX.to_string());
1206 assert_eq!(deserialized, duration);
1207 }
1208
1209 #[rstest]
1210 #[case("-1")]
1211 #[case("1.5")]
1212 #[case("\"1\"")]
1213 fn test_duration_nanos_serde_rejects_non_u64_formats(#[case] json: &str) {
1214 assert!(serde_json::from_str::<DurationNanos>(json).is_err());
1215 }
1216
1217 #[rstest]
1218 fn test_duration_nanos_checked_arithmetic_boundaries() {
1219 let zero = DurationNanos::ZERO;
1220 let one = DurationNanos::new(1);
1221 let max = DurationNanos::MAX;
1222
1223 assert_eq!(zero.checked_sub(one), None);
1224 assert_eq!(max.checked_add(one), None);
1225 assert_eq!(one.checked_sub(one), Some(zero));
1226 assert_eq!(zero.checked_add(max), Some(max));
1227 }
1228
1229 #[rstest]
1230 fn test_duration_nanos_saturating_arithmetic_boundaries() {
1231 let zero = DurationNanos::ZERO;
1232 let one = DurationNanos::new(1);
1233 let max = DurationNanos::MAX;
1234
1235 assert_eq!(zero.saturating_sub(one), zero);
1236 assert_eq!(max.saturating_add(one), max);
1237 assert_eq!(max.saturating_mul(2), max);
1238 }
1239
1240 #[rstest]
1241 fn test_duration_nanos_scalar_arithmetic() {
1242 let duration = DurationNanos::new(12);
1243
1244 assert_eq!(duration.checked_mul(3), Some(DurationNanos::new(36)));
1245 assert_eq!(DurationNanos::MAX.checked_mul(2), None);
1246 assert_eq!(duration.checked_div(3), Some(DurationNanos::new(4)));
1247 assert_eq!(duration.checked_div(0), None);
1248 assert_eq!(duration * 3, DurationNanos::new(36));
1249 assert_eq!(duration / 3, DurationNanos::new(4));
1250 }
1251
1252 #[rstest]
1253 fn test_duration_nanos_assign_operators() {
1254 let mut value = DurationNanos::new(100);
1255 value += DurationNanos::new(23);
1256 assert_eq!(value, DurationNanos::new(123));
1257 value -= DurationNanos::new(23);
1258 assert_eq!(value, DurationNanos::new(100));
1259 value *= 3;
1260 assert_eq!(value, DurationNanos::new(300));
1261 value /= 4;
1262 assert_eq!(value, DurationNanos::new(75));
1263 }
1264
1265 #[rstest]
1266 #[should_panic(expected = "DurationNanos overflow in addition")]
1267 fn test_duration_nanos_addition_panics_on_overflow() {
1268 let _ = DurationNanos::MAX + DurationNanos::new(1);
1269 }
1270
1271 #[rstest]
1272 #[should_panic(expected = "DurationNanos underflow in subtraction")]
1273 fn test_duration_nanos_subtraction_panics_on_underflow() {
1274 let _ = DurationNanos::default() - DurationNanos::new(1);
1275 }
1276
1277 #[rstest]
1278 #[should_panic(expected = "DurationNanos overflow in multiplication")]
1279 fn test_duration_nanos_multiplication_panics_on_overflow() {
1280 let _ = DurationNanos::MAX * 2;
1281 }
1282
1283 #[rstest]
1284 #[should_panic(expected = "DurationNanos division by zero")]
1285 fn test_duration_nanos_division_panics_on_zero() {
1286 let _ = DurationNanos::new(1) / 0;
1287 }
1288
1289 #[rstest]
1290 #[should_panic(expected = "DurationNanos overflow in from_micros")]
1291 fn test_duration_nanos_from_micros_panics_on_overflow() {
1292 let _ = DurationNanos::from_micros(u64::MAX);
1293 }
1294
1295 #[rstest]
1296 #[should_panic(expected = "DurationNanos overflow in from_millis")]
1297 fn test_duration_nanos_from_millis_panics_on_overflow() {
1298 let _ = DurationNanos::from_millis(u64::MAX);
1299 }
1300
1301 #[rstest]
1302 #[should_panic(expected = "DurationNanos overflow in from_secs")]
1303 fn test_duration_nanos_from_secs_panics_on_overflow() {
1304 let _ = DurationNanos::from_secs(u64::MAX);
1305 }
1306
1307 #[rstest]
1308 #[should_panic(expected = "DurationNanos overflow in from_mins")]
1309 fn test_duration_nanos_from_mins_panics_on_overflow() {
1310 let _ = DurationNanos::from_mins(u64::MAX);
1311 }
1312
1313 #[rstest]
1314 #[should_panic(expected = "DurationNanos overflow in from_hours")]
1315 fn test_duration_nanos_from_hours_panics_on_overflow() {
1316 let _ = DurationNanos::from_hours(u64::MAX);
1317 }
1318
1319 #[rstest]
1320 #[should_panic(expected = "DurationNanos overflow in from_days")]
1321 fn test_duration_nanos_from_days_panics_on_overflow() {
1322 let _ = DurationNanos::from_days(u64::MAX);
1323 }
1324
1325 #[rstest]
1326 fn test_new() {
1327 let nanos = UnixNanos::new(123);
1328 assert_eq!(nanos.as_u64(), 123);
1329 assert_eq!(nanos.as_i64(), 123);
1330 }
1331
1332 #[rstest]
1333 fn test_max() {
1334 let nanos = UnixNanos::max();
1335 assert_eq!(nanos.as_u64(), u64::MAX);
1336 }
1337
1338 #[rstest]
1339 fn test_is_zero() {
1340 assert!(UnixNanos::default().is_zero());
1341 assert!(!UnixNanos::max().is_zero());
1342 }
1343
1344 #[rstest]
1345 fn test_from_u64() {
1346 let nanos = UnixNanos::from(123);
1347 assert_eq!(nanos.as_u64(), 123);
1348 assert_eq!(nanos.as_i64(), 123);
1349 }
1350
1351 #[rstest]
1352 fn test_default() {
1353 let nanos = UnixNanos::default();
1354 assert_eq!(nanos.as_u64(), 0);
1355 assert_eq!(nanos.as_i64(), 0);
1356 }
1357
1358 #[rstest]
1359 fn test_into_from() {
1360 let nanos: UnixNanos = 456.into();
1361 let value: u64 = nanos.into();
1362 assert_eq!(value, 456);
1363 }
1364
1365 #[rstest]
1366 #[case(0, "1970-01-01T00:00:00+00:00")]
1367 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
1368 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
1369 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
1370 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
1371 fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
1372 let nanos = UnixNanos::from(nanos);
1373 let datetime = nanos.to_datetime_utc();
1374 assert_eq!(
1375 datetime.display_with_offset(Offset::UTC).to_string(),
1376 expected
1377 );
1378 }
1379
1380 #[rstest]
1381 #[case(0, "1970-01-01T00:00:00+00:00")]
1382 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
1383 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
1384 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
1385 #[case(1_500_000_000_500_000_000, "2017-07-14T02:40:00.500+00:00")]
1386 #[case(1_500_000_000_123_456_000, "2017-07-14T02:40:00.123456+00:00")]
1387 #[case(1_500_000_000_123_456_789, "2017-07-14T02:40:00.123456789+00:00")]
1388 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
1389 fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
1390 let nanos = UnixNanos::from(nanos);
1391 assert_eq!(nanos.to_rfc3339(), expected);
1392 }
1393
1394 #[rstest]
1395 fn test_from_str() {
1396 let nanos: UnixNanos = "123".parse().unwrap();
1397 assert_eq!(nanos.as_u64(), 123);
1398 }
1399
1400 #[rstest]
1401 fn test_from_str_invalid() {
1402 let result = "abc".parse::<UnixNanos>();
1403 assert!(result.is_err());
1404 }
1405
1406 #[rstest]
1407 fn test_from_str_date() {
1408 let nanos: UnixNanos = "2024-02-10".parse().unwrap();
1409 assert_eq!(nanos.as_u64(), 1_707_523_200_000_000_000);
1410 }
1411
1412 #[rstest]
1413 fn test_from_str_pre_epoch_date() {
1414 let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
1415 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
1416 }
1417
1418 #[rstest]
1419 fn test_from_str_pre_epoch_rfc3339() {
1420 let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
1421 assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
1422 }
1423
1424 #[rstest]
1425 fn test_from_borrowed_str_and_string() {
1426 assert_eq!(UnixNanos::from("123").as_u64(), 123);
1427 assert_eq!(UnixNanos::from("123".to_string()).as_u64(), 123);
1428 }
1429
1430 #[rstest]
1431 #[should_panic(expected = "Failed to parse string")]
1432 fn test_from_borrowed_str_panics_on_invalid() {
1433 let _ = UnixNanos::from("abc");
1434 }
1435
1436 #[rstest]
1437 #[should_panic(expected = "Failed to parse string")]
1438 fn test_from_string_trait_panics_on_invalid() {
1439 let _ = UnixNanos::from("abc".to_string());
1440 }
1441
1442 #[rstest]
1443 fn test_into_timestamp() {
1444 let nanos = UnixNanos::from(1_000_000_000);
1445 let datetime = Timestamp::from(nanos);
1446 assert_eq!(datetime, timestamp("1970-01-01T00:00:01Z"));
1447 assert_eq!(UnixNanos::from(datetime), nanos);
1448 }
1449
1450 #[rstest]
1451 fn test_try_from_datetime_valid() {
1452 let datetime = Timestamp::from_second(1_000_000_000).unwrap();
1453 let nanos = UnixNanos::from(datetime);
1454 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
1455 }
1456
1457 #[rstest]
1458 fn test_from_system_time() {
1459 let system_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
1460 let nanos = UnixNanos::from(system_time);
1461 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
1462 }
1463
1464 #[rstest]
1465 #[should_panic(expected = "SystemTime before UNIX EPOCH")]
1466 fn test_from_system_time_before_epoch() {
1467 let system_time = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
1468 let _ = UnixNanos::from(system_time);
1469 }
1470
1471 #[rstest]
1472 #[should_panic(expected = "SystemTime overflowed u64 nanoseconds")]
1473 fn test_from_system_time_overflow_panics() {
1474 let system_time =
1476 std::time::UNIX_EPOCH + std::time::Duration::from_secs(u64::MAX / 1_000_000_000 + 1);
1477 let _ = UnixNanos::from(system_time);
1478 }
1479
1480 #[rstest]
1481 fn test_eq() {
1482 let nanos = UnixNanos::from(100);
1483 assert_eq!(nanos, 100);
1484 assert_eq!(nanos, Some(100));
1485 assert_ne!(nanos, 200);
1486 assert_ne!(nanos, Some(200));
1487 assert_ne!(nanos, None);
1488 }
1489
1490 #[rstest]
1491 fn test_partial_cmp() {
1492 let nanos = UnixNanos::from(100);
1493 assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
1494 assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
1495 assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
1496 assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
1497 }
1498
1499 #[rstest]
1500 fn test_u64_comparison_and_deref() {
1501 let nanos = UnixNanos::from(100);
1502 assert_eq!(100u64, nanos);
1503 assert_eq!(99u64.partial_cmp(&nanos), Some(Ordering::Less));
1504 assert_eq!(nanos.partial_cmp(&Some(100u64)), Some(Ordering::Equal));
1505 assert_eq!(*nanos, 100u64);
1506 }
1507
1508 #[rstest]
1509 fn test_edge_case_max_value() {
1510 let nanos = UnixNanos::from(u64::MAX);
1511 assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
1512 }
1513
1514 #[rstest]
1515 fn test_display() {
1516 let nanos = UnixNanos::from(123);
1517 assert_eq!(format!("{nanos}"), "123");
1518 }
1519
1520 #[rstest]
1521 fn test_addition() {
1522 let nanos = UnixNanos::from(100);
1523 let duration = DurationNanos::new(200);
1524 let result = nanos + duration;
1525 assert_eq!(result.as_u64(), 300);
1526 }
1527
1528 #[rstest]
1529 fn test_add_assign() {
1530 let mut nanos = UnixNanos::from(100);
1531 nanos += DurationNanos::new(50);
1532 assert_eq!(nanos.as_u64(), 150);
1533 }
1534
1535 #[rstest]
1536 fn test_subtraction() {
1537 let nanos1 = UnixNanos::from(200);
1538 let nanos2 = UnixNanos::from(100);
1539 let result = nanos1 - nanos2;
1540 assert_eq!(result, DurationNanos::new(100));
1541 }
1542
1543 #[rstest]
1544 fn test_sub_assign() {
1545 let mut nanos = UnixNanos::from(200);
1546 nanos -= DurationNanos::new(50);
1547 assert_eq!(nanos.as_u64(), 150);
1548 }
1549
1550 #[rstest]
1551 #[should_panic(expected = "UnixNanos overflow")]
1552 fn test_overflow_add() {
1553 let nanos = UnixNanos::from(u64::MAX);
1554 let _ = nanos + DurationNanos::new(1);
1555 }
1556
1557 #[rstest]
1558 #[should_panic(expected = "UnixNanos underflow")]
1559 fn test_overflow_sub() {
1560 let _ = UnixNanos::default() - DurationNanos::new(1);
1561 }
1562
1563 #[rstest]
1564 #[case(100, 50, Some(DurationNanos::new(50)))]
1565 #[case(1_000_000_000, 500_000_000, Some(DurationNanos::new(500_000_000)))]
1566 #[case(u64::MAX, u64::MAX - 1, Some(DurationNanos::new(1)))]
1567 #[case(50, 50, Some(DurationNanos::ZERO))]
1568 #[case(50, 100, None)]
1569 #[case(0, 1, None)]
1570 fn test_duration_since(
1571 #[case] time1: u64,
1572 #[case] time2: u64,
1573 #[case] expected: Option<DurationNanos>,
1574 ) {
1575 let nanos1 = UnixNanos::from(time1);
1576 let nanos2 = UnixNanos::from(time2);
1577 assert_eq!(nanos1.duration_since(&nanos2), expected);
1578 }
1579
1580 #[rstest]
1581 fn test_duration_since_same_moment() {
1582 let moment = UnixNanos::from(1_707_577_123_456_789_000);
1583 assert_eq!(
1584 moment.duration_since(&moment),
1585 Some(DurationNanos::default())
1586 );
1587 }
1588
1589 #[rstest]
1590 #[case::later(100, 50, DurationNanos::new(50))]
1591 #[case::same(50, 50, DurationNanos::ZERO)]
1592 #[case::earlier(50, 100, DurationNanos::ZERO)]
1593 #[case::full_range(u64::MAX, 0, DurationNanos::new(u64::MAX))]
1594 fn test_saturating_duration_since(
1595 #[case] time: u64,
1596 #[case] earlier: u64,
1597 #[case] expected: DurationNanos,
1598 ) {
1599 assert_eq!(
1600 UnixNanos::from(time).saturating_duration_since(UnixNanos::from(earlier)),
1601 expected
1602 );
1603 }
1604
1605 #[rstest]
1606 #[case(100, 30, 90)]
1607 #[case(90, 30, 90)]
1608 #[case(100, 101, 0)]
1609 #[case(u64::MAX, u64::MAX, u64::MAX)]
1610 #[case(u64::MAX, 1_000_000_000, 18_446_744_073_000_000_000)]
1611 fn test_floor(#[case] time: u64, #[case] interval: u64, #[case] expected: u64) {
1612 assert_eq!(
1613 UnixNanos::new(time).floor(DurationNanos::new(interval)),
1614 UnixNanos::new(expected)
1615 );
1616 }
1617
1618 #[rstest]
1619 #[should_panic(expected = "cannot floor UnixNanos to a zero interval")]
1620 fn test_floor_panics_for_zero_interval() {
1621 let _ = UnixNanos::default().floor(DurationNanos::ZERO);
1622 }
1623
1624 #[rstest]
1625 fn test_duration_since_chronological() {
1626 let earlier = timestamp("2024-02-10T12:00:00Z");
1628
1629 let later = earlier
1631 + SignedDuration::from_hours(1)
1632 + SignedDuration::from_mins(30)
1633 + SignedDuration::from_secs(45)
1634 + SignedDuration::from_nanos(500_000_000);
1635
1636 let earlier_nanos = UnixNanos::from(earlier);
1637 let later_nanos = UnixNanos::from(later);
1638
1639 let expected_duration =
1641 (60 * 60 + 30 * 60 + 45) * NANOSECONDS_IN_SECOND + 500 * NANOSECONDS_IN_MILLISECOND;
1642
1643 assert_eq!(
1644 later_nanos.duration_since(&earlier_nanos),
1645 Some(DurationNanos::new(expected_duration))
1646 );
1647 assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
1648 }
1649
1650 #[rstest]
1651 fn test_duration_since_with_edge_cases() {
1652 let max = UnixNanos::from(u64::MAX);
1654 let smaller = UnixNanos::from(u64::MAX - 1000);
1655
1656 assert_eq!(max.duration_since(&smaller), Some(DurationNanos::new(1000)));
1657 assert_eq!(smaller.duration_since(&max), None);
1658
1659 let min = UnixNanos::default(); let larger = UnixNanos::from(1000);
1662
1663 assert_eq!(min.duration_since(&min), Some(DurationNanos::default()));
1664 assert_eq!(larger.duration_since(&min), Some(DurationNanos::new(1000)));
1665 assert_eq!(min.duration_since(&larger), None);
1666 }
1667
1668 #[rstest]
1669 fn test_serde_json() {
1670 let nanos = UnixNanos::from(123);
1671 let json = serde_json::to_string(&nanos).unwrap();
1672 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1673 assert_eq!(deserialized, nanos);
1674 }
1675
1676 #[rstest]
1677 fn test_serde_edge_cases() {
1678 let nanos = UnixNanos::from(u64::MAX);
1679 let json = serde_json::to_string(&nanos).unwrap();
1680 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1681 assert_eq!(deserialized, nanos);
1682 }
1683
1684 #[rstest]
1685 #[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) {
1695 let parsed: UnixNanos = input.parse().unwrap();
1696 assert_eq!(parsed.as_u64(), expected);
1697 }
1698
1699 #[rstest]
1700 #[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) {
1705 let result = input.parse::<UnixNanos>();
1706 assert!(result.is_err());
1707 }
1708
1709 #[rstest]
1710 fn test_from_str_integer_overflow() {
1711 let input = "184467440737095516160";
1713 let result = input.parse::<UnixNanos>();
1714 assert!(result.is_err());
1715 }
1716
1717 #[rstest]
1718 fn test_checked_add_overflow_returns_none() {
1719 let max = UnixNanos::from(u64::MAX);
1720 assert_eq!(max.checked_add(DurationNanos::new(1)), None);
1721 }
1722
1723 #[rstest]
1724 fn test_checked_sub_underflow_returns_none() {
1725 let zero = UnixNanos::default();
1726 assert_eq!(zero.checked_sub(DurationNanos::new(1)), None);
1727 }
1728
1729 #[rstest]
1730 fn test_saturating_add_overflow() {
1731 let max = UnixNanos::from(u64::MAX);
1732 let result = max.saturating_add(DurationNanos::new(1));
1733 assert_eq!(result, UnixNanos::from(u64::MAX));
1734 }
1735
1736 #[rstest]
1737 fn test_saturating_sub_underflow() {
1738 let zero = UnixNanos::default();
1739 let result = zero.saturating_sub(DurationNanos::new(1));
1740 assert_eq!(result, UnixNanos::default());
1741 }
1742
1743 #[rstest]
1744 fn test_from_str_float_overflow() {
1745 let input = "2e10"; let err = input.parse::<UnixNanos>().unwrap_err();
1748 assert!(err.to_string().contains("out of range"));
1749 }
1750
1751 #[rstest]
1752 #[case("NaN")]
1753 #[case("nan")]
1754 #[case("inf")]
1755 #[case("-inf")]
1756 fn test_from_str_non_finite_float_errors(#[case] input: &str) {
1757 let err = input.parse::<UnixNanos>().unwrap_err();
1758 assert!(err.to_string().contains("must be finite"));
1759 }
1760
1761 #[rstest]
1762 #[case("-1.5")]
1763 #[case("-0.000001")]
1764 fn test_from_str_negative_float_errors(#[case] input: &str) {
1765 let err = input.parse::<UnixNanos>().unwrap_err();
1766 assert!(err.to_string().contains("cannot be negative"));
1767 }
1768
1769 #[rstest]
1770 fn test_deserialize_u64() {
1771 let json = "123456789";
1772 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1773 assert_eq!(deserialized.as_u64(), 123_456_789);
1774 }
1775
1776 #[rstest]
1777 fn test_deserialize_string_with_int() {
1778 let json = "\"123456789\"";
1779 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1780 assert_eq!(deserialized.as_u64(), 123_456_789);
1781 }
1782
1783 #[rstest]
1784 fn test_deserialize_float() {
1785 let json = "1234.567";
1786 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1787 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1788 }
1789
1790 #[rstest]
1791 fn test_deserialize_string_with_float() {
1792 let json = "\"1234.567\"";
1793 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1794 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1795 }
1796
1797 #[rstest]
1798 fn test_deserialize_float_uses_truncation() {
1799 let json = "0.9999999999";
1801 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1802 assert_eq!(deserialized.as_u64(), 999_999_999); }
1804
1805 #[rstest]
1806 #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
1807 #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
1808 fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
1809 let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
1810 assert_eq!(deserialized.as_u64(), expected);
1811 }
1812
1813 #[rstest]
1814 fn test_deserialize_negative_int_fails() {
1815 let json = "-123456789";
1816 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1817 assert!(
1818 result
1819 .unwrap_err()
1820 .to_string()
1821 .contains("cannot be negative")
1822 );
1823 }
1824
1825 #[rstest]
1826 fn test_deserialize_negative_float_fails() {
1827 let json = "-1234.567";
1828 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1829 assert!(
1830 result
1831 .unwrap_err()
1832 .to_string()
1833 .contains("cannot be negative")
1834 );
1835 }
1836
1837 #[rstest]
1838 fn test_deserialize_nan_fails() {
1839 use serde::de::{
1841 IntoDeserializer,
1842 value::{Error as ValueError, F64Deserializer},
1843 };
1844 let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
1845 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1846 assert!(result.is_err());
1847 assert!(result.unwrap_err().to_string().contains("must be finite"));
1848 }
1849
1850 #[rstest]
1851 fn test_deserialize_infinity_fails() {
1852 use serde::de::{
1853 IntoDeserializer,
1854 value::{Error as ValueError, F64Deserializer},
1855 };
1856 let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
1857 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1858 assert!(result.is_err());
1859 assert!(result.unwrap_err().to_string().contains("must be finite"));
1860 }
1861
1862 #[rstest]
1863 fn test_deserialize_negative_infinity_fails() {
1864 use serde::de::{
1865 IntoDeserializer,
1866 value::{Error as ValueError, F64Deserializer},
1867 };
1868 let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
1869 let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1870 assert!(result.is_err());
1871 assert!(result.unwrap_err().to_string().contains("must be finite"));
1872 }
1873
1874 #[rstest]
1875 fn test_deserialize_overflow_float_fails() {
1876 let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
1879 assert!(result.is_err());
1880 assert!(result.unwrap_err().to_string().contains("out of range"));
1881 }
1882
1883 #[rstest]
1884 fn test_deserialize_float_u64_boundary_fails() {
1885 let deserializer = serde::de::value::F64Deserializer::<serde::de::value::Error>::new(
1886 18_446_744_073.709_553,
1887 );
1888 let err = UnixNanos::deserialize(deserializer).unwrap_err();
1889 assert!(err.to_string().contains("out of range"));
1890 }
1891
1892 #[rstest]
1893 fn test_deserialize_invalid_string_fails() {
1894 let json = "\"not a timestamp\"";
1895 let result: Result<UnixNanos, _> = serde_json::from_str(json);
1896 assert!(result.is_err());
1897 }
1898
1899 #[rstest]
1900 fn test_deserialize_edge_cases() {
1901 let json = "0";
1903 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1904 assert_eq!(deserialized.as_u64(), 0);
1905
1906 let json = "18446744073709551615"; let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1909 assert_eq!(deserialized.as_u64(), u64::MAX);
1910 }
1911
1912 #[rstest]
1913 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1914 fn test_as_i64_overflow_panics() {
1915 let nanos = UnixNanos::from(u64::MAX);
1916 let _ = nanos.as_i64(); }
1918
1919 #[rstest]
1920 fn test_as_i64_at_i64_max_boundary() {
1921 let nanos = UnixNanos::from(i64::MAX.cast_unsigned());
1922 assert_eq!(nanos.as_i64(), i64::MAX);
1923 }
1924
1925 #[rstest]
1926 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1927 fn test_as_i64_just_above_i64_max_panics() {
1928 let nanos = UnixNanos::from(i64::MAX.cast_unsigned() + 1);
1929 let _ = nanos.as_i64();
1930 }
1931
1932 use proptest::prelude::*;
1933
1934 fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
1935 prop_oneof![
1936 0u64..1_000_000u64,
1938 1_000_000u64..1_000_000_000_000u64,
1940 1_000_000_000_000u64..=i64::MAX.cast_unsigned(),
1942 (i64::MAX.cast_unsigned() + 1)..=u64::MAX,
1944 Just(0u64),
1946 Just(1u64),
1947 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), ]
1954 .prop_map(UnixNanos::from)
1955 }
1956
1957 fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
1958 (unix_nanos_strategy(), unix_nanos_strategy())
1959 }
1960
1961 fn duration_nanos_strategy() -> impl Strategy<Value = DurationNanos> {
1962 any::<u64>().prop_map(DurationNanos::new)
1963 }
1964
1965 fn duration_nanos_pair_strategy() -> impl Strategy<Value = (DurationNanos, DurationNanos)> {
1966 (duration_nanos_strategy(), duration_nanos_strategy())
1967 }
1968
1969 proptest! {
1970 #[rstest]
1971 #[expect(
1972 clippy::float_cmp,
1973 clippy::cast_precision_loss,
1974 reason = "roundtrip: both sides go through the same u64->f64 cast"
1975 )]
1976 fn prop_unix_nanos_construction_roundtrip(nanos in unix_nanos_strategy()) {
1977 let value = nanos.as_u64();
1978 prop_assert_eq!(UnixNanos::from(value).as_u64(), value);
1979 prop_assert_eq!(nanos.as_f64(), value as f64);
1980
1981 if i64::try_from(value).is_ok() {
1983 prop_assert_eq!(nanos.as_i64(), value.cast_signed());
1984 }
1985 }
1986
1987 #[rstest]
1988 fn prop_duration_nanos_addition_commutative(
1989 (duration1, duration2) in duration_nanos_pair_strategy()
1990 ) {
1991 if let (Some(sum1), Some(sum2)) = (
1992 duration1.checked_add(duration2),
1993 duration2.checked_add(duration1)
1994 ) {
1995 prop_assert_eq!(sum1, sum2, "Addition should be commutative");
1996 }
1997 }
1998
1999 #[rstest]
2000 fn prop_duration_nanos_addition_associative(
2001 duration1 in duration_nanos_strategy(),
2002 duration2 in duration_nanos_strategy(),
2003 duration3 in duration_nanos_strategy(),
2004 ) {
2005 let expected = duration1
2006 .checked_add(duration2)
2007 .and_then(|sum| sum.checked_add(duration3));
2008
2009 if let Some(expected) = expected {
2010 let left = (duration1 + duration2) + duration3;
2011 let right = duration1 + (duration2 + duration3);
2012 prop_assert_eq!(left, expected);
2013 prop_assert_eq!(right, expected);
2014 }
2015 }
2016
2017 #[rstest]
2018 fn prop_unix_nanos_duration_arithmetic_roundtrip(
2019 nanos in unix_nanos_strategy(),
2020 duration in duration_nanos_strategy(),
2021 ) {
2022 if let Some(sum) = nanos.checked_add(duration) {
2023 prop_assert_eq!(sum - duration, nanos);
2024 prop_assert_eq!(sum - nanos, duration);
2025 }
2026 }
2027
2028 #[rstest]
2029 fn prop_duration_nanos_zero_identity(duration in duration_nanos_strategy()) {
2030 let zero = DurationNanos::default();
2031 prop_assert_eq!(duration + zero, duration);
2032 prop_assert_eq!(zero + duration, duration);
2033 prop_assert!(zero.is_zero());
2034 }
2035
2036 #[rstest]
2037 fn prop_unix_nanos_ordering_consistency(
2038 (nanos1, nanos2) in unix_nanos_pair_strategy()
2039 ) {
2040 let eq = nanos1 == nanos2;
2042 let lt = nanos1 < nanos2;
2043 let gt = nanos1 > nanos2;
2044 let le = nanos1 <= nanos2;
2045 let ge = nanos1 >= nanos2;
2046
2047 let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
2049 prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
2050
2051 prop_assert_eq!(le, eq || lt, "<= should equal == || <");
2053 prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
2054 prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
2055 prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
2056 }
2057
2058 #[rstest]
2059 fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
2060 let string_repr = nanos.to_string();
2062 let parsed = UnixNanos::from_str(&string_repr);
2063 prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
2064 if let Ok(parsed_nanos) = parsed {
2065 prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
2066 }
2067 }
2068
2069 #[rstest]
2070 fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
2071 if i64::try_from(nanos.as_u64()).is_ok() {
2073 let datetime = nanos.to_datetime_utc();
2074 let converted_back = UnixNanos::from(datetime);
2075 prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
2076
2077 let rfc3339 = nanos.to_rfc3339();
2079 if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
2080 prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
2081 }
2082 }
2083 }
2084
2085 #[rstest]
2086 fn prop_unix_nanos_duration_since(
2087 (nanos1, nanos2) in unix_nanos_pair_strategy()
2088 ) {
2089 let duration = nanos1.duration_since(&nanos2);
2091 let saturating_duration = nanos1.saturating_duration_since(nanos2);
2092
2093 if nanos1 >= nanos2 {
2094 prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
2096 if let Some(dur) = duration {
2097 prop_assert_eq!(dur.as_u64(), nanos1.as_u64() - nanos2.as_u64(),
2098 "Duration should equal the difference");
2099 prop_assert_eq!(saturating_duration, dur,
2100 "Saturating duration should equal the difference");
2101 prop_assert_eq!(nanos2 + dur, nanos1,
2102 "second + duration should equal first");
2103 }
2104 } else {
2105 prop_assert!(duration.is_none(), "Duration should be None when first < second");
2107 prop_assert_eq!(saturating_duration, DurationNanos::default(),
2108 "Saturating duration should be zero when first < second");
2109 }
2110 }
2111
2112 #[rstest]
2113 fn prop_unix_nanos_checked_arithmetic(
2114 nanos in unix_nanos_strategy(),
2115 duration in duration_nanos_strategy(),
2116 ) {
2117 let checked_add = nanos.checked_add(duration);
2118 let checked_sub = nanos.checked_sub(duration);
2119
2120 if let Some(sum) = checked_add {
2121 prop_assert_eq!(sum, nanos + duration, "Checked add should match regular add when no overflow");
2122 }
2123
2124 if let Some(diff) = checked_sub {
2125 prop_assert_eq!(diff, nanos - duration, "Checked sub should match regular sub when no underflow");
2126 }
2127 }
2128
2129 #[rstest]
2130 fn prop_unix_nanos_saturating_arithmetic(
2131 nanos in unix_nanos_strategy(),
2132 duration in duration_nanos_strategy(),
2133 ) {
2134 let sat_add = nanos.saturating_add(duration);
2135 let sat_sub = nanos.saturating_sub(duration);
2136
2137 prop_assert!(sat_add >= nanos, "Saturating add result should be >= timestamp");
2138 prop_assert!(sat_sub <= nanos, "Saturating sub result should be <= timestamp");
2139
2140 if let Some(checked_sum) = nanos.checked_add(duration) {
2141 prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
2142 } else {
2143 prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
2144 }
2145
2146 if let Some(checked_diff) = nanos.checked_sub(duration) {
2147 prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
2148 } else {
2149 prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
2150 }
2151 }
2152
2153 #[rstest]
2154 fn prop_unix_nanos_assign_mirrors_op(
2155 nanos in unix_nanos_strategy(),
2156 duration in duration_nanos_strategy(),
2157 ) {
2158 if let Some(expected) = nanos.checked_add(duration) {
2159 let mut add_result = nanos;
2160 add_result += duration;
2161 prop_assert_eq!(add_result, expected, "AddAssign should mirror Add");
2162 }
2163
2164 if let Some(expected) = nanos.checked_sub(duration) {
2165 let mut sub_result = nanos;
2166 sub_result -= duration;
2167 prop_assert_eq!(sub_result, expected, "SubAssign should mirror Sub");
2168 }
2169 }
2170
2171 #[rstest]
2172 fn prop_unix_nanos_serde_roundtrip(nanos in unix_nanos_strategy()) {
2173 let json = serde_json::to_string(&nanos).unwrap();
2174 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
2175 prop_assert_eq!(deserialized, nanos, "Serde JSON should round-trip exactly");
2176 }
2177
2178 #[rstest]
2179 fn prop_unix_nanos_f64_deserialize_never_panics(val: f64) {
2180 use serde::de::{IntoDeserializer, value::{Error as ValueError, F64Deserializer}};
2183 let deserializer: F64Deserializer<ValueError> = val.into_deserializer();
2184 let result = UnixNanos::deserialize(deserializer);
2185
2186 let upper_bound = 2.0_f64.powi(64);
2187 if val.is_finite() && val >= 0.0 && val * 1_000_000_000.0 < upper_bound {
2188 prop_assert!(result.is_ok(), "Should succeed for valid f64: {}", val);
2189 } else {
2190 prop_assert!(result.is_err(), "Should error for invalid f64: {}", val);
2191 }
2192 }
2193 }
2194
2195 #[rstest]
2196 fn test_from_seconds_zero() {
2197 let nanos = UnixNanos::from_seconds(0);
2198 assert_eq!(nanos.as_u64(), 0);
2199 }
2200
2201 #[rstest]
2202 fn test_from_seconds_one() {
2203 let nanos = UnixNanos::from_seconds(1);
2204 assert_eq!(nanos.as_u64(), 1_000_000_000);
2205 }
2206
2207 #[rstest]
2208 fn test_from_seconds_realistic_timestamp() {
2209 let nanos = UnixNanos::from_seconds(1_700_000_000);
2210 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2211 assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
2212 }
2213
2214 #[rstest]
2215 fn test_from_seconds_max_safe() {
2216 let max_seconds = u64::MAX / 1_000_000_000;
2217 let nanos = UnixNanos::from_seconds(max_seconds);
2218 assert_eq!(nanos.as_u64(), max_seconds * 1_000_000_000);
2219 }
2220
2221 #[rstest]
2222 fn test_from_millis_zero() {
2223 let nanos = UnixNanos::from_millis(0);
2224 assert_eq!(nanos.as_u64(), 0);
2225 }
2226
2227 #[rstest]
2228 fn test_from_millis_one() {
2229 let nanos = UnixNanos::from_millis(1);
2230 assert_eq!(nanos.as_u64(), 1_000_000);
2231 }
2232
2233 #[rstest]
2234 fn test_from_millis_one_second() {
2235 let nanos = UnixNanos::from_millis(1_000);
2236 assert_eq!(nanos.as_u64(), 1_000_000_000);
2237 }
2238
2239 #[rstest]
2240 fn test_from_millis_realistic_timestamp() {
2241 let nanos = UnixNanos::from_millis(1_700_000_000_000);
2243 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2244 assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
2245 }
2246
2247 #[rstest]
2248 fn test_from_millis_max_safe() {
2249 let max_ms = u64::MAX / 1_000_000;
2250 let nanos = UnixNanos::from_millis(max_ms);
2251 assert_eq!(nanos.as_u64(), max_ms * 1_000_000);
2252 }
2253
2254 #[rstest]
2255 fn test_from_millis_matches_manual_conversion() {
2256 let ms = 1_625_474_304_765_u64;
2257 let expected = ms * 1_000_000;
2258 assert_eq!(UnixNanos::from_millis(ms).as_u64(), expected);
2259 }
2260
2261 #[rstest]
2262 #[case::valid(1_700_000_000_123, Some(1_700_000_000_123_000_000))]
2263 #[case::negative(-1, None)]
2264 #[case::overflow(i64::MAX, None)]
2265 #[case::zero(0, Some(0))]
2266 fn test_from_millis_checked(#[case] millis: i64, #[case] expected: Option<u64>) {
2267 assert_eq!(
2268 UnixNanos::from_millis_checked(millis).map(|value| value.as_u64()),
2269 expected
2270 );
2271 }
2272
2273 #[rstest]
2274 #[case(0, 0)]
2275 #[case(999_999_999, 0)]
2276 #[case(1_000_000_000, 1)]
2277 #[case(1_700_000_000_123_456_789, 1_700_000_000)]
2278 fn test_as_seconds(#[case] nanos: u64, #[case] expected: u64) {
2279 assert_eq!(UnixNanos::from(nanos).as_seconds(), expected);
2280 }
2281
2282 #[rstest]
2283 #[case(0, 0)]
2284 #[case(999_999, 0)]
2285 #[case(1_000_000, 1)]
2286 #[case(1_700_000_000_000_123_456, 1_700_000_000_000)]
2287 fn test_as_millis(#[case] nanos: u64, #[case] expected: u64) {
2288 assert_eq!(UnixNanos::from(nanos).as_millis(), expected);
2289 }
2290
2291 #[rstest]
2292 #[case(0, 0)]
2293 #[case(999, 0)]
2294 #[case(1_000, 1)]
2295 #[case(1_700_000_000_000_123_456, 1_700_000_000_000_123)]
2296 fn test_as_micros(#[case] nanos: u64, #[case] expected: u64) {
2297 assert_eq!(UnixNanos::from(nanos).as_micros(), expected);
2298 }
2299
2300 #[rstest]
2301 fn test_from_micros_zero() {
2302 let nanos = UnixNanos::from_micros(0);
2303 assert_eq!(nanos.as_u64(), 0);
2304 }
2305
2306 #[rstest]
2307 fn test_from_micros_one() {
2308 let nanos = UnixNanos::from_micros(1);
2309 assert_eq!(nanos.as_u64(), 1_000);
2310 }
2311
2312 #[rstest]
2313 fn test_from_micros_one_second() {
2314 let nanos = UnixNanos::from_micros(1_000_000);
2315 assert_eq!(nanos.as_u64(), 1_000_000_000);
2316 }
2317
2318 #[rstest]
2319 fn test_from_micros_one_millisecond() {
2320 let nanos = UnixNanos::from_micros(1_000);
2321 assert_eq!(nanos.as_u64(), 1_000_000);
2322 assert_eq!(UnixNanos::from_micros(1_000), UnixNanos::from_millis(1));
2323 }
2324
2325 #[rstest]
2326 fn test_from_micros_realistic_timestamp() {
2327 let micros = 1_700_000_000_000_000_u64;
2328 let nanos = UnixNanos::from_micros(micros);
2329 assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2330 }
2331
2332 #[rstest]
2333 fn test_from_micros_max_safe() {
2334 let max_us = u64::MAX / 1_000;
2335 let nanos = UnixNanos::from_micros(max_us);
2336 assert_eq!(nanos.as_u64(), max_us * 1_000);
2337 }
2338
2339 #[rstest]
2340 fn test_from_micros_matches_manual_conversion() {
2341 let us = 1_000_000_123_456_u64;
2342 let expected = us * 1_000;
2343 assert_eq!(UnixNanos::from_micros(us).as_u64(), expected);
2344 }
2345
2346 #[rstest]
2347 #[case::valid(1_700_000_000_123_456, Some(1_700_000_000_123_456_000))]
2348 #[case::negative(-1, None)]
2349 #[case::overflow(i64::MAX, None)]
2350 #[case::zero(0, Some(0))]
2351 fn test_from_micros_checked(#[case] micros: i64, #[case] expected: Option<u64>) {
2352 assert_eq!(
2353 UnixNanos::from_micros_checked(micros).map(|value| value.as_u64()),
2354 expected
2355 );
2356 }
2357
2358 #[rstest]
2359 fn test_from_seconds_millis_and_micros_consistency() {
2360 assert_eq!(UnixNanos::from_seconds(1), UnixNanos::from_millis(1_000));
2361 assert_eq!(
2362 UnixNanos::from_seconds(60),
2363 UnixNanos::from_micros(60_000_000)
2364 );
2365 assert_eq!(
2366 UnixNanos::from_millis(1_000),
2367 UnixNanos::from_micros(1_000_000)
2368 );
2369 assert_eq!(
2370 UnixNanos::from_millis(60_000),
2371 UnixNanos::from_micros(60_000_000)
2372 );
2373 }
2374
2375 #[rstest]
2376 fn test_from_millis_round_trip_to_datetime() {
2377 let ms = 1_707_577_123_456_u64;
2378 let nanos = UnixNanos::from_millis(ms);
2379 let dt = nanos.to_datetime_utc();
2380 assert_eq!(dt.as_millisecond().cast_unsigned(), ms);
2381 }
2382
2383 #[rstest]
2384 fn test_from_micros_preserves_sub_millisecond() {
2385 let micros = 1_700_000_000_000_123_u64;
2386 let nanos = UnixNanos::from_micros(micros);
2387 assert_eq!(nanos.as_u64() % 1_000_000, 123_000);
2388 }
2389
2390 #[rstest]
2391 #[should_panic(expected = "UnixNanos overflow in from_seconds")]
2392 fn test_from_seconds_overflow_panics() {
2393 let _ = UnixNanos::from_seconds(u64::MAX / 1_000_000_000 + 1);
2394 }
2395
2396 #[rstest]
2397 #[should_panic(expected = "UnixNanos overflow in from_millis")]
2398 fn test_from_millis_overflow_panics() {
2399 let _ = UnixNanos::from_millis(u64::MAX / 1_000_000 + 1);
2400 }
2401
2402 #[rstest]
2403 #[should_panic(expected = "UnixNanos overflow in from_micros")]
2404 fn test_from_micros_overflow_panics() {
2405 let _ = UnixNanos::from_micros(u64::MAX / 1_000 + 1);
2406 }
2407}