1#![allow(clippy::indexing_slicing)]
13#![allow(clippy::excessive_precision)]
14#![allow(clippy::approx_constant)]
15#![allow(clippy::eq_op)]
16
17use crate::{Dt, DtErr, DtErrKind, Real, an_err};
18use alloc::string::String;
19use alloc::vec::Vec;
20use core::cmp::Ordering;
21
22#[derive(Debug, Clone, Copy, Default)]
23pub enum Separator {
24 #[default]
25 Whitespace,
26 Comma,
27 Tab,
28 Pipe,
29 Semicolon,
30}
31
32#[derive(Debug, Clone, Copy, Default)]
44pub enum EopFormat {
45 #[default]
47 Finals2000A,
48 C04,
50 Custom(CustomEopCols),
52}
53
54#[derive(Debug, Clone, Copy)]
56pub struct CustomEopCols {
57 pub mjd: usize,
58 pub offset: usize,
59 pub pm_x: Option<usize>,
60 pub pm_y: Option<usize>,
61}
62
63#[derive(Debug, Clone, Copy)]
64pub struct EopDataRow {
65 pub mjd: Real,
66 pub offset: Real,
68 pub pm_x: Real,
70 pub pm_y: Real,
72}
73
74#[derive(Debug, Clone)]
80pub struct EopData {
81 rows: Vec<EopDataRow>,
82}
83
84#[cfg(feature = "std")]
85impl EopData {
86 pub fn data_from_reader<R: std::io::BufRead>(
87 mut reader: R,
88 format: EopFormat,
89 separator: Separator,
90 ) -> Result<Vec<EopDataRow>, DtErr> {
91 let mut line_buf = String::with_capacity(256);
92 let mut rows = Vec::new();
93
94 loop {
95 line_buf.clear();
96
97 let bytes_read = match reader.read_line(&mut line_buf) {
98 Ok(0) => break,
99 Ok(n) => n,
100 Err(e) => {
101 return Err(an_err!(DtErrKind::IOErr, "read line: {}", e));
102 }
103 };
104
105 if bytes_read > Self::MAX_LINE_LEN {
106 continue;
107 }
108
109 let trimmed = line_buf.trim();
110 if trimmed.is_empty() || trimmed.starts_with('#') {
111 continue;
112 }
113
114 if let Some(row) = Self::try_parse_row(trimmed, format, separator) {
115 rows.push(row);
116 }
117 }
118
119 if rows.is_empty() {
120 return Err(an_err!(DtErrKind::Incomplete, "no valid rows"));
121 }
122
123 rows.sort_by(|a, b| a.mjd.partial_cmp(&b.mjd).unwrap_or(Ordering::Equal));
124 Ok(rows)
125 }
126
127 pub fn data_from_text_file<P: AsRef<std::path::Path>>(
128 path: P,
129 format: EopFormat,
130 separator: Separator,
131 ) -> Result<Vec<EopDataRow>, DtErr> {
132 use std::fs::File;
133 use std::io::BufReader;
134
135 let path = path.as_ref();
136 let file = File::open(path)
137 .map_err(|e| an_err!(DtErrKind::IOErr, "open file: '{}': {}", path.display(), e))?;
138
139 let reader = BufReader::new(file);
140 Self::data_from_reader(reader, format, separator)
141 }
142
143 pub fn from_text_file<P: AsRef<std::path::Path>>(
144 path: P,
145 format: EopFormat,
146 separator: Separator,
147 ) -> Result<Self, DtErr> {
148 let rows = Self::data_from_text_file(path, format, separator)?;
149 Ok(Self { rows })
150 }
151}
152
153impl EopData {
154 pub const MAX_LINE_LEN: usize = 8192;
155
156 fn try_parse_row(trimmed: &str, format: EopFormat, separator: Separator) -> Option<EopDataRow> {
158 let parts: Vec<&str> = match separator {
159 Separator::Whitespace => trimmed.split_whitespace().collect(),
160 Separator::Comma => trimmed.split(',').map(|s| s.trim()).collect(),
161 Separator::Tab => trimmed.split('\t').map(|s| s.trim()).collect(),
162 Separator::Pipe => trimmed.split('|').map(|s| s.trim()).collect(),
163 Separator::Semicolon => trimmed.split(';').map(|s| s.trim()).collect(),
164 };
165
166 if parts.len() < 2 {
167 return None;
168 }
169
170 let (mjd, offset, pm_x, pm_y) = match format {
171 EopFormat::Finals2000A => {
172 let mjd_idx = parts.iter().position(|p| {
173 p.contains('.') && p.parse::<Real>().is_ok_and(|v| v > 30000.0)
174 })?;
175
176 let mut flag_count = 0;
177 let mut offset_value: Option<Real> = None;
178 let mut pm_x_val: Real = 0.0;
179 let mut pm_y_val: Real = 0.0;
180
181 for i in (mjd_idx + 1)..parts.len() {
182 let token = parts[i];
183
184 let is_flag = token == "I"
185 || token == "P"
186 || token.starts_with("I-")
187 || token.starts_with("P-");
188
189 if is_flag {
190 flag_count += 1;
191
192 if flag_count == 1 {
193 if let (Some(x_str), Some(y_str)) = (parts.get(i + 1), parts.get(i + 3))
195 {
196 pm_x_val = x_str.parse::<Real>().unwrap_or(0.0);
197 pm_y_val = y_str.parse::<Real>().unwrap_or(0.0);
198 }
199 }
200
201 if flag_count == 2 {
202 let value_str = if token.starts_with("I-") || token.starts_with("P-") {
203 &token[1..]
204 } else if i + 1 < parts.len() {
205 parts[i + 1]
206 } else {
207 break;
208 };
209 if let Ok(val) = value_str.parse::<Real>() {
210 offset_value = Some(val);
211 }
212 break;
213 }
214 }
215 }
216
217 let offset = offset_value?;
218 let mjd_val = parts[mjd_idx].parse::<Real>().ok()?;
219
220 (mjd_val, offset, pm_x_val, pm_y_val)
221 }
222
223 EopFormat::C04 => {
224 let mjd = parts.get(4)?.parse::<Real>().ok()?;
225 let pm_x = parts
226 .get(5)
227 .unwrap_or(&"0.0")
228 .parse::<Real>()
229 .unwrap_or(0.0);
230 let pm_y = parts
231 .get(6)
232 .unwrap_or(&"0.0")
233 .parse::<Real>()
234 .unwrap_or(0.0);
235 let offset = parts.get(7)?.parse::<Real>().ok()?;
236 (mjd, offset, pm_x, pm_y)
237 }
238
239 EopFormat::Custom(cols) => {
240 let mjd = parts.get(cols.mjd)?.parse::<Real>().ok()?;
241 let offset = parts.get(cols.offset)?.parse::<Real>().ok()?;
242 let pm_x = if let Some(pm_x_col) = cols.pm_x {
243 parts
244 .get(pm_x_col)
245 .unwrap_or(&"0.0")
246 .parse::<Real>()
247 .ok()
248 .unwrap_or(0.0)
249 } else {
250 0.0
251 };
252 let pm_y = if let Some(pm_y_col) = cols.pm_y {
253 parts
254 .get(pm_y_col)
255 .unwrap_or(&"0.0")
256 .parse::<Real>()
257 .ok()
258 .unwrap_or(0.0)
259 } else {
260 0.0
261 };
262 (mjd, offset, pm_x, pm_y)
263 }
264 };
265
266 Some(EopDataRow {
267 mjd,
268 offset,
269 pm_x,
270 pm_y,
271 })
272 }
273
274 fn parse_lines<'a>(
275 lines: impl Iterator<Item = &'a str>,
276 format: EopFormat,
277 separator: Separator,
278 ) -> Result<Vec<EopDataRow>, DtErr> {
279 let mut rows = Vec::new();
280
281 for line in lines {
282 let trimmed = line.trim();
283 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.len() > Self::MAX_LINE_LEN
284 {
285 continue;
286 }
287
288 if let Some(row) = Self::try_parse_row(trimmed, format, separator) {
289 rows.push(row);
290 }
291 }
292
293 if rows.is_empty() {
294 return Err(an_err!(DtErrKind::Incomplete, "no valid rows"));
295 }
296
297 rows.sort_by(|a, b| a.mjd.partial_cmp(&b.mjd).unwrap_or(Ordering::Equal));
298 Ok(rows)
299 }
300
301 pub fn data_from_str(
302 s: &str,
303 format: EopFormat,
304 separator: Separator,
305 ) -> Result<Vec<EopDataRow>, DtErr> {
306 Self::parse_lines(s.lines(), format, separator)
307 }
308
309 pub fn data_from_bytes(
310 bytes: &[u8],
311 format: EopFormat,
312 separator: Separator,
313 ) -> Result<Vec<EopDataRow>, DtErr> {
314 let s = core::str::from_utf8(bytes).unwrap_or("");
315 Self::data_from_str(s, format, separator)
316 }
317
318 pub fn from_str(s: &str, format: EopFormat, separator: Separator) -> Result<Self, DtErr> {
319 let rows = Self::data_from_str(s, format, separator)?;
320 Ok(Self { rows })
321 }
322
323 pub fn from_bytes(
324 bytes: &[u8],
325 format: EopFormat,
326 separator: Separator,
327 ) -> Result<Self, DtErr> {
328 let rows = Self::data_from_bytes(bytes, format, separator)?;
329 Ok(Self { rows })
330 }
331
332 pub fn eop_offset(&self, mjd: Real) -> Option<EopOffset> {
338 if self.rows.is_empty() {
339 return None;
340 }
341
342 let idx = match self
343 .rows
344 .binary_search_by(|probe| probe.mjd.partial_cmp(&mjd).unwrap_or(Ordering::Equal))
345 {
346 Ok(i) => i,
347 Err(i) => {
348 if i == 0 {
349 let row = &self.rows[0];
350 return Some(EopOffset {
351 offset: row.offset,
352 pm_x: row.pm_x,
353 pm_y: row.pm_y,
354 });
355 }
356 if i >= self.rows.len() {
357 let row = &self.rows[self.rows.len() - 1];
358 return Some(EopOffset {
359 offset: row.offset,
360 pm_x: row.pm_x,
361 pm_y: row.pm_y,
362 });
363 }
364 i - 1
365 }
366 };
367
368 if idx + 1 < self.rows.len() {
369 let e0 = &self.rows[idx];
370 let e1 = &self.rows[idx + 1];
371
372 let t = (mjd - e0.mjd) / (e1.mjd - e0.mjd);
373
374 let offset = e0.offset + t * (e1.offset - e0.offset);
375 let pm_x = e0.pm_x + t * (e1.pm_x - e0.pm_x);
376 let pm_y = e0.pm_y + t * (e1.pm_y - e0.pm_y);
377
378 Some(EopOffset { offset, pm_x, pm_y })
379 } else {
380 let row = &self.rows[idx];
381 Some(EopOffset {
382 offset: row.offset,
383 pm_x: row.pm_x,
384 pm_y: row.pm_y,
385 })
386 }
387 }
388}
389
390#[derive(Debug, Clone, Copy, Default)]
395pub struct EopOffset {
396 pub offset: Real,
398 pub pm_x: Real,
400 pub pm_y: Real,
402}
403
404impl Dt {
405 pub fn mjd_to_eop_offset(mjd: Real, op_data: &EopData) -> Result<EopOffset, DtErr> {
411 let offset = op_data
412 .eop_offset(mjd)
413 .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "mjd: {mjd}"))?;
414 Ok(offset)
415 }
416
417 #[inline]
422 pub fn mjd_to_eop_offset_f(mjd: Real, op_data: &EopData) -> Result<Real, DtErr> {
423 Self::mjd_to_eop_offset(mjd, op_data).map(|res| res.offset)
424 }
425
426 #[inline]
431 pub fn to_eop(&self, op_data: &EopData) -> Result<Self, DtErr> {
432 Ok(self.add(Dt::from_sec_f(Self::mjd_to_eop_offset_f(
433 self.to_mjd_f(),
434 op_data,
435 )?)))
436 }
437
438 pub fn from_eop(&self, op_data: &EopData) -> Result<Self, DtErr> {
444 if op_data.rows.is_empty() {
445 return Err(an_err!(DtErrKind::InternalErr, "contains no data"));
446 }
447 let mut guess = *self;
448
449 for _ in 0..8 {
450 let mjd = guess.to_mjd_f();
451 let offset = op_data
452 .eop_offset(mjd)
453 .ok_or_else(|| an_err!(DtErrKind::OutOfRange, "mjd: {mjd}"))?
454 .offset;
455
456 guess = self.sub(Dt::from_sec_f(offset)); }
458
459 Ok(guess)
460 }
461}