1#![allow(
32 clippy::cast_possible_truncation,
33 clippy::cast_sign_loss,
34 clippy::cast_precision_loss,
35 clippy::cast_possible_wrap
36)]
37use std::{
54 cmp::Ordering,
55 fmt::Display,
56 ops::{Add, AddAssign, Deref, Sub, SubAssign},
57 str::FromStr,
58};
59
60use chrono::{DateTime, NaiveDate, Utc};
61use serde::{
62 Deserialize, Deserializer, Serialize,
63 de::{self, Visitor},
64};
65
66pub type DurationNanos = u64;
68
69#[repr(C)]
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
72pub struct UnixNanos(u64);
73
74impl UnixNanos {
75 #[must_use]
77 pub const fn new(value: u64) -> Self {
78 Self(value)
79 }
80
81 #[must_use]
83 pub const fn max() -> Self {
84 Self(u64::MAX)
85 }
86
87 #[must_use]
89 pub const fn is_zero(&self) -> bool {
90 self.0 == 0
91 }
92
93 #[must_use]
95 pub const fn as_u64(&self) -> u64 {
96 self.0
97 }
98
99 #[must_use]
105 pub const fn as_i64(&self) -> i64 {
106 assert!(
107 (self.0 <= i64::MAX as u64),
108 "UnixNanos value exceeds i64::MAX"
109 );
110 self.0 as i64
111 }
112
113 #[must_use]
115 pub const fn as_f64(&self) -> f64 {
116 self.0 as f64
117 }
118
119 #[must_use]
125 pub const fn to_datetime_utc(&self) -> DateTime<Utc> {
126 DateTime::from_timestamp_nanos(self.as_i64())
127 }
128
129 #[must_use]
131 pub fn to_rfc3339(&self) -> String {
132 self.to_datetime_utc().to_rfc3339()
133 }
134
135 #[must_use]
140 pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
141 self.0.checked_sub(other.0)
142 }
143
144 fn parse_string(s: &str) -> Result<Self, String> {
145 if let Ok(int_value) = s.parse::<u64>() {
147 return Ok(Self(int_value));
148 }
149
150 if s.chars().all(|c| c.is_ascii_digit()) {
156 return Err("Unix timestamp is out of range".into());
157 }
158
159 if let Ok(float_value) = s.parse::<f64>() {
161 if !float_value.is_finite() {
162 return Err("Unix timestamp must be finite".into());
163 }
164
165 if float_value < 0.0 {
166 return Err("Unix timestamp cannot be negative".into());
167 }
168
169 const MAX_NS_F64: f64 = u64::MAX as f64;
173 let nanos_f64 = float_value * 1_000_000_000.0;
174
175 if nanos_f64 > MAX_NS_F64 {
176 return Err("Unix timestamp is out of range".into());
177 }
178
179 let nanos = nanos_f64.round() as u64;
180 return Ok(Self(nanos));
181 }
182
183 if let Ok(datetime) = DateTime::parse_from_rfc3339(s) {
185 let nanos = datetime
186 .timestamp_nanos_opt()
187 .ok_or_else(|| "Timestamp out of range".to_string())?;
188
189 if nanos < 0 {
190 return Err("Unix timestamp cannot be negative".into());
191 }
192
193 return Ok(Self(nanos as u64));
195 }
196
197 if let Ok(datetime) = NaiveDate::parse_from_str(s, "%Y-%m-%d")
199 .map(|date| date.and_hms_opt(0, 0, 0).unwrap())
202 .map(|naive_dt| DateTime::<Utc>::from_naive_utc_and_offset(naive_dt, Utc))
203 {
204 let nanos = datetime
205 .timestamp_nanos_opt()
206 .ok_or_else(|| "Timestamp out of range".to_string())?;
207 return Ok(Self(nanos as u64));
209 }
210
211 Err(format!("Invalid format: {s}"))
212 }
213
214 #[must_use]
216 pub fn checked_add<T: Into<u64>>(self, rhs: T) -> Option<Self> {
217 self.0.checked_add(rhs.into()).map(Self)
218 }
219
220 #[must_use]
222 pub fn checked_sub<T: Into<u64>>(self, rhs: T) -> Option<Self> {
223 self.0.checked_sub(rhs.into()).map(Self)
224 }
225
226 #[must_use]
228 pub fn saturating_add_ns<T: Into<u64>>(self, rhs: T) -> Self {
229 Self(self.0.saturating_add(rhs.into()))
230 }
231
232 #[must_use]
234 pub fn saturating_sub_ns<T: Into<u64>>(self, rhs: T) -> Self {
235 Self(self.0.saturating_sub(rhs.into()))
236 }
237}
238
239impl Deref for UnixNanos {
240 type Target = u64;
241
242 fn deref(&self) -> &Self::Target {
243 &self.0
244 }
245}
246
247impl PartialEq<u64> for UnixNanos {
248 fn eq(&self, other: &u64) -> bool {
249 self.0 == *other
250 }
251}
252
253impl PartialOrd<u64> for UnixNanos {
254 fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
255 self.0.partial_cmp(other)
256 }
257}
258
259impl PartialEq<Option<u64>> for UnixNanos {
260 fn eq(&self, other: &Option<u64>) -> bool {
261 match other {
262 Some(value) => self.0 == *value,
263 None => false,
264 }
265 }
266}
267
268impl PartialOrd<Option<u64>> for UnixNanos {
269 fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
270 match other {
271 Some(value) => self.0.partial_cmp(value),
272 None => Some(Ordering::Greater),
273 }
274 }
275}
276
277impl PartialEq<UnixNanos> for u64 {
278 fn eq(&self, other: &UnixNanos) -> bool {
279 *self == other.0
280 }
281}
282
283impl PartialOrd<UnixNanos> for u64 {
284 fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
285 self.partial_cmp(&other.0)
286 }
287}
288
289impl From<u64> for UnixNanos {
290 fn from(value: u64) -> Self {
291 Self(value)
292 }
293}
294
295impl From<UnixNanos> for u64 {
296 fn from(value: UnixNanos) -> Self {
297 value.0
298 }
299}
300
301impl From<&str> for UnixNanos {
302 fn from(value: &str) -> Self {
303 value
304 .parse()
305 .unwrap_or_else(|e| panic!("Failed to parse string into UnixNanos: {e}"))
306 }
307}
308
309impl From<String> for UnixNanos {
310 fn from(value: String) -> Self {
311 value
312 .parse()
313 .unwrap_or_else(|e| panic!("Failed to parse string into UnixNanos: {e}"))
314 }
315}
316
317impl From<DateTime<Utc>> for UnixNanos {
318 fn from(value: DateTime<Utc>) -> Self {
319 let nanos = value
320 .timestamp_nanos_opt()
321 .expect("DateTime timestamp out of range for UnixNanos");
322
323 assert!(
324 (nanos >= 0),
325 "DateTime timestamp cannot be negative: {nanos}"
326 );
327
328 Self::from(nanos as u64)
329 }
330}
331
332impl FromStr for UnixNanos {
333 type Err = Box<dyn std::error::Error>;
334
335 fn from_str(s: &str) -> Result<Self, Self::Err> {
336 Self::parse_string(s).map_err(std::convert::Into::into)
337 }
338}
339
340impl Add for UnixNanos {
341 type Output = Self;
342
343 fn add(self, rhs: Self) -> Self::Output {
344 Self(
345 self.0
346 .checked_add(rhs.0)
347 .expect("Error adding with overflow"),
348 )
349 }
350}
351
352impl Sub for UnixNanos {
353 type Output = Self;
354
355 fn sub(self, rhs: Self) -> Self::Output {
356 Self(
357 self.0
358 .checked_sub(rhs.0)
359 .expect("Error subtracting with underflow"),
360 )
361 }
362}
363
364impl Add<u64> for UnixNanos {
365 type Output = Self;
366
367 fn add(self, rhs: u64) -> Self::Output {
368 Self(self.0.checked_add(rhs).expect("Error adding with overflow"))
369 }
370}
371
372impl Sub<u64> for UnixNanos {
373 type Output = Self;
374
375 fn sub(self, rhs: u64) -> Self::Output {
376 Self(
377 self.0
378 .checked_sub(rhs)
379 .expect("Error subtracting with underflow"),
380 )
381 }
382}
383
384impl<T: Into<u64>> AddAssign<T> for UnixNanos {
385 fn add_assign(&mut self, other: T) {
386 let other_u64 = other.into();
387 self.0 = self
388 .0
389 .checked_add(other_u64)
390 .expect("Error adding with overflow");
391 }
392}
393
394impl<T: Into<u64>> SubAssign<T> for UnixNanos {
395 fn sub_assign(&mut self, other: T) {
396 let other_u64 = other.into();
397 self.0 = self
398 .0
399 .checked_sub(other_u64)
400 .expect("Error subtracting with underflow");
401 }
402}
403
404impl Display for UnixNanos {
405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406 write!(f, "{}", self.0)
407 }
408}
409
410impl From<UnixNanos> for DateTime<Utc> {
411 fn from(value: UnixNanos) -> Self {
412 value.to_datetime_utc()
413 }
414}
415
416impl<'de> Deserialize<'de> for UnixNanos {
417 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
418 where
419 D: Deserializer<'de>,
420 {
421 struct UnixNanosVisitor;
422
423 impl Visitor<'_> for UnixNanosVisitor {
424 type Value = UnixNanos;
425
426 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
427 formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
428 }
429
430 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
431 where
432 E: de::Error,
433 {
434 Ok(UnixNanos(value))
435 }
436
437 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
438 where
439 E: de::Error,
440 {
441 if value < 0 {
442 return Err(E::custom("Unix timestamp cannot be negative"));
443 }
444 Ok(UnixNanos(value as u64))
445 }
446
447 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
448 where
449 E: de::Error,
450 {
451 if value < 0.0 {
452 return Err(E::custom("Unix timestamp cannot be negative"));
453 }
454 let nanos = (value * 1_000_000_000.0).round() as u64;
456 Ok(UnixNanos(nanos))
457 }
458
459 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
460 where
461 E: de::Error,
462 {
463 UnixNanos::parse_string(value).map_err(E::custom)
464 }
465 }
466
467 deserializer.deserialize_any(UnixNanosVisitor)
468 }
469}
470
471#[cfg(test)]
475mod tests {
476 use chrono::{Duration, TimeZone};
477 use rstest::rstest;
478
479 use super::*;
480
481 #[rstest]
482 fn test_new() {
483 let nanos = UnixNanos::new(123);
484 assert_eq!(nanos.as_u64(), 123);
485 assert_eq!(nanos.as_i64(), 123);
486 }
487
488 #[rstest]
489 fn test_max() {
490 let nanos = UnixNanos::max();
491 assert_eq!(nanos.as_u64(), u64::MAX);
492 }
493
494 #[rstest]
495 fn test_is_zero() {
496 assert!(UnixNanos::default().is_zero());
497 assert!(!UnixNanos::max().is_zero());
498 }
499
500 #[rstest]
501 fn test_from_u64() {
502 let nanos = UnixNanos::from(123);
503 assert_eq!(nanos.as_u64(), 123);
504 assert_eq!(nanos.as_i64(), 123);
505 }
506
507 #[rstest]
508 fn test_default() {
509 let nanos = UnixNanos::default();
510 assert_eq!(nanos.as_u64(), 0);
511 assert_eq!(nanos.as_i64(), 0);
512 }
513
514 #[rstest]
515 fn test_into_from() {
516 let nanos: UnixNanos = 456.into();
517 let value: u64 = nanos.into();
518 assert_eq!(value, 456);
519 }
520
521 #[rstest]
522 #[case(0, "1970-01-01T00:00:00+00:00")]
523 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
524 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
525 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
526 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
527 fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
528 let nanos = UnixNanos::from(nanos);
529 let datetime = nanos.to_datetime_utc();
530 assert_eq!(datetime.to_rfc3339(), expected);
531 }
532
533 #[rstest]
534 #[case(0, "1970-01-01T00:00:00+00:00")]
535 #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
536 #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
537 #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
538 #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
539 fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
540 let nanos = UnixNanos::from(nanos);
541 assert_eq!(nanos.to_rfc3339(), expected);
542 }
543
544 #[rstest]
545 fn test_from_str() {
546 let nanos: UnixNanos = "123".parse().unwrap();
547 assert_eq!(nanos.as_u64(), 123);
548 }
549
550 #[rstest]
551 fn test_from_str_invalid() {
552 let result = "abc".parse::<UnixNanos>();
553 assert!(result.is_err());
554 }
555
556 #[rstest]
557 fn test_try_from_datetime_valid() {
558 use chrono::TimeZone;
559 let datetime = Utc.timestamp_opt(1_000_000_000, 0).unwrap(); let nanos = UnixNanos::from(datetime);
561 assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
562 }
563
564 #[rstest]
565 fn test_eq() {
566 let nanos = UnixNanos::from(100);
567 assert_eq!(nanos, 100);
568 assert_eq!(nanos, Some(100));
569 assert_ne!(nanos, 200);
570 assert_ne!(nanos, Some(200));
571 assert_ne!(nanos, None);
572 }
573
574 #[rstest]
575 fn test_partial_cmp() {
576 let nanos = UnixNanos::from(100);
577 assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
578 assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
579 assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
580 assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
581 }
582
583 #[rstest]
584 fn test_edge_case_max_value() {
585 let nanos = UnixNanos::from(u64::MAX);
586 assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
587 }
588
589 #[rstest]
590 fn test_display() {
591 let nanos = UnixNanos::from(123);
592 assert_eq!(format!("{nanos}"), "123");
593 }
594
595 #[rstest]
596 fn test_addition() {
597 let nanos1 = UnixNanos::from(100);
598 let nanos2 = UnixNanos::from(200);
599 let result = nanos1 + nanos2;
600 assert_eq!(result.as_u64(), 300);
601 }
602
603 #[rstest]
604 fn test_add_assign() {
605 let mut nanos = UnixNanos::from(100);
606 nanos += 50_u64;
607 assert_eq!(nanos.as_u64(), 150);
608 }
609
610 #[rstest]
611 fn test_subtraction() {
612 let nanos1 = UnixNanos::from(200);
613 let nanos2 = UnixNanos::from(100);
614 let result = nanos1 - nanos2;
615 assert_eq!(result.as_u64(), 100);
616 }
617
618 #[rstest]
619 fn test_sub_assign() {
620 let mut nanos = UnixNanos::from(200);
621 nanos -= 50_u64;
622 assert_eq!(nanos.as_u64(), 150);
623 }
624
625 #[rstest]
626 #[should_panic(expected = "Error adding with overflow")]
627 fn test_overflow_add() {
628 let nanos = UnixNanos::from(u64::MAX);
629 let _ = nanos + UnixNanos::from(1); }
631
632 #[rstest]
633 #[should_panic(expected = "Error adding with overflow")]
634 fn test_overflow_add_u64() {
635 let nanos = UnixNanos::from(u64::MAX);
636 let _ = nanos + 1_u64; }
638
639 #[rstest]
640 #[should_panic(expected = "Error subtracting with underflow")]
641 fn test_overflow_sub() {
642 let _ = UnixNanos::default() - UnixNanos::from(1); }
644
645 #[rstest]
646 #[should_panic(expected = "Error subtracting with underflow")]
647 fn test_overflow_sub_u64() {
648 let _ = UnixNanos::default() - 1_u64; }
650
651 #[rstest]
652 #[case(100, 50, Some(50))]
653 #[case(1_000_000_000, 500_000_000, Some(500_000_000))]
654 #[case(u64::MAX, u64::MAX - 1, Some(1))]
655 #[case(50, 50, Some(0))]
656 #[case(50, 100, None)]
657 #[case(0, 1, None)]
658 fn test_duration_since(
659 #[case] time1: u64,
660 #[case] time2: u64,
661 #[case] expected: Option<DurationNanos>,
662 ) {
663 let nanos1 = UnixNanos::from(time1);
664 let nanos2 = UnixNanos::from(time2);
665 assert_eq!(nanos1.duration_since(&nanos2), expected);
666 }
667
668 #[rstest]
669 fn test_duration_since_same_moment() {
670 let moment = UnixNanos::from(1_707_577_123_456_789_000);
671 assert_eq!(moment.duration_since(&moment), Some(0));
672 }
673
674 #[rstest]
675 fn test_duration_since_chronological() {
676 let earlier = Utc.with_ymd_and_hms(2024, 2, 10, 12, 0, 0).unwrap();
678
679 let later = earlier
681 + Duration::hours(1)
682 + Duration::minutes(30)
683 + Duration::seconds(45)
684 + Duration::nanoseconds(500_000_000);
685
686 let earlier_nanos = UnixNanos::from(earlier);
687 let later_nanos = UnixNanos::from(later);
688
689 let expected_duration = 60 * 60 * 1_000_000_000 + 30 * 60 * 1_000_000_000 + 45 * 1_000_000_000 + 500_000_000; assert_eq!(
696 later_nanos.duration_since(&earlier_nanos),
697 Some(expected_duration)
698 );
699 assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
700 }
701
702 #[rstest]
703 fn test_duration_since_with_edge_cases() {
704 let max = UnixNanos::from(u64::MAX);
706 let smaller = UnixNanos::from(u64::MAX - 1000);
707
708 assert_eq!(max.duration_since(&smaller), Some(1000));
709 assert_eq!(smaller.duration_since(&max), None);
710
711 let min = UnixNanos::default(); let larger = UnixNanos::from(1000);
714
715 assert_eq!(min.duration_since(&min), Some(0));
716 assert_eq!(larger.duration_since(&min), Some(1000));
717 assert_eq!(min.duration_since(&larger), None);
718 }
719
720 #[rstest]
721 fn test_serde_json() {
722 let nanos = UnixNanos::from(123);
723 let json = serde_json::to_string(&nanos).unwrap();
724 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
725 assert_eq!(deserialized, nanos);
726 }
727
728 #[rstest]
729 fn test_serde_edge_cases() {
730 let nanos = UnixNanos::from(u64::MAX);
731 let json = serde_json::to_string(&nanos).unwrap();
732 let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
733 assert_eq!(deserialized, nanos);
734 }
735
736 #[rstest]
737 #[case("123", 123)] #[case("1234.567", 1_234_567_000_000)] #[case("2024-02-10", 1_707_523_200_000_000_000)] #[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] #[case("2024-02-10T14:58:43.456789Z", 1_707_577_123_456_789_000)] fn test_from_str_formats(#[case] input: &str, #[case] expected: u64) {
743 let parsed: UnixNanos = input.parse().unwrap();
744 assert_eq!(parsed.as_u64(), expected);
745 }
746
747 #[rstest]
748 #[case("abc")] #[case("not a timestamp")] #[case("2024-02-10 14:58:43")] fn test_from_str_invalid_formats(#[case] input: &str) {
752 let result = input.parse::<UnixNanos>();
753 assert!(result.is_err());
754 }
755
756 #[rstest]
757 fn test_from_str_integer_overflow() {
758 let input = "184467440737095516160";
760 let result = input.parse::<UnixNanos>();
761 assert!(result.is_err());
762 }
763
764 #[rstest]
767 fn test_checked_add_overflow_returns_none() {
768 let max = UnixNanos::from(u64::MAX);
769 assert_eq!(max.checked_add(1_u64), None);
770 }
771
772 #[rstest]
773 fn test_checked_sub_underflow_returns_none() {
774 let zero = UnixNanos::default();
775 assert_eq!(zero.checked_sub(1_u64), None);
776 }
777
778 #[rstest]
779 fn test_saturating_add_overflow() {
780 let max = UnixNanos::from(u64::MAX);
781 let result = max.saturating_add_ns(1_u64);
782 assert_eq!(result, UnixNanos::from(u64::MAX));
783 }
784
785 #[rstest]
786 fn test_saturating_sub_underflow() {
787 let zero = UnixNanos::default();
788 let result = zero.saturating_sub_ns(1_u64);
789 assert_eq!(result, UnixNanos::default());
790 }
791
792 #[rstest]
793 fn test_from_str_float_overflow() {
794 let input = "2e10"; let result = input.parse::<UnixNanos>();
797 assert!(result.is_err());
798 }
799
800 #[rstest]
801 fn test_deserialize_u64() {
802 let json = "123456789";
803 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
804 assert_eq!(deserialized.as_u64(), 123_456_789);
805 }
806
807 #[rstest]
808 fn test_deserialize_string_with_int() {
809 let json = "\"123456789\"";
810 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
811 assert_eq!(deserialized.as_u64(), 123_456_789);
812 }
813
814 #[rstest]
815 fn test_deserialize_float() {
816 let json = "1234.567";
817 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
818 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
819 }
820
821 #[rstest]
822 fn test_deserialize_string_with_float() {
823 let json = "\"1234.567\"";
824 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
825 assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
826 }
827
828 #[rstest]
829 #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
830 #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
831 fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
832 let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
833 assert_eq!(deserialized.as_u64(), expected);
834 }
835
836 #[rstest]
837 fn test_deserialize_negative_int_fails() {
838 let json = "-123456789";
839 let result: Result<UnixNanos, _> = serde_json::from_str(json);
840 assert!(result.is_err());
841 }
842
843 #[rstest]
844 fn test_deserialize_negative_float_fails() {
845 let json = "-1234.567";
846 let result: Result<UnixNanos, _> = serde_json::from_str(json);
847 assert!(result.is_err());
848 }
849
850 #[rstest]
851 fn test_deserialize_invalid_string_fails() {
852 let json = "\"not a timestamp\"";
853 let result: Result<UnixNanos, _> = serde_json::from_str(json);
854 assert!(result.is_err());
855 }
856
857 #[rstest]
858 fn test_deserialize_edge_cases() {
859 let json = "0";
861 let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
862 assert_eq!(deserialized.as_u64(), 0);
863
864 let json = "18446744073709551615"; let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
867 assert_eq!(deserialized.as_u64(), u64::MAX);
868 }
869
870 #[rstest]
871 #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
872 fn test_as_i64_overflow_panics() {
873 let nanos = UnixNanos::from(u64::MAX);
874 let _ = nanos.as_i64(); }
876
877 use proptest::prelude::*;
882
883 fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
884 prop_oneof![
885 0u64..1_000_000u64,
887 1_000_000u64..1_000_000_000_000u64,
889 1_000_000_000_000u64..=i64::MAX as u64,
891 Just(0u64),
893 Just(1u64),
894 Just(1_000_000_000u64), Just(1_000_000_000_000u64), Just(1_700_000_000_000_000_000u64), Just((i64::MAX / 2) as u64), ]
899 .prop_map(UnixNanos::from)
900 }
901
902 fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
903 (unix_nanos_strategy(), unix_nanos_strategy())
904 }
905
906 proptest! {
907 #[test]
908 fn prop_unix_nanos_construction_roundtrip(value in 0u64..=i64::MAX as u64) {
909 let nanos = UnixNanos::from(value);
910 prop_assert_eq!(nanos.as_u64(), value);
911 prop_assert_eq!(nanos.as_f64(), value as f64);
912
913 if i64::try_from(value).is_ok() {
915 prop_assert_eq!(nanos.as_i64(), value as i64);
916 }
917 }
918
919 #[test]
920 fn prop_unix_nanos_addition_commutative(
921 (nanos1, nanos2) in unix_nanos_pair_strategy()
922 ) {
923 if let (Some(sum1), Some(sum2)) = (
925 nanos1.checked_add(nanos2.as_u64()),
926 nanos2.checked_add(nanos1.as_u64())
927 ) {
928 prop_assert_eq!(sum1, sum2, "Addition should be commutative");
929 }
930 }
931
932 #[test]
933 fn prop_unix_nanos_addition_associative(
934 nanos1 in unix_nanos_strategy(),
935 nanos2 in unix_nanos_strategy(),
936 nanos3 in unix_nanos_strategy(),
937 ) {
938 if let (Some(sum1), Some(sum2)) = (
940 nanos1.as_u64().checked_add(nanos2.as_u64()),
941 nanos2.as_u64().checked_add(nanos3.as_u64())
942 ) {
943 if let (Some(left), Some(right)) = (
944 sum1.checked_add(nanos3.as_u64()),
945 nanos1.as_u64().checked_add(sum2)
946 ) {
947 let left_result = UnixNanos::from(left);
948 let right_result = UnixNanos::from(right);
949 prop_assert_eq!(left_result, right_result, "Addition should be associative");
950 }
951 }
952 }
953
954 #[test]
955 fn prop_unix_nanos_subtraction_inverse(
956 (nanos1, nanos2) in unix_nanos_pair_strategy()
957 ) {
958 if let Some(sum) = nanos1.checked_add(nanos2.as_u64()) {
960 let diff = sum - nanos2;
961 prop_assert_eq!(diff, nanos1, "Subtraction should be inverse of addition");
962 }
963 }
964
965 #[test]
966 fn prop_unix_nanos_zero_identity(nanos in unix_nanos_strategy()) {
967 let zero = UnixNanos::default();
969 prop_assert_eq!(nanos + zero, nanos, "Zero should be additive identity");
970 prop_assert_eq!(zero + nanos, nanos, "Zero should be additive identity (commutative)");
971 prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
972 }
973
974 #[test]
975 fn prop_unix_nanos_ordering_consistency(
976 (nanos1, nanos2) in unix_nanos_pair_strategy()
977 ) {
978 let eq = nanos1 == nanos2;
980 let lt = nanos1 < nanos2;
981 let gt = nanos1 > nanos2;
982 let le = nanos1 <= nanos2;
983 let ge = nanos1 >= nanos2;
984
985 let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
987 prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
988
989 prop_assert_eq!(le, eq || lt, "<= should equal == || <");
991 prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
992 prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
993 prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
994 }
995
996 #[test]
997 fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
998 let string_repr = nanos.to_string();
1000 let parsed = UnixNanos::from_str(&string_repr);
1001 prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
1002 if let Ok(parsed_nanos) = parsed {
1003 prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
1004 }
1005 }
1006
1007 #[test]
1008 fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
1009 if i64::try_from(nanos.as_u64()).is_ok() {
1011 let datetime = nanos.to_datetime_utc();
1012 let converted_back = UnixNanos::from(datetime);
1013 prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
1014
1015 let rfc3339 = nanos.to_rfc3339();
1017 if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
1018 prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
1019 }
1020 }
1021 }
1022
1023 #[test]
1024 fn prop_unix_nanos_duration_since(
1025 (nanos1, nanos2) in unix_nanos_pair_strategy()
1026 ) {
1027 let duration = nanos1.duration_since(&nanos2);
1029
1030 if nanos1 >= nanos2 {
1031 prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
1033 if let Some(dur) = duration {
1034 prop_assert_eq!(dur, nanos1.as_u64() - nanos2.as_u64(),
1035 "Duration should equal the difference");
1036 prop_assert_eq!(nanos2 + dur, nanos1.as_u64(),
1037 "second + duration should equal first");
1038 }
1039 } else {
1040 prop_assert!(duration.is_none(), "Duration should be None when first < second");
1042 }
1043 }
1044
1045 #[test]
1046 fn prop_unix_nanos_checked_arithmetic(
1047 (nanos1, nanos2) in unix_nanos_pair_strategy()
1048 ) {
1049 let checked_add = nanos1.checked_add(nanos2.as_u64());
1051 let checked_sub = nanos1.checked_sub(nanos2.as_u64());
1052
1053 if let Some(sum) = checked_add {
1055 if nanos1.as_u64().checked_add(nanos2.as_u64()).is_some() {
1056 prop_assert_eq!(sum, nanos1 + nanos2, "Checked add should match regular add when no overflow");
1057 }
1058 }
1059
1060 if let Some(diff) = checked_sub {
1062 if nanos1.as_u64() >= nanos2.as_u64() {
1063 prop_assert_eq!(diff, nanos1 - nanos2, "Checked sub should match regular sub when no underflow");
1064 }
1065 }
1066 }
1067
1068 #[test]
1069 fn prop_unix_nanos_saturating_arithmetic(
1070 (nanos1, nanos2) in unix_nanos_pair_strategy()
1071 ) {
1072 let sat_add = nanos1.saturating_add_ns(nanos2.as_u64());
1074 let sat_sub = nanos1.saturating_sub_ns(nanos2.as_u64());
1075
1076 prop_assert!(sat_add >= nanos1, "Saturating add result should be >= first operand");
1078 prop_assert!(sat_add.as_u64() >= nanos2.as_u64(), "Saturating add result should be >= second operand");
1079
1080 prop_assert!(sat_sub <= nanos1, "Saturating sub result should be <= first operand");
1082
1083 if let Some(checked_sum) = nanos1.checked_add(nanos2.as_u64()) {
1085 prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
1086 } else {
1087 prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
1088 }
1089
1090 if let Some(checked_diff) = nanos1.checked_sub(nanos2.as_u64()) {
1091 prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
1092 } else {
1093 prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
1094 }
1095 }
1096 }
1097}