cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Signed-seconds duration value object.
//!
//! Returned by [`Timestamp::duration_since`](super::timestamp::Timestamp::duration_since)
//! and accumulated by stats and rollup use cases. The signed internal
//! representation lets callers tell "before / after" apart without
//! sentinel values.

use std::ops::AddAssign;

/// Signed-seconds duration. Positive when the later moment comes after
/// the earlier one; negative when the order is reversed.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Duration(i64);

impl Duration {
    pub fn from_seconds(seconds: i64) -> Self {
        Self(seconds)
    }

    pub fn from_days(days: i64) -> Self {
        Self(days * 86_400)
    }

    pub fn as_seconds(self) -> i64 {
        self.0
    }

    /// Duration expressed in fractional days (86 400 seconds = 1 day).
    pub fn as_days(self) -> f64 {
        self.0 as f64 / 86_400.0
    }

    /// Duration truncated to whole calendar days. Useful when the
    /// source was a date-granularity computation and a downstream
    /// cast back to integer days is needed.
    pub fn as_whole_days(self) -> i64 {
        self.0 / 86_400
    }

    pub fn is_positive(self) -> bool {
        self.0 > 0
    }

    pub fn is_zero(self) -> bool {
        self.0 == 0
    }
}

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

#[cfg(test)]
pub mod strategy {
    use super::Duration;
    use proptest::prelude::*;

    pub fn duration() -> impl Strategy<Value = Duration> {
        any::<i64>().prop_map(Duration::from_seconds)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn from_seconds_roundtrips_through_as_seconds(s in any::<i64>()) {
            prop_assert_eq!(Duration::from_seconds(s).as_seconds(), s);
        }

        #[test]
        fn is_positive_iff_strictly_greater_than_zero(d in strategy::duration()) {
            prop_assert_eq!(d.is_positive(), d.as_seconds() > 0);
        }
    }

    #[test]
    fn as_days_converts_at_full_day_boundary() {
        assert_eq!(Duration::from_seconds(86_400).as_days(), 1.0);
        assert_eq!(Duration::from_seconds(172_800).as_days(), 2.0);
    }

    #[test]
    fn as_days_handles_fractional_amounts() {
        assert!((Duration::from_seconds(43_200).as_days() - 0.5).abs() < 1e-9);
    }

    #[test]
    fn is_positive_excludes_zero_and_negative() {
        assert!(Duration::from_seconds(1).is_positive());
        assert!(!Duration::from_seconds(0).is_positive());
        assert!(!Duration::from_seconds(-1).is_positive());
    }

    #[test]
    fn add_assign_accumulates() {
        let mut total = Duration::default();
        total += Duration::from_seconds(60);
        total += Duration::from_seconds(30);
        assert_eq!(total.as_seconds(), 90);
    }
}