astrotime 0.1.0

Time related types
Documentation

use std::ops::{Neg, Add, Sub, Mul};
use std::fmt;

#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};

/// Duration is an interval of time
///
/// Durations can handle lengths of time about 40 times as long as the age of the
/// universe, and have attosecond (10^-18) precision.
///
/// Negative values are supported.
///
/// Stored in 128 bits.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature ="serde", derive(Serialize, Deserialize))]
pub struct Duration {
    pub(crate) secs: i64,

    // attos are normalized such that
    // -1_000_000_000_000_000_000 < attos < 1_000_000_000_000_000_000
    // and maintain the same sign as secs.
    pub(crate) attos: i64
}

impl Duration {
    pub(crate) fn normalize(&mut self) {
        // This doesn't need divmod_i64 euclidean modulus because we reflect
        // negatives through zero
        self.secs += (self.attos / 1_000_000_000_000_000_000) as i64;
        self.attos %= 1_000_000_000_000_000_000;
        if self.secs < 0 && self.attos > 0 {
            self.attos -= 1_000_000_000_000_000_000;
            self.secs += 1;
        } else if self.secs > 0 && self.attos < 0 {
            self.attos += 1_000_000_000_000_000_000;
            self.secs -= 1;
        }
    }

    /// Make a new `Duration` with given number of seconds and attoseconds.
    #[must_use]
    pub fn new(secs: i64, attos: i64) -> Self {
        let mut d = Self { secs, attos };
        d.normalize();
        d
    }

    /// The seconds part
    #[inline]
    #[must_use]
    pub const fn seconds_part(&self) -> i64 {
        self.secs
    }

    /// The sub-second attoseconds part
    #[inline]
    #[must_use]
    pub const fn attos_part(&self) -> i64 {
        self.attos
    }

    /// The full value expressed in attoseconds. Returns None on overflow.
    ///
    /// This overflows on durations more than about 18 seconds.
    #[must_use]
    pub const fn as_attos(&self) -> Option<i64> {
        let sec_part = match self.secs.checked_mul(1_000_000_000_000_000_000) {
            Some(ns) => ns,
            None => return None,
        };
        sec_part.checked_add(self.attos as i64)
    }

    /// Determine if the duration is zero
    #[must_use]
    pub const fn is_zero(&self) -> bool {
        self.secs==0 && self.attos==0
    }
}

impl fmt::Display for Duration {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // herein we reflect through 0, so no div_modulo.
        // We only show a negative sign at the front
        if self.secs<0 {
            write!(f, "-P")?; // negative period designator
        } else {
            write!(f, "P")?; // period designator
        }

        let mut s = self.secs.abs();
        let a = self.attos_part().abs();

        let days =  s / 86400;
        s %= 86400; // only days should show any negative values
        if days!=0 {
            write!(f, "{}D", days)?;
        }

        if s!=0 || a!=0 {
            write!(f, "T")?;
        }

        let hours = s / 3600;
        s %= 3600;
        if hours!=0 {
            write!(f, "{}H", hours)?;
        }

        let minutes = s / 60;
        s %= 60;
        if minutes!=0 {
            write!(f, "{}M", minutes)?;
        }
        if s!=0 || a!=0 {
            if a==0 {
                write!(f, "{}S", s)?;
            } else {
                write!(f, "{}.{:018}S", s, a)?;
            }
        }
        Ok(())
    }
}

impl Neg for Duration {
    type Output = Self;

    #[inline]
    fn neg(self) -> Self {
        Self {
            secs: -self.secs,
            attos: -self.attos
        }
    }
}

impl Add for Duration {
    type Output = Self;

    fn add(self, rhs: Self) -> Self {
        let mut d = Self {
            secs: self.secs + rhs.secs,
            attos: self.attos + rhs.attos
        };
        d.normalize();
        d
    }
}

impl Sub for Duration {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self {
        let mut d = Self {
            secs: self.secs - rhs.secs,
            attos: self.attos - rhs.attos
        };
        d.normalize();
        d
    }
}

impl Mul<f64> for Duration {
    type Output = Self;

    #[allow(clippy::cast_possible_truncation)]
    #[allow(clippy::cast_precision_loss)]
    fn mul(self, rhs: f64) -> Self {
        let newsecs = self.secs as f64 * rhs;
        let secs = newsecs.trunc() as i64;
        let overflow_attos = (newsecs.fract() * 1_000_000_000_000_000_000.) as i64;

        let mut d = Self {
            secs,
            attos: ((self.attos as f64) * rhs) as i64 + overflow_attos
        };
        d.normalize();
        d
    }
}

#[cfg(test)]
mod test {
    use super::Duration;

    #[test]
    fn test_duration_normalize() {
        crate::setup_logging();

        let mut d = Duration { secs: 12, attos: -15 };
        d.normalize();
        assert_eq!(d.secs, 11);
        assert_eq!(d.attos, 1_000_000_000_000_000_000 - 15);

        let mut d = Duration { secs: -1, attos: 1_100_000_000_000_000_000 };
        d.normalize();
        assert_eq!(d.secs, 0);
        assert_eq!(d.attos, 100_000_000_000_000_000);
    }

    #[test]
    fn test_add_duration() {
        crate::setup_logging();

        let d1 = Duration { secs: 8000, attos: 12000 };
        let d2 = Duration { secs: 788, attos: 15000 };
        let d3 = d1 + d2;
        assert_eq!(d3.secs, 8788);
        assert_eq!(d3.attos, 27000);

        let d1 = Duration { secs: -1, attos: -101 };
        let d2 = Duration { secs: 5, attos: 31 };
        let d3 = d1 + d2;
        assert_eq!(d3.secs, 3);
        assert_eq!(d3.attos, 999_999_999_999_999_930);
    }

    #[test]
    fn test_sub_duration_vs_neg() {
        crate::setup_logging();

        let d1 = Duration { secs: 8000, attos: 12000 };
        let d2 = Duration { secs: 788, attos: 15000 };
        let d3 = d1 - d2;
        let d4 = d1 + (-d2);
        assert_eq!(d3, d4);
        assert_eq!(d3.secs, 7211);
        assert_eq!(d3.attos, 999_999_999_999_997_000);
    }

    #[test]
    fn test_duration_display() {
        crate::setup_logging();

        let d = Duration { secs: 86400 * 100, attos: 12000 };
        assert_eq!(&*format!("{}", d), "P100DT0.000000000000012000S");
        let d = Duration { secs: 86400 + 3600*2 + 60 + 1, attos: 120 };
        assert_eq!(&*format!("{}", d), "P1DT2H1M1.000000000000000120S");
        let d = Duration { secs: 60 * 3 + 5, attos: 15000 };
        assert_eq!(&*format!("{}", d), "PT3M5.000000000000015000S");
        let d = Duration { secs: -1, attos: -101 };
        assert_eq!(&*format!("{}", d), "-PT1.000000000000000101S");
        let d = Duration { secs: -86400*3, attos: 31 };
        assert_eq!(&*format!("{}", d), "-P3DT0.000000000000000031S");
        let d = Duration { secs: 0, attos: 31 };
        assert_eq!(&*format!("{}", d), "PT0.000000000000000031S");
        let d = Duration { secs: 0, attos: 0 };
        assert_eq!(&*format!("{}", d), "P");
    }
}