transforms 2.1.1

A transform library to track reference frames and provide transforms between them.
Documentation
//! The default nanosecond-resolution timestamp type.

use core::{
    ops::{Add, Sub},
    time::Duration,
};

use crate::time::{TimeError, TimePoint};

#[cfg(feature = "std")]
use std::time::{SystemTime, UNIX_EPOCH};

/// Default concrete time type used by this crate.
///
/// `Timestamp` stores a time value in `u64` nanoseconds, which spans about
/// 584 years from the epoch of the chosen clock — mid-2554 for a Unix-epoch
/// clock. A clock that outlives that range needs a custom
/// [`TimePoint`] type.
///
/// No value is reserved: staticness is expressed by
/// [`Stamp::Static`](crate::time::Stamp), not by a sentinel instant, so
/// every value including `t = 0` is an ordinary dynamic timestamp. This
/// makes `Timestamp` safe for boot-relative clocks whose first reading is
/// zero.
///
/// For custom clocks, implement `crate::time::TimePoint` on your own type and
/// use it with `Registry<T>`.
///
/// With the optional `serde` feature, this type implements `Serialize` and
/// `Deserialize` (the docs.rs listing cannot banner derive-generated impls).
/// It is `#[serde(transparent)]`: the wire carries the bare nanosecond
/// integer, not a one-field record, so every format encodes it natively and
/// a foreign-language consumer reads a plain number.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Timestamp {
    /// Nanoseconds since the epoch of the chosen clock.
    t: u64,
}

impl Timestamp {
    /// Returns a `Timestamp` initialized to the current time.
    ///
    /// This functionality is useful for dynamic transforms.
    ///
    /// # Panics
    ///
    /// Panics if the system clock reads outside the range `Timestamp` can
    /// represent: before `UNIX_EPOCH` (January 1, 1970), or more than
    /// `u64::MAX` nanoseconds after it (mid-2554). Use
    /// [`Timestamp::try_now`] for the panic-free variant.
    ///
    /// # Examples
    ///
    /// ```
    /// use transforms::time::Timestamp;
    ///
    /// let now = Timestamp::now();
    /// assert!(now.as_nanos() > 0);
    /// ```
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    #[must_use]
    #[allow(
        clippy::expect_used,
        reason = "the out-of-range panic is documented above; no meaningful recovery exists"
    )]
    pub fn now() -> Self {
        Self::try_now().expect("system clock outside the representable timestamp range")
    }

    /// Returns a `Timestamp` initialized to the current time, or an error
    /// if the system clock reads outside the representable range.
    ///
    /// The panic-free counterpart of [`Timestamp::now`].
    ///
    /// # Errors
    ///
    /// Returns `TimeError::DurationUnderflow` if the system clock is set
    /// before the Unix epoch, and `TimeError::DurationOverflow` if it is set
    /// more than `u64::MAX` nanoseconds after it (mid-2554).
    ///
    /// # Examples
    ///
    /// ```
    /// use transforms::time::Timestamp;
    ///
    /// let now = Timestamp::try_now().unwrap();
    /// assert!(now.as_nanos() > 0);
    /// ```
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    pub fn try_now() -> Result<Self, TimeError> {
        let since_epoch = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(|_| TimeError::DurationUnderflow)?;

        u64::try_from(since_epoch.as_nanos())
            .map(|nanos| Timestamp { t: nanos })
            .map_err(|_| TimeError::DurationOverflow)
    }

    /// Returns a `Timestamp` initialized at zero.
    ///
    /// Zero is an ordinary dynamic instant — the epoch of the chosen
    /// clock. Staticness is expressed by
    /// [`Stamp::Static`](crate::time::Stamp), not by any timestamp value,
    /// so a boot-relative clock's first reading needs no special handling.
    ///
    /// # Examples
    ///
    /// ```
    /// use transforms::time::Timestamp;
    ///
    /// let zero = Timestamp::zero();
    /// assert_eq!(zero.as_nanos(), 0);
    /// ```
    #[must_use]
    pub const fn zero() -> Self {
        Timestamp { t: 0 }
    }

    /// Creates a `Timestamp` from a number of nanoseconds.
    ///
    /// # Examples
    ///
    /// ```
    /// use transforms::time::Timestamp;
    ///
    /// let timestamp = Timestamp::from_nanos(1_000_000_000);
    /// assert_eq!(timestamp.as_seconds().unwrap(), 1.0);
    /// ```
    #[must_use]
    pub const fn from_nanos(nanos: u64) -> Self {
        Timestamp { t: nanos }
    }

    /// Returns the timestamp as nanoseconds.
    ///
    /// # Examples
    ///
    /// ```
    /// use transforms::time::Timestamp;
    ///
    /// let timestamp = Timestamp::from_nanos(1_000_000_000);
    /// assert_eq!(timestamp.as_nanos(), 1_000_000_000);
    /// ```
    #[must_use]
    pub const fn as_nanos(&self) -> u64 {
        self.t
    }

    /// Converts the `Timestamp` to seconds as a floating-point number.
    ///
    /// `f64` has a 53-bit mantissa, so timestamps up to 2^53 nanoseconds
    /// (about 104 days) convert with sub-nanosecond accuracy; beyond that the
    /// conversion silently loses precision, which this method refuses to do.
    /// Use [`Timestamp::as_seconds_lossy`] (or
    /// [`TimePoint::as_seconds_lossy`]) for a best-effort conversion of
    /// larger values, such as wall-clock times.
    ///
    /// # Errors
    ///
    /// Returns `TimeError::AccuracyLoss` if the timestamp exceeds 2^53
    /// nanoseconds.
    ///
    /// # Examples
    ///
    /// ```
    /// use transforms::time::Timestamp;
    ///
    /// let timestamp = Timestamp::from_nanos(1_000_000_000);
    /// assert_eq!(timestamp.as_seconds().unwrap(), 1.0);
    ///
    /// // Beyond 2^53 ns, sub-nanosecond accuracy is unrepresentable.
    /// let timestamp = Timestamp::from_nanos(1_000_000_000_000_000_001);
    /// assert!(timestamp.as_seconds().is_err());
    /// ```
    pub fn as_seconds(&self) -> Result<f64, TimeError> {
        const NANOSECONDS_PER_SECOND: f64 = 1_000_000_000.0;
        /// 2^53: the largest range in which `f64` represents every integer
        /// nanosecond count exactly.
        const MAX_ACCURATE_NANOS: u64 = 1 << 53;

        if self.t > MAX_ACCURATE_NANOS {
            return Err(TimeError::AccuracyLoss);
        }
        #[allow(clippy::cast_precision_loss)]
        Ok(self.t as f64 / NANOSECONDS_PER_SECOND)
    }

    /// Converts the `Timestamp` to seconds as a floating-point number,
    /// accepting precision loss beyond 2^53 nanoseconds.
    ///
    /// Inherent counterpart of [`TimePoint::as_seconds_lossy`], callable
    /// without importing the trait.
    ///
    /// # Examples
    ///
    /// ```
    /// use transforms::time::Timestamp;
    ///
    /// let timestamp = Timestamp::from_nanos(1_000_000_000_000_000_001);
    /// let seconds = timestamp.as_seconds_lossy();
    /// assert_eq!(seconds, 1_000_000_000.0);
    /// ```
    #[must_use = "this returns the result of the operation, without modifying the original"]
    #[allow(clippy::cast_precision_loss)]
    pub fn as_seconds_lossy(&self) -> f64 {
        const NANOSECONDS_PER_SECOND: f64 = 1_000_000_000.0;
        self.t as f64 / NANOSECONDS_PER_SECOND
    }
}

impl Sub<Timestamp> for Timestamp {
    type Output = Result<Duration, TimeError>;

    fn sub(
        self,
        other: Timestamp,
    ) -> Self::Output {
        self.t
            .checked_sub(other.t)
            .map(Duration::from_nanos)
            .ok_or(TimeError::DurationUnderflow)
    }
}

impl Add<Duration> for Timestamp {
    type Output = Result<Timestamp, TimeError>;

    fn add(
        self,
        rhs: Duration,
    ) -> Self::Output {
        u64::try_from(rhs.as_nanos())
            .ok()
            .and_then(|duration_nanos| self.t.checked_add(duration_nanos))
            .map(|final_nanos| Timestamp { t: final_nanos })
            .ok_or(TimeError::DurationOverflow)
    }
}

impl Sub<Duration> for Timestamp {
    type Output = Result<Timestamp, TimeError>;

    fn sub(
        self,
        rhs: Duration,
    ) -> Self::Output {
        u64::try_from(rhs.as_nanos())
            .ok()
            .and_then(|duration_nanos| self.t.checked_sub(duration_nanos))
            .map(|final_nanos| Timestamp { t: final_nanos })
            .ok_or(TimeError::DurationUnderflow)
    }
}

impl TimePoint for Timestamp {
    fn duration_since(
        self,
        earlier: Self,
    ) -> Result<Duration, TimeError> {
        self - earlier
    }

    fn checked_sub(
        self,
        rhs: Duration,
    ) -> Result<Self, TimeError> {
        self - rhs
    }

    fn as_seconds_lossy(self) -> f64 {
        Timestamp::as_seconds_lossy(&self)
    }
}

#[cfg(test)]
mod tests;