nautilus_core/
nanos.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! A `UnixNanos` type for working with timestamps in nanoseconds since the UNIX epoch.
17//!
18//! This module provides a strongly-typed representation of timestamps as nanoseconds
19//! since the UNIX epoch (January 1, 1970, 00:00:00 UTC). The `UnixNanos` type offers
20//! conversion utilities, arithmetic operations, and comparison methods.
21//!
22//! # Features
23//!
24//! - Zero-cost abstraction with appropriate operator implementations.
25//! - Conversion to/from `DateTime<Utc>`.
26//! - RFC 3339 string formatting.
27//! - Duration calculations.
28//! - Flexible parsing and serialization.
29//!
30//! # Parsing and Serialization
31#![allow(
32    clippy::cast_possible_truncation,
33    clippy::cast_sign_loss,
34    clippy::cast_precision_loss,
35    clippy::cast_possible_wrap
36)]
37//!
38//! `UnixNanos` can be created from and serialized to various formats:
39//!
40//! * Integer values are interpreted as nanoseconds since the UNIX epoch.
41//! * Floating-point values are interpreted as seconds since the UNIX epoch (converted to nanoseconds
42//!   using truncation, not rounding, for consistency with [`secs_to_nanos`](crate::datetime::secs_to_nanos)).
43//! * String values may be:
44//!   - A numeric string (interpreted as nanoseconds).
45//!   - A floating-point string (interpreted as seconds, converted to nanoseconds).
46//!   - An RFC 3339 formatted timestamp (ISO 8601 with timezone).
47//!   - A simple date string in YYYY-MM-DD format (interpreted as midnight UTC on that date).
48//!
49//! # Limitations
50//!
51//! * Negative timestamps are invalid and will result in an error.
52//! * Arithmetic operations will panic on overflow/underflow rather than wrapping.
53//! * The `as_i64()` method and `DateTime<Utc>` conversions will panic for timestamps
54//!   beyond approximately year 2262 (when nanoseconds exceed `i64::MAX`).
55
56use std::{
57    cmp::Ordering,
58    fmt::Display,
59    ops::{Add, AddAssign, Deref, Sub, SubAssign},
60    str::FromStr,
61    time::SystemTime,
62};
63
64use chrono::{DateTime, NaiveDate, Utc};
65use serde::{
66    Deserialize, Deserializer, Serialize,
67    de::{self, Visitor},
68};
69
70/// Represents a duration in nanoseconds.
71pub type DurationNanos = u64;
72
73/// Represents a timestamp in nanoseconds since the UNIX epoch.
74#[repr(C)]
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
76pub struct UnixNanos(u64);
77
78impl UnixNanos {
79    /// Creates a new [`UnixNanos`] instance.
80    #[must_use]
81    pub const fn new(value: u64) -> Self {
82        Self(value)
83    }
84
85    /// Creates a new [`UnixNanos`] instance with the maximum valid value.
86    #[must_use]
87    pub const fn max() -> Self {
88        Self(u64::MAX)
89    }
90
91    /// Returns `true` if the value of this instance is zero.
92    #[must_use]
93    pub const fn is_zero(&self) -> bool {
94        self.0 == 0
95    }
96
97    /// Returns the underlying value as `u64`.
98    #[must_use]
99    pub const fn as_u64(&self) -> u64 {
100        self.0
101    }
102
103    /// Returns the underlying value as `i64`.
104    ///
105    /// # Panics
106    ///
107    /// Panics if the value exceeds `i64::MAX` (approximately year 2262).
108    #[must_use]
109    pub const fn as_i64(&self) -> i64 {
110        assert!(
111            self.0 <= i64::MAX as u64,
112            "UnixNanos value exceeds i64::MAX"
113        );
114        self.0 as i64
115    }
116
117    /// Returns the underlying value as `f64`.
118    #[must_use]
119    pub const fn as_f64(&self) -> f64 {
120        self.0 as f64
121    }
122
123    /// Converts the underlying value to a datetime (UTC).
124    ///
125    /// # Panics
126    ///
127    /// Panics if the value exceeds `i64::MAX` (approximately year 2262).
128    #[must_use]
129    pub const fn to_datetime_utc(&self) -> DateTime<Utc> {
130        DateTime::from_timestamp_nanos(self.as_i64())
131    }
132
133    /// Converts the underlying value to an ISO 8601 (RFC 3339) string.
134    #[must_use]
135    pub fn to_rfc3339(&self) -> String {
136        self.to_datetime_utc().to_rfc3339()
137    }
138
139    /// Calculates the duration in nanoseconds since another [`UnixNanos`] instance.
140    ///
141    /// Returns `Some(duration)` if `self` is later than `other`, otherwise `None` if `other` is
142    /// greater than `self` (indicating a negative duration is not possible with `DurationNanos`).
143    #[must_use]
144    pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
145        self.0.checked_sub(other.0)
146    }
147
148    fn parse_string(s: &str) -> Result<Self, String> {
149        const MAX_NS_F64: f64 = u64::MAX as f64;
150
151        // Try parsing as an integer (nanoseconds)
152        if let Ok(int_value) = s.parse::<u64>() {
153            return Ok(Self(int_value));
154        }
155
156        // If the string is composed solely of digits but didn't fit in a u64 we
157        // treat that as an overflow error rather than attempting to interpret
158        // it as seconds in floating-point form. This avoids the surprising
159        // situation where a caller provides nanoseconds but gets an out-of-
160        // range float interpretation instead.
161        if s.chars().all(|c| c.is_ascii_digit()) {
162            return Err("Unix timestamp is out of range".into());
163        }
164
165        // Try parsing as a floating point number (seconds)
166        if let Ok(float_value) = s.parse::<f64>() {
167            if !float_value.is_finite() {
168                return Err("Unix timestamp must be finite".into());
169            }
170
171            if float_value < 0.0 {
172                return Err("Unix timestamp cannot be negative".into());
173            }
174
175            // Convert seconds to nanoseconds while checking for overflow
176            // We perform the multiplication in `f64`, then validate the
177            // result fits inside `u64` *before* rounding / casting.
178            let nanos_f64 = float_value * 1_000_000_000.0;
179
180            if nanos_f64 > MAX_NS_F64 {
181                return Err("Unix timestamp is out of range".into());
182            }
183
184            let nanos = nanos_f64.trunc() as u64;
185            return Ok(Self(nanos));
186        }
187
188        // Try parsing as an RFC 3339 timestamp
189        if let Ok(datetime) = DateTime::parse_from_rfc3339(s) {
190            let nanos = datetime
191                .timestamp_nanos_opt()
192                .ok_or_else(|| "Timestamp out of range".to_string())?;
193
194            if nanos < 0 {
195                return Err("Unix timestamp cannot be negative".into());
196            }
197
198            // SAFETY: Checked that nanos >= 0, so cast to u64 is safe
199            return Ok(Self(nanos as u64));
200        }
201
202        // Try parsing as a simple date string (YYYY-MM-DD format)
203        if let Ok(datetime) = NaiveDate::parse_from_str(s, "%Y-%m-%d")
204            // SAFETY: unwrap() is safe here because and_hms_opt(0, 0, 0) always succeeds
205            // for valid dates (midnight is always a valid time)
206            .map(|date| date.and_hms_opt(0, 0, 0).unwrap())
207            .map(|naive_dt| DateTime::<Utc>::from_naive_utc_and_offset(naive_dt, Utc))
208        {
209            let nanos = datetime
210                .timestamp_nanos_opt()
211                .ok_or_else(|| "Timestamp out of range".to_string())?;
212            if nanos < 0 {
213                return Err("Unix timestamp cannot be negative".into());
214            }
215            return Ok(Self(nanos as u64));
216        }
217
218        Err(format!("Invalid format: {s}"))
219    }
220
221    /// Returns `Some(self + rhs)` or `None` if the addition would overflow
222    #[must_use]
223    pub fn checked_add<T: Into<u64>>(self, rhs: T) -> Option<Self> {
224        self.0.checked_add(rhs.into()).map(Self)
225    }
226
227    /// Returns `Some(self - rhs)` or `None` if the subtraction would underflow
228    #[must_use]
229    pub fn checked_sub<T: Into<u64>>(self, rhs: T) -> Option<Self> {
230        self.0.checked_sub(rhs.into()).map(Self)
231    }
232
233    /// Saturating addition – if overflow occurs the value is clamped to `u64::MAX`.
234    #[must_use]
235    pub fn saturating_add_ns<T: Into<u64>>(self, rhs: T) -> Self {
236        Self(self.0.saturating_add(rhs.into()))
237    }
238
239    /// Saturating subtraction – if underflow occurs the value is clamped to `0`.
240    #[must_use]
241    pub fn saturating_sub_ns<T: Into<u64>>(self, rhs: T) -> Self {
242        Self(self.0.saturating_sub(rhs.into()))
243    }
244}
245
246impl Deref for UnixNanos {
247    type Target = u64;
248
249    fn deref(&self) -> &Self::Target {
250        &self.0
251    }
252}
253
254impl PartialEq<u64> for UnixNanos {
255    fn eq(&self, other: &u64) -> bool {
256        self.0 == *other
257    }
258}
259
260impl PartialOrd<u64> for UnixNanos {
261    fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
262        self.0.partial_cmp(other)
263    }
264}
265
266impl PartialEq<Option<u64>> for UnixNanos {
267    fn eq(&self, other: &Option<u64>) -> bool {
268        match other {
269            Some(value) => self.0 == *value,
270            None => false,
271        }
272    }
273}
274
275impl PartialOrd<Option<u64>> for UnixNanos {
276    fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
277        match other {
278            Some(value) => self.0.partial_cmp(value),
279            None => Some(Ordering::Greater),
280        }
281    }
282}
283
284impl PartialEq<UnixNanos> for u64 {
285    fn eq(&self, other: &UnixNanos) -> bool {
286        *self == other.0
287    }
288}
289
290impl PartialOrd<UnixNanos> for u64 {
291    fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
292        self.partial_cmp(&other.0)
293    }
294}
295
296impl From<u64> for UnixNanos {
297    fn from(value: u64) -> Self {
298        Self(value)
299    }
300}
301
302impl From<UnixNanos> for u64 {
303    fn from(value: UnixNanos) -> Self {
304        value.0
305    }
306}
307
308/// Converts a string slice to [`UnixNanos`].
309///
310/// # Panics
311///
312/// This implementation will panic if the string cannot be parsed into a valid [`UnixNanos`].
313/// This is intentional fail-fast behavior where invalid timestamps indicate a critical
314/// logic error that should halt execution rather than silently propagate incorrect data.
315///
316/// For error handling without panicking, use [`str::parse::<UnixNanos>()`] which returns
317/// a [`Result`].
318impl From<&str> for UnixNanos {
319    fn from(value: &str) -> Self {
320        value
321            .parse()
322            .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
323    }
324}
325
326/// Converts a [`String`] to [`UnixNanos`].
327///
328/// # Panics
329///
330/// This implementation will panic if the string cannot be parsed into a valid [`UnixNanos`].
331/// This is intentional fail-fast behavior where invalid timestamps indicate a critical
332/// logic error that should halt execution rather than silently propagate incorrect data.
333///
334/// For error handling without panicking, use [`str::parse::<UnixNanos>()`] which returns
335/// a [`Result`].
336impl From<String> for UnixNanos {
337    fn from(value: String) -> Self {
338        value
339            .parse()
340            .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
341    }
342}
343
344impl From<DateTime<Utc>> for UnixNanos {
345    fn from(value: DateTime<Utc>) -> Self {
346        let nanos = value
347            .timestamp_nanos_opt()
348            .expect("DateTime timestamp out of range for UnixNanos");
349
350        assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
351
352        Self::from(nanos as u64)
353    }
354}
355
356impl From<SystemTime> for UnixNanos {
357    fn from(value: SystemTime) -> Self {
358        let duration = value
359            .duration_since(std::time::UNIX_EPOCH)
360            .expect("SystemTime before UNIX EPOCH");
361
362        let nanos = duration.as_nanos();
363        assert!(
364            nanos <= u64::MAX as u128,
365            "SystemTime overflowed u64 nanoseconds"
366        );
367
368        Self::from(nanos as u64)
369    }
370}
371
372impl FromStr for UnixNanos {
373    type Err = Box<dyn std::error::Error>;
374
375    fn from_str(s: &str) -> Result<Self, Self::Err> {
376        Self::parse_string(s).map_err(std::convert::Into::into)
377    }
378}
379
380/// Adds two [`UnixNanos`] values.
381///
382/// # Panics
383///
384/// Panics on overflow. This is intentional fail-fast behavior: overflow in timestamp
385/// arithmetic indicates a logic error in calculations that would corrupt data.
386/// Use [`UnixNanos::checked_add()`] or [`UnixNanos::saturating_add_ns()`] if you need
387/// explicit overflow handling.
388impl Add for UnixNanos {
389    type Output = Self;
390
391    fn add(self, rhs: Self) -> Self::Output {
392        Self(
393            self.0
394                .checked_add(rhs.0)
395                .expect("UnixNanos overflow in addition - invalid timestamp calculation"),
396        )
397    }
398}
399
400/// Subtracts one [`UnixNanos`] from another.
401///
402/// # Panics
403///
404/// Panics on underflow. This is intentional fail-fast behavior: underflow in timestamp
405/// arithmetic indicates a logic error in calculations that would corrupt data.
406/// Use [`UnixNanos::checked_sub()`] or [`UnixNanos::saturating_sub_ns()`] if you need
407/// explicit underflow handling.
408impl Sub for UnixNanos {
409    type Output = Self;
410
411    fn sub(self, rhs: Self) -> Self::Output {
412        Self(
413            self.0
414                .checked_sub(rhs.0)
415                .expect("UnixNanos underflow in subtraction - invalid timestamp calculation"),
416        )
417    }
418}
419
420/// Adds a `u64` nanosecond value to [`UnixNanos`].
421///
422/// # Panics
423///
424/// Panics on overflow. This is intentional fail-fast behavior for timestamp arithmetic.
425/// Use [`UnixNanos::checked_add()`] for explicit overflow handling.
426impl Add<u64> for UnixNanos {
427    type Output = Self;
428
429    fn add(self, rhs: u64) -> Self::Output {
430        Self(
431            self.0
432                .checked_add(rhs)
433                .expect("UnixNanos overflow in addition"),
434        )
435    }
436}
437
438/// Subtracts a `u64` nanosecond value from [`UnixNanos`].
439///
440/// # Panics
441///
442/// Panics on underflow. This is intentional fail-fast behavior for timestamp arithmetic.
443/// Use [`UnixNanos::checked_sub()`] for explicit underflow handling.
444impl Sub<u64> for UnixNanos {
445    type Output = Self;
446
447    fn sub(self, rhs: u64) -> Self::Output {
448        Self(
449            self.0
450                .checked_sub(rhs)
451                .expect("UnixNanos underflow in subtraction"),
452        )
453    }
454}
455
456/// Add-assigns a value to [`UnixNanos`].
457///
458/// # Panics
459///
460/// Panics on overflow. This is intentional fail-fast behavior for timestamp arithmetic.
461impl<T: Into<u64>> AddAssign<T> for UnixNanos {
462    fn add_assign(&mut self, other: T) {
463        let other_u64 = other.into();
464        self.0 = self
465            .0
466            .checked_add(other_u64)
467            .expect("UnixNanos overflow in add_assign");
468    }
469}
470
471/// Sub-assigns a value from [`UnixNanos`].
472///
473/// # Panics
474///
475/// Panics on underflow. This is intentional fail-fast behavior for timestamp arithmetic.
476impl<T: Into<u64>> SubAssign<T> for UnixNanos {
477    fn sub_assign(&mut self, other: T) {
478        let other_u64 = other.into();
479        self.0 = self
480            .0
481            .checked_sub(other_u64)
482            .expect("UnixNanos underflow in sub_assign");
483    }
484}
485
486impl Display for UnixNanos {
487    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488        write!(f, "{}", self.0)
489    }
490}
491
492impl From<UnixNanos> for DateTime<Utc> {
493    fn from(value: UnixNanos) -> Self {
494        value.to_datetime_utc()
495    }
496}
497
498impl<'de> Deserialize<'de> for UnixNanos {
499    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
500    where
501        D: Deserializer<'de>,
502    {
503        struct UnixNanosVisitor;
504
505        impl Visitor<'_> for UnixNanosVisitor {
506            type Value = UnixNanos;
507
508            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
509                formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
510            }
511
512            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
513            where
514                E: de::Error,
515            {
516                Ok(UnixNanos(value))
517            }
518
519            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
520            where
521                E: de::Error,
522            {
523                if value < 0 {
524                    return Err(E::custom("Unix timestamp cannot be negative"));
525                }
526                Ok(UnixNanos(value as u64))
527            }
528
529            fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
530            where
531                E: de::Error,
532            {
533                const MAX_NS_F64: f64 = u64::MAX as f64;
534
535                if !value.is_finite() {
536                    return Err(E::custom(format!(
537                        "Unix timestamp must be finite, was {value}"
538                    )));
539                }
540                if value < 0.0 {
541                    return Err(E::custom("Unix timestamp cannot be negative"));
542                }
543
544                // Convert from seconds to nanoseconds with overflow check
545                let nanos_f64 = value * 1_000_000_000.0;
546                if nanos_f64 > MAX_NS_F64 {
547                    return Err(E::custom(format!(
548                        "Unix timestamp {value} seconds is out of range"
549                    )));
550                }
551                let nanos = nanos_f64.trunc() as u64;
552                Ok(UnixNanos(nanos))
553            }
554
555            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
556            where
557                E: de::Error,
558            {
559                UnixNanos::parse_string(value).map_err(E::custom)
560            }
561        }
562
563        deserializer.deserialize_any(UnixNanosVisitor)
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use chrono::{Duration, TimeZone};
570    use rstest::rstest;
571
572    use super::*;
573
574    #[rstest]
575    fn test_new() {
576        let nanos = UnixNanos::new(123);
577        assert_eq!(nanos.as_u64(), 123);
578        assert_eq!(nanos.as_i64(), 123);
579    }
580
581    #[rstest]
582    fn test_max() {
583        let nanos = UnixNanos::max();
584        assert_eq!(nanos.as_u64(), u64::MAX);
585    }
586
587    #[rstest]
588    fn test_is_zero() {
589        assert!(UnixNanos::default().is_zero());
590        assert!(!UnixNanos::max().is_zero());
591    }
592
593    #[rstest]
594    fn test_from_u64() {
595        let nanos = UnixNanos::from(123);
596        assert_eq!(nanos.as_u64(), 123);
597        assert_eq!(nanos.as_i64(), 123);
598    }
599
600    #[rstest]
601    fn test_default() {
602        let nanos = UnixNanos::default();
603        assert_eq!(nanos.as_u64(), 0);
604        assert_eq!(nanos.as_i64(), 0);
605    }
606
607    #[rstest]
608    fn test_into_from() {
609        let nanos: UnixNanos = 456.into();
610        let value: u64 = nanos.into();
611        assert_eq!(value, 456);
612    }
613
614    #[rstest]
615    #[case(0, "1970-01-01T00:00:00+00:00")]
616    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
617    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
618    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
619    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
620    fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
621        let nanos = UnixNanos::from(nanos);
622        let datetime = nanos.to_datetime_utc();
623        assert_eq!(datetime.to_rfc3339(), expected);
624    }
625
626    #[rstest]
627    #[case(0, "1970-01-01T00:00:00+00:00")]
628    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
629    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
630    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
631    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
632    fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
633        let nanos = UnixNanos::from(nanos);
634        assert_eq!(nanos.to_rfc3339(), expected);
635    }
636
637    #[rstest]
638    fn test_from_str() {
639        let nanos: UnixNanos = "123".parse().unwrap();
640        assert_eq!(nanos.as_u64(), 123);
641    }
642
643    #[rstest]
644    fn test_from_str_invalid() {
645        let result = "abc".parse::<UnixNanos>();
646        assert!(result.is_err());
647    }
648
649    #[rstest]
650    fn test_from_str_pre_epoch_date() {
651        let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
652        assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
653    }
654
655    #[rstest]
656    fn test_from_str_pre_epoch_rfc3339() {
657        let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
658        assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
659    }
660
661    #[rstest]
662    fn test_try_from_datetime_valid() {
663        use chrono::TimeZone;
664        let datetime = Utc.timestamp_opt(1_000_000_000, 0).unwrap(); // 1 billion seconds since epoch
665        let nanos = UnixNanos::from(datetime);
666        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
667    }
668
669    #[rstest]
670    fn test_from_system_time() {
671        let system_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
672        let nanos = UnixNanos::from(system_time);
673        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
674    }
675
676    #[rstest]
677    #[should_panic(expected = "SystemTime before UNIX EPOCH")]
678    fn test_from_system_time_before_epoch() {
679        let system_time = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
680        let _ = UnixNanos::from(system_time);
681    }
682
683    #[rstest]
684    fn test_eq() {
685        let nanos = UnixNanos::from(100);
686        assert_eq!(nanos, 100);
687        assert_eq!(nanos, Some(100));
688        assert_ne!(nanos, 200);
689        assert_ne!(nanos, Some(200));
690        assert_ne!(nanos, None);
691    }
692
693    #[rstest]
694    fn test_partial_cmp() {
695        let nanos = UnixNanos::from(100);
696        assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
697        assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
698        assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
699        assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
700    }
701
702    #[rstest]
703    fn test_edge_case_max_value() {
704        let nanos = UnixNanos::from(u64::MAX);
705        assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
706    }
707
708    #[rstest]
709    fn test_display() {
710        let nanos = UnixNanos::from(123);
711        assert_eq!(format!("{nanos}"), "123");
712    }
713
714    #[rstest]
715    fn test_addition() {
716        let nanos1 = UnixNanos::from(100);
717        let nanos2 = UnixNanos::from(200);
718        let result = nanos1 + nanos2;
719        assert_eq!(result.as_u64(), 300);
720    }
721
722    #[rstest]
723    fn test_add_assign() {
724        let mut nanos = UnixNanos::from(100);
725        nanos += 50_u64;
726        assert_eq!(nanos.as_u64(), 150);
727    }
728
729    #[rstest]
730    fn test_subtraction() {
731        let nanos1 = UnixNanos::from(200);
732        let nanos2 = UnixNanos::from(100);
733        let result = nanos1 - nanos2;
734        assert_eq!(result.as_u64(), 100);
735    }
736
737    #[rstest]
738    fn test_sub_assign() {
739        let mut nanos = UnixNanos::from(200);
740        nanos -= 50_u64;
741        assert_eq!(nanos.as_u64(), 150);
742    }
743
744    #[rstest]
745    #[should_panic(expected = "UnixNanos overflow")]
746    fn test_overflow_add() {
747        let nanos = UnixNanos::from(u64::MAX);
748        let _ = nanos + UnixNanos::from(1); // This should panic due to overflow
749    }
750
751    #[rstest]
752    #[should_panic(expected = "UnixNanos overflow")]
753    fn test_overflow_add_u64() {
754        let nanos = UnixNanos::from(u64::MAX);
755        let _ = nanos + 1_u64; // This should panic due to overflow
756    }
757
758    #[rstest]
759    #[should_panic(expected = "UnixNanos underflow")]
760    fn test_overflow_sub() {
761        let _ = UnixNanos::default() - UnixNanos::from(1); // This should panic due to underflow
762    }
763
764    #[rstest]
765    #[should_panic(expected = "UnixNanos underflow")]
766    fn test_overflow_sub_u64() {
767        let _ = UnixNanos::default() - 1_u64; // This should panic due to underflow
768    }
769
770    #[rstest]
771    #[case(100, 50, Some(50))]
772    #[case(1_000_000_000, 500_000_000, Some(500_000_000))]
773    #[case(u64::MAX, u64::MAX - 1, Some(1))]
774    #[case(50, 50, Some(0))]
775    #[case(50, 100, None)]
776    #[case(0, 1, None)]
777    fn test_duration_since(
778        #[case] time1: u64,
779        #[case] time2: u64,
780        #[case] expected: Option<DurationNanos>,
781    ) {
782        let nanos1 = UnixNanos::from(time1);
783        let nanos2 = UnixNanos::from(time2);
784        assert_eq!(nanos1.duration_since(&nanos2), expected);
785    }
786
787    #[rstest]
788    fn test_duration_since_same_moment() {
789        let moment = UnixNanos::from(1_707_577_123_456_789_000);
790        assert_eq!(moment.duration_since(&moment), Some(0));
791    }
792
793    #[rstest]
794    fn test_duration_since_chronological() {
795        // Create a reference time (Feb 10, 2024)
796        let earlier = Utc.with_ymd_and_hms(2024, 2, 10, 12, 0, 0).unwrap();
797
798        // Create a time 1 hour, 30 minutes, and 45 seconds later (with nanoseconds)
799        let later = earlier
800            + Duration::hours(1)
801            + Duration::minutes(30)
802            + Duration::seconds(45)
803            + Duration::nanoseconds(500_000_000);
804
805        let earlier_nanos = UnixNanos::from(earlier);
806        let later_nanos = UnixNanos::from(later);
807
808        // Calculate expected duration in nanoseconds
809        let expected_duration = 60 * 60 * 1_000_000_000 + // 1 hour
810        30 * 60 * 1_000_000_000 + // 30 minutes
811        45 * 1_000_000_000 + // 45 seconds
812        500_000_000; // 500 million nanoseconds
813
814        assert_eq!(
815            later_nanos.duration_since(&earlier_nanos),
816            Some(expected_duration)
817        );
818        assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
819    }
820
821    #[rstest]
822    fn test_duration_since_with_edge_cases() {
823        // Test with maximum value
824        let max = UnixNanos::from(u64::MAX);
825        let smaller = UnixNanos::from(u64::MAX - 1000);
826
827        assert_eq!(max.duration_since(&smaller), Some(1000));
828        assert_eq!(smaller.duration_since(&max), None);
829
830        // Test with minimum value
831        let min = UnixNanos::default(); // Zero timestamp
832        let larger = UnixNanos::from(1000);
833
834        assert_eq!(min.duration_since(&min), Some(0));
835        assert_eq!(larger.duration_since(&min), Some(1000));
836        assert_eq!(min.duration_since(&larger), None);
837    }
838
839    #[rstest]
840    fn test_serde_json() {
841        let nanos = UnixNanos::from(123);
842        let json = serde_json::to_string(&nanos).unwrap();
843        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
844        assert_eq!(deserialized, nanos);
845    }
846
847    #[rstest]
848    fn test_serde_edge_cases() {
849        let nanos = UnixNanos::from(u64::MAX);
850        let json = serde_json::to_string(&nanos).unwrap();
851        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
852        assert_eq!(deserialized, nanos);
853    }
854
855    #[rstest]
856    #[case("123", 123)] // Integer string
857    #[case("1234.567", 1_234_567_000_000)] // Float string (seconds to nanos)
858    #[case("2024-02-10", 1_707_523_200_000_000_000)] // Simple date (midnight UTC)
859    #[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] // RFC3339 without fractions
860    #[case("2024-02-10T14:58:43.456789Z", 1_707_577_123_456_789_000)] // RFC3339 with fractions
861    fn test_from_str_formats(#[case] input: &str, #[case] expected: u64) {
862        let parsed: UnixNanos = input.parse().unwrap();
863        assert_eq!(parsed.as_u64(), expected);
864    }
865
866    #[rstest]
867    #[case("abc")] // Random string
868    #[case("not a timestamp")] // Non-timestamp string
869    #[case("2024-02-10 14:58:43")] // Space-separated format (not RFC3339)
870    fn test_from_str_invalid_formats(#[case] input: &str) {
871        let result = input.parse::<UnixNanos>();
872        assert!(result.is_err());
873    }
874
875    #[rstest]
876    fn test_from_str_integer_overflow() {
877        // One more digit than u64::MAX (20 digits) so definitely overflows
878        let input = "184467440737095516160";
879        let result = input.parse::<UnixNanos>();
880        assert!(result.is_err());
881    }
882
883    // ---------- checked / saturating arithmetic ----------
884
885    #[rstest]
886    fn test_checked_add_overflow_returns_none() {
887        let max = UnixNanos::from(u64::MAX);
888        assert_eq!(max.checked_add(1_u64), None);
889    }
890
891    #[rstest]
892    fn test_checked_sub_underflow_returns_none() {
893        let zero = UnixNanos::default();
894        assert_eq!(zero.checked_sub(1_u64), None);
895    }
896
897    #[rstest]
898    fn test_saturating_add_overflow() {
899        let max = UnixNanos::from(u64::MAX);
900        let result = max.saturating_add_ns(1_u64);
901        assert_eq!(result, UnixNanos::from(u64::MAX));
902    }
903
904    #[rstest]
905    fn test_saturating_sub_underflow() {
906        let zero = UnixNanos::default();
907        let result = zero.saturating_sub_ns(1_u64);
908        assert_eq!(result, UnixNanos::default());
909    }
910
911    #[rstest]
912    fn test_from_str_float_overflow() {
913        // Use scientific notation so we take the floating-point parsing path.
914        let input = "2e10"; // 20 billion seconds ~ 634 years (> u64::MAX nanoseconds)
915        let result = input.parse::<UnixNanos>();
916        assert!(result.is_err());
917    }
918
919    #[rstest]
920    fn test_deserialize_u64() {
921        let json = "123456789";
922        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
923        assert_eq!(deserialized.as_u64(), 123_456_789);
924    }
925
926    #[rstest]
927    fn test_deserialize_string_with_int() {
928        let json = "\"123456789\"";
929        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
930        assert_eq!(deserialized.as_u64(), 123_456_789);
931    }
932
933    #[rstest]
934    fn test_deserialize_float() {
935        let json = "1234.567";
936        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
937        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
938    }
939
940    #[rstest]
941    fn test_deserialize_string_with_float() {
942        let json = "\"1234.567\"";
943        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
944        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
945    }
946
947    #[rstest]
948    fn test_deserialize_float_uses_truncation() {
949        // Truncation (not rounding) for consistency with secs_to_nanos() etc
950        let json = "0.9999999999";
951        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
952        assert_eq!(deserialized.as_u64(), 999_999_999); // Truncated, not rounded to 1B
953    }
954
955    #[rstest]
956    #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
957    #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
958    fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
959        let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
960        assert_eq!(deserialized.as_u64(), expected);
961    }
962
963    #[rstest]
964    fn test_deserialize_negative_int_fails() {
965        let json = "-123456789";
966        let result: Result<UnixNanos, _> = serde_json::from_str(json);
967        assert!(result.is_err());
968    }
969
970    #[rstest]
971    fn test_deserialize_negative_float_fails() {
972        let json = "-1234.567";
973        let result: Result<UnixNanos, _> = serde_json::from_str(json);
974        assert!(result.is_err());
975    }
976
977    #[rstest]
978    fn test_deserialize_nan_fails() {
979        // JSON doesn't support NaN directly, test the internal deserializer
980        use serde::de::{
981            IntoDeserializer,
982            value::{Error as ValueError, F64Deserializer},
983        };
984        let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
985        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
986        assert!(result.is_err());
987        assert!(result.unwrap_err().to_string().contains("must be finite"));
988    }
989
990    #[rstest]
991    fn test_deserialize_infinity_fails() {
992        use serde::de::{
993            IntoDeserializer,
994            value::{Error as ValueError, F64Deserializer},
995        };
996        let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
997        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
998        assert!(result.is_err());
999        assert!(result.unwrap_err().to_string().contains("must be finite"));
1000    }
1001
1002    #[rstest]
1003    fn test_deserialize_negative_infinity_fails() {
1004        use serde::de::{
1005            IntoDeserializer,
1006            value::{Error as ValueError, F64Deserializer},
1007        };
1008        let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
1009        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1010        assert!(result.is_err());
1011        assert!(result.unwrap_err().to_string().contains("must be finite"));
1012    }
1013
1014    #[rstest]
1015    fn test_deserialize_overflow_float_fails() {
1016        // Test a float that would overflow u64 when converted to nanoseconds
1017        // u64::MAX is ~18.4e18, so u64::MAX / 1e9 = ~18.4e9 seconds
1018        let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
1019        assert!(result.is_err());
1020        assert!(result.unwrap_err().to_string().contains("out of range"));
1021    }
1022
1023    #[rstest]
1024    fn test_deserialize_invalid_string_fails() {
1025        let json = "\"not a timestamp\"";
1026        let result: Result<UnixNanos, _> = serde_json::from_str(json);
1027        assert!(result.is_err());
1028    }
1029
1030    #[rstest]
1031    fn test_deserialize_edge_cases() {
1032        // Test zero
1033        let json = "0";
1034        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1035        assert_eq!(deserialized.as_u64(), 0);
1036
1037        // Test large value
1038        let json = "18446744073709551615"; // u64::MAX
1039        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1040        assert_eq!(deserialized.as_u64(), u64::MAX);
1041    }
1042
1043    #[rstest]
1044    #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
1045    fn test_as_i64_overflow_panics() {
1046        let nanos = UnixNanos::from(u64::MAX);
1047        let _ = nanos.as_i64(); // Should panic
1048    }
1049
1050    ////////////////////////////////////////////////////////////////////////////////
1051    // Property-based testing
1052    ////////////////////////////////////////////////////////////////////////////////
1053
1054    use proptest::prelude::*;
1055
1056    fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
1057        prop_oneof![
1058            // Small values
1059            0u64..1_000_000u64,
1060            // Medium values (microseconds range)
1061            1_000_000u64..1_000_000_000_000u64,
1062            // Large values (nanoseconds since 1970, but safe for arithmetic)
1063            1_000_000_000_000u64..=i64::MAX as u64,
1064            // Edge cases
1065            Just(0u64),
1066            Just(1u64),
1067            Just(1_000_000_000u64),             // 1 second in nanos
1068            Just(1_000_000_000_000u64),         // ~2001 timestamp
1069            Just(1_700_000_000_000_000_000u64), // ~2023 timestamp
1070            Just((i64::MAX / 2) as u64),        // Safe for doubling
1071        ]
1072        .prop_map(UnixNanos::from)
1073    }
1074
1075    fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
1076        (unix_nanos_strategy(), unix_nanos_strategy())
1077    }
1078
1079    proptest! {
1080        #[rstest]
1081        fn prop_unix_nanos_construction_roundtrip(value in 0u64..=i64::MAX as u64) {
1082            let nanos = UnixNanos::from(value);
1083            prop_assert_eq!(nanos.as_u64(), value);
1084            prop_assert_eq!(nanos.as_f64(), value as f64);
1085
1086            // Test i64 conversion only for values within i64 range
1087            if i64::try_from(value).is_ok() {
1088                prop_assert_eq!(nanos.as_i64(), value as i64);
1089            }
1090        }
1091
1092        #[rstest]
1093        fn prop_unix_nanos_addition_commutative(
1094            (nanos1, nanos2) in unix_nanos_pair_strategy()
1095        ) {
1096            // Addition should be commutative when no overflow occurs
1097            if let (Some(sum1), Some(sum2)) = (
1098                nanos1.checked_add(nanos2.as_u64()),
1099                nanos2.checked_add(nanos1.as_u64())
1100            ) {
1101                prop_assert_eq!(sum1, sum2, "Addition should be commutative");
1102            }
1103        }
1104
1105        #[rstest]
1106        fn prop_unix_nanos_addition_associative(
1107            nanos1 in unix_nanos_strategy(),
1108            nanos2 in unix_nanos_strategy(),
1109            nanos3 in unix_nanos_strategy(),
1110        ) {
1111            // Addition should be associative when no overflow occurs
1112            if let (Some(sum1), Some(sum2)) = (
1113                nanos1.as_u64().checked_add(nanos2.as_u64()),
1114                nanos2.as_u64().checked_add(nanos3.as_u64())
1115            )
1116                && let (Some(left), Some(right)) = (
1117                    sum1.checked_add(nanos3.as_u64()),
1118                    nanos1.as_u64().checked_add(sum2)
1119                ) {
1120                    let left_result = UnixNanos::from(left);
1121                    let right_result = UnixNanos::from(right);
1122                    prop_assert_eq!(left_result, right_result, "Addition should be associative");
1123                }
1124        }
1125
1126        #[rstest]
1127        fn prop_unix_nanos_subtraction_inverse(
1128            (nanos1, nanos2) in unix_nanos_pair_strategy()
1129        ) {
1130            // Subtraction should be the inverse of addition when no underflow occurs
1131            if let Some(sum) = nanos1.checked_add(nanos2.as_u64()) {
1132                let diff = sum - nanos2;
1133                prop_assert_eq!(diff, nanos1, "Subtraction should be inverse of addition");
1134            }
1135        }
1136
1137        #[rstest]
1138        fn prop_unix_nanos_zero_identity(nanos in unix_nanos_strategy()) {
1139            // Zero should be additive identity
1140            let zero = UnixNanos::default();
1141            prop_assert_eq!(nanos + zero, nanos, "Zero should be additive identity");
1142            prop_assert_eq!(zero + nanos, nanos, "Zero should be additive identity (commutative)");
1143            prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
1144        }
1145
1146        #[rstest]
1147        fn prop_unix_nanos_ordering_consistency(
1148            (nanos1, nanos2) in unix_nanos_pair_strategy()
1149        ) {
1150            // Ordering operations should be consistent
1151            let eq = nanos1 == nanos2;
1152            let lt = nanos1 < nanos2;
1153            let gt = nanos1 > nanos2;
1154            let le = nanos1 <= nanos2;
1155            let ge = nanos1 >= nanos2;
1156
1157            // Exactly one of eq, lt, gt should be true
1158            let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
1159            prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
1160
1161            // Consistency checks
1162            prop_assert_eq!(le, eq || lt, "<= should equal == || <");
1163            prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
1164            prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
1165            prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
1166        }
1167
1168        #[rstest]
1169        fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
1170            // String serialization should round-trip correctly
1171            let string_repr = nanos.to_string();
1172            let parsed = UnixNanos::from_str(&string_repr);
1173            prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
1174            if let Ok(parsed_nanos) = parsed {
1175                prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
1176            }
1177        }
1178
1179        #[rstest]
1180        fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
1181            // DateTime conversion should be consistent (only test values within i64 range)
1182            if i64::try_from(nanos.as_u64()).is_ok() {
1183                let datetime = nanos.to_datetime_utc();
1184                let converted_back = UnixNanos::from(datetime);
1185                prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
1186
1187                // RFC3339 string should also round-trip for valid dates
1188                let rfc3339 = nanos.to_rfc3339();
1189                if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
1190                    prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
1191                }
1192            }
1193        }
1194
1195        #[rstest]
1196        fn prop_unix_nanos_duration_since(
1197            (nanos1, nanos2) in unix_nanos_pair_strategy()
1198        ) {
1199            // duration_since should be consistent with comparison and arithmetic
1200            let duration = nanos1.duration_since(&nanos2);
1201
1202            if nanos1 >= nanos2 {
1203                // If nanos1 >= nanos2, duration should be Some and equal to difference
1204                prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
1205                if let Some(dur) = duration {
1206                    prop_assert_eq!(dur, nanos1.as_u64() - nanos2.as_u64(),
1207                        "Duration should equal the difference");
1208                    prop_assert_eq!(nanos2 + dur, nanos1.as_u64(),
1209                        "second + duration should equal first");
1210                }
1211            } else {
1212                // If nanos1 < nanos2, duration should be None
1213                prop_assert!(duration.is_none(), "Duration should be None when first < second");
1214            }
1215        }
1216
1217        #[rstest]
1218        fn prop_unix_nanos_checked_arithmetic(
1219            (nanos1, nanos2) in unix_nanos_pair_strategy()
1220        ) {
1221            // Checked arithmetic should be consistent with regular arithmetic when no overflow/underflow
1222            let checked_add = nanos1.checked_add(nanos2.as_u64());
1223            let checked_sub = nanos1.checked_sub(nanos2.as_u64());
1224
1225            // If checked_add succeeds, regular addition should produce the same result
1226            if let Some(sum) = checked_add
1227                && nanos1.as_u64().checked_add(nanos2.as_u64()).is_some() {
1228                    prop_assert_eq!(sum, nanos1 + nanos2, "Checked add should match regular add when no overflow");
1229                }
1230
1231            // If checked_sub succeeds, regular subtraction should produce the same result
1232            if let Some(diff) = checked_sub
1233                && nanos1.as_u64() >= nanos2.as_u64() {
1234                    prop_assert_eq!(diff, nanos1 - nanos2, "Checked sub should match regular sub when no underflow");
1235                }
1236        }
1237
1238        #[rstest]
1239        fn prop_unix_nanos_saturating_arithmetic(
1240            (nanos1, nanos2) in unix_nanos_pair_strategy()
1241        ) {
1242            // Saturating arithmetic should never panic and produce reasonable results
1243            let sat_add = nanos1.saturating_add_ns(nanos2.as_u64());
1244            let sat_sub = nanos1.saturating_sub_ns(nanos2.as_u64());
1245
1246            // Saturating add should be >= both operands
1247            prop_assert!(sat_add >= nanos1, "Saturating add result should be >= first operand");
1248            prop_assert!(sat_add.as_u64() >= nanos2.as_u64(), "Saturating add result should be >= second operand");
1249
1250            // Saturating sub should be <= first operand
1251            prop_assert!(sat_sub <= nanos1, "Saturating sub result should be <= first operand");
1252
1253            // If no overflow/underflow would occur, saturating should match checked
1254            if let Some(checked_sum) = nanos1.checked_add(nanos2.as_u64()) {
1255                prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
1256            } else {
1257                prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
1258            }
1259
1260            if let Some(checked_diff) = nanos1.checked_sub(nanos2.as_u64()) {
1261                prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
1262            } else {
1263                prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
1264            }
1265        }
1266    }
1267}