1use core::fmt;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
26pub enum GnssSystem {
27 Gps,
29 Glonass,
31 Galileo,
33 BeiDou,
35 Qzss,
37 Navic,
39 Sbas,
41}
42
43impl GnssSystem {
44 pub const fn letter(self) -> char {
46 match self {
47 GnssSystem::Gps => 'G',
48 GnssSystem::Glonass => 'R',
49 GnssSystem::Galileo => 'E',
50 GnssSystem::BeiDou => 'C',
51 GnssSystem::Qzss => 'J',
52 GnssSystem::Navic => 'I',
53 GnssSystem::Sbas => 'S',
54 }
55 }
56
57 pub const fn from_letter(letter: char) -> Option<Self> {
62 match letter {
63 'G' => Some(GnssSystem::Gps),
64 'R' => Some(GnssSystem::Glonass),
65 'E' => Some(GnssSystem::Galileo),
66 'C' => Some(GnssSystem::BeiDou),
67 'J' => Some(GnssSystem::Qzss),
68 'I' => Some(GnssSystem::Navic),
69 'S' => Some(GnssSystem::Sbas),
70 _ => None,
71 }
72 }
73}
74
75impl fmt::Display for GnssSystem {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.write_str(match self {
78 GnssSystem::Gps => "GPS",
79 GnssSystem::Glonass => "GLO",
80 GnssSystem::Galileo => "GAL",
81 GnssSystem::BeiDou => "BDS",
82 GnssSystem::Qzss => "QZSS",
83 GnssSystem::Navic => "NavIC",
84 GnssSystem::Sbas => "SBAS",
85 })
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
96pub struct GnssSatelliteId {
97 pub system: GnssSystem,
99 pub prn: u8,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
105pub enum SatelliteIdError {
106 #[error("invalid GNSS satellite {field}: {reason}")]
108 InvalidInput {
109 field: &'static str,
110 reason: &'static str,
111 },
112}
113
114const fn invalid_input(field: &'static str, reason: &'static str) -> SatelliteIdError {
115 SatelliteIdError::InvalidInput { field, reason }
116}
117
118impl GnssSatelliteId {
119 pub const fn new(system: GnssSystem, prn: u8) -> Result<Self, SatelliteIdError> {
121 if !is_valid_prn(system, prn) {
122 return Err(invalid_input("prn", "out of range for constellation"));
123 }
124 Ok(Self { system, prn })
125 }
126}
127
128impl fmt::Display for GnssSatelliteId {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 write!(f, "{}{:02}", self.system.letter(), self.prn)
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub struct ParseSatelliteIdError;
142
143impl fmt::Display for ParseSatelliteIdError {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 f.write_str("invalid GNSS satellite token")
146 }
147}
148
149impl std::error::Error for ParseSatelliteIdError {}
150
151impl core::str::FromStr for GnssSatelliteId {
152 type Err = ParseSatelliteIdError;
153
154 fn from_str(token: &str) -> Result<Self, Self::Err> {
160 let token = token.trim();
161 let first = token.chars().next().ok_or(ParseSatelliteIdError)?;
162 let system = GnssSystem::from_letter(first).ok_or(ParseSatelliteIdError)?;
163 let prn_token = token[first.len_utf8()..].trim();
164 if prn_token.len() != 2 || !prn_token.bytes().all(|b| b.is_ascii_digit()) {
165 return Err(ParseSatelliteIdError);
166 }
167 let prn = prn_token.parse::<u8>().map_err(|_| ParseSatelliteIdError)?;
168 if !is_valid_prn(system, prn) {
169 return Err(ParseSatelliteIdError);
170 }
171 Self::new(system, prn).map_err(|_| ParseSatelliteIdError)
172 }
173}
174
175pub(crate) const fn is_valid_prn(system: GnssSystem, prn: u8) -> bool {
176 match system {
177 GnssSystem::Gps => prn >= 1 && prn <= 32,
178 GnssSystem::Glonass => prn >= 1 && prn <= 27,
179 GnssSystem::Galileo => prn >= 1 && prn <= 36,
180 GnssSystem::BeiDou => prn >= 1 && prn <= 63,
181 GnssSystem::Qzss => prn >= 1 && prn <= 9,
182 GnssSystem::Navic => prn >= 1 && prn <= 14,
183 GnssSystem::Sbas => prn >= 20 && prn <= 58,
184 }
185}
186
187pub(crate) fn constellation_letter(id: &str) -> &str {
198 id.get(..1).unwrap_or("")
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn letter_round_trips() {
207 for sys in [
208 GnssSystem::Gps,
209 GnssSystem::Glonass,
210 GnssSystem::Galileo,
211 GnssSystem::BeiDou,
212 GnssSystem::Qzss,
213 GnssSystem::Navic,
214 GnssSystem::Sbas,
215 ] {
216 assert_eq!(GnssSystem::from_letter(sys.letter()), Some(sys));
217 }
218 assert_eq!(GnssSystem::from_letter('X'), None);
219 }
220
221 #[test]
222 fn satellite_token_formats_padded() {
223 let id = GnssSatelliteId::new(GnssSystem::Gps, 1).expect("valid satellite id");
224 assert_eq!(id.to_string(), "G01");
225 assert_eq!(
226 GnssSatelliteId::new(GnssSystem::BeiDou, 30)
227 .expect("valid satellite id")
228 .to_string(),
229 "C30"
230 );
231 }
232
233 #[test]
234 fn satellite_constructor_validates_prn_range() {
235 let id = GnssSatelliteId::new(GnssSystem::Gps, 1).expect("valid satellite id");
236 assert_eq!(id.system, GnssSystem::Gps);
237 assert_eq!(id.prn, 1);
238
239 assert_eq!(
240 GnssSatelliteId::new(GnssSystem::Gps, 0),
241 Err(SatelliteIdError::InvalidInput {
242 field: "prn",
243 reason: "out of range for constellation"
244 })
245 );
246 assert_eq!(
247 GnssSatelliteId::new(GnssSystem::Sbas, 19),
248 Err(SatelliteIdError::InvalidInput {
249 field: "prn",
250 reason: "out of range for constellation"
251 })
252 );
253 }
254
255 #[test]
256 fn satellite_token_parses_via_from_str() {
257 assert_eq!(
258 "G01".parse(),
259 Ok(GnssSatelliteId::new(GnssSystem::Gps, 1).expect("valid satellite id"))
260 );
261 assert_eq!(
262 "G32".parse(),
263 Ok(GnssSatelliteId::new(GnssSystem::Gps, 32).expect("valid satellite id"))
264 );
265 assert_eq!(
266 "R27".parse(),
267 Ok(GnssSatelliteId::new(GnssSystem::Glonass, 27).expect("valid satellite id"))
268 );
269 assert_eq!(
270 "E36".parse(),
271 Ok(GnssSatelliteId::new(GnssSystem::Galileo, 36).expect("valid satellite id"))
272 );
273 assert_eq!(
274 "C30".parse(),
275 Ok(GnssSatelliteId::new(GnssSystem::BeiDou, 30).expect("valid satellite id"))
276 );
277 assert_eq!(
278 "C63".parse(),
279 Ok(GnssSatelliteId::new(GnssSystem::BeiDou, 63).expect("valid satellite id"))
280 );
281 assert_eq!(
282 "J09".parse(),
283 Ok(GnssSatelliteId::new(GnssSystem::Qzss, 9).expect("valid satellite id"))
284 );
285 assert_eq!(
286 "I14".parse(),
287 Ok(GnssSatelliteId::new(GnssSystem::Navic, 14).expect("valid satellite id"))
288 );
289 assert_eq!(
290 "S20".parse(),
291 Ok(GnssSatelliteId::new(GnssSystem::Sbas, 20).expect("valid satellite id"))
292 );
293 assert_eq!(
294 "S58".parse(),
295 Ok(GnssSatelliteId::new(GnssSystem::Sbas, 58).expect("valid satellite id"))
296 );
297 assert_eq!(
300 " E12 ".parse(),
301 Ok(GnssSatelliteId::new(GnssSystem::Galileo, 12).expect("valid satellite id"))
302 );
303 let id = GnssSatelliteId::new(GnssSystem::Qzss, 7).expect("valid satellite id");
305 assert_eq!(id.to_string().parse(), Ok(id));
306 assert_eq!("".parse::<GnssSatelliteId>(), Err(ParseSatelliteIdError));
308 assert_eq!("X01".parse::<GnssSatelliteId>(), Err(ParseSatelliteIdError));
309 assert_eq!("G".parse::<GnssSatelliteId>(), Err(ParseSatelliteIdError));
310 assert_eq!("GAB".parse::<GnssSatelliteId>(), Err(ParseSatelliteIdError));
311 }
312
313 #[test]
314 fn satellite_token_rejects_bad_prn_width_and_range() {
315 for token in [
316 "G0", "G1", "G001", "G00", "G33", "G255", "R28", "E37", "C64", "J10", "I15", "S01",
317 "S19", "S59",
318 ] {
319 assert_eq!(
320 token.parse::<GnssSatelliteId>(),
321 Err(ParseSatelliteIdError),
322 "{token}"
323 );
324 }
325 }
326
327 #[test]
328 fn constellation_letter_extracts_leading_token_byte() {
329 assert_eq!(constellation_letter("G01"), "G");
330 assert_eq!(constellation_letter("C30"), "C");
331 assert_eq!(constellation_letter("E12~ra1"), "E");
332 assert_eq!(constellation_letter("R07:base=R07,rover=R07"), "R");
333 assert_eq!(constellation_letter(""), "");
334 }
335}