celestial_coords/eop/
parse.rs1use 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 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 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 let line = String::from_utf8(line).unwrap();
200 let record = parse_finals_line(&line).unwrap();
201 assert!(!record.flags.has_cip_offsets);
202 }
203}