rstime 0.1.0

A zero-dependency Rust time library providing date, time, datetime types with formatting, parsing, Unix timestamps, and clock functionality.
Documentation
//! Duration module
//!
//! Provides `Duration` (positive) and `TimeDelta` (signed) types
//! with arithmetic operations and conversion methods.

use std::fmt;
use std::ops::{Add, AddAssign, Neg, Sub, SubAssign};
use std::time::Duration as StdDuration;

/// A positive duration with second and nanosecond precision
///
/// Supports arithmetic operations (`+`, `-`, `+=`, `-=`) and
/// conversion to/from standard library types.
///
/// Use [`try_new`](Duration::try_new) to safely create a `Duration` from values
/// that may be negative.
///
/// # Examples
///
/// ```rust
/// use rstime::Duration;
///
/// let d = Duration::try_new(5, 500_000_000).unwrap();
/// assert_eq!(d.total_seconds(), 5.5);
/// ```
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct Duration {
    /// Seconds component (must be >= 0 for valid Duration)
    pub secs: i64,
    /// Nanoseconds component (0..1_000_000_000, must be >= 0)
    pub nanos: i32,
}

/// A signed time delta, can be negative
///
/// Unlike [`Duration`], `TimeDelta` can represent negative time
/// differences. Used for date/time arithmetic that may go backwards.
///
/// # Examples
///
/// ```rust
/// use rstime::TimeDelta;
///
/// let delta = TimeDelta::new(-1, 0);
/// assert_eq!(delta.total_seconds(), -1.0);
/// ```
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct TimeDelta {
    /// Seconds component (can be negative)
    pub secs: i64,
    /// Nanoseconds component
    pub nanos: i32,
}

impl Duration {
    /// Zero duration
    pub const ZERO: Duration = Duration { secs: 0, nanos: 0 };
    /// One second
    pub const SECOND: Duration = Duration { secs: 1, nanos: 0 };
    /// One minute (60 seconds)
    pub const MINUTE: Duration = Duration { secs: 60, nanos: 0 };
    /// One hour (3600 seconds)
    pub const HOUR: Duration = Duration { secs: 3600, nanos: 0 };
    /// One day (86400 seconds)
    pub const DAY: Duration = Duration {
        secs: 86400,
        nanos: 0,
    };

    /// Try to create a duration from seconds and nanoseconds.
    ///
    /// Returns `None` if the result would be negative (duration must be positive).
    /// Normalizes nanoseconds into the 0..1_000_000_000 range.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use rstime::Duration;
    ///
    /// assert!(Duration::try_new(5, 500_000_000).is_some());
    /// assert!(Duration::try_new(-1, 0).is_none());
    /// ```
    pub fn try_new(secs: i64, nanos: i32) -> Option<Duration> {
        let d = Self::normalize(secs, nanos);
        if d.secs < 0 || d.nanos < 0 {
            None
        } else {
            Some(d)
        }
    }

    /// Create a duration from seconds and nanoseconds (panics if negative).
    ///
    /// Use [`try_new`](Duration::try_new) for safe creation.
    ///
    /// # Panics
    ///
    /// Panics if the resulting duration would be negative.
    pub fn new(secs: i64, nanos: i32) -> Duration {
        Self::try_new(secs, nanos).expect("Duration must not be negative")
    }

    /// Internal: normalize secs/nanos without positivity check.
    fn normalize(secs: i64, nanos: i32) -> Duration {
        let total_nanos = secs as i128 * 1_000_000_000 + nanos as i128;
        let s = (total_nanos / 1_000_000_000) as i64;
        let n = (total_nanos % 1_000_000_000) as i32;
        let n = if n < 0 {
            (n + 1_000_000_000) as i32
        } else {
            n
        };
        Duration { secs: s, nanos: n }
    }

    /// Create from seconds only
    pub fn from_secs(secs: i64) -> Duration {
        Duration::new(secs, 0)
    }

    /// Create from milliseconds
    pub fn from_millis(ms: i64) -> Duration {
        Duration::new(ms / 1000, ((ms % 1000) * 1_000_000) as i32)
    }

    /// Create from nanoseconds
    pub fn from_nanos(ns: i64) -> Duration {
        Duration::new(ns / 1_000_000_000, (ns % 1_000_000_000) as i32)
    }

    /// Convert from `std::time::Duration`
    pub fn from_std(d: StdDuration) -> Duration {
        Duration {
            secs: d.as_secs() as i64,
            nanos: d.subsec_nanos() as i32,
        }
    }

    /// Convert to `std::time::Duration`, returns `None` if negative
    pub fn to_std(self) -> Option<StdDuration> {
        if self.secs >= 0 {
            Some(StdDuration::new(self.secs as u64, self.nanos as u32))
        } else {
            None
        }
    }

    /// Convert to a [`TimeDelta`]
    pub fn to_time_delta(self) -> TimeDelta {
        TimeDelta {
            secs: self.secs,
            nanos: self.nanos,
        }
    }

    /// Total duration in seconds as a floating-point value
    pub fn total_seconds(&self) -> f64 {
        self.secs as f64 + self.nanos as f64 / 1_000_000_000.0
    }

    /// Total duration in milliseconds
    pub fn total_millis(&self) -> i64 {
        self.secs * 1000 + self.nanos as i64 / 1_000_000
    }
}

impl TimeDelta {
    /// Create a time delta from seconds and nanoseconds
    pub fn new(secs: i64, nanos: i32) -> TimeDelta {
        let total_nanos = secs as i128 * 1_000_000_000 + nanos as i128;
        let s = (total_nanos / 1_000_000_000) as i64;
        let n = (total_nanos % 1_000_000_000) as i32;
        let n = if n < 0 && s <= 0 {
            n
        } else if n < 0 {
            (n + 1_000_000_000) as i32
        } else {
            n
        };
        TimeDelta { secs: s, nanos: n }
    }

    /// Absolute value as a positive [`Duration`]
    pub fn abs(self) -> Duration {
        Duration {
            secs: self.secs.abs(),
            nanos: self.nanos.abs(),
        }
    }

    /// Total time delta in seconds
    pub fn total_seconds(&self) -> f64 {
        self.secs as f64 + self.nanos as f64 / 1_000_000_000.0
    }
}

impl Add for Duration {
    type Output = Duration;
    fn add(self, rhs: Duration) -> Duration {
        // Addition of two positive values is always positive
        Self::normalize(self.secs + rhs.secs, self.nanos + rhs.nanos)
    }
}

impl AddAssign for Duration {
    fn add_assign(&mut self, rhs: Duration) {
        *self = *self + rhs;
    }
}

impl Sub for Duration {
    type Output = Duration;
    fn sub(self, rhs: Duration) -> Duration {
        // Subtraction may produce a negative Duration;
        // decompose into TimeDelta to preserve the result
        let secs = self.secs - rhs.secs;
        let nanos = self.nanos - rhs.nanos;
        let d = Self::normalize(secs, nanos);
        if d.secs < 0 {
            panic!("Duration subtraction resulted in negative value: {} - {}", self, rhs);
        }
        d
    }
}

impl SubAssign for Duration {
    fn sub_assign(&mut self, rhs: Duration) {
        *self = *self - rhs;
    }
}

impl Neg for TimeDelta {
    type Output = TimeDelta;
    fn neg(self) -> TimeDelta {
        TimeDelta::new(-self.secs, -self.nanos)
    }
}

impl Add for TimeDelta {
    type Output = TimeDelta;
    fn add(self, rhs: TimeDelta) -> TimeDelta {
        TimeDelta::new(self.secs + rhs.secs, self.nanos + rhs.nanos)
    }
}

impl Sub for TimeDelta {
    type Output = TimeDelta;
    fn sub(self, rhs: TimeDelta) -> TimeDelta {
        TimeDelta::new(self.secs - rhs.secs, self.nanos - rhs.nanos)
    }
}

impl From<Duration> for TimeDelta {
    fn from(d: Duration) -> TimeDelta {
        d.to_time_delta()
    }
}

impl fmt::Display for Duration {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}s", self.total_seconds())
    }
}

impl fmt::Display for TimeDelta {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}s", self.total_seconds())
    }
}