const JD_UNIX_EPOCH: f64 = 2_440_587.5;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UtcEpoch {
days: i64,
sec_of_day: f64,
}
const MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let mp = (m as i64 + 9) % 12;
let doy = (153 * mp + 2) / 5 + d as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
(if m <= 2 { y + 1 } else { y }, m, d)
}
struct Civil {
y: i64,
mo: u32,
d: u32,
h: i64,
mi: i64,
s: i64,
us: i64,
}
impl UtcEpoch {
pub fn from_calendar(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: f64,
) -> Self {
let days = days_from_civil(year as i64, month, day);
Self::normalised(days, hour as f64 * 3600.0 + minute as f64 * 60.0 + second)
}
pub fn from_epoch_utc(e: &crate::rinex::EpochUtc) -> Self {
Self::from_calendar(e.year, e.month, e.day, e.hour, e.minute, e.second)
}
pub fn from_jd_utc(jd: f64) -> Self {
let x = jd - JD_UNIX_EPOCH;
let days = x.floor();
Self::normalised(days as i64, (x - days) * 86_400.0)
}
fn normalised(days: i64, sec: f64) -> Self {
let carry = (sec / 86_400.0).floor();
Self {
days: days + carry as i64,
sec_of_day: sec - carry * 86_400.0,
}
}
pub fn jd_utc(&self, offset_s: f64) -> f64 {
JD_UNIX_EPOCH + self.days as f64 + (self.sec_of_day + offset_s) / 86_400.0
}
fn civil(&self, offset_s: f64) -> Civil {
let total_us = ((self.sec_of_day + offset_s) * 1e6).round() as i64;
let day_us = 86_400_000_000i64;
let carry = total_us.div_euclid(day_us);
let rem = total_us.rem_euclid(day_us);
let (y, mo, d) = civil_from_days(self.days + carry);
let us = rem % 1_000_000;
let secs = rem / 1_000_000;
Civil {
y,
mo,
d,
h: secs / 3600,
mi: (secs % 3600) / 60,
s: secs % 60,
us,
}
}
pub fn iso(&self, offset_s: f64) -> String {
let c = self.civil(offset_s);
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
c.y, c.mo, c.d, c.h, c.mi, c.s, c.us
)
}
pub fn stk(&self, offset_s: f64) -> String {
let c = self.civil(offset_s);
format!(
"{} {} {:04} {:02}:{:02}:{:02}.{:06}",
c.d,
MONTHS[(c.mo - 1) as usize],
c.y,
c.h,
c.mi,
c.s,
c.us
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calendar_round_trips_through_iso_and_stk() {
let e = UtcEpoch::from_calendar(2024, 2, 29, 23, 59, 59.5);
assert_eq!(e.iso(0.0), "2024-02-29T23:59:59.500000Z");
assert_eq!(e.iso(0.5), "2024-03-01T00:00:00.000000Z");
assert_eq!(e.stk(0.5), "1 Mar 2024 00:00:00.000000");
assert_eq!(e.iso(-86_400.0), "2024-02-28T23:59:59.500000Z");
}
#[test]
fn julian_date_matches_the_engine_convention() {
let e = UtcEpoch::from_calendar(2000, 1, 1, 12, 0, 0.0);
assert_eq!(e.jd_utc(0.0), 2_451_545.0);
let f = UtcEpoch::from_jd_utc(2_451_545.0);
assert_eq!(f.iso(0.0), "2000-01-01T12:00:00.000000Z");
let w = UtcEpoch::from_jd_utc(crate::walker::walker_epoch_jd());
assert_eq!(w.iso(0.0), "2018-06-12T00:00:00.000000Z");
assert_eq!(
crate::timescales::julian_date(2018, 6, 12, 0, 0, 0.0),
crate::walker::walker_epoch_jd()
);
}
}