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