1use crate::data::runtime_data::{active_time_data, time_data_eop_at};
16use qtty::{Day, Second};
17
18pub fn eop_start() -> Option<Day> {
21 active_time_data()
22 .eop_start_mjd()
23 .map(|v| Day::new(v as f64))
24}
25
26pub fn eop_observed_end() -> Option<Day> {
29 active_time_data()
30 .eop_observed_end_mjd()
31 .map(|v| Day::new(v as f64))
32}
33
34pub fn eop_end() -> Option<Day> {
37 active_time_data().eop_end_mjd().map(|v| Day::new(v as f64))
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct EopValues {
54 pub mjd_utc: Day,
55 pub pm_xp: Option<qtty::f64::Arcsecond>,
56 pub pm_yp: Option<qtty::f64::Arcsecond>,
57 pub ut1_minus_utc: Second,
58 pub lod: Option<qtty::f64::Millisecond>,
59 pub dx: Option<qtty::f64::MilliArcsecond>,
60 pub dy: Option<qtty::f64::MilliArcsecond>,
61 pub ut1_observed: bool,
63}
64
65#[inline]
67pub fn builtin_eop_covers(mjd_utc: Day) -> bool {
68 let data = active_time_data();
69 time_data_eop_at(data.as_ref(), mjd_utc).is_some()
70}
71
72pub fn builtin_eop_at(mjd_utc: Day) -> Option<EopValues> {
83 let data = active_time_data();
84 time_data_eop_at(data.as_ref(), mjd_utc)
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::archive::time::{EopPoint, TimeDataBundle, TimeDataProvenance, UtcTaiSegment};
91 use crate::data::runtime_data::with_test_time_data;
92 use qtty::{Arcsecond, Day, MilliArcsecond, Millisecond, Second};
93
94 fn make_test_eop_bundle(points: Vec<EopPoint>) -> TimeDataBundle {
95 TimeDataBundle::new(
96 vec![UtcTaiSegment {
97 start_mjd: 41317,
98 end_mjd: None,
99 base: Second::new(37.0),
100 reference_mjd: 41317.0,
101 slope_seconds_per_day: 0.0,
102 }],
103 vec![(41714.0, 42.184), (42369.0, 45.0)],
104 41714.0,
105 points,
106 TimeDataProvenance::new("test", "x", "x", "x", "x"),
107 )
108 }
109
110 fn three_point_fixture() -> Vec<EopPoint> {
111 vec![
112 EopPoint {
113 mjd: 50000,
114 pm_observed: true,
115 ut1_observed: true,
116 nutation_observed: true,
117 pm_xp: Some(Arcsecond::new(0.1)),
118 pm_yp: Some(Arcsecond::new(0.2)),
119 ut1_minus_utc: Second::new(0.3),
120 lod: Some(Millisecond::new(1.0)),
121 dx: Some(MilliArcsecond::new(0.01)),
122 dy: Some(MilliArcsecond::new(0.02)),
123 },
124 EopPoint {
125 mjd: 50001,
126 pm_observed: true,
127 ut1_observed: true,
128 nutation_observed: true,
129 pm_xp: Some(Arcsecond::new(0.2)),
130 pm_yp: Some(Arcsecond::new(0.4)),
131 ut1_minus_utc: Second::new(0.5),
132 lod: Some(Millisecond::new(2.0)),
133 dx: None,
134 dy: None,
135 },
136 EopPoint {
137 mjd: 50002,
138 pm_observed: false,
139 ut1_observed: false,
140 nutation_observed: false,
141 pm_xp: Some(Arcsecond::new(0.3)),
142 pm_yp: Some(Arcsecond::new(0.6)),
143 ut1_minus_utc: Second::new(0.7),
144 lod: None,
145 dx: None,
146 dy: None,
147 },
148 ]
149 }
150
151 #[test]
152 fn covers_start_and_end() {
153 let bundle = make_test_eop_bundle(three_point_fixture());
154 with_test_time_data(bundle, || {
155 assert!(builtin_eop_covers(Day::new(50000.0)));
156 assert!(builtin_eop_covers(Day::new(50002.0)));
157 assert!(!builtin_eop_covers(Day::new(49999.0)));
158 assert!(!builtin_eop_covers(Day::new(50003.0)));
159 });
160 }
161
162 #[test]
163 fn exact_point_matches_source() {
164 let points = three_point_fixture();
165 let bundle = make_test_eop_bundle(points.clone());
166 with_test_time_data(bundle, || {
167 let mid = &points[1];
168 let got = builtin_eop_at(Day::new(mid.mjd as f64)).unwrap();
169 assert_eq!(got.pm_xp.map(|v| v.value()), mid.pm_xp.map(|v| v.value()));
170 assert_eq!(got.pm_yp.map(|v| v.value()), mid.pm_yp.map(|v| v.value()));
171 assert!(
172 (got.ut1_minus_utc.value() - mid.ut1_minus_utc.value()).abs() < 1e-12,
173 "ut1: {} vs {}",
174 got.ut1_minus_utc.value(),
175 mid.ut1_minus_utc.value()
176 );
177 assert_eq!(got.dx.map(|v| v.value()), mid.dx.map(|v| v.value()));
178 assert_eq!(got.dy.map(|v| v.value()), mid.dy.map(|v| v.value()));
179 });
180 }
181
182 #[test]
183 fn midpoint_is_halfway() {
184 let points = three_point_fixture();
185 let bundle = make_test_eop_bundle(points.clone());
186 with_test_time_data(bundle, || {
187 let got = builtin_eop_at(Day::new(50000.5)).unwrap();
188 let expected =
189 0.5 * (points[0].ut1_minus_utc.value() + points[1].ut1_minus_utc.value());
190 assert!((got.ut1_minus_utc.value() - expected).abs() < 1e-12);
191 });
192 }
193
194 #[test]
195 fn missing_optional_fields_remain_missing() {
196 let bundle = make_test_eop_bundle(three_point_fixture());
197 with_test_time_data(bundle, || {
198 let got = builtin_eop_at(Day::new(50001.5)).unwrap();
200 assert_eq!(got.dx, None);
201 assert_eq!(got.dy, None);
202 });
203 }
204
205 #[test]
206 fn out_of_range_returns_none() {
207 let bundle = make_test_eop_bundle(three_point_fixture());
208 with_test_time_data(bundle, || {
209 assert!(builtin_eop_at(Day::new(49990.0)).is_none());
210 assert!(builtin_eop_at(Day::new(50010.0)).is_none());
211 });
212 }
213
214 #[test]
215 fn no_eop_data_returns_none() {
216 let bundle = make_test_eop_bundle(Vec::new());
217 with_test_time_data(bundle, || {
218 assert!(builtin_eop_at(Day::new(50000.0)).is_none());
219 assert!(!builtin_eop_covers(Day::new(50000.0)));
220 });
221 }
222}