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};
59
60use chrono::{DateTime, NaiveDate, Utc};
61use serde::{
62    Deserialize, Deserializer, Serialize,
63    de::{self, Visitor},
64};
65
66/// Represents a duration in nanoseconds.
67pub type DurationNanos = u64;
68
69/// Represents a timestamp in nanoseconds since the UNIX epoch.
70#[repr(C)]
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
72pub struct UnixNanos(u64);
73
74impl UnixNanos {
75    /// Creates a new [`UnixNanos`] instance.
76    #[must_use]
77    pub const fn new(value: u64) -> Self {
78        Self(value)
79    }
80
81    /// Creates a new [`UnixNanos`] instance with the maximum valid value.
82    #[must_use]
83    pub const fn max() -> Self {
84        Self(u64::MAX)
85    }
86
87    /// Returns `true` if the value of this instance is zero.
88    #[must_use]
89    pub const fn is_zero(&self) -> bool {
90        self.0 == 0
91    }
92
93    /// Returns the underlying value as `u64`.
94    #[must_use]
95    pub const fn as_u64(&self) -> u64 {
96        self.0
97    }
98
99    /// Returns the underlying value as `i64`.
100    ///
101    /// # Panics
102    ///
103    /// Panics if the value exceeds `i64::MAX` (approximately year 2262).
104    #[must_use]
105    pub const fn as_i64(&self) -> i64 {
106        assert!(
107            (self.0 <= i64::MAX as u64),
108            "UnixNanos value exceeds i64::MAX"
109        );
110        self.0 as i64
111    }
112
113    /// Returns the underlying value as `f64`.
114    #[must_use]
115    pub const fn as_f64(&self) -> f64 {
116        self.0 as f64
117    }
118
119    /// Converts the underlying value to a datetime (UTC).
120    #[must_use]
121    pub const fn to_datetime_utc(&self) -> DateTime<Utc> {
122        DateTime::from_timestamp_nanos(self.0 as i64)
123    }
124
125    /// Converts the underlying value to an ISO 8601 (RFC 3339) string.
126    #[must_use]
127    pub fn to_rfc3339(&self) -> String {
128        self.to_datetime_utc().to_rfc3339()
129    }
130
131    /// Calculates the duration in nanoseconds since another [`UnixNanos`] instance.
132    ///
133    /// Returns `Some(duration)` if `self` is later than `other`, otherwise `None` if `other` is
134    /// greater than `self` (indicating a negative duration is not possible with `DurationNanos`).
135    #[must_use]
136    pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
137        self.0.checked_sub(other.0)
138    }
139
140    fn parse_string(s: &str) -> Result<Self, String> {
141        // Try parsing as an integer (nanoseconds)
142        if let Ok(int_value) = s.parse::<u64>() {
143            return Ok(Self(int_value));
144        }
145
146        // If the string is composed solely of digits but didn't fit in a u64 we
147        // treat that as an overflow error rather than attempting to interpret
148        // it as seconds in floating-point form. This avoids the surprising
149        // situation where a caller provides nanoseconds but gets an out-of-
150        // range float interpretation instead.
151        if s.chars().all(|c| c.is_ascii_digit()) {
152            return Err("Unix timestamp is out of range".into());
153        }
154
155        // Try parsing as a floating point number (seconds)
156        if let Ok(float_value) = s.parse::<f64>() {
157            if !float_value.is_finite() {
158                return Err("Unix timestamp must be finite".into());
159            }
160
161            if float_value < 0.0 {
162                return Err("Unix timestamp cannot be negative".into());
163            }
164
165            // Convert seconds to nanoseconds while checking for overflow
166            // We perform the multiplication in `f64`, then validate the
167            // result fits inside `u64` *before* rounding / casting.
168            const MAX_NS_F64: f64 = u64::MAX as f64;
169            let nanos_f64 = float_value * 1_000_000_000.0;
170
171            if nanos_f64 > MAX_NS_F64 {
172                return Err("Unix timestamp is out of range".into());
173            }
174
175            let nanos = nanos_f64.round() as u64;
176            return Ok(Self(nanos));
177        }
178
179        // Try parsing as an RFC 3339 timestamp
180        if let Ok(datetime) = DateTime::parse_from_rfc3339(s) {
181            let nanos = datetime
182                .timestamp_nanos_opt()
183                .ok_or_else(|| "Timestamp out of range".to_string())?;
184
185            if nanos < 0 {
186                return Err("Unix timestamp cannot be negative".into());
187            }
188
189            // SAFETY: Checked that nanos >= 0, so cast to u64 is safe
190            return Ok(Self(nanos as u64));
191        }
192
193        // Try parsing as a simple date string (YYYY-MM-DD format)
194        if let Ok(datetime) = NaiveDate::parse_from_str(s, "%Y-%m-%d")
195            .map(|date| date.and_hms_opt(0, 0, 0).unwrap())
196            .map(|naive_dt| DateTime::<Utc>::from_naive_utc_and_offset(naive_dt, Utc))
197        {
198            let nanos = datetime
199                .timestamp_nanos_opt()
200                .ok_or_else(|| "Timestamp out of range".to_string())?;
201            // SAFETY: timestamp_nanos_opt() returns >= 0 for valid dates
202            return Ok(Self(nanos as u64));
203        }
204
205        Err(format!("Invalid format: {s}"))
206    }
207
208    /// Returns `Some(self + rhs)` or `None` if the addition would overflow
209    #[must_use]
210    pub fn checked_add<T: Into<u64>>(self, rhs: T) -> Option<Self> {
211        self.0.checked_add(rhs.into()).map(Self)
212    }
213
214    /// Returns `Some(self - rhs)` or `None` if the subtraction would underflow
215    #[must_use]
216    pub fn checked_sub<T: Into<u64>>(self, rhs: T) -> Option<Self> {
217        self.0.checked_sub(rhs.into()).map(Self)
218    }
219
220    /// Saturating addition – if overflow occurs the value is clamped to `u64::MAX`.
221    #[must_use]
222    pub fn saturating_add_ns<T: Into<u64>>(self, rhs: T) -> Self {
223        Self(self.0.saturating_add(rhs.into()))
224    }
225
226    /// Saturating subtraction – if underflow occurs the value is clamped to `0`.
227    #[must_use]
228    pub fn saturating_sub_ns<T: Into<u64>>(self, rhs: T) -> Self {
229        Self(self.0.saturating_sub(rhs.into()))
230    }
231}
232
233impl Deref for UnixNanos {
234    type Target = u64;
235
236    fn deref(&self) -> &Self::Target {
237        &self.0
238    }
239}
240
241impl PartialEq<u64> for UnixNanos {
242    fn eq(&self, other: &u64) -> bool {
243        self.0 == *other
244    }
245}
246
247impl PartialOrd<u64> for UnixNanos {
248    fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
249        self.0.partial_cmp(other)
250    }
251}
252
253impl PartialEq<Option<u64>> for UnixNanos {
254    fn eq(&self, other: &Option<u64>) -> bool {
255        match other {
256            Some(value) => self.0 == *value,
257            None => false,
258        }
259    }
260}
261
262impl PartialOrd<Option<u64>> for UnixNanos {
263    fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
264        match other {
265            Some(value) => self.0.partial_cmp(value),
266            None => Some(Ordering::Greater),
267        }
268    }
269}
270
271impl PartialEq<UnixNanos> for u64 {
272    fn eq(&self, other: &UnixNanos) -> bool {
273        *self == other.0
274    }
275}
276
277impl PartialOrd<UnixNanos> for u64 {
278    fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
279        self.partial_cmp(&other.0)
280    }
281}
282
283impl From<u64> for UnixNanos {
284    fn from(value: u64) -> Self {
285        Self(value)
286    }
287}
288
289impl From<UnixNanos> for u64 {
290    fn from(value: UnixNanos) -> Self {
291        value.0
292    }
293}
294
295impl From<&str> for UnixNanos {
296    fn from(value: &str) -> Self {
297        value
298            .parse()
299            .unwrap_or_else(|e| panic!("Failed to parse string into UnixNanos: {e}"))
300    }
301}
302
303impl From<String> for UnixNanos {
304    fn from(value: String) -> Self {
305        value
306            .parse()
307            .unwrap_or_else(|e| panic!("Failed to parse string into UnixNanos: {e}"))
308    }
309}
310
311impl From<DateTime<Utc>> for UnixNanos {
312    fn from(value: DateTime<Utc>) -> Self {
313        Self::from(value.timestamp_nanos_opt().expect("Invalid timestamp") as u64)
314    }
315}
316
317impl FromStr for UnixNanos {
318    type Err = Box<dyn std::error::Error>;
319
320    fn from_str(s: &str) -> Result<Self, Self::Err> {
321        Self::parse_string(s).map_err(std::convert::Into::into)
322    }
323}
324
325impl Add for UnixNanos {
326    type Output = Self;
327
328    fn add(self, rhs: Self) -> Self::Output {
329        Self(
330            self.0
331                .checked_add(rhs.0)
332                .expect("Error adding with overflow"),
333        )
334    }
335}
336
337impl Sub for UnixNanos {
338    type Output = Self;
339
340    fn sub(self, rhs: Self) -> Self::Output {
341        Self(
342            self.0
343                .checked_sub(rhs.0)
344                .expect("Error subtracting with underflow"),
345        )
346    }
347}
348
349impl Add<u64> for UnixNanos {
350    type Output = Self;
351
352    fn add(self, rhs: u64) -> Self::Output {
353        Self(self.0.checked_add(rhs).expect("Error adding with overflow"))
354    }
355}
356
357impl Sub<u64> for UnixNanos {
358    type Output = Self;
359
360    fn sub(self, rhs: u64) -> Self::Output {
361        Self(
362            self.0
363                .checked_sub(rhs)
364                .expect("Error subtracting with underflow"),
365        )
366    }
367}
368
369impl<T: Into<u64>> AddAssign<T> for UnixNanos {
370    fn add_assign(&mut self, other: T) {
371        let other_u64 = other.into();
372        self.0 = self
373            .0
374            .checked_add(other_u64)
375            .expect("Error adding with overflow");
376    }
377}
378
379impl<T: Into<u64>> SubAssign<T> for UnixNanos {
380    fn sub_assign(&mut self, other: T) {
381        let other_u64 = other.into();
382        self.0 = self
383            .0
384            .checked_sub(other_u64)
385            .expect("Error subtracting with underflow");
386    }
387}
388
389impl Display for UnixNanos {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        write!(f, "{}", self.0)
392    }
393}
394
395impl From<UnixNanos> for DateTime<Utc> {
396    fn from(value: UnixNanos) -> Self {
397        value.to_datetime_utc()
398    }
399}
400
401impl<'de> Deserialize<'de> for UnixNanos {
402    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
403    where
404        D: Deserializer<'de>,
405    {
406        struct UnixNanosVisitor;
407
408        impl Visitor<'_> for UnixNanosVisitor {
409            type Value = UnixNanos;
410
411            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
412                formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
413            }
414
415            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
416            where
417                E: de::Error,
418            {
419                Ok(UnixNanos(value))
420            }
421
422            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
423            where
424                E: de::Error,
425            {
426                if value < 0 {
427                    return Err(E::custom("Unix timestamp cannot be negative"));
428                }
429                Ok(UnixNanos(value as u64))
430            }
431
432            fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
433            where
434                E: de::Error,
435            {
436                if value < 0.0 {
437                    return Err(E::custom("Unix timestamp cannot be negative"));
438                }
439                // Convert from seconds to nanoseconds
440                let nanos = (value * 1_000_000_000.0).round() as u64;
441                Ok(UnixNanos(nanos))
442            }
443
444            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
445            where
446                E: de::Error,
447            {
448                UnixNanos::parse_string(value).map_err(E::custom)
449            }
450        }
451
452        deserializer.deserialize_any(UnixNanosVisitor)
453    }
454}
455
456////////////////////////////////////////////////////////////////////////////////
457// Tests
458////////////////////////////////////////////////////////////////////////////////
459#[cfg(test)]
460mod tests {
461    use chrono::{Duration, TimeZone};
462    use rstest::rstest;
463
464    use super::*;
465
466    #[rstest]
467    fn test_new() {
468        let nanos = UnixNanos::new(123);
469        assert_eq!(nanos.as_u64(), 123);
470        assert_eq!(nanos.as_i64(), 123);
471    }
472
473    #[rstest]
474    fn test_max() {
475        let nanos = UnixNanos::max();
476        assert_eq!(nanos.as_u64(), u64::MAX);
477    }
478
479    #[rstest]
480    fn test_is_zero() {
481        assert!(UnixNanos::default().is_zero());
482        assert!(!UnixNanos::max().is_zero());
483    }
484
485    #[rstest]
486    fn test_from_u64() {
487        let nanos = UnixNanos::from(123);
488        assert_eq!(nanos.as_u64(), 123);
489        assert_eq!(nanos.as_i64(), 123);
490    }
491
492    #[rstest]
493    fn test_default() {
494        let nanos = UnixNanos::default();
495        assert_eq!(nanos.as_u64(), 0);
496        assert_eq!(nanos.as_i64(), 0);
497    }
498
499    #[rstest]
500    fn test_into_from() {
501        let nanos: UnixNanos = 456.into();
502        let value: u64 = nanos.into();
503        assert_eq!(value, 456);
504    }
505
506    #[rstest]
507    #[case(0, "1970-01-01T00:00:00+00:00")]
508    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
509    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
510    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
511    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
512    fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
513        let nanos = UnixNanos::from(nanos);
514        let datetime = nanos.to_datetime_utc();
515        assert_eq!(datetime.to_rfc3339(), expected);
516    }
517
518    #[rstest]
519    #[case(0, "1970-01-01T00:00:00+00:00")]
520    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
521    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
522    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
523    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
524    fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
525        let nanos = UnixNanos::from(nanos);
526        assert_eq!(nanos.to_rfc3339(), expected);
527    }
528
529    #[rstest]
530    fn test_from_str() {
531        let nanos: UnixNanos = "123".parse().unwrap();
532        assert_eq!(nanos.as_u64(), 123);
533    }
534
535    #[rstest]
536    fn test_from_str_invalid() {
537        let result = "abc".parse::<UnixNanos>();
538        assert!(result.is_err());
539    }
540
541    #[rstest]
542    fn test_try_from_datetime_valid() {
543        use chrono::TimeZone;
544        let datetime = Utc.timestamp_opt(1_000_000_000, 0).unwrap(); // 1 billion seconds since epoch
545        let nanos = UnixNanos::from(datetime);
546        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
547    }
548
549    #[rstest]
550    fn test_eq() {
551        let nanos = UnixNanos::from(100);
552        assert_eq!(nanos, 100);
553        assert_eq!(nanos, Some(100));
554        assert_ne!(nanos, 200);
555        assert_ne!(nanos, Some(200));
556        assert_ne!(nanos, None);
557    }
558
559    #[rstest]
560    fn test_partial_cmp() {
561        let nanos = UnixNanos::from(100);
562        assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
563        assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
564        assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
565        assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
566    }
567
568    #[rstest]
569    fn test_edge_case_max_value() {
570        let nanos = UnixNanos::from(u64::MAX);
571        assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
572    }
573
574    #[rstest]
575    fn test_display() {
576        let nanos = UnixNanos::from(123);
577        assert_eq!(format!("{nanos}"), "123");
578    }
579
580    #[rstest]
581    fn test_addition() {
582        let nanos1 = UnixNanos::from(100);
583        let nanos2 = UnixNanos::from(200);
584        let result = nanos1 + nanos2;
585        assert_eq!(result.as_u64(), 300);
586    }
587
588    #[rstest]
589    fn test_add_assign() {
590        let mut nanos = UnixNanos::from(100);
591        nanos += 50_u64;
592        assert_eq!(nanos.as_u64(), 150);
593    }
594
595    #[rstest]
596    fn test_subtraction() {
597        let nanos1 = UnixNanos::from(200);
598        let nanos2 = UnixNanos::from(100);
599        let result = nanos1 - nanos2;
600        assert_eq!(result.as_u64(), 100);
601    }
602
603    #[rstest]
604    fn test_sub_assign() {
605        let mut nanos = UnixNanos::from(200);
606        nanos -= 50_u64;
607        assert_eq!(nanos.as_u64(), 150);
608    }
609
610    #[rstest]
611    #[should_panic(expected = "Error adding with overflow")]
612    fn test_overflow_add() {
613        let nanos = UnixNanos::from(u64::MAX);
614        let _ = nanos + UnixNanos::from(1); // This should panic due to overflow
615    }
616
617    #[rstest]
618    #[should_panic(expected = "Error adding with overflow")]
619    fn test_overflow_add_u64() {
620        let nanos = UnixNanos::from(u64::MAX);
621        let _ = nanos + 1_u64; // This should panic due to overflow
622    }
623
624    #[rstest]
625    #[should_panic(expected = "Error subtracting with underflow")]
626    fn test_overflow_sub() {
627        let _ = UnixNanos::default() - UnixNanos::from(1); // This should panic due to underflow
628    }
629
630    #[rstest]
631    #[should_panic(expected = "Error subtracting with underflow")]
632    fn test_overflow_sub_u64() {
633        let _ = UnixNanos::default() - 1_u64; // This should panic due to underflow
634    }
635
636    #[rstest]
637    #[case(100, 50, Some(50))]
638    #[case(1_000_000_000, 500_000_000, Some(500_000_000))]
639    #[case(u64::MAX, u64::MAX - 1, Some(1))]
640    #[case(50, 50, Some(0))]
641    #[case(50, 100, None)]
642    #[case(0, 1, None)]
643    fn test_duration_since(
644        #[case] time1: u64,
645        #[case] time2: u64,
646        #[case] expected: Option<DurationNanos>,
647    ) {
648        let nanos1 = UnixNanos::from(time1);
649        let nanos2 = UnixNanos::from(time2);
650        assert_eq!(nanos1.duration_since(&nanos2), expected);
651    }
652
653    #[rstest]
654    fn test_duration_since_same_moment() {
655        let moment = UnixNanos::from(1_707_577_123_456_789_000);
656        assert_eq!(moment.duration_since(&moment), Some(0));
657    }
658
659    #[rstest]
660    fn test_duration_since_chronological() {
661        // Create a reference time (Feb 10, 2024)
662        let earlier = Utc.with_ymd_and_hms(2024, 2, 10, 12, 0, 0).unwrap();
663
664        // Create a time 1 hour, 30 minutes, and 45 seconds later (with nanoseconds)
665        let later = earlier
666            + Duration::hours(1)
667            + Duration::minutes(30)
668            + Duration::seconds(45)
669            + Duration::nanoseconds(500_000_000);
670
671        let earlier_nanos = UnixNanos::from(earlier);
672        let later_nanos = UnixNanos::from(later);
673
674        // Calculate expected duration in nanoseconds
675        let expected_duration = 60 * 60 * 1_000_000_000 + // 1 hour
676        30 * 60 * 1_000_000_000 + // 30 minutes
677        45 * 1_000_000_000 + // 45 seconds
678        500_000_000; // 500 million nanoseconds
679
680        assert_eq!(
681            later_nanos.duration_since(&earlier_nanos),
682            Some(expected_duration)
683        );
684        assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
685    }
686
687    #[rstest]
688    fn test_duration_since_with_edge_cases() {
689        // Test with maximum value
690        let max = UnixNanos::from(u64::MAX);
691        let smaller = UnixNanos::from(u64::MAX - 1000);
692
693        assert_eq!(max.duration_since(&smaller), Some(1000));
694        assert_eq!(smaller.duration_since(&max), None);
695
696        // Test with minimum value
697        let min = UnixNanos::default(); // Zero timestamp
698        let larger = UnixNanos::from(1000);
699
700        assert_eq!(min.duration_since(&min), Some(0));
701        assert_eq!(larger.duration_since(&min), Some(1000));
702        assert_eq!(min.duration_since(&larger), None);
703    }
704
705    #[rstest]
706    fn test_serde_json() {
707        let nanos = UnixNanos::from(123);
708        let json = serde_json::to_string(&nanos).unwrap();
709        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
710        assert_eq!(deserialized, nanos);
711    }
712
713    #[rstest]
714    fn test_serde_edge_cases() {
715        let nanos = UnixNanos::from(u64::MAX);
716        let json = serde_json::to_string(&nanos).unwrap();
717        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
718        assert_eq!(deserialized, nanos);
719    }
720
721    #[rstest]
722    #[case("123", 123)] // Integer string
723    #[case("1234.567", 1_234_567_000_000)] // Float string (seconds to nanos)
724    #[case("2024-02-10", 1_707_523_200_000_000_000)] // Simple date (midnight UTC)
725    #[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] // RFC3339 without fractions
726    #[case("2024-02-10T14:58:43.456789Z", 1_707_577_123_456_789_000)] // RFC3339 with fractions
727    fn test_from_str_formats(#[case] input: &str, #[case] expected: u64) {
728        let parsed: UnixNanos = input.parse().unwrap();
729        assert_eq!(parsed.as_u64(), expected);
730    }
731
732    #[rstest]
733    #[case("abc")] // Random string
734    #[case("not a timestamp")] // Non-timestamp string
735    #[case("2024-02-10 14:58:43")] // Space-separated format (not RFC3339)
736    fn test_from_str_invalid_formats(#[case] input: &str) {
737        let result = input.parse::<UnixNanos>();
738        assert!(result.is_err());
739    }
740
741    #[rstest]
742    fn test_from_str_integer_overflow() {
743        // One more digit than u64::MAX (20 digits) so definitely overflows
744        let input = "184467440737095516160";
745        let result = input.parse::<UnixNanos>();
746        assert!(result.is_err());
747    }
748
749    // ---------- checked / saturating arithmetic ----------
750
751    #[rstest]
752    fn test_checked_add_overflow_returns_none() {
753        let max = UnixNanos::from(u64::MAX);
754        assert_eq!(max.checked_add(1_u64), None);
755    }
756
757    #[rstest]
758    fn test_checked_sub_underflow_returns_none() {
759        let zero = UnixNanos::default();
760        assert_eq!(zero.checked_sub(1_u64), None);
761    }
762
763    #[rstest]
764    fn test_saturating_add_overflow() {
765        let max = UnixNanos::from(u64::MAX);
766        let result = max.saturating_add_ns(1_u64);
767        assert_eq!(result, UnixNanos::from(u64::MAX));
768    }
769
770    #[rstest]
771    fn test_saturating_sub_underflow() {
772        let zero = UnixNanos::default();
773        let result = zero.saturating_sub_ns(1_u64);
774        assert_eq!(result, UnixNanos::default());
775    }
776
777    #[rstest]
778    fn test_from_str_float_overflow() {
779        // Use scientific notation so we take the floating-point parsing path.
780        let input = "2e10"; // 20 billion seconds ~ 634 years (> u64::MAX nanoseconds)
781        let result = input.parse::<UnixNanos>();
782        assert!(result.is_err());
783    }
784
785    #[rstest]
786    fn test_deserialize_u64() {
787        let json = "123456789";
788        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
789        assert_eq!(deserialized.as_u64(), 123_456_789);
790    }
791
792    #[rstest]
793    fn test_deserialize_string_with_int() {
794        let json = "\"123456789\"";
795        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
796        assert_eq!(deserialized.as_u64(), 123_456_789);
797    }
798
799    #[rstest]
800    fn test_deserialize_float() {
801        let json = "1234.567";
802        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
803        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
804    }
805
806    #[rstest]
807    fn test_deserialize_string_with_float() {
808        let json = "\"1234.567\"";
809        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
810        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
811    }
812
813    #[rstest]
814    #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
815    #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
816    fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
817        let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
818        assert_eq!(deserialized.as_u64(), expected);
819    }
820
821    #[rstest]
822    fn test_deserialize_negative_int_fails() {
823        let json = "-123456789";
824        let result: Result<UnixNanos, _> = serde_json::from_str(json);
825        assert!(result.is_err());
826    }
827
828    #[rstest]
829    fn test_deserialize_negative_float_fails() {
830        let json = "-1234.567";
831        let result: Result<UnixNanos, _> = serde_json::from_str(json);
832        assert!(result.is_err());
833    }
834
835    #[rstest]
836    fn test_deserialize_invalid_string_fails() {
837        let json = "\"not a timestamp\"";
838        let result: Result<UnixNanos, _> = serde_json::from_str(json);
839        assert!(result.is_err());
840    }
841
842    #[rstest]
843    fn test_deserialize_edge_cases() {
844        // Test zero
845        let json = "0";
846        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
847        assert_eq!(deserialized.as_u64(), 0);
848
849        // Test large value
850        let json = "18446744073709551615"; // u64::MAX
851        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
852        assert_eq!(deserialized.as_u64(), u64::MAX);
853    }
854
855    #[rstest]
856    #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
857    fn test_as_i64_overflow_panics() {
858        let nanos = UnixNanos::from(u64::MAX);
859        let _ = nanos.as_i64(); // Should panic
860    }
861}