use hifitime::{Duration as HifiDuration, Epoch as HifiEpoch, TimeScale};
use pyo3::prelude::*;
#[pyclass(module = "astrora._core.time")]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Epoch {
inner: HifiEpoch,
}
impl Epoch {
pub fn new(epoch: HifiEpoch) -> Self {
Self { inner: epoch }
}
pub fn inner(&self) -> HifiEpoch {
self.inner
}
pub fn from_gregorian_utc(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanos: u32,
) -> Self {
Self::new(HifiEpoch::from_gregorian_utc(year, month, day, hour, minute, second, nanos))
}
pub fn from_gregorian_tai(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanos: u32,
) -> Self {
Self::new(HifiEpoch::from_gregorian_tai(year, month, day, hour, minute, second, nanos))
}
pub fn from_gregorian_tt(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanos: u32,
) -> Self {
Self::new(HifiEpoch::from_gregorian(year, month, day, hour, minute, second, nanos, TimeScale::TT))
}
pub fn from_gregorian_tdb(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanos: u32,
) -> Self {
Self::new(HifiEpoch::from_gregorian(year, month, day, hour, minute, second, nanos, TimeScale::TDB))
}
pub fn from_gregorian_utc_midnight(year: i32, month: u8, day: u8) -> Self {
Self::new(HifiEpoch::from_gregorian_utc_at_midnight(year, month, day))
}
pub fn from_gregorian_utc_noon(year: i32, month: u8, day: u8) -> Self {
Self::new(HifiEpoch::from_gregorian_utc_at_noon(year, month, day))
}
pub fn from_mjd(mjd_days: f64, time_scale: TimeScale) -> Self {
Self::new(HifiEpoch::from_mjd_in_time_scale(mjd_days, time_scale))
}
pub fn from_jd(jd_days: f64, time_scale: TimeScale) -> Self {
Self::new(HifiEpoch::from_jde_in_time_scale(jd_days, time_scale))
}
pub fn j2000() -> Self {
Self::from_gregorian_tt(2000, 1, 1, 12, 0, 0, 0)
}
pub fn now() -> Self {
Self::new(HifiEpoch::now().unwrap())
}
pub fn to_time_scale(&self, time_scale: TimeScale) -> Self {
Self::new(self.inner.to_time_scale(time_scale))
}
pub fn to_utc(&self) -> Self {
self.to_time_scale(TimeScale::UTC)
}
pub fn to_tai(&self) -> Self {
self.to_time_scale(TimeScale::TAI)
}
pub fn to_tt(&self) -> Self {
self.to_time_scale(TimeScale::TT)
}
pub fn to_tdb(&self) -> Self {
self.to_time_scale(TimeScale::TDB)
}
pub fn to_gpst(&self) -> Self {
self.to_time_scale(TimeScale::GPST)
}
pub fn to_mjd_tt(&self) -> f64 {
self.inner.to_mjd_tt_days()
}
pub fn to_mjd_utc(&self) -> f64 {
self.inner.to_mjd_utc_days()
}
pub fn to_mjd_tai(&self) -> f64 {
self.inner.to_mjd_tai_days()
}
pub fn to_mjd_tdb(&self) -> f64 {
let tdb_epoch = self.to_tdb();
tdb_epoch.to_mjd_tai() }
pub fn to_jd_tt(&self) -> f64 {
self.inner.to_jde_tt_days()
}
pub fn to_jd_utc(&self) -> f64 {
self.inner.to_jde_utc_days()
}
pub fn to_jd_tt_two_part(&self) -> (f64, f64) {
let jd_tt = self.to_jd_tt();
let jd1 = 2451545.0;
let jd2 = jd_tt - jd1;
(jd1, jd2)
}
pub fn to_tt_seconds_since_j2000(&self) -> f64 {
let j2000 = Epoch::j2000();
let tt_self = self.to_tt();
let tt_j2000 = j2000.to_tt();
(tt_self.inner - tt_j2000.inner).to_seconds()
}
pub fn to_tdb_seconds_since_j2000(&self) -> f64 {
let j2000 = Epoch::j2000();
let tdb_self = self.to_tdb();
let tdb_j2000 = j2000.to_tdb();
(tdb_self.inner - tdb_j2000.inner).to_seconds()
}
pub fn duration_since_j2000(&self) -> Duration {
let j2000 = Epoch::j2000();
self.duration_since(&j2000)
}
pub fn to_gregorian_utc(&self) -> (i32, u8, u8, u8, u8, u8, u32) {
let (year, month, day, hour, minute, second, nanos) = self.inner.to_gregorian_utc();
(year, month, day, hour, minute, second, nanos)
}
pub fn to_gregorian_tai(&self) -> (i32, u8, u8, u8, u8, u8, u32) {
let (year, month, day, hour, minute, second, nanos) = self.inner.to_gregorian_tai();
(year, month, day, hour, minute, second, nanos)
}
pub fn add_duration(&self, duration: Duration) -> Self {
Self::new(self.inner + duration.inner())
}
pub fn sub_duration(&self, duration: Duration) -> Self {
Self::new(self.inner - duration.inner())
}
pub fn duration_since(&self, other: &Epoch) -> Duration {
Duration::new(self.inner - other.inner)
}
pub fn to_iso_string(&self) -> String {
format!("{}", self.inner)
}
}
#[pyclass(module = "astrora._core.time")]
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Duration {
inner: HifiDuration,
}
impl Duration {
pub fn new(duration: HifiDuration) -> Self {
Self { inner: duration }
}
pub fn inner(&self) -> HifiDuration {
self.inner
}
pub fn from_seconds(seconds: f64) -> Self {
Self::new(HifiDuration::from_seconds(seconds))
}
pub fn from_minutes(minutes: f64) -> Self {
Self::new(HifiDuration::from_seconds(minutes * 60.0))
}
pub fn from_hours(hours: f64) -> Self {
Self::new(HifiDuration::from_seconds(hours * 3600.0))
}
pub fn from_days(days: f64) -> Self {
Self::new(HifiDuration::from_seconds(days * 86400.0))
}
pub fn to_seconds(&self) -> f64 {
self.inner.to_seconds()
}
pub fn to_minutes(&self) -> f64 {
self.to_seconds() / 60.0
}
pub fn to_hours(&self) -> f64 {
self.to_seconds() / 3600.0
}
pub fn to_days(&self) -> f64 {
self.to_seconds() / 86400.0
}
pub fn abs(&self) -> Self {
Self::new(self.inner.abs())
}
pub fn add(&self, other: &Duration) -> Self {
Self::new(self.inner + other.inner)
}
pub fn sub(&self, other: &Duration) -> Self {
Self::new(self.inner - other.inner)
}
pub fn mul(&self, scalar: f64) -> Self {
Self::new(self.inner * scalar)
}
pub fn div(&self, scalar: f64) -> Self {
Self::new(self.inner / scalar)
}
pub fn is_zero(&self) -> bool {
self.inner.total_nanoseconds() == 0
}
pub fn is_positive(&self) -> bool {
self.inner.total_nanoseconds() > 0
}
pub fn is_negative(&self) -> bool {
self.inner.total_nanoseconds() < 0
}
}
#[pymethods]
impl Epoch {
#[new]
#[pyo3(signature = (year, month, day, hour=0, minute=0, second=0, nanos=0))]
fn py_new(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanos: u32,
) -> Self {
Self::from_gregorian_utc(year, month, day, hour, minute, second, nanos)
}
#[staticmethod]
fn from_midnight_utc(year: i32, month: u8, day: u8) -> Self {
Self::from_gregorian_utc_midnight(year, month, day)
}
#[staticmethod]
fn from_noon_utc(year: i32, month: u8, day: u8) -> Self {
Self::from_gregorian_utc_noon(year, month, day)
}
#[staticmethod]
fn j2000_epoch() -> Self {
Self::j2000()
}
#[staticmethod]
fn from_jd_scale(jd: f64, scale: &str) -> PyResult<Self> {
let time_scale = match scale.to_uppercase().as_str() {
"UTC" => TimeScale::UTC,
"TAI" => TimeScale::TAI,
"TT" => TimeScale::TT,
"TDB" => TimeScale::TDB,
"GPST" => TimeScale::GPST,
_ => return Err(pyo3::exceptions::PyValueError::new_err(
format!("Unsupported time scale: {scale}. Use UTC, TAI, TT, TDB, or GPST")
)),
};
Ok(Self::from_jd(jd, time_scale))
}
#[staticmethod]
fn from_mjd_scale(mjd: f64, scale: &str) -> PyResult<Self> {
let time_scale = match scale.to_uppercase().as_str() {
"UTC" => TimeScale::UTC,
"TAI" => TimeScale::TAI,
"TT" => TimeScale::TT,
"TDB" => TimeScale::TDB,
"GPST" => TimeScale::GPST,
_ => return Err(pyo3::exceptions::PyValueError::new_err(
format!("Unsupported time scale: {scale}. Use UTC, TAI, TT, TDB, or GPST")
)),
};
Ok(Self::from_mjd(mjd, time_scale))
}
fn as_utc(&self) -> Self {
self.to_utc()
}
fn as_tai(&self) -> Self {
self.to_tai()
}
fn as_tt(&self) -> Self {
self.to_tt()
}
fn as_tdb(&self) -> Self {
self.to_tdb()
}
#[getter]
fn mjd_tt(&self) -> f64 {
self.to_mjd_tt()
}
#[getter]
fn mjd_utc(&self) -> f64 {
self.to_mjd_utc()
}
#[getter]
fn mjd_tai(&self) -> f64 {
self.to_mjd_tai()
}
#[getter]
fn mjd_tdb(&self) -> f64 {
self.to_mjd_tdb()
}
#[getter]
fn jd_tt(&self) -> f64 {
self.to_jd_tt()
}
#[getter]
fn jd_utc(&self) -> f64 {
self.to_jd_utc()
}
#[getter]
fn tt_seconds(&self) -> f64 {
self.to_tt_seconds_since_j2000()
}
#[getter]
fn tdb_seconds(&self) -> f64 {
self.to_tdb_seconds_since_j2000()
}
fn __add__(&self, duration: &Duration) -> Self {
self.add_duration(*duration)
}
fn __sub__(&self, other: PyObject, py: Python) -> PyResult<PyObject> {
if let Ok(duration) = other.extract::<Duration>(py) {
return Ok(self.sub_duration(duration).into_py(py));
}
if let Ok(epoch) = other.extract::<Epoch>(py) {
return Ok(self.duration_since(&epoch).into_py(py));
}
Err(pyo3::exceptions::PyTypeError::new_err(
"Can only subtract Duration or Epoch from Epoch"
))
}
fn __eq__(&self, other: &Self) -> bool {
self == other
}
fn __lt__(&self, other: &Self) -> bool {
self.inner < other.inner
}
fn __le__(&self, other: &Self) -> bool {
self.inner <= other.inner
}
fn __gt__(&self, other: &Self) -> bool {
self.inner > other.inner
}
fn __ge__(&self, other: &Self) -> bool {
self.inner >= other.inner
}
fn __repr__(&self) -> String {
format!("Epoch('{}')", self.to_iso_string())
}
fn __str__(&self) -> String {
self.to_iso_string()
}
}
#[pymethods]
impl Duration {
#[new]
fn py_new(seconds: f64) -> Self {
Self::from_seconds(seconds)
}
#[staticmethod]
fn from_min(minutes: f64) -> Self {
Self::from_minutes(minutes)
}
#[staticmethod]
fn from_hrs(hours: f64) -> Self {
Self::from_hours(hours)
}
#[staticmethod]
fn from_day(days: f64) -> Self {
Self::from_days(days)
}
#[getter]
fn seconds(&self) -> f64 {
self.to_seconds()
}
#[getter]
fn minutes(&self) -> f64 {
self.to_minutes()
}
#[getter]
fn hours(&self) -> f64 {
self.to_hours()
}
#[getter]
fn days(&self) -> f64 {
self.to_days()
}
fn __add__(&self, other: &Duration) -> Self {
self.add(other)
}
fn __sub__(&self, other: &Duration) -> Self {
self.sub(other)
}
fn __mul__(&self, scalar: f64) -> Self {
self.mul(scalar)
}
fn __truediv__(&self, scalar: f64) -> Self {
self.div(scalar)
}
fn __neg__(&self) -> Self {
Self::new(-self.inner)
}
fn __abs__(&self) -> Self {
self.abs()
}
fn __eq__(&self, other: &Self) -> bool {
self == other
}
fn __lt__(&self, other: &Self) -> bool {
self < other
}
fn __le__(&self, other: &Self) -> bool {
self <= other
}
fn __gt__(&self, other: &Self) -> bool {
self > other
}
fn __ge__(&self, other: &Self) -> bool {
self >= other
}
fn __repr__(&self) -> String {
format!("Duration({} s)", self.to_seconds())
}
fn __str__(&self) -> String {
let days = self.to_days();
if days.abs() >= 1.0 {
format!("{days:.6} days")
} else {
format!("{:.9} s", self.to_seconds())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_j2000_epoch() {
let j2000 = Epoch::j2000();
assert_relative_eq!(j2000.to_mjd_tt(), 51544.5, epsilon = 1e-10);
assert_relative_eq!(j2000.to_jd_tt(), 2451545.0, epsilon = 1e-10);
assert_relative_eq!(j2000.to_tt_seconds_since_j2000(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_epoch_from_gregorian_utc() {
let epoch = Epoch::from_gregorian_utc(2000, 1, 1, 12, 0, 0, 0);
let j2000 = Epoch::j2000();
let diff = epoch.duration_since(&j2000);
assert_relative_eq!(diff.to_seconds().abs(), 64.184, epsilon = 0.1);
}
#[test]
fn test_time_scale_conversions() {
let utc_epoch = Epoch::from_gregorian_utc(2020, 3, 15, 10, 30, 45, 0);
let tai_epoch = utc_epoch.to_tai();
let diff = tai_epoch.duration_since(&utc_epoch);
assert_relative_eq!(diff.to_seconds(), 0.0, epsilon = 1e-6);
let mjd_utc = utc_epoch.to_mjd_utc();
let mjd_tai = tai_epoch.to_mjd_tai();
let mjd_diff = (mjd_tai - mjd_utc).abs();
assert!(mjd_diff > 0.0003); assert!(mjd_diff < 0.0005); }
#[test]
fn test_mjd_jd_conversions() {
let epoch = Epoch::from_gregorian_utc(2000, 1, 1, 0, 0, 0, 0);
let mjd = epoch.to_mjd_utc();
let jd = epoch.to_jd_utc();
assert_relative_eq!(jd, mjd + 2400000.5, epsilon = 1e-10);
assert_relative_eq!(mjd, 51544.0, epsilon = 0.001);
}
#[test]
fn test_duration_operations() {
let d1 = Duration::from_hours(1.0);
let d2 = Duration::from_minutes(30.0);
let sum = d1.add(&d2);
assert_relative_eq!(sum.to_minutes(), 90.0, epsilon = 1e-9);
let diff = d1.sub(&d2);
assert_relative_eq!(diff.to_minutes(), 30.0, epsilon = 1e-9);
let scaled = d1.mul(2.5);
assert_relative_eq!(scaled.to_hours(), 2.5, epsilon = 1e-9);
let divided = d1.div(2.0);
assert_relative_eq!(divided.to_minutes(), 30.0, epsilon = 1e-9);
}
#[test]
fn test_epoch_duration_arithmetic() {
let epoch = Epoch::j2000();
let duration = Duration::from_days(1.0);
let future = epoch.add_duration(duration);
let diff = future.duration_since(&epoch);
assert_relative_eq!(diff.to_days(), 1.0, epsilon = 1e-9);
let past = epoch.sub_duration(duration);
let diff2 = epoch.duration_since(&past);
assert_relative_eq!(diff2.to_days(), 1.0, epsilon = 1e-9);
}
#[test]
fn test_duration_conversions() {
let duration = Duration::from_days(1.5);
assert_relative_eq!(duration.to_days(), 1.5, epsilon = 1e-9);
assert_relative_eq!(duration.to_hours(), 36.0, epsilon = 1e-9);
assert_relative_eq!(duration.to_minutes(), 2160.0, epsilon = 1e-9);
assert_relative_eq!(duration.to_seconds(), 129600.0, epsilon = 1e-9);
}
#[test]
fn test_gregorian_roundtrip() {
let original_year = 2024;
let original_month = 10;
let original_day = 22;
let original_hour = 14;
let original_minute = 30;
let original_second = 45;
let original_nanos = 123456789;
let epoch = Epoch::from_gregorian_utc(
original_year,
original_month,
original_day,
original_hour,
original_minute,
original_second,
original_nanos,
);
let (year, month, day, hour, minute, second, nanos) = epoch.to_gregorian_utc();
assert_eq!(year, original_year);
assert_eq!(month, original_month);
assert_eq!(day, original_day);
assert_eq!(hour, original_hour);
assert_eq!(minute, original_minute);
assert_eq!(second, original_second);
assert_eq!(nanos, original_nanos);
}
#[test]
fn test_epoch_comparison() {
let epoch1 = Epoch::j2000();
let epoch2 = epoch1.add_duration(Duration::from_days(1.0));
assert!(epoch1.inner < epoch2.inner);
assert!(epoch2.inner > epoch1.inner);
assert_eq!(epoch1, epoch1);
}
#[test]
fn test_midnight_and_noon() {
let midnight = Epoch::from_gregorian_utc_midnight(2000, 1, 1);
let noon = Epoch::from_gregorian_utc_noon(2000, 1, 1);
let diff = noon.duration_since(&midnight);
assert_relative_eq!(diff.to_hours(), 12.0, epsilon = 1e-9);
}
#[test]
fn test_duration_signs() {
let pos = Duration::from_seconds(100.0);
let neg = Duration::from_seconds(-50.0);
let zero = Duration::from_seconds(0.0);
assert!(pos.is_positive());
assert!(!pos.is_negative());
assert!(neg.is_negative());
assert!(!neg.is_positive());
assert!(zero.is_zero());
assert!(!zero.is_positive());
assert!(!zero.is_negative());
}
#[test]
fn test_from_gregorian_tai() {
let epoch_tai = Epoch::from_gregorian_tai(2000, 1, 1, 12, 0, 0, 0);
let epoch_utc = Epoch::from_gregorian_utc(2000, 1, 1, 12, 0, 0, 0);
let diff = epoch_tai.duration_since(&epoch_utc);
assert!(diff.to_seconds().abs() > 0.0);
}
#[test]
fn test_from_gregorian_tt() {
let epoch_tt = Epoch::from_gregorian_tt(2000, 1, 1, 12, 0, 0, 0);
let j2000 = Epoch::j2000();
let diff = epoch_tt.duration_since(&j2000);
assert_relative_eq!(diff.to_seconds().abs(), 0.0, epsilon = 1e-6);
}
#[test]
fn test_from_gregorian_tdb() {
let epoch_tdb = Epoch::from_gregorian_tdb(2000, 1, 1, 12, 0, 0, 0);
let j2000 = Epoch::j2000();
let diff = epoch_tdb.duration_since(&j2000);
assert!(diff.to_seconds().abs() < 0.01); }
#[test]
fn test_from_mjd() {
let mjd = 51544.5; let epoch = Epoch::from_mjd(mjd, TimeScale::TT);
let j2000 = Epoch::j2000();
let diff = epoch.duration_since(&j2000);
assert_relative_eq!(diff.to_seconds(), 0.0, epsilon = 1e-6);
}
#[test]
fn test_from_jd() {
let jd = 2451545.0; let epoch = Epoch::from_jd(jd, TimeScale::TT);
let j2000 = Epoch::j2000();
let diff = epoch.duration_since(&j2000);
assert_relative_eq!(diff.to_seconds(), 0.0, epsilon = 1e-6);
}
#[test]
fn test_to_time_scale() {
let utc_epoch = Epoch::from_gregorian_utc(2020, 6, 15, 10, 30, 0, 0);
let tt_epoch = utc_epoch.to_time_scale(TimeScale::TT);
let tt_epoch2 = utc_epoch.to_tt();
let diff = tt_epoch.duration_since(&tt_epoch2);
assert_relative_eq!(diff.to_seconds(), 0.0, epsilon = 1e-9);
}
#[test]
fn test_to_tt_and_tdb() {
let epoch = Epoch::from_gregorian_utc(2015, 7, 1, 0, 0, 0, 0);
let tt_epoch = epoch.to_tt();
let tdb_epoch = epoch.to_tdb();
let diff = tt_epoch.duration_since(&tdb_epoch);
assert!(diff.to_seconds().abs() < 0.01); }
#[test]
fn test_to_gpst() {
let epoch = Epoch::from_gregorian_utc(2020, 1, 1, 0, 0, 0, 0);
let gps_epoch = epoch.to_gpst();
assert!(gps_epoch.to_mjd_utc() > 0.0);
}
#[test]
fn test_to_mjd_tdb() {
let j2000 = Epoch::j2000();
let mjd_tdb = j2000.to_mjd_tdb();
assert_relative_eq!(mjd_tdb, 51544.5, epsilon = 0.001);
}
#[test]
fn test_to_jd_tt_two_part() {
let j2000 = Epoch::j2000();
let (jd1, jd2) = j2000.to_jd_tt_two_part();
let total_jd = jd1 + jd2;
assert_relative_eq!(total_jd, 2451545.0, epsilon = 1e-10);
assert_relative_eq!(jd1, 2451545.0, epsilon = 0.1);
}
#[test]
fn test_to_tdb_seconds_since_j2000() {
let j2000 = Epoch::j2000();
let seconds_tdb = j2000.to_tdb_seconds_since_j2000();
assert_relative_eq!(seconds_tdb, 0.0, epsilon = 0.01);
let future = j2000.add_duration(Duration::from_days(1.0));
let seconds_future = future.to_tdb_seconds_since_j2000();
assert_relative_eq!(seconds_future, 86400.0, epsilon = 0.1);
}
#[test]
fn test_duration_since_j2000() {
let j2000 = Epoch::j2000();
let duration = j2000.duration_since_j2000();
assert_relative_eq!(duration.to_seconds(), 0.0, epsilon = 1e-6);
let future = j2000.add_duration(Duration::from_days(10.0));
let dur_future = future.duration_since_j2000();
assert_relative_eq!(dur_future.to_days(), 10.0, epsilon = 1e-9);
}
#[test]
fn test_to_gregorian_tai() {
let epoch = Epoch::from_gregorian_tai(2024, 3, 15, 14, 30, 45, 123456789);
let (year, month, day, hour, minute, second, nanos) = epoch.to_gregorian_tai();
assert_eq!(year, 2024);
assert_eq!(month, 3);
assert_eq!(day, 15);
assert_eq!(hour, 14);
assert_eq!(minute, 30);
assert_eq!(second, 45);
assert_eq!(nanos, 123456789);
}
#[test]
fn test_to_iso_string() {
let epoch = Epoch::from_gregorian_utc(2000, 1, 1, 12, 0, 0, 0);
let iso = epoch.to_iso_string();
assert!(iso.contains("2000"));
assert!(iso.contains("01"));
assert!(iso.contains("12"));
}
#[test]
fn test_duration_abs() {
let pos = Duration::from_seconds(100.0);
let neg = Duration::from_seconds(-100.0);
let abs_pos = pos.abs();
let abs_neg = neg.abs();
assert_relative_eq!(abs_pos.to_seconds(), 100.0, epsilon = 1e-9);
assert_relative_eq!(abs_neg.to_seconds(), 100.0, epsilon = 1e-9);
}
#[test]
fn test_duration_from_days() {
let duration = Duration::from_days(2.5);
assert_relative_eq!(duration.to_seconds(), 2.5 * 86400.0, epsilon = 1e-9);
assert_relative_eq!(duration.to_hours(), 60.0, epsilon = 1e-9);
}
#[test]
fn test_epoch_now() {
let now = Epoch::now();
let j2000 = Epoch::j2000();
let diff = now.duration_since(&j2000);
assert!(diff.to_days() > 9000.0); }
#[test]
fn test_mjd_jd_relationship() {
let epoch = Epoch::from_gregorian_utc(2020, 6, 15, 12, 0, 0, 0);
let mjd_utc = epoch.to_mjd_utc();
let jd_utc = epoch.to_jd_utc();
assert_relative_eq!(jd_utc, mjd_utc + 2400000.5, epsilon = 1e-10);
}
#[test]
fn test_time_scale_consistency() {
let year = 2020;
let month = 3;
let day = 15;
let utc = Epoch::from_gregorian_utc(year, month, day, 12, 0, 0, 0);
let tai = Epoch::from_gregorian_tai(year, month, day, 12, 0, 0, 0);
let mjd_utc = utc.to_mjd_utc();
let mjd_tai_native = tai.to_mjd_tai();
assert!((mjd_tai_native - mjd_utc).abs() < 1.0); }
#[test]
fn test_duration_edge_cases() {
let tiny = Duration::from_seconds(1e-9);
assert!(tiny.to_seconds() > 0.0);
assert!(tiny.is_positive());
let large = Duration::from_days(365.25 * 100.0); assert_relative_eq!(large.to_days(), 36525.0, epsilon = 1.0);
}
#[test]
fn test_leap_second_aware() {
let pre_leap = Epoch::from_gregorian_utc(2015, 6, 30, 23, 59, 59, 0);
let post_leap = Epoch::from_gregorian_utc(2015, 7, 1, 0, 0, 0, 0);
let diff = post_leap.duration_since(&pre_leap);
assert_relative_eq!(diff.to_seconds(), 1.0, epsilon = 0.1);
}
}