Skip to main content

celestial_coords/eop/
parse.rs

1use super::record::{EopFlags, EopQuality, EopRecord, EopSource};
2use crate::CoordResult;
3
4pub fn parse_finals(content: &str) -> CoordResult<Vec<EopRecord>> {
5    let mut records = Vec::new();
6
7    for line in content.lines() {
8        if let Some(record) = parse_finals_line(line) {
9            records.push(record);
10        }
11    }
12
13    if records.is_empty() {
14        return Err(crate::CoordError::parsing_error(
15            "No valid records found in finals2000A data",
16        ));
17    }
18
19    records.sort_by(|a, b| a.mjd.partial_cmp(&b.mjd).unwrap());
20    Ok(records)
21}
22
23pub fn parse_finals_line(line: &str) -> Option<EopRecord> {
24    if line.len() < 79 {
25        return None;
26    }
27
28    let mjd = parse_field(line, 7, 15)?;
29    let xp = parse_field(line, 18, 27)?;
30    let yp = parse_field(line, 37, 46)?;
31    let ut1_utc = parse_field(line, 58, 68)?;
32    let lod = parse_field(line, 79, 86).unwrap_or(0.0) * 0.001;
33    let dx = parse_field(line, 97, 106).unwrap_or(0.0);
34    let dy = parse_field(line, 116, 125).unwrap_or(0.0);
35
36    let mut record = EopRecord::new(mjd, xp, yp, ut1_utc, lod).ok()?;
37
38    let has_cip = dx != 0.0 || dy != 0.0;
39    if has_cip {
40        record = record.with_cip_offsets(dx, dy).ok()?;
41    }
42
43    let flags = EopFlags {
44        source: EopSource::IersFinals,
45        quality: EopQuality::HighPrecision,
46        has_polar_motion: true,
47        has_ut1_utc: true,
48        has_cip_offsets: has_cip,
49        has_pole_rates: false,
50    };
51    record = record.with_flags(flags);
52
53    Some(record)
54}
55
56fn parse_field(line: &str, start: usize, end: usize) -> Option<f64> {
57    let s = line.get(start..end)?.trim();
58    if s.is_empty() {
59        return None;
60    }
61    s.parse::<f64>().ok()
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    fn sample_finals_line() -> String {
69        let mut line = vec![b' '; 188];
70
71        let mjd = b"60000.00";
72        line[7..15].copy_from_slice(mjd);
73
74        let xp = b"  0.10000";
75        line[18..27].copy_from_slice(xp);
76
77        let yp = b"  0.25000";
78        line[37..46].copy_from_slice(yp);
79
80        let ut1 = b" -0.050000";
81        line[58..68].copy_from_slice(ut1);
82
83        // LOD in milliseconds
84        let lod = b"  1.500";
85        line[79..86].copy_from_slice(lod);
86
87        let dx = b"   0.2000";
88        line[97..106].copy_from_slice(dx);
89
90        let dy = b"  -0.1000";
91        line[116..125].copy_from_slice(dy);
92
93        String::from_utf8(line).unwrap()
94    }
95
96    #[test]
97    fn test_parse_single_line() {
98        let line = sample_finals_line();
99        let record = parse_finals_line(&line).unwrap();
100        let params = record.to_parameters();
101
102        assert_eq!(params.mjd, 60000.0);
103        assert!((params.x_p - 0.1).abs() < 1e-6);
104        assert!((params.y_p - 0.25).abs() < 1e-6);
105        assert!((params.ut1_utc - (-0.05)).abs() < 1e-6);
106        assert!((params.lod - 0.0015).abs() < 1e-7);
107        assert_eq!(params.dx, Some(0.2));
108        assert_eq!(params.dy, Some(-0.1));
109        assert_eq!(params.flags.source, EopSource::IersFinals);
110        assert!(params.flags.has_cip_offsets);
111    }
112
113    #[test]
114    fn test_parse_line_too_short() {
115        assert!(parse_finals_line("short line").is_none());
116    }
117
118    #[test]
119    fn test_parse_line_missing_required() {
120        let line = " ".repeat(188);
121        assert!(parse_finals_line(&line).is_none());
122    }
123
124    fn sample_finals_line_at(mjd: &[u8]) -> String {
125        let mut line = vec![b' '; 188];
126        line[7..7 + mjd.len()].copy_from_slice(mjd);
127        let xp = b"  0.10000";
128        line[18..27].copy_from_slice(xp);
129        let yp = b"  0.25000";
130        line[37..46].copy_from_slice(yp);
131        let ut1 = b" -0.050000";
132        line[58..68].copy_from_slice(ut1);
133        let lod = b"  1.500";
134        line[79..86].copy_from_slice(lod);
135        String::from_utf8(line).unwrap()
136    }
137
138    #[test]
139    fn test_parse_finals_multi_line() {
140        let line1 = sample_finals_line_at(b"60000.00");
141        let line2 = sample_finals_line_at(b"60001.00");
142
143        let content = format!("{}\n{}\n", line1, line2);
144        let records = parse_finals(&content).unwrap();
145
146        assert_eq!(records.len(), 2);
147        assert_eq!(records[0].mjd, 60000.0);
148        assert_eq!(records[1].mjd, 60001.0);
149    }
150
151    #[test]
152    fn test_parse_finals_skips_bad_lines() {
153        let good = sample_finals_line();
154        let content = format!("bad line\n{}\nalso bad\n", good);
155        let records = parse_finals(&content).unwrap();
156        assert_eq!(records.len(), 1);
157    }
158
159    #[test]
160    fn test_parse_finals_empty_errors() {
161        let result = parse_finals("bad\nlines\nonly\n");
162        assert!(result.is_err());
163    }
164
165    #[test]
166    fn test_lod_zero_when_missing() {
167        let mut line = vec![b' '; 188];
168
169        let mjd = b"60000.00";
170        line[7..15].copy_from_slice(mjd);
171        let xp = b"  0.10000";
172        line[18..27].copy_from_slice(xp);
173        let yp = b"  0.25000";
174        line[37..46].copy_from_slice(yp);
175        let ut1 = b" -0.050000";
176        line[58..68].copy_from_slice(ut1);
177        // LOD columns left blank
178
179        let line = String::from_utf8(line).unwrap();
180        let record = parse_finals_line(&line).unwrap();
181        let params = record.to_parameters();
182        assert_eq!(params.lod, 0.0);
183    }
184
185    #[test]
186    fn test_no_cip_when_zero() {
187        let mut line = vec![b' '; 188];
188
189        let mjd = b"60000.00";
190        line[7..15].copy_from_slice(mjd);
191        let xp = b"  0.10000";
192        line[18..27].copy_from_slice(xp);
193        let yp = b"  0.25000";
194        line[37..46].copy_from_slice(yp);
195        let ut1 = b" -0.050000";
196        line[58..68].copy_from_slice(ut1);
197        // dX/dY columns left blank
198
199        let line = String::from_utf8(line).unwrap();
200        let record = parse_finals_line(&line).unwrap();
201        assert!(!record.flags.has_cip_offsets);
202    }
203}