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 as_str(&self) -> &'static str {
47 match *self {
48 GnssSystem::Gps => "GPS",
49 GnssSystem::Glonass => "GLONASS",
50 GnssSystem::Galileo => "Galileo",
51 GnssSystem::BeiDou => "BeiDou",
52 GnssSystem::Qzss => "QZSS",
53 GnssSystem::Navic => "NavIC",
54 GnssSystem::Sbas => "SBAS",
55 }
56 }
57
58 pub const fn letter(self) -> char {
60 match self {
61 GnssSystem::Gps => 'G',
62 GnssSystem::Glonass => 'R',
63 GnssSystem::Galileo => 'E',
64 GnssSystem::BeiDou => 'C',
65 GnssSystem::Qzss => 'J',
66 GnssSystem::Navic => 'I',
67 GnssSystem::Sbas => 'S',
68 }
69 }
70
71 pub const fn from_letter(letter: char) -> Option<Self> {
76 match letter {
77 'G' => Some(GnssSystem::Gps),
78 'R' => Some(GnssSystem::Glonass),
79 'E' => Some(GnssSystem::Galileo),
80 'C' => Some(GnssSystem::BeiDou),
81 'J' => Some(GnssSystem::Qzss),
82 'I' => Some(GnssSystem::Navic),
83 'S' => Some(GnssSystem::Sbas),
84 _ => None,
85 }
86 }
87}
88
89impl fmt::Display for GnssSystem {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.write_str(self.as_str())
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
102pub struct GnssSatelliteId {
103 pub system: GnssSystem,
105 pub prn: u8,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
111pub enum SatelliteIdError {
112 #[error("invalid GNSS satellite {field}: {reason}")]
114 InvalidInput {
115 field: &'static str,
116 reason: &'static str,
117 },
118}
119
120const fn invalid_input(field: &'static str, reason: &'static str) -> SatelliteIdError {
121 SatelliteIdError::InvalidInput { field, reason }
122}
123
124impl GnssSatelliteId {
125 pub const fn new(system: GnssSystem, prn: u8) -> Result<Self, SatelliteIdError> {
127 if !is_valid_prn(system, prn) {
128 return Err(invalid_input("prn", "out of range for constellation"));
129 }
130 Ok(Self { system, prn })
131 }
132}
133
134impl fmt::Display for GnssSatelliteId {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "{}{:02}", self.system.letter(), self.prn)
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct ParseSatelliteIdError;
148
149impl fmt::Display for ParseSatelliteIdError {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 f.write_str("invalid GNSS satellite token")
152 }
153}
154
155impl std::error::Error for ParseSatelliteIdError {}
156
157impl core::str::FromStr for GnssSatelliteId {
158 type Err = ParseSatelliteIdError;
159
160 fn from_str(token: &str) -> Result<Self, Self::Err> {
166 let token = token.trim();
167 let first = token.chars().next().ok_or(ParseSatelliteIdError)?;
168 let system = GnssSystem::from_letter(first).ok_or(ParseSatelliteIdError)?;
169 let prn_token = token[first.len_utf8()..].trim();
170 if prn_token.len() != 2 || !prn_token.bytes().all(|b| b.is_ascii_digit()) {
171 return Err(ParseSatelliteIdError);
172 }
173 let prn = prn_token.parse::<u8>().map_err(|_| ParseSatelliteIdError)?;
174 if !is_valid_prn(system, prn) {
175 return Err(ParseSatelliteIdError);
176 }
177 Self::new(system, prn).map_err(|_| ParseSatelliteIdError)
178 }
179}
180
181pub(crate) const fn is_valid_prn(system: GnssSystem, prn: u8) -> bool {
182 match system {
183 GnssSystem::Gps => prn >= 1 && prn <= 32,
184 GnssSystem::Glonass => prn >= 1 && prn <= 27,
185 GnssSystem::Galileo => prn >= 1 && prn <= 36,
186 GnssSystem::BeiDou => prn >= 1 && prn <= 63,
187 GnssSystem::Qzss => prn >= 1 && prn <= 9,
188 GnssSystem::Navic => prn >= 1 && prn <= 14,
189 GnssSystem::Sbas => prn >= 20 && prn <= 58,
190 }
191}
192
193pub(crate) fn constellation_letter(id: &str) -> &str {
204 id.get(..1).unwrap_or("")
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn letter_round_trips() {
213 for sys in [
214 GnssSystem::Gps,
215 GnssSystem::Glonass,
216 GnssSystem::Galileo,
217 GnssSystem::BeiDou,
218 GnssSystem::Qzss,
219 GnssSystem::Navic,
220 GnssSystem::Sbas,
221 ] {
222 assert_eq!(GnssSystem::from_letter(sys.letter()), Some(sys));
223 }
224 assert_eq!(GnssSystem::from_letter('X'), None);
225 }
226
227 #[test]
228 fn system_labels_are_canonical() {
229 let cases = [
230 (GnssSystem::Gps, "GPS"),
231 (GnssSystem::Glonass, "GLONASS"),
232 (GnssSystem::Galileo, "Galileo"),
233 (GnssSystem::BeiDou, "BeiDou"),
234 (GnssSystem::Qzss, "QZSS"),
235 (GnssSystem::Navic, "NavIC"),
236 (GnssSystem::Sbas, "SBAS"),
237 ];
238 for (system, label) in cases {
239 assert_eq!(system.as_str(), label);
240 assert_eq!(system.to_string(), label);
241 }
242 }
243
244 #[test]
245 fn satellite_token_formats_padded() {
246 let id = GnssSatelliteId::new(GnssSystem::Gps, 1).expect("valid satellite id");
247 assert_eq!(id.to_string(), "G01");
248 assert_eq!(
249 GnssSatelliteId::new(GnssSystem::BeiDou, 30)
250 .expect("valid satellite id")
251 .to_string(),
252 "C30"
253 );
254 }
255
256 #[test]
257 fn satellite_constructor_validates_prn_range() {
258 let id = GnssSatelliteId::new(GnssSystem::Gps, 1).expect("valid satellite id");
259 assert_eq!(id.system, GnssSystem::Gps);
260 assert_eq!(id.prn, 1);
261
262 assert_eq!(
263 GnssSatelliteId::new(GnssSystem::Gps, 0),
264 Err(SatelliteIdError::InvalidInput {
265 field: "prn",
266 reason: "out of range for constellation"
267 })
268 );
269 assert_eq!(
270 GnssSatelliteId::new(GnssSystem::Sbas, 19),
271 Err(SatelliteIdError::InvalidInput {
272 field: "prn",
273 reason: "out of range for constellation"
274 })
275 );
276 }
277
278 #[test]
279 fn satellite_token_parses_via_from_str() {
280 assert_eq!(
281 "G01".parse(),
282 Ok(GnssSatelliteId::new(GnssSystem::Gps, 1).expect("valid satellite id"))
283 );
284 assert_eq!(
285 "G32".parse(),
286 Ok(GnssSatelliteId::new(GnssSystem::Gps, 32).expect("valid satellite id"))
287 );
288 assert_eq!(
289 "R27".parse(),
290 Ok(GnssSatelliteId::new(GnssSystem::Glonass, 27).expect("valid satellite id"))
291 );
292 assert_eq!(
293 "E36".parse(),
294 Ok(GnssSatelliteId::new(GnssSystem::Galileo, 36).expect("valid satellite id"))
295 );
296 assert_eq!(
297 "C30".parse(),
298 Ok(GnssSatelliteId::new(GnssSystem::BeiDou, 30).expect("valid satellite id"))
299 );
300 assert_eq!(
301 "C63".parse(),
302 Ok(GnssSatelliteId::new(GnssSystem::BeiDou, 63).expect("valid satellite id"))
303 );
304 assert_eq!(
305 "J09".parse(),
306 Ok(GnssSatelliteId::new(GnssSystem::Qzss, 9).expect("valid satellite id"))
307 );
308 assert_eq!(
309 "I14".parse(),
310 Ok(GnssSatelliteId::new(GnssSystem::Navic, 14).expect("valid satellite id"))
311 );
312 assert_eq!(
313 "S20".parse(),
314 Ok(GnssSatelliteId::new(GnssSystem::Sbas, 20).expect("valid satellite id"))
315 );
316 assert_eq!(
317 "S58".parse(),
318 Ok(GnssSatelliteId::new(GnssSystem::Sbas, 58).expect("valid satellite id"))
319 );
320 assert_eq!(
323 " E12 ".parse(),
324 Ok(GnssSatelliteId::new(GnssSystem::Galileo, 12).expect("valid satellite id"))
325 );
326 let id = GnssSatelliteId::new(GnssSystem::Qzss, 7).expect("valid satellite id");
328 assert_eq!(id.to_string().parse(), Ok(id));
329 assert_eq!("".parse::<GnssSatelliteId>(), Err(ParseSatelliteIdError));
331 assert_eq!("X01".parse::<GnssSatelliteId>(), Err(ParseSatelliteIdError));
332 assert_eq!("G".parse::<GnssSatelliteId>(), Err(ParseSatelliteIdError));
333 assert_eq!("GAB".parse::<GnssSatelliteId>(), Err(ParseSatelliteIdError));
334 }
335
336 #[test]
337 fn satellite_token_rejects_bad_prn_width_and_range() {
338 for token in [
339 "G0", "G1", "G001", "G00", "G33", "G255", "R28", "E37", "C64", "J10", "I15", "S01",
340 "S19", "S59",
341 ] {
342 assert_eq!(
343 token.parse::<GnssSatelliteId>(),
344 Err(ParseSatelliteIdError),
345 "{token}"
346 );
347 }
348 }
349
350 #[test]
351 fn constellation_letter_extracts_leading_token_byte() {
352 assert_eq!(constellation_letter("G01"), "G");
353 assert_eq!(constellation_letter("C30"), "C");
354 assert_eq!(constellation_letter("E12~ra1"), "E");
355 assert_eq!(constellation_letter("R07:base=R07,rover=R07"), "R");
356 assert_eq!(constellation_letter(""), "");
357 }
358}