nautilus_core/
nanos.rs

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