Skip to main content

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//! Nanosecond timestamp and duration types.
17//!
18//! [`UnixNanos`] represents a timestamp since the UNIX epoch, while [`DurationNanos`]
19//! represents an unsigned elapsed duration. Timestamp differences produce durations, and
20//! timestamps accept durations for arithmetic so the two concepts cannot be mixed implicitly.
21//!
22//! # Features
23//!
24//! - Zero-cost abstraction with appropriate operator implementations.
25//! - Conversion to/from `Timestamp` and [`std::time::Duration`].
26//! - RFC 3339 string formatting.
27//! - Duration calculations.
28//! - Flexible parsing and serialization.
29//!
30//! # Parsing and Serialization
31//!
32//! `UnixNanos` can be created from and serialized to various formats:
33//!
34//! - Integer values are interpreted as nanoseconds since the UNIX epoch.
35//! - Floating-point values are interpreted as seconds since the UNIX epoch (converted to nanoseconds
36//!   using truncation, not rounding, for consistency with [`secs_to_nanos`](crate::datetime::secs_to_nanos)).
37//! - String values may be:
38//!   - A numeric string (interpreted as nanoseconds).
39//!   - A floating-point string (interpreted as seconds, converted to nanoseconds).
40//!   - An RFC 3339 formatted timestamp (ISO 8601 with timezone).
41//!   - A simple date string in YYYY-MM-DD format (interpreted as midnight UTC on that date).
42//!
43//! # Limitations
44//!
45//! - Negative timestamps are invalid and will result in an error.
46//! - Arithmetic operations will panic on overflow/underflow rather than wrapping.
47//! - The `as_i64()` method will panic for timestamps beyond approximately year 2262
48//!   (when nanoseconds exceed `i64::MAX`).
49
50use std::{
51    cmp::Ordering,
52    fmt::Display,
53    ops::{Add, AddAssign, Deref, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
54    str::FromStr,
55    time::{Duration, SystemTime},
56};
57
58use jiff::{SignedDuration, Timestamp, civil::Date, tz::Offset};
59use serde::{
60    Deserialize, Deserializer, Serialize,
61    de::{self, Visitor},
62};
63use thiserror::Error;
64
65use crate::datetime::{
66    NANOSECONDS_IN_DAY, NANOSECONDS_IN_MICROSECOND, NANOSECONDS_IN_MILLISECOND,
67    NANOSECONDS_IN_MINUTE, NANOSECONDS_IN_SECOND, SECONDS_IN_HOUR, U64_UPPER_BOUND_F64,
68};
69
70/// Represents an unsigned duration in nanoseconds.
71#[repr(transparent)]
72#[derive(
73    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
74)]
75#[serde(transparent)]
76pub struct DurationNanos(u64);
77
78/// Error returned when a duration cannot be represented as nanoseconds.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
80#[error("duration {value} {unit} exceeds the nanosecond range")]
81pub struct DurationNanosOutOfRangeError {
82    value: u64,
83    unit: &'static str,
84}
85
86impl DurationNanos {
87    /// A duration of zero nanoseconds.
88    pub const ZERO: Self = Self(0);
89
90    /// The maximum duration representable by this type.
91    pub const MAX: Self = Self(u64::MAX);
92
93    /// Creates a duration from an exact nanosecond count.
94    #[must_use]
95    pub const fn new(nanos: u64) -> Self {
96        Self(nanos)
97    }
98
99    /// Creates a duration from a number of whole microseconds.
100    ///
101    /// # Panics
102    ///
103    /// Panics if the result exceeds [`DurationNanos::MAX`].
104    #[must_use]
105    pub const fn from_micros(micros: u64) -> Self {
106        match Self::try_from_micros(micros) {
107            Ok(duration) => duration,
108            Err(_) => panic!("DurationNanos overflow in from_micros"),
109        }
110    }
111
112    /// Creates a duration from a number of whole microseconds.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
117    pub const fn try_from_micros(micros: u64) -> Result<Self, DurationNanosOutOfRangeError> {
118        Self::try_from_units(micros, NANOSECONDS_IN_MICROSECOND, "microseconds")
119    }
120
121    /// Creates a duration from a number of whole milliseconds.
122    ///
123    /// # Panics
124    ///
125    /// Panics if the result exceeds [`DurationNanos::MAX`].
126    #[must_use]
127    pub const fn from_millis(millis: u64) -> Self {
128        match Self::try_from_millis(millis) {
129            Ok(duration) => duration,
130            Err(_) => panic!("DurationNanos overflow in from_millis"),
131        }
132    }
133
134    /// Creates a duration from a number of whole milliseconds.
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
139    pub const fn try_from_millis(millis: u64) -> Result<Self, DurationNanosOutOfRangeError> {
140        Self::try_from_units(millis, NANOSECONDS_IN_MILLISECOND, "milliseconds")
141    }
142
143    /// Creates a duration from a number of whole seconds.
144    ///
145    /// # Panics
146    ///
147    /// Panics if the result exceeds [`DurationNanos::MAX`].
148    #[must_use]
149    pub const fn from_secs(secs: u64) -> Self {
150        match Self::try_from_secs(secs) {
151            Ok(duration) => duration,
152            Err(_) => panic!("DurationNanos overflow in from_secs"),
153        }
154    }
155
156    /// Creates a duration from a number of whole seconds.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
161    pub const fn try_from_secs(secs: u64) -> Result<Self, DurationNanosOutOfRangeError> {
162        Self::try_from_units(secs, NANOSECONDS_IN_SECOND, "seconds")
163    }
164
165    /// Creates a duration from a number of whole minutes.
166    ///
167    /// # Panics
168    ///
169    /// Panics if the result exceeds [`DurationNanos::MAX`].
170    #[must_use]
171    pub const fn from_mins(mins: u64) -> Self {
172        match Self::try_from_mins(mins) {
173            Ok(duration) => duration,
174            Err(_) => panic!("DurationNanos overflow in from_mins"),
175        }
176    }
177
178    /// Creates a duration from a number of whole minutes.
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
183    pub const fn try_from_mins(mins: u64) -> Result<Self, DurationNanosOutOfRangeError> {
184        Self::try_from_units(mins, NANOSECONDS_IN_MINUTE, "minutes")
185    }
186
187    /// Creates a duration from a number of whole hours.
188    ///
189    /// # Panics
190    ///
191    /// Panics if the result exceeds [`DurationNanos::MAX`].
192    #[must_use]
193    pub const fn from_hours(hours: u64) -> Self {
194        match Self::try_from_hours(hours) {
195            Ok(duration) => duration,
196            Err(_) => panic!("DurationNanos overflow in from_hours"),
197        }
198    }
199
200    /// Creates a duration from a number of whole hours.
201    ///
202    /// # Errors
203    ///
204    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
205    pub const fn try_from_hours(hours: u64) -> Result<Self, DurationNanosOutOfRangeError> {
206        Self::try_from_units(hours, SECONDS_IN_HOUR * NANOSECONDS_IN_SECOND, "hours")
207    }
208
209    /// Creates a duration from a number of whole days.
210    ///
211    /// # Panics
212    ///
213    /// Panics if the result exceeds [`DurationNanos::MAX`].
214    #[must_use]
215    pub const fn from_days(days: u64) -> Self {
216        match Self::try_from_days(days) {
217            Ok(duration) => duration,
218            Err(_) => panic!("DurationNanos overflow in from_days"),
219        }
220    }
221
222    /// Creates a duration from a number of whole days.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if the result exceeds [`DurationNanos::MAX`].
227    pub const fn try_from_days(days: u64) -> Result<Self, DurationNanosOutOfRangeError> {
228        Self::try_from_units(days, NANOSECONDS_IN_DAY, "days")
229    }
230
231    const fn try_from_units(
232        value: u64,
233        nanos_per_unit: u64,
234        unit: &'static str,
235    ) -> Result<Self, DurationNanosOutOfRangeError> {
236        match value.checked_mul(nanos_per_unit) {
237            Some(nanos) => Ok(Self(nanos)),
238            None => Err(DurationNanosOutOfRangeError { value, unit }),
239        }
240    }
241
242    /// Returns `true` if the duration is zero.
243    #[must_use]
244    pub const fn is_zero(&self) -> bool {
245        self.0 == 0
246    }
247
248    /// Returns the exact duration in nanoseconds as `u64`.
249    #[must_use]
250    pub const fn as_u64(&self) -> u64 {
251        self.0
252    }
253
254    /// Returns the total duration in whole microseconds.
255    #[must_use]
256    pub const fn as_micros(&self) -> u64 {
257        self.0 / NANOSECONDS_IN_MICROSECOND
258    }
259
260    /// Returns the total duration in whole milliseconds.
261    #[must_use]
262    pub const fn as_millis(&self) -> u64 {
263        self.0 / NANOSECONDS_IN_MILLISECOND
264    }
265
266    /// Returns the total duration in whole seconds.
267    #[must_use]
268    pub const fn as_secs(&self) -> u64 {
269        self.0 / NANOSECONDS_IN_SECOND
270    }
271
272    /// Returns the total duration in seconds as `f64`.
273    #[must_use]
274    #[expect(
275        clippy::cast_precision_loss,
276        reason = "subnanosecond precision is unavailable and large durations may lose precision"
277    )]
278    pub const fn as_secs_f64(&self) -> f64 {
279        self.as_secs() as f64 + self.subsec_nanos() as f64 / NANOSECONDS_IN_SECOND as f64
280    }
281
282    /// Returns the total duration in whole minutes.
283    #[must_use]
284    pub const fn as_mins(&self) -> u64 {
285        self.0 / NANOSECONDS_IN_MINUTE
286    }
287
288    /// Returns the total duration in whole hours.
289    #[must_use]
290    pub const fn as_hours(&self) -> u64 {
291        self.0 / (SECONDS_IN_HOUR * NANOSECONDS_IN_SECOND)
292    }
293
294    /// Returns the total duration in whole 24-hour days.
295    #[must_use]
296    pub const fn as_days(&self) -> u64 {
297        self.0 / NANOSECONDS_IN_DAY
298    }
299
300    /// Returns the fractional part of this duration in whole milliseconds.
301    #[must_use]
302    pub const fn subsec_millis(&self) -> u64 {
303        (self.0 % NANOSECONDS_IN_SECOND) / NANOSECONDS_IN_MILLISECOND
304    }
305
306    /// Returns the fractional part of this duration in whole microseconds.
307    #[must_use]
308    pub const fn subsec_micros(&self) -> u64 {
309        (self.0 % NANOSECONDS_IN_SECOND) / NANOSECONDS_IN_MICROSECOND
310    }
311
312    /// Returns the fractional part of this duration in nanoseconds.
313    #[must_use]
314    pub const fn subsec_nanos(&self) -> u64 {
315        self.0 % NANOSECONDS_IN_SECOND
316    }
317
318    /// Returns `Some(self + rhs)` or `None` if the addition would overflow.
319    #[must_use]
320    pub const fn checked_add(self, rhs: Self) -> Option<Self> {
321        match self.0.checked_add(rhs.0) {
322            Some(value) => Some(Self(value)),
323            None => None,
324        }
325    }
326
327    /// Returns `Some(self - rhs)` or `None` if the subtraction would underflow.
328    #[must_use]
329    pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
330        match self.0.checked_sub(rhs.0) {
331            Some(value) => Some(Self(value)),
332            None => None,
333        }
334    }
335
336    /// Adds `rhs`, saturating at [`DurationNanos::MAX`].
337    #[must_use]
338    pub const fn saturating_add(self, rhs: Self) -> Self {
339        Self(self.0.saturating_add(rhs.0))
340    }
341
342    /// Subtracts `rhs`, saturating at zero.
343    #[must_use]
344    pub const fn saturating_sub(self, rhs: Self) -> Self {
345        Self(self.0.saturating_sub(rhs.0))
346    }
347
348    /// Returns `Some(self * rhs)` or `None` if the multiplication would overflow.
349    #[must_use]
350    pub const fn checked_mul(self, rhs: u64) -> Option<Self> {
351        match self.0.checked_mul(rhs) {
352            Some(value) => Some(Self(value)),
353            None => None,
354        }
355    }
356
357    /// Multiplies by `rhs`, saturating at [`DurationNanos::MAX`].
358    #[must_use]
359    pub const fn saturating_mul(self, rhs: u64) -> Self {
360        Self(self.0.saturating_mul(rhs))
361    }
362
363    /// Returns `Some(self / rhs)` or `None` if `rhs` is zero.
364    #[must_use]
365    pub const fn checked_div(self, rhs: u64) -> Option<Self> {
366        match self.0.checked_div(rhs) {
367            Some(value) => Some(Self(value)),
368            None => None,
369        }
370    }
371}
372
373/// Represents a timestamp in nanoseconds since the UNIX epoch.
374#[repr(C)]
375#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
376pub struct UnixNanos(u64);
377
378impl UnixNanos {
379    /// Creates a new [`UnixNanos`] instance.
380    #[must_use]
381    pub const fn new(value: u64) -> Self {
382        Self(value)
383    }
384
385    /// Creates a new [`UnixNanos`] instance with the maximum valid value.
386    #[must_use]
387    pub const fn max() -> Self {
388        Self(u64::MAX)
389    }
390
391    /// Returns `true` if the value of this instance is zero.
392    #[must_use]
393    pub const fn is_zero(&self) -> bool {
394        self.0 == 0
395    }
396
397    /// Returns the underlying value as `u64`.
398    #[must_use]
399    pub const fn as_u64(&self) -> u64 {
400        self.0
401    }
402
403    /// Returns the timestamp as seconds, truncating sub-second precision.
404    #[must_use]
405    pub const fn as_seconds(&self) -> u64 {
406        self.0 / NANOSECONDS_IN_SECOND
407    }
408
409    /// Returns the timestamp as milliseconds, truncating sub-millisecond precision.
410    #[must_use]
411    pub const fn as_millis(&self) -> u64 {
412        self.0 / NANOSECONDS_IN_MILLISECOND
413    }
414
415    /// Returns the timestamp as microseconds, truncating sub-microsecond precision.
416    #[must_use]
417    pub const fn as_micros(&self) -> u64 {
418        self.0 / NANOSECONDS_IN_MICROSECOND
419    }
420
421    /// Creates a new [`UnixNanos`] from a second timestamp.
422    ///
423    /// # Panics
424    ///
425    /// Panics if the result overflows `u64`.
426    #[must_use]
427    pub const fn from_seconds(seconds: u64) -> Self {
428        match seconds.checked_mul(NANOSECONDS_IN_SECOND) {
429            Some(nanos) => Self(nanos),
430            None => panic!("UnixNanos overflow in from_seconds"),
431        }
432    }
433
434    /// Creates a new [`UnixNanos`] from a millisecond timestamp.
435    ///
436    /// # Panics
437    ///
438    /// Panics if the result overflows `u64`.
439    #[must_use]
440    pub const fn from_millis(millis: u64) -> Self {
441        match millis.checked_mul(NANOSECONDS_IN_MILLISECOND) {
442            Some(nanos) => Self(nanos),
443            None => panic!("UnixNanos overflow in from_millis"),
444        }
445    }
446
447    /// Creates a new [`UnixNanos`] from a signed millisecond timestamp.
448    ///
449    /// Returns `None` if `millis` is negative or the result overflows `u64`.
450    #[must_use]
451    pub const fn from_millis_checked(millis: i64) -> Option<Self> {
452        Self::from_units_checked(millis, NANOSECONDS_IN_MILLISECOND)
453    }
454
455    /// Creates a new [`UnixNanos`] from a microsecond timestamp.
456    ///
457    /// # Panics
458    ///
459    /// Panics if the result overflows `u64`.
460    #[must_use]
461    pub const fn from_micros(micros: u64) -> Self {
462        match micros.checked_mul(NANOSECONDS_IN_MICROSECOND) {
463            Some(nanos) => Self(nanos),
464            None => panic!("UnixNanos overflow in from_micros"),
465        }
466    }
467
468    /// Creates a new [`UnixNanos`] from a signed microsecond timestamp.
469    ///
470    /// Returns `None` if `micros` is negative or the result overflows `u64`.
471    #[must_use]
472    pub const fn from_micros_checked(micros: i64) -> Option<Self> {
473        Self::from_units_checked(micros, NANOSECONDS_IN_MICROSECOND)
474    }
475
476    const fn from_units_checked(value: i64, nanos_per_unit: u64) -> Option<Self> {
477        if value < 0 {
478            return None;
479        }
480
481        match value.cast_unsigned().checked_mul(nanos_per_unit) {
482            Some(nanos) => Some(Self(nanos)),
483            None => None,
484        }
485    }
486
487    /// Returns the underlying value as `i64`.
488    ///
489    /// # Panics
490    ///
491    /// Panics if the value exceeds `i64::MAX` (approximately year 2262).
492    #[must_use]
493    pub const fn as_i64(&self) -> i64 {
494        assert!(
495            self.0 <= i64::MAX.cast_unsigned(),
496            "UnixNanos value exceeds i64::MAX"
497        );
498        self.0.cast_signed()
499    }
500
501    /// Returns the underlying value as `f64`.
502    #[must_use]
503    #[expect(
504        clippy::cast_precision_loss,
505        reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
506    )]
507    pub const fn as_f64(&self) -> f64 {
508        self.0 as f64
509    }
510
511    /// Converts the underlying value to a datetime (UTC).
512    ///
513    /// # Panics
514    ///
515    /// Panics if Jiff's supported timestamp range no longer includes all `u64` nanosecond values.
516    #[must_use]
517    pub fn to_datetime_utc(&self) -> Timestamp {
518        Timestamp::from_nanosecond(i128::from(self.0))
519            .expect("UnixNanos is within Jiff's timestamp range")
520    }
521
522    /// Converts the underlying value to an ISO 8601 (RFC 3339) string.
523    #[must_use]
524    pub fn to_rfc3339(&self) -> String {
525        let datetime = self.to_datetime_utc();
526        let display = datetime.display_with_offset(Offset::UTC);
527
528        match datetime.subsec_nanosecond() {
529            0 => format!("{display:.0}"),
530            nanos if nanos % 1_000_000 == 0 => format!("{display:.3}"),
531            nanos if nanos % 1_000 == 0 => format!("{display:.6}"),
532            _ => format!("{display:.9}"),
533        }
534    }
535
536    /// Calculates the duration in nanoseconds since another [`UnixNanos`] instance.
537    ///
538    /// Returns `Some(duration)` if `self` is later than `other`, otherwise `None` if `other` is
539    /// greater than `self` (indicating a negative duration is not possible with `DurationNanos`).
540    #[must_use]
541    pub const fn duration_since(&self, other: &Self) -> Option<DurationNanos> {
542        match self.0.checked_sub(other.0) {
543            Some(duration) => Some(DurationNanos(duration)),
544            None => None,
545        }
546    }
547
548    /// Calculates the duration in nanoseconds since `earlier`, saturating at zero.
549    #[must_use]
550    pub const fn saturating_duration_since(&self, earlier: Self) -> DurationNanos {
551        DurationNanos(self.0.saturating_sub(earlier.0))
552    }
553
554    /// Rounds this timestamp down to the nearest multiple of `interval` since the UNIX epoch.
555    ///
556    /// # Panics
557    ///
558    /// Panics if `interval` is zero.
559    #[must_use]
560    pub const fn floor(self, interval: DurationNanos) -> Self {
561        assert!(
562            !interval.is_zero(),
563            "cannot floor UnixNanos to a zero interval"
564        );
565        Self(self.0 - self.0 % interval.0)
566    }
567
568    fn parse_string(s: &str) -> Result<Self, String> {
569        // Try parsing as an integer (nanoseconds)
570        if (16..=19).contains(&s.len())
571            && let Some(value) = parse_ascii_digits(s.as_bytes())
572        {
573            return Ok(Self(value));
574        }
575
576        if let Ok(int_value) = s.parse::<u64>() {
577            return Ok(Self(int_value));
578        }
579
580        // If the string is composed solely of digits but didn't fit in a u64 we
581        // treat that as an overflow error rather than attempting to interpret
582        // it as seconds in floating-point form. This avoids the surprising
583        // situation where a caller provides nanoseconds but gets an out-of-
584        // range float interpretation instead.
585        if s.chars().all(|c| c.is_ascii_digit()) {
586            return Err("Unix timestamp is out of range".into());
587        }
588
589        // Try parsing as a floating point number (seconds)
590        if let Ok(float_value) = s.parse::<f64>() {
591            return f64_seconds_to_nanos(float_value).map(Self);
592        }
593
594        // The legacy parser accepted upper/lowercase `T` and a space separator, but not RFC 9557
595        // annotations. Preserve that input contract instead of adopting Jiff's broader grammar.
596        let is_compatible_rfc3339 = matches!(s.as_bytes().get(10), Some(b'T' | b't' | b' '))
597            && !s.as_bytes().contains(&b'[');
598        if is_compatible_rfc3339 && let Ok(datetime) = s.parse::<Timestamp>() {
599            let nanos = datetime.as_nanosecond();
600            let nanos = u64::try_from(nanos)
601                .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
602            return Ok(Self(nanos));
603        }
604
605        // The legacy `%Y-%m-%d` parser accepted one- or two-digit months and days.
606        if let Ok(date) = Date::strptime("%Y-%m-%d", s) {
607            let datetime = date
608                .at(0, 0, 0, 0)
609                .to_zoned(jiff::tz::TimeZone::UTC)
610                .map_err(|e| e.to_string())?;
611            let nanos = datetime.timestamp().as_nanosecond();
612            let nanos = u64::try_from(nanos)
613                .map_err(|_| "Unix timestamp cannot be negative".to_string())?;
614            return Ok(Self(nanos));
615        }
616
617        Err(format!("Invalid format: {s}"))
618    }
619
620    /// Returns `Some(self + rhs)` or `None` if the addition would overflow
621    #[must_use]
622    pub const fn checked_add(self, rhs: DurationNanos) -> Option<Self> {
623        match self.0.checked_add(rhs.0) {
624            Some(value) => Some(Self(value)),
625            None => None,
626        }
627    }
628
629    /// Returns `Some(self - rhs)` or `None` if the subtraction would underflow
630    #[must_use]
631    pub const fn checked_sub(self, rhs: DurationNanos) -> Option<Self> {
632        match self.0.checked_sub(rhs.0) {
633            Some(value) => Some(Self(value)),
634            None => None,
635        }
636    }
637
638    /// Adds `rhs`, saturating at [`UnixNanos::max`].
639    #[must_use]
640    pub const fn saturating_add(self, rhs: DurationNanos) -> Self {
641        Self(self.0.saturating_add(rhs.0))
642    }
643
644    /// Subtracts `rhs`, saturating at zero.
645    #[must_use]
646    pub const fn saturating_sub(self, rhs: DurationNanos) -> Self {
647        Self(self.0.saturating_sub(rhs.0))
648    }
649}
650
651// Converts non-negative float seconds to nanoseconds, truncating (not rounding)
652// sub-nanosecond precision for consistency with `datetime::secs_to_nanos`.
653#[expect(
654    clippy::cast_possible_truncation,
655    clippy::cast_sign_loss,
656    reason = "value is checked finite, non-negative, and within u64 range before the cast"
657)]
658fn f64_seconds_to_nanos(value: f64) -> Result<u64, String> {
659    if !value.is_finite() {
660        return Err(format!("Unix timestamp must be finite, was {value}"));
661    }
662
663    if value < 0.0 {
664        return Err("Unix timestamp cannot be negative".to_string());
665    }
666
667    // Convert seconds to nanoseconds while checking for overflow.
668    // We perform the multiplication in `f64`, then validate the
669    // result fits inside `u64` *before* truncating / casting.
670    let nanos_f64 = value * 1_000_000_000.0;
671
672    if nanos_f64 >= U64_UPPER_BOUND_F64 {
673        return Err(format!("Unix timestamp {value} seconds is out of range"));
674    }
675
676    Ok(nanos_f64.trunc() as u64)
677}
678
679impl From<DurationNanos> for Duration {
680    fn from(value: DurationNanos) -> Self {
681        Self::from_nanos(value.0)
682    }
683}
684
685impl From<DurationNanos> for SignedDuration {
686    fn from(value: DurationNanos) -> Self {
687        Self::from_nanos_i128(i128::from(value.0))
688    }
689}
690
691impl TryFrom<SignedDuration> for DurationNanos {
692    type Error = std::num::TryFromIntError;
693
694    fn try_from(value: SignedDuration) -> Result<Self, Self::Error> {
695        u64::try_from(value.as_nanos()).map(Self)
696    }
697}
698
699impl TryFrom<Duration> for DurationNanos {
700    type Error = std::num::TryFromIntError;
701
702    fn try_from(value: Duration) -> Result<Self, Self::Error> {
703        u64::try_from(value.as_nanos()).map(Self)
704    }
705}
706
707/// Adds two [`DurationNanos`] values.
708///
709/// # Panics
710///
711/// Panics if the result exceeds [`DurationNanos::MAX`]. Use
712/// [`DurationNanos::checked_add`] or [`DurationNanos::saturating_add`] for explicit overflow
713/// handling.
714impl Add for DurationNanos {
715    type Output = Self;
716
717    fn add(self, rhs: Self) -> Self::Output {
718        self.checked_add(rhs)
719            .expect("DurationNanos overflow in addition")
720    }
721}
722
723/// Subtracts one [`DurationNanos`] value from another.
724///
725/// # Panics
726///
727/// Panics if `rhs` exceeds `self`. Use [`DurationNanos::checked_sub`] or
728/// [`DurationNanos::saturating_sub`] for explicit underflow handling.
729impl Sub for DurationNanos {
730    type Output = Self;
731
732    fn sub(self, rhs: Self) -> Self::Output {
733        self.checked_sub(rhs)
734            .expect("DurationNanos underflow in subtraction")
735    }
736}
737
738/// Add-assigns a duration.
739///
740/// # Panics
741///
742/// Panics if the result exceeds [`DurationNanos::MAX`].
743impl AddAssign for DurationNanos {
744    fn add_assign(&mut self, rhs: Self) {
745        *self = *self + rhs;
746    }
747}
748
749/// Sub-assigns a duration.
750///
751/// # Panics
752///
753/// Panics if `rhs` exceeds `self`.
754impl SubAssign for DurationNanos {
755    fn sub_assign(&mut self, rhs: Self) {
756        *self = *self - rhs;
757    }
758}
759
760/// Multiplies a duration by an unsigned scalar.
761///
762/// # Panics
763///
764/// Panics if the result exceeds [`DurationNanos::MAX`]. Use
765/// [`DurationNanos::checked_mul`] or [`DurationNanos::saturating_mul`] for explicit overflow
766/// handling.
767impl Mul<u64> for DurationNanos {
768    type Output = Self;
769
770    fn mul(self, rhs: u64) -> Self::Output {
771        self.checked_mul(rhs)
772            .expect("DurationNanos overflow in multiplication")
773    }
774}
775
776/// Multiply-assigns a duration by an unsigned scalar.
777///
778/// # Panics
779///
780/// Panics if the result exceeds [`DurationNanos::MAX`].
781impl MulAssign<u64> for DurationNanos {
782    fn mul_assign(&mut self, rhs: u64) {
783        *self = *self * rhs;
784    }
785}
786
787/// Divides a duration by an unsigned scalar.
788///
789/// # Panics
790///
791/// Panics if `rhs` is zero. Use [`DurationNanos::checked_div`] when the divisor may be zero.
792impl Div<u64> for DurationNanos {
793    type Output = Self;
794
795    fn div(self, rhs: u64) -> Self::Output {
796        self.checked_div(rhs)
797            .expect("DurationNanos division by zero")
798    }
799}
800
801/// Divide-assigns a duration by an unsigned scalar.
802///
803/// # Panics
804///
805/// Panics if `rhs` is zero.
806impl DivAssign<u64> for DurationNanos {
807    fn div_assign(&mut self, rhs: u64) {
808        *self = *self / rhs;
809    }
810}
811
812impl Display for DurationNanos {
813    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
814        write!(f, "{}", self.0)
815    }
816}
817
818impl Deref for UnixNanos {
819    type Target = u64;
820
821    fn deref(&self) -> &Self::Target {
822        &self.0
823    }
824}
825
826impl PartialEq<u64> for UnixNanos {
827    fn eq(&self, other: &u64) -> bool {
828        self.0 == *other
829    }
830}
831
832impl PartialOrd<u64> for UnixNanos {
833    fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
834        self.0.partial_cmp(other)
835    }
836}
837
838impl PartialEq<Option<u64>> for UnixNanos {
839    fn eq(&self, other: &Option<u64>) -> bool {
840        match other {
841            Some(value) => self.0 == *value,
842            None => false,
843        }
844    }
845}
846
847impl PartialOrd<Option<u64>> for UnixNanos {
848    fn partial_cmp(&self, other: &Option<u64>) -> Option<Ordering> {
849        match other {
850            Some(value) => self.0.partial_cmp(value),
851            None => Some(Ordering::Greater),
852        }
853    }
854}
855
856impl PartialEq<UnixNanos> for u64 {
857    fn eq(&self, other: &UnixNanos) -> bool {
858        *self == other.0
859    }
860}
861
862impl PartialOrd<UnixNanos> for u64 {
863    fn partial_cmp(&self, other: &UnixNanos) -> Option<Ordering> {
864        self.partial_cmp(&other.0)
865    }
866}
867
868impl From<u64> for UnixNanos {
869    fn from(value: u64) -> Self {
870        Self(value)
871    }
872}
873
874impl From<UnixNanos> for u64 {
875    fn from(value: UnixNanos) -> Self {
876        value.0
877    }
878}
879
880/// Converts a string slice to [`UnixNanos`].
881///
882/// # Panics
883///
884/// Panics if the string cannot be parsed into a valid [`UnixNanos`]. This is intentional fail-fast
885/// behavior where invalid timestamps indicate a critical logic error that should halt execution
886/// rather than silently propagate incorrect data.
887///
888/// For error handling without panicking, use [`str::parse::<UnixNanos>()`] which returns
889/// a [`Result`].
890impl From<&str> for UnixNanos {
891    fn from(value: &str) -> Self {
892        value
893            .parse()
894            .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
895    }
896}
897
898/// Converts a [`String`] to [`UnixNanos`].
899///
900/// # Panics
901///
902/// Panics if the string cannot be parsed into a valid [`UnixNanos`]. This is intentional fail-fast
903/// behavior where invalid timestamps indicate a critical logic error that should halt execution
904/// rather than silently propagate incorrect data.
905///
906/// For error handling without panicking, use [`str::parse::<UnixNanos>()`] which returns
907/// a [`Result`].
908impl From<String> for UnixNanos {
909    fn from(value: String) -> Self {
910        value
911            .parse()
912            .unwrap_or_else(|e| panic!("Failed to parse string '{value}' into UnixNanos: {e}. Use str::parse() for non-panicking error handling."))
913    }
914}
915
916impl From<Timestamp> for UnixNanos {
917    fn from(value: Timestamp) -> Self {
918        let nanos = value.as_nanosecond();
919
920        assert!(nanos >= 0, "DateTime timestamp cannot be negative: {nanos}");
921
922        Self::from(u64::try_from(nanos).expect("DateTime timestamp out of range for UnixNanos"))
923    }
924}
925
926impl From<SystemTime> for UnixNanos {
927    fn from(value: SystemTime) -> Self {
928        let duration = value
929            .duration_since(std::time::UNIX_EPOCH)
930            .expect("SystemTime before UNIX EPOCH");
931
932        let nanos =
933            u64::try_from(duration.as_nanos()).expect("SystemTime overflowed u64 nanoseconds");
934
935        Self::from(nanos)
936    }
937}
938
939impl FromStr for UnixNanos {
940    type Err = Box<dyn std::error::Error>;
941
942    fn from_str(s: &str) -> Result<Self, Self::Err> {
943        Self::parse_string(s).map_err(std::convert::Into::into)
944    }
945}
946
947/// Returns the elapsed duration between two timestamps.
948///
949/// # Panics
950///
951/// Panics if `rhs` is later than `self`. Use [`UnixNanos::duration_since`] or
952/// [`UnixNanos::saturating_duration_since`] when the timestamps may be out of order.
953impl Sub for UnixNanos {
954    type Output = DurationNanos;
955
956    fn sub(self, rhs: Self) -> Self::Output {
957        self.duration_since(&rhs)
958            .expect("UnixNanos underflow in timestamp subtraction")
959    }
960}
961
962/// Adds a duration to a timestamp.
963///
964/// # Panics
965///
966/// Panics if the operation overflows. Use [`UnixNanos::checked_add`] or
967/// [`UnixNanos::saturating_add`] for explicit overflow handling.
968impl Add<DurationNanos> for UnixNanos {
969    type Output = Self;
970
971    fn add(self, rhs: DurationNanos) -> Self::Output {
972        self.checked_add(rhs)
973            .expect("UnixNanos overflow in duration addition")
974    }
975}
976
977/// Subtracts a duration from a timestamp.
978///
979/// # Panics
980///
981/// Panics if the operation underflows. Use [`UnixNanos::checked_sub`] or
982/// [`UnixNanos::saturating_sub`] for explicit underflow handling.
983impl Sub<DurationNanos> for UnixNanos {
984    type Output = Self;
985
986    fn sub(self, rhs: DurationNanos) -> Self::Output {
987        self.checked_sub(rhs)
988            .expect("UnixNanos underflow in duration subtraction")
989    }
990}
991
992/// Add-assigns a duration to [`UnixNanos`].
993///
994/// # Panics
995///
996/// Panics if the operation overflows. This is intentional fail-fast behavior for timestamp
997/// arithmetic.
998impl AddAssign<DurationNanos> for UnixNanos {
999    fn add_assign(&mut self, rhs: DurationNanos) {
1000        *self = *self + rhs;
1001    }
1002}
1003
1004/// Sub-assigns a duration from [`UnixNanos`].
1005///
1006/// # Panics
1007///
1008/// Panics if the operation underflows. This is intentional fail-fast behavior for timestamp
1009/// arithmetic.
1010impl SubAssign<DurationNanos> for UnixNanos {
1011    fn sub_assign(&mut self, rhs: DurationNanos) {
1012        *self = *self - rhs;
1013    }
1014}
1015
1016impl Display for UnixNanos {
1017    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1018        write!(f, "{}", self.0)
1019    }
1020}
1021
1022impl From<UnixNanos> for Timestamp {
1023    fn from(value: UnixNanos) -> Self {
1024        value.to_datetime_utc()
1025    }
1026}
1027
1028impl<'de> Deserialize<'de> for UnixNanos {
1029    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1030    where
1031        D: Deserializer<'de>,
1032    {
1033        struct UnixNanosVisitor;
1034
1035        impl<'de> Visitor<'de> for UnixNanosVisitor {
1036            type Value = UnixNanos;
1037
1038            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1039                formatter.write_str("an integer, a string integer, or an RFC 3339 timestamp")
1040            }
1041
1042            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
1043            where
1044                E: de::Error,
1045            {
1046                Ok(UnixNanos(value))
1047            }
1048
1049            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
1050            where
1051                E: de::Error,
1052            {
1053                u64::try_from(value)
1054                    .map(UnixNanos)
1055                    .map_err(|_| E::custom("Unix timestamp cannot be negative"))
1056            }
1057
1058            fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1059            where
1060                E: de::Error,
1061            {
1062                f64_seconds_to_nanos(value)
1063                    .map(UnixNanos)
1064                    .map_err(E::custom)
1065            }
1066
1067            fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
1068                let number =
1069                    serde_json::Number::deserialize(de::value::MapAccessDeserializer::new(map))?;
1070
1071                if let Some(value) = number.as_u64() {
1072                    self.visit_u64(value)
1073                } else if let Some(value) = number.as_i64() {
1074                    self.visit_i64(value)
1075                } else {
1076                    let value = number
1077                        .as_f64()
1078                        .ok_or_else(|| de::Error::custom("Unix timestamp is outside f64 bounds"))?;
1079                    self.visit_f64(value)
1080                }
1081            }
1082
1083            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1084            where
1085                E: de::Error,
1086            {
1087                UnixNanos::parse_string(value).map_err(E::custom)
1088            }
1089        }
1090
1091        deserializer.deserialize_any(UnixNanosVisitor)
1092    }
1093}
1094
1095// Callers bound the length to at most 19 bytes; ASCII digit validation bounds the value to 10^19 - 1.
1096fn parse_ascii_digits(bytes: &[u8]) -> Option<u64> {
1097    const HIGH_BITS: u64 = 0x8080_8080_8080_8080;
1098    const DIGIT_MASK: u64 = 0x00ff_00ff_00ff_00ff;
1099    const PAIR_MASK: u64 = 0x0000_ffff_0000_ffff;
1100    debug_assert!(
1101        bytes.len() <= 19,
1102        "Invariant: digit inputs have at most 19 bytes"
1103    );
1104    let (chunks, tail) = bytes.as_chunks::<8>();
1105    let mut value = 0;
1106
1107    for chunk in chunks {
1108        let word = u64::from_le_bytes(*chunk);
1109        let ascii = word & !HIGH_BITS;
1110        // Each subtraction stays within its byte lane, so bit 7 selects exactly '0'..='9'.
1111        let lower = (ascii | HIGH_BITS) - 0x3030_3030_3030_3030;
1112        let upper = 0xb9b9_b9b9_b9b9_b9b9 - ascii;
1113        if word & HIGH_BITS != 0 || lower & upper & HIGH_BITS != HIGH_BITS {
1114            return None;
1115        }
1116        let digits = word ^ 0x3030_3030_3030_3030;
1117        let pairs = (digits & DIGIT_MASK) * 10 + ((digits >> 8) & DIGIT_MASK);
1118        let quads = (pairs & PAIR_MASK) * 100 + ((pairs >> 16) & PAIR_MASK);
1119        let eight = (quads & 0xffff_ffff) * 10_000 + (quads >> 32);
1120        value = value * 100_000_000 + eight;
1121    }
1122
1123    for &digit in tail {
1124        if !digit.is_ascii_digit() {
1125            return None;
1126        }
1127        value = value * 10 + u64::from(digit - b'0');
1128    }
1129    Some(value)
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134    use jiff::SignedDuration;
1135    use proptest::prelude::*;
1136    use rstest::rstest;
1137
1138    use super::*;
1139    use crate::approx_eq;
1140
1141    fn timestamp(value: &str) -> Timestamp {
1142        value.parse().unwrap()
1143    }
1144
1145    #[rstest]
1146    fn test_digit_parser_validates_every_byte_position() {
1147        for len in 1..=19 {
1148            for position in 0..len {
1149                for byte in 0..=u8::MAX {
1150                    let mut input = vec![b'1'; len];
1151                    input[position] = byte;
1152                    let expected = std::str::from_utf8(&input)
1153                        .ok()
1154                        .filter(|text| text.bytes().all(|digit| digit.is_ascii_digit()))
1155                        .and_then(|text| text.parse::<u64>().ok());
1156                    assert_eq!(parse_ascii_digits(&input), expected);
1157                }
1158            }
1159        }
1160    }
1161
1162    #[rstest]
1163    fn test_duration_nanos_construction_and_conversion() {
1164        let duration = DurationNanos::new(123);
1165        let standard = Duration::from(duration);
1166        let signed = SignedDuration::from(duration);
1167        let signed_max = SignedDuration::from(DurationNanos::MAX);
1168
1169        assert_eq!(duration.as_u64(), 123);
1170        assert_eq!(standard, Duration::from_nanos(123));
1171        assert_eq!(DurationNanos::try_from(standard), Ok(duration));
1172        assert_eq!(signed, SignedDuration::from_nanos(123));
1173        assert_eq!(DurationNanos::try_from(signed), Ok(duration));
1174        assert_eq!(signed_max.as_nanos(), i128::from(u64::MAX));
1175        assert_eq!(DurationNanos::try_from(signed_max), Ok(DurationNanos::MAX));
1176        assert!(DurationNanos::try_from(SignedDuration::from_nanos(-1)).is_err());
1177    }
1178
1179    #[rstest]
1180    fn test_duration_nanos_unit_construction_and_accessors() {
1181        let duration = DurationNanos::from_hours(1)
1182            + DurationNanos::from_mins(2)
1183            + DurationNanos::from_secs(3)
1184            + DurationNanos::new(456_789_123);
1185
1186        assert_eq!(DurationNanos::from_micros(1), DurationNanos::new(1_000));
1187        assert_eq!(DurationNanos::from_millis(1), DurationNanos::new(1_000_000));
1188        assert_eq!(DurationNanos::from_days(1), DurationNanos::from_hours(24));
1189        assert_eq!(duration.as_micros(), 3_723_456_789);
1190        assert_eq!(duration.as_millis(), 3_723_456);
1191        assert_eq!(duration.as_secs(), 3_723);
1192        assert!(approx_eq!(
1193            f64,
1194            duration.as_secs_f64(),
1195            3_723.456_789_123,
1196            epsilon = 1e-12
1197        ));
1198        let expected_max_secs = Duration::from_nanos(u64::MAX).as_secs_f64();
1199        assert!(approx_eq!(
1200            f64,
1201            DurationNanos::MAX.as_secs_f64(),
1202            expected_max_secs,
1203            epsilon = expected_max_secs * f64::EPSILON
1204        ));
1205        assert_eq!(duration.as_mins(), 62);
1206        assert_eq!(duration.as_hours(), 1);
1207        assert_eq!(DurationNanos::from_hours(49).as_days(), 2);
1208        assert_eq!(duration.subsec_millis(), 456);
1209        assert_eq!(duration.subsec_micros(), 456_789);
1210        assert_eq!(duration.subsec_nanos(), 456_789_123);
1211    }
1212
1213    #[rstest]
1214    fn test_duration_nanos_fallible_unit_construction() {
1215        assert_eq!(
1216            DurationNanos::try_from_secs(1),
1217            Ok(DurationNanos::from_secs(1))
1218        );
1219        assert_eq!(
1220            DurationNanos::try_from_secs(u64::MAX)
1221                .unwrap_err()
1222                .to_string(),
1223            "duration 18446744073709551615 seconds exceeds the nanosecond range"
1224        );
1225        assert!(DurationNanos::try_from_micros(u64::MAX).is_err());
1226        assert!(DurationNanos::try_from_millis(u64::MAX).is_err());
1227        assert!(DurationNanos::try_from_secs(u64::MAX).is_err());
1228        assert!(DurationNanos::try_from_mins(u64::MAX).is_err());
1229        assert!(DurationNanos::try_from_hours(u64::MAX).is_err());
1230        assert!(DurationNanos::try_from_days(u64::MAX).is_err());
1231
1232        let max_hours = DurationNanos::MAX.as_hours();
1233        assert_eq!(
1234            DurationNanos::try_from_hours(max_hours),
1235            Ok(DurationNanos::from_hours(max_hours))
1236        );
1237        assert!(DurationNanos::try_from_hours(max_hours + 1).is_err());
1238
1239        let max_days = DurationNanos::MAX.as_days();
1240        assert_eq!(
1241            DurationNanos::try_from_days(max_days),
1242            Ok(DurationNanos::from_days(max_days))
1243        );
1244        assert!(DurationNanos::try_from_days(max_days + 1).is_err());
1245    }
1246
1247    #[rstest]
1248    fn test_duration_nanos_zero_and_max() {
1249        assert_eq!(DurationNanos::default(), DurationNanos::ZERO);
1250        assert!(DurationNanos::ZERO.is_zero());
1251        assert_eq!(DurationNanos::MAX.as_u64(), u64::MAX);
1252        assert!(!DurationNanos::MAX.is_zero());
1253    }
1254
1255    #[rstest]
1256    fn test_duration_nanos_layout_matches_u64() {
1257        assert_eq!(
1258            std::mem::size_of::<DurationNanos>(),
1259            std::mem::size_of::<u64>()
1260        );
1261        assert_eq!(
1262            std::mem::align_of::<DurationNanos>(),
1263            std::mem::align_of::<u64>()
1264        );
1265    }
1266
1267    #[rstest]
1268    fn test_duration_nanos_format_and_ordering() {
1269        let shorter = DurationNanos::new(123);
1270        let longer = DurationNanos::new(456);
1271
1272        assert_eq!(shorter.to_string(), "123");
1273        assert_eq!(format!("{shorter:?}"), "DurationNanos(123)");
1274        assert!(shorter < longer);
1275        assert_eq!(shorter, DurationNanos::new(123));
1276    }
1277
1278    #[rstest]
1279    fn test_duration_nanos_serde_preserves_u64_format() {
1280        let duration = DurationNanos::MAX;
1281        let json = serde_json::to_string(&duration).unwrap();
1282        let deserialized: DurationNanos = serde_json::from_str(&json).unwrap();
1283
1284        assert_eq!(json, u64::MAX.to_string());
1285        assert_eq!(deserialized, duration);
1286    }
1287
1288    #[rstest]
1289    #[case("-1")]
1290    #[case("1.5")]
1291    #[case("\"1\"")]
1292    fn test_duration_nanos_serde_rejects_non_u64_formats(#[case] json: &str) {
1293        assert!(serde_json::from_str::<DurationNanos>(json).is_err());
1294    }
1295
1296    #[rstest]
1297    fn test_duration_nanos_checked_arithmetic_boundaries() {
1298        let zero = DurationNanos::ZERO;
1299        let one = DurationNanos::new(1);
1300        let max = DurationNanos::MAX;
1301
1302        assert_eq!(zero.checked_sub(one), None);
1303        assert_eq!(max.checked_add(one), None);
1304        assert_eq!(one.checked_sub(one), Some(zero));
1305        assert_eq!(zero.checked_add(max), Some(max));
1306    }
1307
1308    #[rstest]
1309    fn test_duration_nanos_saturating_arithmetic_boundaries() {
1310        let zero = DurationNanos::ZERO;
1311        let one = DurationNanos::new(1);
1312        let max = DurationNanos::MAX;
1313
1314        assert_eq!(zero.saturating_sub(one), zero);
1315        assert_eq!(max.saturating_add(one), max);
1316        assert_eq!(max.saturating_mul(2), max);
1317    }
1318
1319    #[rstest]
1320    fn test_duration_nanos_scalar_arithmetic() {
1321        let duration = DurationNanos::new(12);
1322
1323        assert_eq!(duration.checked_mul(3), Some(DurationNanos::new(36)));
1324        assert_eq!(DurationNanos::MAX.checked_mul(2), None);
1325        assert_eq!(duration.checked_div(3), Some(DurationNanos::new(4)));
1326        assert_eq!(duration.checked_div(0), None);
1327        assert_eq!(duration * 3, DurationNanos::new(36));
1328        assert_eq!(duration / 3, DurationNanos::new(4));
1329    }
1330
1331    #[rstest]
1332    fn test_duration_nanos_assign_operators() {
1333        let mut value = DurationNanos::new(100);
1334        value += DurationNanos::new(23);
1335        assert_eq!(value, DurationNanos::new(123));
1336        value -= DurationNanos::new(23);
1337        assert_eq!(value, DurationNanos::new(100));
1338        value *= 3;
1339        assert_eq!(value, DurationNanos::new(300));
1340        value /= 4;
1341        assert_eq!(value, DurationNanos::new(75));
1342    }
1343
1344    #[rstest]
1345    #[should_panic(expected = "DurationNanos overflow in addition")]
1346    fn test_duration_nanos_addition_panics_on_overflow() {
1347        let _ = DurationNanos::MAX + DurationNanos::new(1);
1348    }
1349
1350    #[rstest]
1351    #[should_panic(expected = "DurationNanos underflow in subtraction")]
1352    fn test_duration_nanos_subtraction_panics_on_underflow() {
1353        let _ = DurationNanos::default() - DurationNanos::new(1);
1354    }
1355
1356    #[rstest]
1357    #[should_panic(expected = "DurationNanos overflow in multiplication")]
1358    fn test_duration_nanos_multiplication_panics_on_overflow() {
1359        let _ = DurationNanos::MAX * 2;
1360    }
1361
1362    #[rstest]
1363    #[should_panic(expected = "DurationNanos division by zero")]
1364    fn test_duration_nanos_division_panics_on_zero() {
1365        let _ = DurationNanos::new(1) / 0;
1366    }
1367
1368    #[rstest]
1369    #[should_panic(expected = "DurationNanos overflow in from_micros")]
1370    fn test_duration_nanos_from_micros_panics_on_overflow() {
1371        let _ = DurationNanos::from_micros(u64::MAX);
1372    }
1373
1374    #[rstest]
1375    #[should_panic(expected = "DurationNanos overflow in from_millis")]
1376    fn test_duration_nanos_from_millis_panics_on_overflow() {
1377        let _ = DurationNanos::from_millis(u64::MAX);
1378    }
1379
1380    #[rstest]
1381    #[should_panic(expected = "DurationNanos overflow in from_secs")]
1382    fn test_duration_nanos_from_secs_panics_on_overflow() {
1383        let _ = DurationNanos::from_secs(u64::MAX);
1384    }
1385
1386    #[rstest]
1387    #[should_panic(expected = "DurationNanos overflow in from_mins")]
1388    fn test_duration_nanos_from_mins_panics_on_overflow() {
1389        let _ = DurationNanos::from_mins(u64::MAX);
1390    }
1391
1392    #[rstest]
1393    #[should_panic(expected = "DurationNanos overflow in from_hours")]
1394    fn test_duration_nanos_from_hours_panics_on_overflow() {
1395        let _ = DurationNanos::from_hours(u64::MAX);
1396    }
1397
1398    #[rstest]
1399    #[should_panic(expected = "DurationNanos overflow in from_days")]
1400    fn test_duration_nanos_from_days_panics_on_overflow() {
1401        let _ = DurationNanos::from_days(u64::MAX);
1402    }
1403
1404    #[rstest]
1405    fn test_new() {
1406        let nanos = UnixNanos::new(123);
1407        assert_eq!(nanos.as_u64(), 123);
1408        assert_eq!(nanos.as_i64(), 123);
1409    }
1410
1411    #[rstest]
1412    fn test_max() {
1413        let nanos = UnixNanos::max();
1414        assert_eq!(nanos.as_u64(), u64::MAX);
1415    }
1416
1417    #[rstest]
1418    fn test_is_zero() {
1419        assert!(UnixNanos::default().is_zero());
1420        assert!(!UnixNanos::max().is_zero());
1421    }
1422
1423    #[rstest]
1424    fn test_from_u64() {
1425        let nanos = UnixNanos::from(123);
1426        assert_eq!(nanos.as_u64(), 123);
1427        assert_eq!(nanos.as_i64(), 123);
1428    }
1429
1430    #[rstest]
1431    fn test_default() {
1432        let nanos = UnixNanos::default();
1433        assert_eq!(nanos.as_u64(), 0);
1434        assert_eq!(nanos.as_i64(), 0);
1435    }
1436
1437    #[rstest]
1438    fn test_into_from() {
1439        let nanos: UnixNanos = 456.into();
1440        let value: u64 = nanos.into();
1441        assert_eq!(value, 456);
1442    }
1443
1444    #[rstest]
1445    #[case(0, "1970-01-01T00:00:00+00:00")]
1446    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
1447    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
1448    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
1449    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
1450    fn test_to_datetime_utc(#[case] nanos: u64, #[case] expected: &str) {
1451        let nanos = UnixNanos::from(nanos);
1452        let datetime = nanos.to_datetime_utc();
1453        assert_eq!(
1454            datetime.display_with_offset(Offset::UTC).to_string(),
1455            expected
1456        );
1457    }
1458
1459    #[rstest]
1460    #[case(0, "1970-01-01T00:00:00+00:00")]
1461    #[case(1_000_000_000, "1970-01-01T00:00:01+00:00")]
1462    #[case(1_000_000_000_000_000_000, "2001-09-09T01:46:40+00:00")]
1463    #[case(1_500_000_000_000_000_000, "2017-07-14T02:40:00+00:00")]
1464    #[case(1_500_000_000_500_000_000, "2017-07-14T02:40:00.500+00:00")]
1465    #[case(1_500_000_000_123_456_000, "2017-07-14T02:40:00.123456+00:00")]
1466    #[case(1_500_000_000_123_456_789, "2017-07-14T02:40:00.123456789+00:00")]
1467    #[case(1_707_577_123_456_789_000, "2024-02-10T14:58:43.456789+00:00")]
1468    fn test_to_rfc3339(#[case] nanos: u64, #[case] expected: &str) {
1469        let nanos = UnixNanos::from(nanos);
1470        assert_eq!(nanos.to_rfc3339(), expected);
1471    }
1472
1473    #[rstest]
1474    fn test_from_str() {
1475        let nanos: UnixNanos = "123".parse().unwrap();
1476        assert_eq!(nanos.as_u64(), 123);
1477    }
1478
1479    #[rstest]
1480    fn test_from_str_invalid() {
1481        let result = "abc".parse::<UnixNanos>();
1482        assert!(result.is_err());
1483    }
1484
1485    #[rstest]
1486    fn test_from_str_date() {
1487        let nanos: UnixNanos = "2024-02-10".parse().unwrap();
1488        assert_eq!(nanos.as_u64(), 1_707_523_200_000_000_000);
1489    }
1490
1491    #[rstest]
1492    fn test_from_str_pre_epoch_date() {
1493        let err = "1969-12-31".parse::<UnixNanos>().unwrap_err();
1494        assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
1495    }
1496
1497    #[rstest]
1498    fn test_from_str_pre_epoch_rfc3339() {
1499        let err = "1969-12-31T23:59:59Z".parse::<UnixNanos>().unwrap_err();
1500        assert_eq!(err.to_string(), "Unix timestamp cannot be negative");
1501    }
1502
1503    #[rstest]
1504    fn test_from_borrowed_str_and_string() {
1505        assert_eq!(UnixNanos::from("123").as_u64(), 123);
1506        assert_eq!(UnixNanos::from("123".to_string()).as_u64(), 123);
1507    }
1508
1509    #[rstest]
1510    #[should_panic(expected = "Failed to parse string")]
1511    fn test_from_borrowed_str_panics_on_invalid() {
1512        let _ = UnixNanos::from("abc");
1513    }
1514
1515    #[rstest]
1516    #[should_panic(expected = "Failed to parse string")]
1517    fn test_from_string_trait_panics_on_invalid() {
1518        let _ = UnixNanos::from("abc".to_string());
1519    }
1520
1521    #[rstest]
1522    fn test_into_timestamp() {
1523        let nanos = UnixNanos::from(1_000_000_000);
1524        let datetime = Timestamp::from(nanos);
1525        assert_eq!(datetime, timestamp("1970-01-01T00:00:01Z"));
1526        assert_eq!(UnixNanos::from(datetime), nanos);
1527    }
1528
1529    #[rstest]
1530    fn test_try_from_datetime_valid() {
1531        let datetime = Timestamp::from_second(1_000_000_000).unwrap();
1532        let nanos = UnixNanos::from(datetime);
1533        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
1534    }
1535
1536    #[rstest]
1537    fn test_from_system_time() {
1538        let system_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
1539        let nanos = UnixNanos::from(system_time);
1540        assert_eq!(nanos.as_u64(), 1_000_000_000_000_000_000);
1541    }
1542
1543    #[rstest]
1544    #[should_panic(expected = "SystemTime before UNIX EPOCH")]
1545    fn test_from_system_time_before_epoch() {
1546        let system_time = std::time::UNIX_EPOCH - std::time::Duration::from_secs(1);
1547        let _ = UnixNanos::from(system_time);
1548    }
1549
1550    #[rstest]
1551    #[should_panic(expected = "SystemTime overflowed u64 nanoseconds")]
1552    fn test_from_system_time_overflow_panics() {
1553        // One second beyond the largest whole-second duration representable in u64 nanoseconds
1554        let system_time =
1555            std::time::UNIX_EPOCH + std::time::Duration::from_secs(u64::MAX / 1_000_000_000 + 1);
1556        let _ = UnixNanos::from(system_time);
1557    }
1558
1559    #[rstest]
1560    fn test_eq() {
1561        let nanos = UnixNanos::from(100);
1562        assert_eq!(nanos, 100);
1563        assert_eq!(nanos, Some(100));
1564        assert_ne!(nanos, 200);
1565        assert_ne!(nanos, Some(200));
1566        assert_ne!(nanos, None);
1567    }
1568
1569    #[rstest]
1570    fn test_partial_cmp() {
1571        let nanos = UnixNanos::from(100);
1572        assert_eq!(nanos.partial_cmp(&100), Some(Ordering::Equal));
1573        assert_eq!(nanos.partial_cmp(&200), Some(Ordering::Less));
1574        assert_eq!(nanos.partial_cmp(&50), Some(Ordering::Greater));
1575        assert_eq!(nanos.partial_cmp(&None), Some(Ordering::Greater));
1576    }
1577
1578    #[rstest]
1579    fn test_u64_comparison_and_deref() {
1580        let nanos = UnixNanos::from(100);
1581        assert_eq!(100u64, nanos);
1582        assert_eq!(99u64.partial_cmp(&nanos), Some(Ordering::Less));
1583        assert_eq!(nanos.partial_cmp(&Some(100u64)), Some(Ordering::Equal));
1584        assert_eq!(*nanos, 100u64);
1585    }
1586
1587    #[rstest]
1588    fn test_edge_case_max_value() {
1589        let nanos = UnixNanos::from(u64::MAX);
1590        assert_eq!(format!("{nanos}"), format!("{}", u64::MAX));
1591    }
1592
1593    #[rstest]
1594    fn test_display() {
1595        let nanos = UnixNanos::from(123);
1596        assert_eq!(format!("{nanos}"), "123");
1597    }
1598
1599    #[rstest]
1600    fn test_addition() {
1601        let nanos = UnixNanos::from(100);
1602        let duration = DurationNanos::new(200);
1603        let result = nanos + duration;
1604        assert_eq!(result.as_u64(), 300);
1605    }
1606
1607    #[rstest]
1608    fn test_add_assign() {
1609        let mut nanos = UnixNanos::from(100);
1610        nanos += DurationNanos::new(50);
1611        assert_eq!(nanos.as_u64(), 150);
1612    }
1613
1614    #[rstest]
1615    fn test_subtraction() {
1616        let nanos1 = UnixNanos::from(200);
1617        let nanos2 = UnixNanos::from(100);
1618        let result = nanos1 - nanos2;
1619        assert_eq!(result, DurationNanos::new(100));
1620    }
1621
1622    #[rstest]
1623    fn test_sub_assign() {
1624        let mut nanos = UnixNanos::from(200);
1625        nanos -= DurationNanos::new(50);
1626        assert_eq!(nanos.as_u64(), 150);
1627    }
1628
1629    #[rstest]
1630    #[should_panic(expected = "UnixNanos overflow")]
1631    fn test_overflow_add() {
1632        let nanos = UnixNanos::from(u64::MAX);
1633        let _ = nanos + DurationNanos::new(1);
1634    }
1635
1636    #[rstest]
1637    #[should_panic(expected = "UnixNanos underflow")]
1638    fn test_overflow_sub() {
1639        let _ = UnixNanos::default() - DurationNanos::new(1);
1640    }
1641
1642    #[rstest]
1643    #[case(100, 50, Some(DurationNanos::new(50)))]
1644    #[case(1_000_000_000, 500_000_000, Some(DurationNanos::new(500_000_000)))]
1645    #[case(u64::MAX, u64::MAX - 1, Some(DurationNanos::new(1)))]
1646    #[case(50, 50, Some(DurationNanos::ZERO))]
1647    #[case(50, 100, None)]
1648    #[case(0, 1, None)]
1649    fn test_duration_since(
1650        #[case] time1: u64,
1651        #[case] time2: u64,
1652        #[case] expected: Option<DurationNanos>,
1653    ) {
1654        let nanos1 = UnixNanos::from(time1);
1655        let nanos2 = UnixNanos::from(time2);
1656        assert_eq!(nanos1.duration_since(&nanos2), expected);
1657    }
1658
1659    #[rstest]
1660    fn test_duration_since_same_moment() {
1661        let moment = UnixNanos::from(1_707_577_123_456_789_000);
1662        assert_eq!(
1663            moment.duration_since(&moment),
1664            Some(DurationNanos::default())
1665        );
1666    }
1667
1668    #[rstest]
1669    #[case::later(100, 50, DurationNanos::new(50))]
1670    #[case::same(50, 50, DurationNanos::ZERO)]
1671    #[case::earlier(50, 100, DurationNanos::ZERO)]
1672    #[case::full_range(u64::MAX, 0, DurationNanos::new(u64::MAX))]
1673    fn test_saturating_duration_since(
1674        #[case] time: u64,
1675        #[case] earlier: u64,
1676        #[case] expected: DurationNanos,
1677    ) {
1678        assert_eq!(
1679            UnixNanos::from(time).saturating_duration_since(UnixNanos::from(earlier)),
1680            expected
1681        );
1682    }
1683
1684    #[rstest]
1685    #[case(100, 30, 90)]
1686    #[case(90, 30, 90)]
1687    #[case(100, 101, 0)]
1688    #[case(u64::MAX, u64::MAX, u64::MAX)]
1689    #[case(u64::MAX, 1_000_000_000, 18_446_744_073_000_000_000)]
1690    fn test_floor(#[case] time: u64, #[case] interval: u64, #[case] expected: u64) {
1691        assert_eq!(
1692            UnixNanos::new(time).floor(DurationNanos::new(interval)),
1693            UnixNanos::new(expected)
1694        );
1695    }
1696
1697    #[rstest]
1698    #[should_panic(expected = "cannot floor UnixNanos to a zero interval")]
1699    fn test_floor_panics_for_zero_interval() {
1700        let _ = UnixNanos::default().floor(DurationNanos::ZERO);
1701    }
1702
1703    #[rstest]
1704    fn test_duration_since_chronological() {
1705        // Create a reference time (Feb 10, 2024)
1706        let earlier = timestamp("2024-02-10T12:00:00Z");
1707
1708        // Create a time 1 hour, 30 minutes, and 45 seconds later (with nanoseconds)
1709        let later = earlier
1710            + SignedDuration::from_hours(1)
1711            + SignedDuration::from_mins(30)
1712            + SignedDuration::from_secs(45)
1713            + SignedDuration::from_nanos(500_000_000);
1714
1715        let earlier_nanos = UnixNanos::from(earlier);
1716        let later_nanos = UnixNanos::from(later);
1717
1718        // Calculate expected duration in nanoseconds
1719        let expected_duration =
1720            (60 * 60 + 30 * 60 + 45) * NANOSECONDS_IN_SECOND + 500 * NANOSECONDS_IN_MILLISECOND;
1721
1722        assert_eq!(
1723            later_nanos.duration_since(&earlier_nanos),
1724            Some(DurationNanos::new(expected_duration))
1725        );
1726        assert_eq!(earlier_nanos.duration_since(&later_nanos), None);
1727    }
1728
1729    #[rstest]
1730    fn test_duration_since_with_edge_cases() {
1731        // Test with maximum value
1732        let max = UnixNanos::from(u64::MAX);
1733        let smaller = UnixNanos::from(u64::MAX - 1000);
1734
1735        assert_eq!(max.duration_since(&smaller), Some(DurationNanos::new(1000)));
1736        assert_eq!(smaller.duration_since(&max), None);
1737
1738        // Test with minimum value
1739        let min = UnixNanos::default(); // Zero timestamp
1740        let larger = UnixNanos::from(1000);
1741
1742        assert_eq!(min.duration_since(&min), Some(DurationNanos::default()));
1743        assert_eq!(larger.duration_since(&min), Some(DurationNanos::new(1000)));
1744        assert_eq!(min.duration_since(&larger), None);
1745    }
1746
1747    #[rstest]
1748    fn test_serde_json() {
1749        let nanos = UnixNanos::from(123);
1750        let json = serde_json::to_string(&nanos).unwrap();
1751        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1752        assert_eq!(deserialized, nanos);
1753    }
1754
1755    #[rstest]
1756    fn test_serde_edge_cases() {
1757        let nanos = UnixNanos::from(u64::MAX);
1758        let json = serde_json::to_string(&nanos).unwrap();
1759        let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
1760        assert_eq!(deserialized, nanos);
1761    }
1762
1763    #[rstest]
1764    #[case("123", 123)] // Integer string
1765    #[case("1234.567", 1_234_567_000_000)] // Float string (seconds to nanos)
1766    #[case("2024-02-10", 1_707_523_200_000_000_000)] // Simple date (midnight UTC)
1767    #[case("2024-2-10", 1_707_523_200_000_000_000)] // Legacy-compatible short month
1768    #[case("2024-02-1", 1_706_745_600_000_000_000)] // Legacy-compatible short day
1769    #[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] // RFC3339 without fractions
1770    #[case("2024-02-10t14:58:43Z", 1_707_577_123_000_000_000)] // Lowercase RFC3339 separator
1771    #[case("2024-02-10 14:58:43Z", 1_707_577_123_000_000_000)] // Space RFC3339 separator
1772    #[case("2024-02-10T14:58:43.456789Z", 1_707_577_123_456_789_000)] // RFC3339 with fractions
1773    fn test_from_str_formats(#[case] input: &str, #[case] expected: u64) {
1774        let parsed: UnixNanos = input.parse().unwrap();
1775        assert_eq!(parsed.as_u64(), expected);
1776    }
1777
1778    #[rstest]
1779    #[case("abc")] // Random string
1780    #[case("not a timestamp")] // Non-timestamp string
1781    #[case("2024-02-10 14:58:43")] // Space-separated format (not RFC3339)
1782    #[case("2024-02-10T14:58:43Z[UTC]")] // RFC 9557 annotation was not accepted previously
1783    fn test_from_str_invalid_formats(#[case] input: &str) {
1784        let result = input.parse::<UnixNanos>();
1785        assert!(result.is_err());
1786    }
1787
1788    #[rstest]
1789    fn test_from_str_integer_overflow() {
1790        // One more digit than u64::MAX (20 digits) so definitely overflows
1791        let input = "184467440737095516160";
1792        let result = input.parse::<UnixNanos>();
1793        assert!(result.is_err());
1794    }
1795
1796    #[rstest]
1797    fn test_checked_add_overflow_returns_none() {
1798        let max = UnixNanos::from(u64::MAX);
1799        assert_eq!(max.checked_add(DurationNanos::new(1)), None);
1800    }
1801
1802    #[rstest]
1803    fn test_checked_sub_underflow_returns_none() {
1804        let zero = UnixNanos::default();
1805        assert_eq!(zero.checked_sub(DurationNanos::new(1)), None);
1806    }
1807
1808    #[rstest]
1809    fn test_saturating_add_overflow() {
1810        let max = UnixNanos::from(u64::MAX);
1811        let result = max.saturating_add(DurationNanos::new(1));
1812        assert_eq!(result, UnixNanos::from(u64::MAX));
1813    }
1814
1815    #[rstest]
1816    fn test_saturating_sub_underflow() {
1817        let zero = UnixNanos::default();
1818        let result = zero.saturating_sub(DurationNanos::new(1));
1819        assert_eq!(result, UnixNanos::default());
1820    }
1821
1822    #[rstest]
1823    fn test_from_str_float_overflow() {
1824        // Use scientific notation so we take the floating-point parsing path.
1825        let input = "2e10"; // 20 billion seconds ~ 634 years (> u64::MAX nanoseconds)
1826        let err = input.parse::<UnixNanos>().unwrap_err();
1827        assert!(err.to_string().contains("out of range"));
1828    }
1829
1830    #[rstest]
1831    #[case("NaN")]
1832    #[case("nan")]
1833    #[case("inf")]
1834    #[case("-inf")]
1835    fn test_from_str_non_finite_float_errors(#[case] input: &str) {
1836        let err = input.parse::<UnixNanos>().unwrap_err();
1837        assert!(err.to_string().contains("must be finite"));
1838    }
1839
1840    #[rstest]
1841    #[case("-1.5")]
1842    #[case("-0.000001")]
1843    fn test_from_str_negative_float_errors(#[case] input: &str) {
1844        let err = input.parse::<UnixNanos>().unwrap_err();
1845        assert!(err.to_string().contains("cannot be negative"));
1846    }
1847
1848    #[rstest]
1849    #[case("42", Some(42))]
1850    #[case("42.0", Some(42_000_000_000))]
1851    #[case("1e-3", Some(1_000_000))]
1852    #[case("0.9999999999", Some(999_999_999))]
1853    #[case("18446744073709551615", Some(u64::MAX))]
1854    #[case("-1", None)]
1855    #[case("-0.5", None)]
1856    #[case("1e30", None)]
1857    #[case("1e400", None)]
1858    fn test_deserialize_numeric_routes(#[case] token: &str, #[case] expected: Option<u64>) {
1859        #[derive(Deserialize)]
1860        #[serde(tag = "kind")]
1861        enum Tagged {
1862            Time { value: UnixNanos },
1863        }
1864        let direct = serde_json::from_str::<UnixNanos>(token);
1865        let buffered = serde_json::from_str::<serde_json::Value>(token)
1866            .and_then(serde_json::from_value::<UnixNanos>);
1867        let tagged = serde_json::from_str(&format!(r#"{{"kind":"Time","value":{token}}}"#))
1868            .map(|Tagged::Time { value }| value);
1869
1870        assert_eq!(direct.ok().map(|value| value.as_u64()), expected);
1871        assert_eq!(buffered.ok().map(|value| value.as_u64()), expected);
1872        assert_eq!(tagged.ok().map(|value| value.as_u64()), expected);
1873    }
1874
1875    #[rstest]
1876    fn test_deserialize_u64() {
1877        let json = "123456789";
1878        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1879        assert_eq!(deserialized.as_u64(), 123_456_789);
1880    }
1881
1882    #[rstest]
1883    fn test_deserialize_string_with_int() {
1884        let json = "\"123456789\"";
1885        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1886        assert_eq!(deserialized.as_u64(), 123_456_789);
1887    }
1888
1889    #[rstest]
1890    fn test_deserialize_float() {
1891        let json = "1234.567";
1892        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1893        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1894    }
1895
1896    #[rstest]
1897    fn test_deserialize_string_with_float() {
1898        let json = "\"1234.567\"";
1899        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1900        assert_eq!(deserialized.as_u64(), 1_234_567_000_000);
1901    }
1902
1903    #[rstest]
1904    fn test_deserialize_float_uses_truncation() {
1905        // Truncation (not rounding) for consistency with secs_to_nanos() etc
1906        let json = "0.9999999999";
1907        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
1908        assert_eq!(deserialized.as_u64(), 999_999_999); // Truncated, not rounded to 1B
1909    }
1910
1911    #[rstest]
1912    #[case("\"2024-02-10T14:58:43.456789Z\"", 1_707_577_123_456_789_000)]
1913    #[case("\"2024-02-10T14:58:43Z\"", 1_707_577_123_000_000_000)]
1914    fn test_deserialize_timestamp_strings(#[case] input: &str, #[case] expected: u64) {
1915        let deserialized: UnixNanos = serde_json::from_str(input).unwrap();
1916        assert_eq!(deserialized.as_u64(), expected);
1917    }
1918
1919    #[rstest]
1920    fn test_deserialize_negative_int_fails() {
1921        let json = "-123456789";
1922        let result: Result<UnixNanos, _> = serde_json::from_str(json);
1923        assert!(
1924            result
1925                .unwrap_err()
1926                .to_string()
1927                .contains("cannot be negative")
1928        );
1929    }
1930
1931    #[rstest]
1932    fn test_deserialize_negative_float_fails() {
1933        let json = "-1234.567";
1934        let result: Result<UnixNanos, _> = serde_json::from_str(json);
1935        assert!(
1936            result
1937                .unwrap_err()
1938                .to_string()
1939                .contains("cannot be negative")
1940        );
1941    }
1942
1943    #[rstest]
1944    fn test_deserialize_nan_fails() {
1945        // JSON doesn't support NaN directly, test the internal deserializer
1946        use serde::de::{
1947            IntoDeserializer,
1948            value::{Error as ValueError, F64Deserializer},
1949        };
1950        let deserializer: F64Deserializer<ValueError> = f64::NAN.into_deserializer();
1951        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1952        assert!(result.is_err());
1953        assert!(result.unwrap_err().to_string().contains("must be finite"));
1954    }
1955
1956    #[rstest]
1957    fn test_deserialize_infinity_fails() {
1958        use serde::de::{
1959            IntoDeserializer,
1960            value::{Error as ValueError, F64Deserializer},
1961        };
1962        let deserializer: F64Deserializer<ValueError> = f64::INFINITY.into_deserializer();
1963        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1964        assert!(result.is_err());
1965        assert!(result.unwrap_err().to_string().contains("must be finite"));
1966    }
1967
1968    #[rstest]
1969    fn test_deserialize_negative_infinity_fails() {
1970        use serde::de::{
1971            IntoDeserializer,
1972            value::{Error as ValueError, F64Deserializer},
1973        };
1974        let deserializer: F64Deserializer<ValueError> = f64::NEG_INFINITY.into_deserializer();
1975        let result: Result<UnixNanos, _> = UnixNanos::deserialize(deserializer);
1976        assert!(result.is_err());
1977        assert!(result.unwrap_err().to_string().contains("must be finite"));
1978    }
1979
1980    #[rstest]
1981    fn test_deserialize_overflow_float_fails() {
1982        // Test a float that would overflow u64 when converted to nanoseconds
1983        // u64::MAX is ~18.4e18, so u64::MAX / 1e9 = ~18.4e9 seconds
1984        let result: Result<UnixNanos, _> = serde_json::from_str("1e20");
1985        assert!(result.is_err());
1986        assert!(result.unwrap_err().to_string().contains("out of range"));
1987    }
1988
1989    #[rstest]
1990    fn test_deserialize_float_u64_boundary_fails() {
1991        let deserializer = serde::de::value::F64Deserializer::<serde::de::value::Error>::new(
1992            18_446_744_073.709_553,
1993        );
1994        let err = UnixNanos::deserialize(deserializer).unwrap_err();
1995        assert!(err.to_string().contains("out of range"));
1996    }
1997
1998    #[rstest]
1999    fn test_deserialize_invalid_string_fails() {
2000        let json = "\"not a timestamp\"";
2001        let result: Result<UnixNanos, _> = serde_json::from_str(json);
2002        assert!(result.is_err());
2003    }
2004
2005    #[rstest]
2006    fn test_deserialize_edge_cases() {
2007        // Test zero
2008        let json = "0";
2009        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
2010        assert_eq!(deserialized.as_u64(), 0);
2011
2012        // Test large value
2013        let json = "18446744073709551615"; // u64::MAX
2014        let deserialized: UnixNanos = serde_json::from_str(json).unwrap();
2015        assert_eq!(deserialized.as_u64(), u64::MAX);
2016    }
2017
2018    #[rstest]
2019    #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
2020    fn test_as_i64_overflow_panics() {
2021        let nanos = UnixNanos::from(u64::MAX);
2022        let _ = nanos.as_i64(); // Should panic
2023    }
2024
2025    #[rstest]
2026    fn test_as_i64_at_i64_max_boundary() {
2027        let nanos = UnixNanos::from(i64::MAX.cast_unsigned());
2028        assert_eq!(nanos.as_i64(), i64::MAX);
2029    }
2030
2031    #[rstest]
2032    #[should_panic(expected = "UnixNanos value exceeds i64::MAX")]
2033    fn test_as_i64_just_above_i64_max_panics() {
2034        let nanos = UnixNanos::from(i64::MAX.cast_unsigned() + 1);
2035        let _ = nanos.as_i64();
2036    }
2037
2038    fn unix_nanos_strategy() -> impl Strategy<Value = UnixNanos> {
2039        prop_oneof![
2040            // Small values
2041            0u64..1_000_000u64,
2042            // Medium values (microseconds range)
2043            1_000_000u64..1_000_000_000_000u64,
2044            // Large values (nanoseconds since 1970)
2045            1_000_000_000_000u64..=i64::MAX.cast_unsigned(),
2046            // Values above i64::MAX (sentinel range, GTC/infinity)
2047            (i64::MAX.cast_unsigned() + 1)..=u64::MAX,
2048            // Edge cases
2049            Just(0u64),
2050            Just(1u64),
2051            Just(1_000_000_000u64),               // 1 second in nanos
2052            Just(1_000_000_000_000u64),           // ~2001 timestamp
2053            Just(1_700_000_000_000_000_000u64),   // ~2023 timestamp
2054            Just((i64::MAX / 2).cast_unsigned()), // Safe for doubling
2055            Just(i64::MAX.cast_unsigned()),       // i64 boundary
2056            Just(u64::MAX),                       // Sentinel / max value
2057        ]
2058        .prop_map(UnixNanos::from)
2059    }
2060
2061    fn unix_nanos_pair_strategy() -> impl Strategy<Value = (UnixNanos, UnixNanos)> {
2062        (unix_nanos_strategy(), unix_nanos_strategy())
2063    }
2064
2065    fn duration_nanos_strategy() -> impl Strategy<Value = DurationNanos> {
2066        any::<u64>().prop_map(DurationNanos::new)
2067    }
2068
2069    fn duration_nanos_pair_strategy() -> impl Strategy<Value = (DurationNanos, DurationNanos)> {
2070        (duration_nanos_strategy(), duration_nanos_strategy())
2071    }
2072
2073    proptest! {
2074        #[rstest]
2075        #[expect(
2076            clippy::float_cmp,
2077            clippy::cast_precision_loss,
2078            reason = "roundtrip: both sides go through the same u64->f64 cast"
2079        )]
2080        fn prop_unix_nanos_construction_roundtrip(nanos in unix_nanos_strategy()) {
2081            let value = nanos.as_u64();
2082            prop_assert_eq!(UnixNanos::from(value).as_u64(), value);
2083            prop_assert_eq!(nanos.as_f64(), value as f64);
2084
2085            // Test i64 conversion only for values within i64 range
2086            if i64::try_from(value).is_ok() {
2087                prop_assert_eq!(nanos.as_i64(), value.cast_signed());
2088            }
2089        }
2090
2091        #[rstest]
2092        fn prop_duration_nanos_addition_commutative(
2093            (duration1, duration2) in duration_nanos_pair_strategy()
2094        ) {
2095            if let (Some(sum1), Some(sum2)) = (
2096                duration1.checked_add(duration2),
2097                duration2.checked_add(duration1)
2098            ) {
2099                prop_assert_eq!(sum1, sum2, "Addition should be commutative");
2100            }
2101        }
2102
2103        #[rstest]
2104        fn prop_duration_nanos_addition_associative(
2105            duration1 in duration_nanos_strategy(),
2106            duration2 in duration_nanos_strategy(),
2107            duration3 in duration_nanos_strategy(),
2108        ) {
2109            let expected = duration1
2110                .checked_add(duration2)
2111                .and_then(|sum| sum.checked_add(duration3));
2112
2113            if let Some(expected) = expected {
2114                let left = (duration1 + duration2) + duration3;
2115                let right = duration1 + (duration2 + duration3);
2116                prop_assert_eq!(left, expected);
2117                prop_assert_eq!(right, expected);
2118            }
2119        }
2120
2121        #[rstest]
2122        fn prop_unix_nanos_duration_arithmetic_roundtrip(
2123            nanos in unix_nanos_strategy(),
2124            duration in duration_nanos_strategy(),
2125        ) {
2126            if let Some(sum) = nanos.checked_add(duration) {
2127                prop_assert_eq!(sum - duration, nanos);
2128                prop_assert_eq!(sum - nanos, duration);
2129            }
2130        }
2131
2132        #[rstest]
2133        fn prop_duration_nanos_zero_identity(duration in duration_nanos_strategy()) {
2134            let zero = DurationNanos::default();
2135            prop_assert_eq!(duration + zero, duration);
2136            prop_assert_eq!(zero + duration, duration);
2137            prop_assert!(zero.is_zero());
2138        }
2139
2140        #[rstest]
2141        fn prop_unix_nanos_ordering_consistency(
2142            (nanos1, nanos2) in unix_nanos_pair_strategy()
2143        ) {
2144            // Ordering operations should be consistent
2145            let eq = nanos1 == nanos2;
2146            let lt = nanos1 < nanos2;
2147            let gt = nanos1 > nanos2;
2148            let le = nanos1 <= nanos2;
2149            let ge = nanos1 >= nanos2;
2150
2151            // Exactly one of eq, lt, gt should be true
2152            let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
2153            prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
2154
2155            // Consistency checks
2156            prop_assert_eq!(le, eq || lt, "<= should equal == || <");
2157            prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
2158            prop_assert_eq!(lt, nanos2 > nanos1, "< should be symmetric with >");
2159            prop_assert_eq!(le, nanos2 >= nanos1, "<= should be symmetric with >=");
2160        }
2161
2162        #[rstest]
2163        fn prop_unix_nanos_string_roundtrip(nanos in unix_nanos_strategy()) {
2164            // String serialization should round-trip correctly
2165            let string_repr = nanos.to_string();
2166            let parsed = UnixNanos::from_str(&string_repr);
2167            prop_assert!(parsed.is_ok(), "String parsing should succeed for valid UnixNanos");
2168            if let Ok(parsed_nanos) = parsed {
2169                prop_assert_eq!(parsed_nanos, nanos, "String should round-trip exactly");
2170            }
2171        }
2172
2173        #[rstest]
2174        fn prop_unix_nanos_datetime_conversion(nanos in unix_nanos_strategy()) {
2175            // DateTime conversion should be consistent (only test values within i64 range)
2176            if i64::try_from(nanos.as_u64()).is_ok() {
2177                let datetime = nanos.to_datetime_utc();
2178                let converted_back = UnixNanos::from(datetime);
2179                prop_assert_eq!(converted_back, nanos, "DateTime conversion should round-trip");
2180
2181                // RFC3339 string should also round-trip for valid dates
2182                let rfc3339 = nanos.to_rfc3339();
2183                if let Ok(parsed_from_rfc3339) = UnixNanos::from_str(&rfc3339) {
2184                    prop_assert_eq!(parsed_from_rfc3339, nanos, "RFC3339 string should round-trip");
2185                }
2186            }
2187        }
2188
2189        #[rstest]
2190        fn prop_unix_nanos_duration_since(
2191            (nanos1, nanos2) in unix_nanos_pair_strategy()
2192        ) {
2193            // duration_since should be consistent with comparison and arithmetic
2194            let duration = nanos1.duration_since(&nanos2);
2195            let saturating_duration = nanos1.saturating_duration_since(nanos2);
2196
2197            if nanos1 >= nanos2 {
2198                // If nanos1 >= nanos2, duration should be Some and equal to difference
2199                prop_assert!(duration.is_some(), "Duration should be Some when first >= second");
2200                if let Some(dur) = duration {
2201                    prop_assert_eq!(dur.as_u64(), nanos1.as_u64() - nanos2.as_u64(),
2202                        "Duration should equal the difference");
2203                    prop_assert_eq!(saturating_duration, dur,
2204                        "Saturating duration should equal the difference");
2205                    prop_assert_eq!(nanos2 + dur, nanos1,
2206                        "second + duration should equal first");
2207                }
2208            } else {
2209                // If nanos1 < nanos2, duration should be None
2210                prop_assert!(duration.is_none(), "Duration should be None when first < second");
2211                prop_assert_eq!(saturating_duration, DurationNanos::default(),
2212                    "Saturating duration should be zero when first < second");
2213            }
2214        }
2215
2216        #[rstest]
2217        fn prop_unix_nanos_checked_arithmetic(
2218            nanos in unix_nanos_strategy(),
2219            duration in duration_nanos_strategy(),
2220        ) {
2221            let checked_add = nanos.checked_add(duration);
2222            let checked_sub = nanos.checked_sub(duration);
2223
2224            if let Some(sum) = checked_add {
2225                prop_assert_eq!(sum, nanos + duration, "Checked add should match regular add when no overflow");
2226            }
2227
2228            if let Some(diff) = checked_sub {
2229                prop_assert_eq!(diff, nanos - duration, "Checked sub should match regular sub when no underflow");
2230            }
2231        }
2232
2233        #[rstest]
2234        fn prop_unix_nanos_saturating_arithmetic(
2235            nanos in unix_nanos_strategy(),
2236            duration in duration_nanos_strategy(),
2237        ) {
2238            let sat_add = nanos.saturating_add(duration);
2239            let sat_sub = nanos.saturating_sub(duration);
2240
2241            prop_assert!(sat_add >= nanos, "Saturating add result should be >= timestamp");
2242            prop_assert!(sat_sub <= nanos, "Saturating sub result should be <= timestamp");
2243
2244            if let Some(checked_sum) = nanos.checked_add(duration) {
2245                prop_assert_eq!(sat_add, checked_sum, "Saturating add should match checked add when no overflow");
2246            } else {
2247                prop_assert_eq!(sat_add, UnixNanos::from(u64::MAX), "Saturating add should be MAX on overflow");
2248            }
2249
2250            if let Some(checked_diff) = nanos.checked_sub(duration) {
2251                prop_assert_eq!(sat_sub, checked_diff, "Saturating sub should match checked sub when no underflow");
2252            } else {
2253                prop_assert_eq!(sat_sub, UnixNanos::default(), "Saturating sub should be zero on underflow");
2254            }
2255        }
2256
2257        #[rstest]
2258        fn prop_unix_nanos_assign_mirrors_op(
2259            nanos in unix_nanos_strategy(),
2260            duration in duration_nanos_strategy(),
2261        ) {
2262            if let Some(expected) = nanos.checked_add(duration) {
2263                let mut add_result = nanos;
2264                add_result += duration;
2265                prop_assert_eq!(add_result, expected, "AddAssign should mirror Add");
2266            }
2267
2268            if let Some(expected) = nanos.checked_sub(duration) {
2269                let mut sub_result = nanos;
2270                sub_result -= duration;
2271                prop_assert_eq!(sub_result, expected, "SubAssign should mirror Sub");
2272            }
2273        }
2274
2275        #[rstest]
2276        fn prop_unix_nanos_serde_roundtrip(nanos in unix_nanos_strategy()) {
2277            let json = serde_json::to_string(&nanos).unwrap();
2278            let deserialized: UnixNanos = serde_json::from_str(&json).unwrap();
2279            prop_assert_eq!(deserialized, nanos, "Serde JSON should round-trip exactly");
2280        }
2281
2282        #[rstest]
2283        fn prop_unix_nanos_f64_deserialize_never_panics(val: f64) {
2284            // Use IntoDeserializer to hit visit_f64 directly,
2285            // bypassing JSON text encoding ambiguity
2286            use serde::de::{IntoDeserializer, value::{Error as ValueError, F64Deserializer}};
2287            let deserializer: F64Deserializer<ValueError> = val.into_deserializer();
2288            let result = UnixNanos::deserialize(deserializer);
2289
2290            let upper_bound = 2.0_f64.powi(64);
2291            if val.is_finite() && val >= 0.0 && val * 1_000_000_000.0 < upper_bound {
2292                prop_assert!(result.is_ok(), "Should succeed for valid f64: {}", val);
2293            } else {
2294                prop_assert!(result.is_err(), "Should error for invalid f64: {}", val);
2295            }
2296        }
2297    }
2298
2299    #[rstest]
2300    fn test_from_seconds_zero() {
2301        let nanos = UnixNanos::from_seconds(0);
2302        assert_eq!(nanos.as_u64(), 0);
2303    }
2304
2305    #[rstest]
2306    fn test_from_seconds_one() {
2307        let nanos = UnixNanos::from_seconds(1);
2308        assert_eq!(nanos.as_u64(), 1_000_000_000);
2309    }
2310
2311    #[rstest]
2312    fn test_from_seconds_realistic_timestamp() {
2313        let nanos = UnixNanos::from_seconds(1_700_000_000);
2314        assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2315        assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
2316    }
2317
2318    #[rstest]
2319    fn test_from_seconds_max_safe() {
2320        let max_seconds = u64::MAX / 1_000_000_000;
2321        let nanos = UnixNanos::from_seconds(max_seconds);
2322        assert_eq!(nanos.as_u64(), max_seconds * 1_000_000_000);
2323    }
2324
2325    #[rstest]
2326    fn test_from_millis_zero() {
2327        let nanos = UnixNanos::from_millis(0);
2328        assert_eq!(nanos.as_u64(), 0);
2329    }
2330
2331    #[rstest]
2332    fn test_from_millis_one() {
2333        let nanos = UnixNanos::from_millis(1);
2334        assert_eq!(nanos.as_u64(), 1_000_000);
2335    }
2336
2337    #[rstest]
2338    fn test_from_millis_one_second() {
2339        let nanos = UnixNanos::from_millis(1_000);
2340        assert_eq!(nanos.as_u64(), 1_000_000_000);
2341    }
2342
2343    #[rstest]
2344    fn test_from_millis_realistic_timestamp() {
2345        // 2023-11-14T22:13:20Z = 1700000000000 ms
2346        let nanos = UnixNanos::from_millis(1_700_000_000_000);
2347        assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2348        assert_eq!(nanos.to_datetime_utc(), timestamp("2023-11-14T22:13:20Z"));
2349    }
2350
2351    #[rstest]
2352    fn test_from_millis_max_safe() {
2353        let max_ms = u64::MAX / 1_000_000;
2354        let nanos = UnixNanos::from_millis(max_ms);
2355        assert_eq!(nanos.as_u64(), max_ms * 1_000_000);
2356    }
2357
2358    #[rstest]
2359    fn test_from_millis_matches_manual_conversion() {
2360        let ms = 1_625_474_304_765_u64;
2361        let expected = ms * 1_000_000;
2362        assert_eq!(UnixNanos::from_millis(ms).as_u64(), expected);
2363    }
2364
2365    #[rstest]
2366    #[case::valid(1_700_000_000_123, Some(1_700_000_000_123_000_000))]
2367    #[case::negative(-1, None)]
2368    #[case::overflow(i64::MAX, None)]
2369    #[case::zero(0, Some(0))]
2370    fn test_from_millis_checked(#[case] millis: i64, #[case] expected: Option<u64>) {
2371        assert_eq!(
2372            UnixNanos::from_millis_checked(millis).map(|value| value.as_u64()),
2373            expected
2374        );
2375    }
2376
2377    #[rstest]
2378    #[case(0, 0)]
2379    #[case(999_999_999, 0)]
2380    #[case(1_000_000_000, 1)]
2381    #[case(1_700_000_000_123_456_789, 1_700_000_000)]
2382    fn test_as_seconds(#[case] nanos: u64, #[case] expected: u64) {
2383        assert_eq!(UnixNanos::from(nanos).as_seconds(), expected);
2384    }
2385
2386    #[rstest]
2387    #[case(0, 0)]
2388    #[case(999_999, 0)]
2389    #[case(1_000_000, 1)]
2390    #[case(1_700_000_000_000_123_456, 1_700_000_000_000)]
2391    fn test_as_millis(#[case] nanos: u64, #[case] expected: u64) {
2392        assert_eq!(UnixNanos::from(nanos).as_millis(), expected);
2393    }
2394
2395    #[rstest]
2396    #[case(0, 0)]
2397    #[case(999, 0)]
2398    #[case(1_000, 1)]
2399    #[case(1_700_000_000_000_123_456, 1_700_000_000_000_123)]
2400    fn test_as_micros(#[case] nanos: u64, #[case] expected: u64) {
2401        assert_eq!(UnixNanos::from(nanos).as_micros(), expected);
2402    }
2403
2404    #[rstest]
2405    fn test_from_micros_zero() {
2406        let nanos = UnixNanos::from_micros(0);
2407        assert_eq!(nanos.as_u64(), 0);
2408    }
2409
2410    #[rstest]
2411    fn test_from_micros_one() {
2412        let nanos = UnixNanos::from_micros(1);
2413        assert_eq!(nanos.as_u64(), 1_000);
2414    }
2415
2416    #[rstest]
2417    fn test_from_micros_one_second() {
2418        let nanos = UnixNanos::from_micros(1_000_000);
2419        assert_eq!(nanos.as_u64(), 1_000_000_000);
2420    }
2421
2422    #[rstest]
2423    fn test_from_micros_one_millisecond() {
2424        let nanos = UnixNanos::from_micros(1_000);
2425        assert_eq!(nanos.as_u64(), 1_000_000);
2426        assert_eq!(UnixNanos::from_micros(1_000), UnixNanos::from_millis(1));
2427    }
2428
2429    #[rstest]
2430    fn test_from_micros_realistic_timestamp() {
2431        let micros = 1_700_000_000_000_000_u64;
2432        let nanos = UnixNanos::from_micros(micros);
2433        assert_eq!(nanos.as_u64(), 1_700_000_000_000_000_000);
2434    }
2435
2436    #[rstest]
2437    fn test_from_micros_max_safe() {
2438        let max_us = u64::MAX / 1_000;
2439        let nanos = UnixNanos::from_micros(max_us);
2440        assert_eq!(nanos.as_u64(), max_us * 1_000);
2441    }
2442
2443    #[rstest]
2444    fn test_from_micros_matches_manual_conversion() {
2445        let us = 1_000_000_123_456_u64;
2446        let expected = us * 1_000;
2447        assert_eq!(UnixNanos::from_micros(us).as_u64(), expected);
2448    }
2449
2450    #[rstest]
2451    #[case::valid(1_700_000_000_123_456, Some(1_700_000_000_123_456_000))]
2452    #[case::negative(-1, None)]
2453    #[case::overflow(i64::MAX, None)]
2454    #[case::zero(0, Some(0))]
2455    fn test_from_micros_checked(#[case] micros: i64, #[case] expected: Option<u64>) {
2456        assert_eq!(
2457            UnixNanos::from_micros_checked(micros).map(|value| value.as_u64()),
2458            expected
2459        );
2460    }
2461
2462    #[rstest]
2463    fn test_from_seconds_millis_and_micros_consistency() {
2464        assert_eq!(UnixNanos::from_seconds(1), UnixNanos::from_millis(1_000));
2465        assert_eq!(
2466            UnixNanos::from_seconds(60),
2467            UnixNanos::from_micros(60_000_000)
2468        );
2469        assert_eq!(
2470            UnixNanos::from_millis(1_000),
2471            UnixNanos::from_micros(1_000_000)
2472        );
2473        assert_eq!(
2474            UnixNanos::from_millis(60_000),
2475            UnixNanos::from_micros(60_000_000)
2476        );
2477    }
2478
2479    #[rstest]
2480    fn test_from_millis_round_trip_to_datetime() {
2481        let ms = 1_707_577_123_456_u64;
2482        let nanos = UnixNanos::from_millis(ms);
2483        let dt = nanos.to_datetime_utc();
2484        assert_eq!(dt.as_millisecond().cast_unsigned(), ms);
2485    }
2486
2487    #[rstest]
2488    fn test_from_micros_preserves_sub_millisecond() {
2489        let micros = 1_700_000_000_000_123_u64;
2490        let nanos = UnixNanos::from_micros(micros);
2491        assert_eq!(nanos.as_u64() % 1_000_000, 123_000);
2492    }
2493
2494    #[rstest]
2495    #[should_panic(expected = "UnixNanos overflow in from_seconds")]
2496    fn test_from_seconds_overflow_panics() {
2497        let _ = UnixNanos::from_seconds(u64::MAX / 1_000_000_000 + 1);
2498    }
2499
2500    #[rstest]
2501    #[should_panic(expected = "UnixNanos overflow in from_millis")]
2502    fn test_from_millis_overflow_panics() {
2503        let _ = UnixNanos::from_millis(u64::MAX / 1_000_000 + 1);
2504    }
2505
2506    #[rstest]
2507    #[should_panic(expected = "UnixNanos overflow in from_micros")]
2508    fn test_from_micros_overflow_panics() {
2509        let _ = UnixNanos::from_micros(u64::MAX / 1_000 + 1);
2510    }
2511
2512    #[rstest]
2513    #[case("0000000000000000")]
2514    #[case("1234567890123456")]
2515    #[case("01234567890123456")]
2516    #[case("12345678901234567")]
2517    #[case("012345678901234567")]
2518    #[case("123456789012345678")]
2519    #[case("1234567890123456789")]
2520    #[case("9876543210987654321")]
2521    #[case("9999999999999999999")]
2522    fn test_long_numeric_timestamp_matches_integer_parser(#[case] input: &str) {
2523        assert_eq!(
2524            input.parse::<UnixNanos>().unwrap().as_u64(),
2525            input.parse::<u64>().unwrap()
2526        );
2527    }
2528
2529    #[rstest]
2530    #[case("1.500000000000000", 1_500_000_000)]
2531    #[case("00000000000015e-1", 1_500_000_000)]
2532    #[case("+000000000000123", 123)]
2533    #[case("18446744073709551615", u64::MAX)]
2534    #[case("1970-1-2", 86_400_000_000_000)]
2535    #[case("1970-01-01t00:00:01Z", 1_000_000_000)]
2536    fn test_timestamp_parser_preserves_fallback_routes(#[case] input: &str, #[case] expected: u64) {
2537        assert_eq!(input.parse::<UnixNanos>().unwrap().as_u64(), expected);
2538    }
2539
2540    #[rstest]
2541    #[case("18446744073709551616", "Unix timestamp is out of range")]
2542    #[case(
2543        "1970-01-01T00:00:00Z[UTC]",
2544        "Invalid format: 1970-01-01T00:00:00Z[UTC]"
2545    )]
2546    #[case("123456789012345x", "Invalid format: 123456789012345x")]
2547    fn test_timestamp_parser_preserves_errors(#[case] input: &str, #[case] expected: &str) {
2548        assert_eq!(
2549            input.parse::<UnixNanos>().unwrap_err().to_string(),
2550            expected
2551        );
2552    }
2553
2554    proptest! {
2555        #[rstest]
2556        fn prop_long_timestamp_digits_match_integer_parser(input in "[0-9]{16,19}") {
2557            prop_assert_eq!(input.parse::<UnixNanos>().unwrap().as_u64(), input.parse::<u64>().unwrap());
2558        }
2559    }
2560}