use crate::SimulationTime;
use astrodyn_time::epoch::SECONDS_PER_DAY;
use astrodyn_time::leap_second::default_leap_second_table;
pub fn j2000() -> SimulationTime {
SimulationTime::at_j2000(default_leap_second_table())
}
pub fn at_tai_tjt(tai_tjt: f64) -> SimulationTime {
SimulationTime::new(tai_tjt, default_leap_second_table())
}
pub fn clementine_1994() -> SimulationTime {
at_tai_tjt(8_815.000_324_074_073)
}
pub fn dawn_mars_2009() -> SimulationTime {
at_tai_tjt(14_879.958_727)
}
pub fn at_utc(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: f64,
) -> SimulationTime {
assert!(
(1..=12).contains(&month),
"at_utc: month must be in 1..=12, got {month}"
);
assert!(
(1..=31).contains(&day),
"at_utc: day must be in 1..=31, got {day}"
);
assert!(hour < 24, "at_utc: hour must be in 0..24, got {hour}");
assert!(minute < 60, "at_utc: minute must be in 0..60, got {minute}");
assert!(
second.is_finite() && (0.0..=60.0).contains(&second),
"at_utc: second must be in 0.0..=60.0, got {second}"
);
let (y, m) = if month <= 2 {
(year - 1, (month + 12) as i32)
} else {
(year, month as i32)
};
let a = y.div_euclid(100);
let b = 2 - a + a.div_euclid(4); let jd_at_day_start = (365.25 * (y + 4716) as f64).floor()
+ (30.6001 * (m + 1) as f64).floor()
+ day as f64
+ b as f64
- 1524.5;
let frac_day = (hour as f64 * 3600.0 + minute as f64 * 60.0 + second) / SECONDS_PER_DAY;
let utc_tjt = jd_at_day_start + frac_day - 2_440_000.5;
let leap_table = default_leap_second_table();
let tai_tjt = leap_table.utc_to_tai_tjt(utc_tjt);
SimulationTime::new(tai_tjt, leap_table)
}
pub fn at_iso(s: &str) -> SimulationTime {
let bytes = s.as_bytes();
assert!(
bytes.len() >= 19,
"at_iso: timestamp too short ({} bytes); expected RFC 3339 form like \
'2026-01-28T06:42:03.51Z'",
bytes.len()
);
let parse_u32 = |slice: &str, label: &str| -> u32 {
slice
.parse::<u32>()
.unwrap_or_else(|e| panic!("at_iso: invalid {label} {slice:?}: {e}"))
};
let year: i32 = s[0..4]
.parse()
.unwrap_or_else(|e| panic!("at_iso: invalid year {:?}: {e}", &s[0..4]));
let month = parse_u32(&s[5..7], "month");
let day = parse_u32(&s[8..10], "day");
let sep = bytes[10];
assert!(
sep == b'T' || sep == b' ',
"at_iso: expected 'T' or ' ' separator at byte 10, got {:?}",
sep as char
);
let hour = parse_u32(&s[11..13], "hour");
let minute = parse_u32(&s[14..16], "minute");
let end = s.find(['Z', 'z']).unwrap_or(s.len());
let sec_str = &s[17..end];
let second: f64 = sec_str
.parse()
.unwrap_or_else(|e| panic!("at_iso: invalid second {sec_str:?}: {e}"));
at_utc(year, month, day, hour, minute, second)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn at_utc_matches_j2000_constant() {
let t = at_utc(2000, 1, 1, 11, 58, 55.816);
let expected = astrodyn_time::epoch::J2000_TAI_TJT;
let err = (t.tai_tjt_at_epoch - expected).abs();
assert!(
err < 1e-7,
"at_utc(J2000) tai_tjt = {}, expected {expected}, err = {err}",
t.tai_tjt_at_epoch
);
}
#[test]
fn at_iso_matches_at_utc_with_fractional_seconds() {
let from_iso = at_iso("2026-01-28T06:42:03.51Z");
let from_utc = at_utc(2026, 1, 28, 6, 42, 3.51);
assert_eq!(from_iso.tai_tjt_at_epoch, from_utc.tai_tjt_at_epoch);
}
#[test]
fn at_iso_accepts_space_separator() {
let from_t = at_iso("2026-01-28T06:42:03.51Z");
let from_space = at_iso("2026-01-28 06:42:03.51");
assert_eq!(from_t.tai_tjt_at_epoch, from_space.tai_tjt_at_epoch);
}
#[test]
fn at_utc_matches_cc8_epoch() {
let t = at_utc(2026, 1, 28, 6, 42, 3.51);
let expected_tai_tjt = 21_068.279_635_532_407_f64;
let err = (t.tai_tjt_at_epoch - expected_tai_tjt).abs();
assert!(
err < 1e-9,
"at_utc(CC8) tai_tjt = {}, expected {expected_tai_tjt}, err = {err}",
t.tai_tjt_at_epoch
);
}
}