1use crate::data::active::{active_time_data, time_data_eop_at};
15use qtty::{Day, Second};
16
17#[cfg(test)]
18use crate::generated::eop_data::{EOP_END_MJD, EOP_POINTS, EOP_START_MJD};
19
20#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct EopValues {
35 pub mjd_utc: Day,
36 pub pm_xp_arcsec: Option<f64>,
37 pub pm_yp_arcsec: Option<f64>,
38 pub ut1_minus_utc: Second,
39 pub lod_milliseconds: Option<f64>,
40 pub dx_milliarcsec: Option<f64>,
41 pub dy_milliarcsec: Option<f64>,
42 pub ut1_observed: bool,
44}
45
46#[inline]
48pub fn builtin_eop_covers(mjd_utc: Day) -> bool {
49 let data = active_time_data();
50 time_data_eop_at(data.as_ref(), mjd_utc).is_some()
51}
52
53pub fn builtin_eop_at(mjd_utc: Day) -> Option<EopValues> {
60 let data = active_time_data();
61 time_data_eop_at(data.as_ref(), mjd_utc)
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn covers_start_and_end() {
70 assert!(builtin_eop_covers(Day::new(EOP_START_MJD as f64)));
71 assert!(builtin_eop_covers(Day::new(EOP_END_MJD as f64)));
72 assert!(!builtin_eop_covers(Day::new(EOP_START_MJD as f64 - 1.0)));
73 assert!(!builtin_eop_covers(Day::new(EOP_END_MJD as f64 + 1.0)));
74 }
75
76 #[test]
77 fn exact_point_matches_source() {
78 let mid = EOP_POINTS[EOP_POINTS.len() / 2];
79 let got = builtin_eop_at(Day::new(mid.mjd as f64)).unwrap();
80 assert_eq!(got.pm_xp_arcsec, mid.pm_xp_arcsec);
81 assert_eq!(got.pm_yp_arcsec, mid.pm_yp_arcsec);
82 assert!(
83 (got.ut1_minus_utc.value() - mid.ut1_minus_utc_seconds).abs() < 1e-12,
84 "ut1: {} vs {}",
85 got.ut1_minus_utc.value(),
86 mid.ut1_minus_utc_seconds
87 );
88 assert_eq!(got.dx_milliarcsec, mid.dx_milliarcsec);
89 assert_eq!(got.dy_milliarcsec, mid.dy_milliarcsec);
90 }
91
92 #[test]
93 fn midpoint_is_halfway() {
94 let lo = EOP_POINTS[100];
95 let hi = EOP_POINTS[101];
96 let got = builtin_eop_at(Day::new(lo.mjd as f64 + 0.5)).unwrap();
97 let expected = 0.5 * (lo.ut1_minus_utc_seconds + hi.ut1_minus_utc_seconds);
98 assert!((got.ut1_minus_utc.value() - expected).abs() < 1e-12);
99 }
100
101 #[test]
102 fn missing_optional_fields_remain_missing() {
103 let idx = EOP_POINTS
104 .windows(2)
105 .position(|window| {
106 window[0].dx_milliarcsec.is_none() && window[1].dx_milliarcsec.is_none()
107 })
108 .expect("generated EOP tail should include rows with blank nutation fields");
109 let lo = EOP_POINTS[idx];
110 let got = builtin_eop_at(Day::new(lo.mjd as f64 + 0.5)).unwrap();
111 assert_eq!(got.dx_milliarcsec, None);
112 assert_eq!(got.dy_milliarcsec, None);
113 }
114
115 #[test]
116 fn out_of_range_returns_none() {
117 assert!(builtin_eop_at(Day::new(EOP_START_MJD as f64 - 10.0)).is_none());
118 assert!(builtin_eop_at(Day::new(EOP_END_MJD as f64 + 10.0)).is_none());
119 }
120}