Skip to main content

nmea_kit/nmea/
field.rs

1//! Field parsing and formatting helpers for NMEA sentence fields.
2//!
3//! [`FieldReader`] reads fields sequentially from a parsed frame.
4//! [`FieldWriter`] builds fields sequentially for encoding.
5
6/// Sequential field reader for NMEA sentence parsing.
7///
8/// Wraps a slice of `&str` fields and reads them in order,
9/// advancing an internal index after each read.
10pub struct FieldReader<'a> {
11    fields: &'a [&'a str],
12    idx: usize,
13}
14
15impl<'a> FieldReader<'a> {
16    pub fn new(fields: &'a [&'a str]) -> Self {
17        Self { fields, idx: 0 }
18    }
19
20    /// Read an optional f32 and advance.
21    pub fn f32(&mut self) -> Option<f32> {
22        let val = self.fields.get(self.idx).and_then(|f| {
23            if f.is_empty() {
24                None
25            } else {
26                f.parse::<f32>().ok()
27            }
28        });
29        self.idx += 1;
30        val
31    }
32
33    /// Read an optional f64 and advance.
34    pub fn f64(&mut self) -> Option<f64> {
35        let val = self.fields.get(self.idx).and_then(|f| {
36            if f.is_empty() {
37                None
38            } else {
39                f.parse::<f64>().ok()
40            }
41        });
42        self.idx += 1;
43        val
44    }
45
46    /// Read an optional u8 and advance.
47    pub fn u8(&mut self) -> Option<u8> {
48        let val = self.fields.get(self.idx).and_then(|f| {
49            if f.is_empty() {
50                None
51            } else {
52                f.parse::<u8>().ok()
53            }
54        });
55        self.idx += 1;
56        val
57    }
58
59    /// Read an optional u32 and advance.
60    pub fn u32(&mut self) -> Option<u32> {
61        let val = self.fields.get(self.idx).and_then(|f| {
62            if f.is_empty() {
63                None
64            } else {
65                f.parse::<u32>().ok()
66            }
67        });
68        self.idx += 1;
69        val
70    }
71
72    /// Read an optional i8 and advance.
73    pub fn i8(&mut self) -> Option<i8> {
74        let val = self.fields.get(self.idx).and_then(|f| {
75            if f.is_empty() {
76                None
77            } else {
78                f.parse::<i8>().ok()
79            }
80        });
81        self.idx += 1;
82        val
83    }
84
85    /// Read an optional u16 and advance.
86    pub fn u16(&mut self) -> Option<u16> {
87        let val = self.fields.get(self.idx).and_then(|f| {
88            if f.is_empty() { None } else { f.parse::<u16>().ok() }
89        });
90        self.idx += 1;
91        val
92    }
93
94    /// Read an optional i16 and advance.
95    pub fn i16(&mut self) -> Option<i16> {
96        let val = self.fields.get(self.idx).and_then(|f| {
97            if f.is_empty() { None } else { f.parse::<i16>().ok() }
98        });
99        self.idx += 1;
100        val
101    }
102
103    /// Read an optional i32 and advance.
104    pub fn i32(&mut self) -> Option<i32> {
105        let val = self.fields.get(self.idx).and_then(|f| {
106            if f.is_empty() { None } else { f.parse::<i32>().ok() }
107        });
108        self.idx += 1;
109        val
110    }
111
112    /// Read an optional single character and advance.
113    pub fn char(&mut self) -> Option<char> {
114        let val = self
115            .fields
116            .get(self.idx)
117            .and_then(|f| f.chars().next().filter(|_| !f.is_empty()));
118        self.idx += 1;
119        val
120    }
121
122    /// Read an optional non-empty string and advance.
123    pub fn string(&mut self) -> Option<String> {
124        let val = self.fields.get(self.idx).and_then(|f| {
125            if f.is_empty() {
126                None
127            } else {
128                Some((*f).to_string())
129            }
130        });
131        self.idx += 1;
132        val
133    }
134
135    /// Skip one field (fixed indicator) and advance.
136    pub fn skip(&mut self) {
137        self.idx += 1;
138    }
139}
140
141/// Sequential field writer for NMEA sentence encoding.
142///
143/// Builds a `Vec<String>` of field values in wire order.
144pub struct FieldWriter {
145    fields: Vec<String>,
146}
147
148impl FieldWriter {
149    pub fn new() -> Self {
150        Self { fields: Vec::new() }
151    }
152
153    /// Write an optional f32. `None` → empty field.
154    pub fn f32(&mut self, value: Option<f32>) {
155        self.fields.push(match value {
156            Some(v) => format!("{v}"),
157            None => String::new(),
158        });
159    }
160
161    /// Write an optional f64. `None` → empty field.
162    pub fn f64(&mut self, value: Option<f64>) {
163        self.fields.push(match value {
164            Some(v) => format!("{v}"),
165            None => String::new(),
166        });
167    }
168
169    /// Write an optional u8. `None` → empty field.
170    pub fn u8(&mut self, value: Option<u8>) {
171        self.fields.push(match value {
172            Some(v) => v.to_string(),
173            None => String::new(),
174        });
175    }
176
177    /// Write an optional i8. `None` → empty field.
178    pub fn i8(&mut self, value: Option<i8>) {
179        self.fields.push(match value {
180            Some(v) => v.to_string(),
181            None => String::new(),
182        });
183    }
184
185    /// Write an optional u32. `None` → empty field.
186    pub fn u32(&mut self, value: Option<u32>) {
187        self.fields.push(match value {
188            Some(v) => v.to_string(),
189            None => String::new(),
190        });
191    }
192
193    /// Write an optional u16. `None` → empty field.
194    pub fn u16(&mut self, value: Option<u16>) {
195        self.fields.push(match value {
196            Some(v) => v.to_string(),
197            None => String::new(),
198        });
199    }
200
201    /// Write an optional i16. `None` → empty field.
202    pub fn i16(&mut self, value: Option<i16>) {
203        self.fields.push(match value {
204            Some(v) => v.to_string(),
205            None => String::new(),
206        });
207    }
208
209    /// Write an optional i32. `None` → empty field.
210    pub fn i32(&mut self, value: Option<i32>) {
211        self.fields.push(match value {
212            Some(v) => v.to_string(),
213            None => String::new(),
214        });
215    }
216
217    /// Write an optional char. `None` → empty field.
218    pub fn char(&mut self, value: Option<char>) {
219        self.fields.push(match value {
220            Some(c) => c.to_string(),
221            None => String::new(),
222        });
223    }
224
225    /// Write a fixed indicator character (always emitted).
226    pub fn fixed(&mut self, c: char) {
227        self.fields.push(c.to_string());
228    }
229
230    /// Write an optional string. `None` → empty field.
231    pub fn string(&mut self, value: Option<&str>) {
232        self.fields.push(value.unwrap_or("").to_string());
233    }
234
235    /// Consume and return the built field list.
236    pub fn finish(self) -> Vec<String> {
237        self.fields
238    }
239}
240
241impl Default for FieldWriter {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247/// Trait for NMEA sentence types that can be encoded to wire format.
248///
249/// Provides `SENTENCE_TYPE` and `encode()` — the `to_sentence()` default
250/// method combines them with [`encode_frame()`](crate::encode_frame) to
251/// produce a complete NMEA sentence with checksum.
252///
253/// # Standard sentences
254///
255/// ```
256/// use nmea_kit::nmea::{NmeaEncodable, sentences::Dpt};
257///
258/// let dpt = Dpt { depth: Some(4.1), offset: Some(0.0), rangescale: None };
259/// let sentence = dpt.to_sentence("II");
260/// assert!(sentence.starts_with("$IIDPT,"));
261/// ```
262///
263/// # Proprietary sentences
264///
265/// Proprietary types set [`PROPRIETARY_ID`](Self::PROPRIETARY_ID) to the full
266/// address (e.g. `"PASHR"`, `"PSKPDPT"`) and use
267/// [`to_proprietary_sentence()`](Self::to_proprietary_sentence) instead of
268/// `to_sentence()`.
269pub trait NmeaEncodable {
270    /// The 3-character sentence type identifier (e.g. `"MWD"`, `"RMC"`).
271    const SENTENCE_TYPE: &'static str;
272
273    /// Full proprietary address identifier (e.g. `"PASHR"`, `"PSKPDPT"`).
274    /// Empty for standard sentences.
275    const PROPRIETARY_ID: &'static str = "";
276
277    /// Encode fields into a `Vec` of strings in wire order.
278    fn encode(&self) -> Vec<String>;
279
280    /// Encode into a complete standard NMEA 0183 sentence with checksum and `\r\n`.
281    fn to_sentence(&self, talker: &str) -> String {
282        let fields = self.encode();
283        let field_refs: Vec<&str> = fields.iter().map(|s| s.as_str()).collect();
284        crate::encode_frame('$', talker, Self::SENTENCE_TYPE, &field_refs)
285    }
286
287    /// Encode into a complete proprietary NMEA 0183 sentence with checksum and `\r\n`.
288    ///
289    /// Uses [`PROPRIETARY_ID`](Self::PROPRIETARY_ID) as the full address
290    /// (no separate talker).
291    fn to_proprietary_sentence(&self) -> String {
292        let fields = self.encode();
293        let field_refs: Vec<&str> = fields.iter().map(|s| s.as_str()).collect();
294        crate::encode_frame('$', "", Self::PROPRIETARY_ID, &field_refs)
295    }
296}
297
298/// Convert an NMEA `DDMM.MMMM` coordinate to decimal degrees.
299///
300/// NMEA sentences encode latitude as `DDMM.MMMM` (degrees + minutes) and
301/// longitude as `DDDMM.MMMM`. AIS and most application code use decimal degrees.
302/// The sign (N/S, E/W) is not part of `ddmm` — apply it after conversion.
303///
304/// # Example
305///
306/// ```
307/// use nmea_kit::nmea::ddmm_to_decimal;
308///
309/// // 4807.038 → 48°07.038′ → 48.1173°
310/// let lat = ddmm_to_decimal(4807.038);
311/// assert!((lat - 48.1173).abs() < 0.0001);
312/// ```
313pub fn ddmm_to_decimal(ddmm: f64) -> f64 {
314    let degrees = (ddmm / 100.0).floor();
315    let minutes = ddmm - degrees * 100.0;
316    degrees + minutes / 60.0
317}
318
319/// Convert decimal degrees to an NMEA `DDMM.MMMM` coordinate.
320///
321/// This is the inverse of [`ddmm_to_decimal`]. The sign is not encoded —
322/// strip it before calling and re-apply the N/S or E/W indicator separately.
323///
324/// # Example
325///
326/// ```
327/// use nmea_kit::nmea::decimal_to_ddmm;
328///
329/// // 48.1173° → 48°07.038′ → 4807.038
330/// let ddmm = decimal_to_ddmm(48.1173);
331/// assert!((ddmm - 4807.038).abs() < 0.001);
332/// ```
333pub fn decimal_to_ddmm(decimal: f64) -> f64 {
334    let degrees = decimal.floor();
335    let minutes = (decimal - degrees) * 60.0;
336    degrees * 100.0 + minutes
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn reader_char() {
345        let fields = &["T", "", "AB"];
346        let mut r = FieldReader::new(fields);
347        assert_eq!(r.char(), Some('T'));
348        assert_eq!(r.char(), None);
349        assert_eq!(r.char(), Some('A')); // takes first char
350    }
351
352    #[test]
353    fn reader_f32() {
354        let fields = &["270.0", "", "abc"];
355        let mut r = FieldReader::new(fields);
356        assert_eq!(r.f32(), Some(270.0));
357        assert_eq!(r.f32(), None);
358        assert_eq!(r.f32(), None); // invalid
359    }
360
361    #[test]
362    fn reader_past_end() {
363        let fields: &[&str] = &[];
364        let mut r = FieldReader::new(fields);
365        assert_eq!(r.f32(), None);
366        assert_eq!(r.char(), None);
367    }
368
369    #[test]
370    fn reader_skip() {
371        let fields = &["10.0", "T", "20.0"];
372        let mut r = FieldReader::new(fields);
373        assert_eq!(r.f32(), Some(10.0));
374        r.skip();
375        assert_eq!(r.f32(), Some(20.0));
376    }
377
378    #[test]
379    fn reader_string() {
380        let fields = &["DEST", ""];
381        let mut r = FieldReader::new(fields);
382        assert_eq!(r.string(), Some("DEST".to_string()));
383        assert_eq!(r.string(), None);
384    }
385
386    #[test]
387    fn writer_roundtrip() {
388        let mut w = FieldWriter::new();
389        w.f32(Some(270.0));
390        w.fixed('T');
391        w.f32(None);
392        w.fixed('M');
393        let fields = w.finish();
394        assert_eq!(fields, vec!["270", "T", "", "M"]);
395    }
396
397    #[test]
398    fn ddmm_to_decimal_lat() {
399        // 4807.038 → 48°07.038′ → 48.1173°
400        let result = ddmm_to_decimal(4807.038);
401        assert!((result - 48.1173).abs() < 0.0001);
402    }
403
404    #[test]
405    fn ddmm_to_decimal_lon() {
406        // 01131.000 → 11°31.000′ → 11.5167°
407        let result = ddmm_to_decimal(1131.0);
408        assert!((result - 11.5167).abs() < 0.0001);
409    }
410
411    #[test]
412    fn decimal_to_ddmm_lat() {
413        // 48.1173° → 4807.038
414        let result = decimal_to_ddmm(48.1173);
415        assert!((result - 4807.038).abs() < 0.001);
416    }
417
418    #[test]
419    fn decimal_to_ddmm_roundtrip() {
420        let original = 5132.5200_f64;
421        let roundtrip = decimal_to_ddmm(ddmm_to_decimal(original));
422        assert!((roundtrip - original).abs() < 0.0001);
423    }
424}