use crate::constants::{J2000_JD, J2000_MJD, J2000_TAI_UNIX_S, SECONDS_PER_DAY};
use crate::leap_seconds::{get_tai_utc_offset, tai_to_utc_full};
use crate::types::{BrightDateError, BrightDateValue};
use chrono::{DateTime, TimeZone, Utc};
const NANOS_PER_SEC: u32 = 1_000_000_000;
const J2000_TAI_UNIX_S_INT: i64 = 946_727_967;
const J2000_TAI_UNIX_NS: u32 = 816_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BrightInstant {
tai_seconds: i64,
tai_nanos: u32,
}
impl BrightInstant {
pub const J2000: Self = Self {
tai_seconds: 0,
tai_nanos: 0,
};
pub fn from_tai_components(
tai_seconds: i64,
tai_nanos: u32,
) -> Result<Self, BrightDateError> {
if tai_nanos >= NANOS_PER_SEC {
return Err(BrightDateError::OutOfRange(format!(
"tai_nanos must be < 1_000_000_000, got {tai_nanos}"
)));
}
Ok(Self {
tai_seconds,
tai_nanos,
})
}
pub fn from_brightdate(bd: BrightDateValue) -> Result<Self, BrightDateError> {
if !bd.is_finite() {
return Err(BrightDateError::InvalidInput(format!(
"expected finite BrightDate, got {bd}"
)));
}
let total_seconds = bd * SECONDS_PER_DAY;
let secs_floor = total_seconds.floor();
let frac = total_seconds - secs_floor;
let mut nanos = (frac * NANOS_PER_SEC as f64).round() as i64;
let mut secs = secs_floor as i64;
if nanos >= NANOS_PER_SEC as i64 {
nanos -= NANOS_PER_SEC as i64;
secs += 1;
} else if nanos < 0 {
nanos += NANOS_PER_SEC as i64;
secs -= 1;
}
Ok(Self {
tai_seconds: secs,
tai_nanos: nanos as u32,
})
}
pub fn from_unix_ms(ms: i64) -> Self {
let utc_seconds = ms.div_euclid(1000);
let utc_ms_within = ms.rem_euclid(1000) as u32;
let offset = get_tai_utc_offset(utc_seconds) as i64;
let tai_unix_s = utc_seconds + offset;
let mut secs = tai_unix_s - J2000_TAI_UNIX_S_INT;
let target_nanos = (utc_ms_within as i64) * 1_000_000 - J2000_TAI_UNIX_NS as i64;
let nanos = if target_nanos < 0 {
secs -= 1;
(target_nanos + NANOS_PER_SEC as i64) as u32
} else {
target_nanos as u32
};
Self {
tai_seconds: secs,
tai_nanos: nanos,
}
}
pub fn from_julian_date(jd: f64) -> Result<Self, BrightDateError> {
Self::from_brightdate(jd - J2000_JD)
}
pub fn from_modified_julian_date(mjd: f64) -> Result<Self, BrightDateError> {
Self::from_brightdate(mjd - J2000_MJD)
}
#[inline]
pub const fn tai_seconds_since_j2000(self) -> i64 {
self.tai_seconds
}
#[inline]
pub const fn tai_nanos(self) -> u32 {
self.tai_nanos
}
pub fn to_brightdate(self) -> BrightDateValue {
(self.tai_seconds as f64 + self.tai_nanos as f64 / NANOS_PER_SEC as f64)
/ SECONDS_PER_DAY
}
pub fn to_unix_ms(self) -> i64 {
let mut tai_unix_s = J2000_TAI_UNIX_S_INT + self.tai_seconds;
let mut tai_ns = self.tai_nanos as i64 + J2000_TAI_UNIX_NS as i64;
if tai_ns >= NANOS_PER_SEC as i64 {
tai_ns -= NANOS_PER_SEC as i64;
tai_unix_s += 1;
}
let conv = tai_to_utc_full(tai_unix_s);
conv.utc_unix_seconds * 1000 + tai_ns / 1_000_000
}
pub fn to_date_time(self) -> DateTime<Utc> {
Utc.timestamp_millis_opt(self.to_unix_ms())
.single()
.unwrap_or(DateTime::<Utc>::UNIX_EPOCH)
}
pub fn to_julian_date(self) -> f64 {
self.to_brightdate() + J2000_JD
}
pub fn to_modified_julian_date(self) -> f64 {
self.to_brightdate() + J2000_MJD
}
pub fn to_iso(self) -> String {
let mut tai_unix_s = J2000_TAI_UNIX_S_INT + self.tai_seconds;
let mut tai_ns = self.tai_nanos as i64 + J2000_TAI_UNIX_NS as i64;
if tai_ns >= NANOS_PER_SEC as i64 {
tai_ns -= NANOS_PER_SEC as i64;
tai_unix_s += 1;
}
let conv = tai_to_utc_full(tai_unix_s);
let millis = tai_ns / 1_000_000;
if conv.is_leap_second {
let dt = Utc
.timestamp_opt(conv.utc_unix_seconds, 0)
.single()
.unwrap_or(DateTime::<Utc>::UNIX_EPOCH);
return format!(
"{}T{}:{}:60.{:03}Z",
dt.format("%Y-%m-%d"),
dt.format("%H"),
dt.format("%M"),
millis.clamp(0, 999),
);
}
let dt = Utc
.timestamp_opt(conv.utc_unix_seconds, (tai_ns % 1_000_000_000) as u32)
.single()
.unwrap_or(DateTime::<Utc>::UNIX_EPOCH);
format!("{}.{:03}Z", dt.format("%Y-%m-%dT%H:%M:%S"), millis)
}
pub fn add_nanos(self, nanos: i64) -> Self {
let total_nanos = self.tai_nanos as i64 + nanos;
let extra_secs = total_nanos.div_euclid(NANOS_PER_SEC as i64);
let new_nanos = total_nanos.rem_euclid(NANOS_PER_SEC as i64) as u32;
Self {
tai_seconds: self.tai_seconds + extra_secs,
tai_nanos: new_nanos,
}
}
pub fn add_seconds(self, seconds: i64) -> Self {
Self {
tai_seconds: self.tai_seconds + seconds,
tai_nanos: self.tai_nanos,
}
}
pub fn nanos_since(self, earlier: Self) -> i128 {
let ds = self.tai_seconds as i128 - earlier.tai_seconds as i128;
let dn = self.tai_nanos as i128 - earlier.tai_nanos as i128;
ds * NANOS_PER_SEC as i128 + dn
}
pub fn seconds_since(self, earlier: Self) -> f64 {
let ds = (self.tai_seconds - earlier.tai_seconds) as f64;
let dn = (self.tai_nanos as f64 - earlier.tai_nanos as f64) / NANOS_PER_SEC as f64;
ds + dn
}
}
impl From<BrightInstant> for BrightDateValue {
fn from(i: BrightInstant) -> Self {
i.to_brightdate()
}
}
impl TryFrom<BrightDateValue> for BrightInstant {
type Error = BrightDateError;
fn try_from(bd: BrightDateValue) -> Result<Self, Self::Error> {
Self::from_brightdate(bd)
}
}
pub const J2000_TAI_UNIX_S_FRACT_NS: u32 = J2000_TAI_UNIX_NS;
#[allow(dead_code)]
const _STATIC_INVARIANT: () = {
assert!(J2000_TAI_UNIX_S_INT == 946_727_967);
assert!(J2000_TAI_UNIX_NS == 816_000_000);
let _ = J2000_TAI_UNIX_S;
};
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::J2000_UTC_UNIX_MS;
const J2000_UTC_UNIX_MS_I: i64 = 946_727_935_816;
#[test]
fn j2000_constant_is_zero() {
assert_eq!(BrightInstant::J2000.tai_seconds_since_j2000(), 0);
assert_eq!(BrightInstant::J2000.tai_nanos(), 0);
}
#[test]
fn from_unix_ms_at_j2000_is_zero() {
let i = BrightInstant::from_unix_ms(J2000_UTC_UNIX_MS_I);
assert_eq!(i, BrightInstant::J2000);
}
#[test]
fn from_unix_ms_at_j2000_via_f64_constant() {
let i = BrightInstant::from_unix_ms(J2000_UTC_UNIX_MS as i64);
assert_eq!(i, BrightInstant::J2000);
}
#[test]
fn unix_ms_roundtrip_at_j2000() {
let ms = J2000_UTC_UNIX_MS_I;
let i = BrightInstant::from_unix_ms(ms);
assert_eq!(i.to_unix_ms(), ms);
}
#[test]
fn unix_ms_roundtrip_modern() {
let ms = 1_700_000_000_000_i64; let i = BrightInstant::from_unix_ms(ms);
assert_eq!(i.to_unix_ms(), ms);
}
#[test]
fn unix_ms_roundtrip_pre_unix_epoch() {
let ms = -1_000_000_000_000_i64; let i = BrightInstant::from_unix_ms(ms);
assert_eq!(i.to_unix_ms(), ms);
}
#[test]
fn nanosecond_precision_preserved() {
let i = BrightInstant::from_tai_components(0, 1).unwrap();
assert_eq!(i.tai_nanos(), 1);
let later = i.add_nanos(999_999_999);
assert_eq!(later.tai_seconds_since_j2000(), 1);
assert_eq!(later.tai_nanos(), 0);
}
#[test]
fn add_nanos_carries_correctly() {
let i = BrightInstant::J2000.add_nanos(1_500_000_000);
assert_eq!(i.tai_seconds_since_j2000(), 1);
assert_eq!(i.tai_nanos(), 500_000_000);
}
#[test]
fn add_nanos_negative() {
let i = BrightInstant::J2000.add_nanos(-1);
assert_eq!(i.tai_seconds_since_j2000(), -1);
assert_eq!(i.tai_nanos(), 999_999_999);
}
#[test]
fn nanos_since_is_signed() {
let a = BrightInstant::J2000;
let b = BrightInstant::J2000.add_nanos(1_500_000_000);
assert_eq!(b.nanos_since(a), 1_500_000_000);
assert_eq!(a.nanos_since(b), -1_500_000_000);
}
#[test]
fn brightdate_roundtrip_modern() {
let bd = 9_628.5_f64;
let i = BrightInstant::from_brightdate(bd).unwrap();
let back = i.to_brightdate();
assert!((back - bd).abs() < 1e-9, "drift: {}", back - bd);
}
#[test]
fn brightdate_roundtrip_far_future_holds_seconds() {
let bd = 2_922_500.123_456_789_f64;
let i = BrightInstant::from_brightdate(bd).unwrap();
let back = i.to_brightdate();
let drift_us = (back - bd) * SECONDS_PER_DAY * 1_000_000.0;
assert!(drift_us.abs() < 1.0, "drift {drift_us} μs");
}
#[test]
fn julian_date_exact_at_j2000() {
assert_eq!(BrightInstant::J2000.to_julian_date(), 2_451_545.0);
let i = BrightInstant::from_julian_date(2_451_545.0).unwrap();
assert_eq!(i, BrightInstant::J2000);
}
#[test]
fn modified_julian_date_exact_at_j2000() {
assert_eq!(BrightInstant::J2000.to_modified_julian_date(), 51_544.5);
let i = BrightInstant::from_modified_julian_date(51_544.5).unwrap();
assert_eq!(i, BrightInstant::J2000);
}
#[test]
fn iso_at_j2000_is_correct_utc_label() {
let s = BrightInstant::J2000.to_iso();
assert!(s.starts_with("2000-01-01T11:58:55.816"), "got: {s}");
assert!(s.ends_with('Z'));
}
#[test]
fn iso_one_day_after_j2000() {
let i = BrightInstant::J2000.add_seconds(86_400);
let s = i.to_iso();
assert!(s.starts_with("2000-01-02T11:58:55.816"), "got: {s}");
}
#[test]
fn ordering_is_chronological() {
let earlier = BrightInstant::from_tai_components(0, 500).unwrap();
let later = BrightInstant::from_tai_components(0, 501).unwrap();
assert!(earlier < later);
let much_later = BrightInstant::from_tai_components(1, 0).unwrap();
assert!(later < much_later);
}
#[test]
fn rejects_oversized_nanos() {
let r = BrightInstant::from_tai_components(0, NANOS_PER_SEC);
assert!(r.is_err());
}
#[test]
fn rejects_nonfinite_brightdate() {
assert!(BrightInstant::from_brightdate(f64::NAN).is_err());
assert!(BrightInstant::from_brightdate(f64::INFINITY).is_err());
}
}