nmea_parser/
util.rs

1/*
2Copyright 2020 Timo Saarinen
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16use super::*;
17
18use chrono::Duration;
19
20const AIS_CHAR_BITS: usize = 6;
21
22/// Make a key for storing NMEA sentence fragments
23pub(crate) fn make_fragment_key(
24    sentence_type: &str,
25    message_id: u64,
26    fragment_count: u8,
27    fragment_number: u8,
28    radio_channel_code: &str,
29) -> String {
30    format!(
31        "{},{},{},{},{}",
32        sentence_type, fragment_count, fragment_number, message_id, radio_channel_code
33    )
34}
35
36/// Convert AIS VDM/VDO payload armored string into a `BitVec`.
37pub(crate) fn parse_payload(payload: &str) -> Result<BitVec, String> {
38    let mut bv = BitVec::<usize, LocalBits>::with_capacity(payload.len() * 6);
39    for c in payload.chars() {
40        let mut ci = (c as u8) - 48;
41        if ci > 40 {
42            ci -= 8;
43        }
44
45        // Pick bits
46        for i in 0..6 {
47            bv.push(((ci >> (5 - i)) & 0x01) != 0);
48        }
49    }
50
51    Ok(bv)
52}
53
54/// Pick a numberic field from `BitVec`.
55pub(crate) fn pick_u64(bv: &BitVec, index: usize, len: usize) -> u64 {
56    let mut res = 0;
57    for pos in index..(index + len) {
58        if let Some(b) = bv.get(pos) {
59            res = (res << 1) | (*b as u64);
60        } else {
61            res <<= 1;
62        }
63    }
64    res
65}
66
67/// Pick a signed numberic field from `BitVec`.
68pub(crate) fn pick_i64(bv: &BitVec, index: usize, len: usize) -> i64 {
69    let mut res = 0;
70    for pos in index..(index + len) {
71        if let Some(b) = bv.get(pos) {
72            res = (res << 1) | (*b as u64);
73        } else {
74            res <<= 1;
75        }
76    }
77
78    let sign_bit = 1 << (len - 1);
79    if res & sign_bit != 0 {
80        ((res & (sign_bit - 1)) as i64) - (sign_bit as i64)
81    } else {
82        res as i64
83    }
84}
85
86/// Pick a string from BitVec. Field `char_count` defines string length in characters.
87/// Characters consist of 6 bits.
88pub(crate) fn pick_string(bv: &BitVec, index: usize, char_count: usize) -> String {
89    let mut res = String::with_capacity(char_count);
90    for i in 0..char_count {
91        // unwraps below won't panic as char_from::u32 will only ever receive values between
92        // 32..=96, all of which are valid. Catch all branch is unreachable as we only request
93        // 6-bits from the BitVec.
94        match pick_u64(bv, index + i * AIS_CHAR_BITS, AIS_CHAR_BITS) as u32 {
95            0 => break,
96            ch if ch < 32 => res.push(core::char::from_u32(64 + ch).unwrap()),
97            ch if ch < 64 => res.push(core::char::from_u32(ch).unwrap()),
98            ch => unreachable!("6-bit AIS character expected but value {} encountered!", ch),
99        }
100    }
101
102    let trimmed_len = res.trim_end().len();
103    res.truncate(trimmed_len);
104    res
105}
106
107/// Pick ETA based on UTC month, day, hour and minute.
108pub(crate) fn pick_eta(bv: &BitVec, index: usize) -> Result<Option<DateTime<Utc>>, ParseError> {
109    pick_eta_with_now(
110        bv,
111        index,
112        Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).single().unwrap(),
113    )
114}
115
116/// Pick ETA based on UTC month, day, hour and minute. Define also 'now'. This function is needed
117/// to make tests independent of the system time.
118fn pick_eta_with_now(
119    bv: &BitVec,
120    index: usize,
121    now: DateTime<Utc>,
122) -> Result<Option<DateTime<Utc>>, ParseError> {
123    // Pick ETA
124    let mut month = pick_u64(bv, index, 4) as u32;
125    let mut day = pick_u64(bv, index + 4, 5) as u32;
126    let mut hour = pick_u64(bv, index + 4 + 5, 5) as u32;
127    let mut minute = pick_u64(bv, index + 4 + 5 + 5, 6) as u32;
128
129    // Check special case for no value
130    if month == 0 && day == 0 && hour == 24 && minute == 60 {
131        return Ok(None);
132    }
133
134    // Complete partially given datetime
135    if month == 0 {
136        month = now.month();
137    }
138    if day == 0 {
139        day = now.day();
140    }
141    if hour == 24 {
142        hour = 23;
143        minute = 59;
144    }
145    if minute == 60 {
146        minute = 59;
147    }
148
149    // Ensure that that params from nmea are parsable as valid date
150    // Notice that we can't rely on ? operator here because of leap years
151    let res_this = parse_valid_utc(now.year(), month, day, hour, minute, 30, 0);
152    let res_next = parse_valid_utc(now.year() + 1, month, day, hour, minute, 30, 0);
153    if res_this.is_err() && res_next.is_err() {
154        // Both years result invalid date
155        match res_this {
156            Ok(_) => {
157                unreachable!("This should never be reached");
158            }
159            Err(e) => Err(e),
160        }
161    } else if res_this.is_err() {
162        // Only next year results valid date
163        Ok(Some(res_next.unwrap()))
164    } else if res_next.is_err() {
165        // Only this year results valid date
166        Ok(Some(res_this.unwrap()))
167    } else {
168        // Both years result a valid date
169        // If the ETA is more than 180 days in past assume it's about next year
170        let this_year_eta = res_this.unwrap();
171        if now - Duration::days(180) <= this_year_eta {
172            Ok(Some(this_year_eta))
173        } else {
174            Ok(res_next.ok())
175        }
176    }
177}
178
179/// Pick number field from a comma-separated sentence or `None` in case of an empty field.
180pub(crate) fn pick_number_field<T: core::str::FromStr>(
181    split: &[&str],
182    num: usize,
183) -> Result<Option<T>, String> {
184    split
185        .get(num)
186        .filter(|s| !s.is_empty())
187        .map(|s| {
188            s.parse()
189                .map_err(|_| format!("Failed to parse field {}: {}", num, s))
190        })
191        .transpose()
192}
193
194/// Pick hex-formatted field from a comma-separated sentence or `None` in case of an empty field.
195pub(crate) fn pick_hex_field<T: num_traits::Num>(
196    split: &[&str],
197    num: usize,
198) -> Result<Option<T>, String> {
199    split
200        .get(num)
201        .filter(|s| !s.is_empty())
202        .map(|s| {
203            T::from_str_radix(s, 16)
204                .map_err(|_| format!("Failed to parse hex field {}: {}", num, s))
205        })
206        .transpose()
207}
208
209/// Pick field from a comma-separated sentence or `None` in case of an empty field.
210pub(crate) fn pick_string_field(split: &[&str], num: usize) -> Option<String> {
211    let s = split.get(num).unwrap_or(&"");
212    if !s.is_empty() {
213        Some(s.to_string())
214    } else {
215        None
216    }
217}
218
219/// Parse time field of format HHMMSS and convert it to `DateTime<Utc>` using the current time.
220pub(crate) fn parse_hhmmss(hhmmss: &str, now: DateTime<Utc>) -> Result<DateTime<Utc>, ParseError> {
221    let (hour, minute, second) =
222        parse_time(hhmmss).map_err(|_| format!("Invalid time format: {}", hhmmss))?;
223    parse_valid_utc(now.year(), now.month(), now.day(), hour, minute, second, 0)
224}
225
226/// Parse time fields of formats YYMMDD and HHMMSS and convert them to `DateTime<Utc>`.
227pub(crate) fn parse_yymmdd_hhmmss(yymmdd: &str, hhmmss: &str) -> Result<DateTime<Utc>, ParseError> {
228    let now = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
229    let century = (now.year() / 100) * 100;
230    let (day, month, year) =
231        parse_date(yymmdd).map_err(|_| format!("Invalid date format: {}", yymmdd))?;
232    let (hour, minute, second) =
233        parse_time(hhmmss).map_err(|_| format!("Invalid time format: {}", hhmmss))?;
234    parse_valid_utc(century + year, month, day, hour, minute, second, 0)
235}
236
237/// Parse time field of format HHMMSS.SS and convert it to `DateTime<Utc>` using the given date.
238pub(crate) fn parse_hhmmss_ss(
239    hhmmss: &str,
240    date: DateTime<Utc>,
241) -> Result<DateTime<Utc>, ParseError> {
242    let (hour, minute, second, nano) = parse_time_with_fractions(hhmmss)
243        .map_err(|_| format!("Invalid time format: {}", hhmmss))?;
244    parse_valid_utc(
245        date.year(),
246        date.month(),
247        date.day(),
248        hour,
249        minute,
250        second,
251        nano,
252    )
253}
254
255/// Pick date by picking the given field numbers. Set time part to midnight.
256pub(crate) fn pick_date_with_fields(
257    split: &[&str],
258    year_field: usize,
259    month_field: usize,
260    day_field: usize,
261    hour: u32,
262    minute: u32,
263    second: u32,
264    nanos: u32,
265) -> Result<DateTime<Utc>, ParseError> {
266    let year = split.get(year_field).unwrap_or(&"").parse::<i32>()?;
267    let month = split.get(month_field).unwrap_or(&"").parse::<u32>()?;
268    let day = split.get(day_field).unwrap_or(&"").parse::<u32>()?;
269    parse_valid_utc(year, month, day, hour, minute, second, nanos)
270}
271
272/// Pick time zone (`FixedOffset`) with the given field numbers.
273pub(crate) fn pick_timezone_with_fields(
274    split: &[&str],
275    hour_field: usize,
276    minute_field: usize,
277) -> Result<FixedOffset, ParseError> {
278    let hour = split.get(hour_field).unwrap_or(&"").parse::<i32>()?;
279    let minute = split.get(minute_field).unwrap_or(&"0").parse::<i32>()?;
280
281    if let Some(offset) = FixedOffset::east_opt(hour * 3600 + hour.signum() * minute * 60) {
282        Ok(offset)
283    } else {
284        Err(ParseError::InvalidSentence(format!(
285            "Time zone offset out of bounds: {}:{}",
286            hour, minute
287        )))
288    }
289}
290
291/// Parse day, month and year from YYMMDD string.
292fn parse_date(yymmdd: &str) -> Result<(u32, u32, i32), ParseError> {
293    let day = pick_s2(yymmdd, 0).parse::<u32>()?;
294    let month = pick_s2(yymmdd, 2).parse::<u32>()?;
295    let year = pick_s2(yymmdd, 4).parse::<i32>()?;
296    Ok((day, month, year))
297}
298
299/// Parse hour, minute and second from HHMMSS string.
300fn parse_time(hhmmss: &str) -> Result<(u32, u32, u32), ParseError> {
301    let hour = pick_s2(hhmmss, 0).parse::<u32>()?;
302    let minute = pick_s2(hhmmss, 2).parse::<u32>()?;
303    let second = pick_s2(hhmmss, 4).parse::<u32>()?;
304    Ok((hour, minute, second))
305}
306
307/// Parse hour, minute, second and nano seconds from HHMMSS.SS string.
308fn parse_time_with_fractions(hhmmss: &str) -> Result<(u32, u32, u32, u32), ParseError> {
309    let hour = pick_s2(hhmmss, 0).parse::<u32>()?;
310    let minute = pick_s2(hhmmss, 2).parse::<u32>()?;
311    let second = pick_s2(hhmmss, 4).parse::<u32>()?;
312    let nano = {
313        let nano_str = hhmmss.get(6..).unwrap_or(".0");
314        if !nano_str.is_empty() {
315            (nano_str.parse::<f64>()? * 1000000000.0).round() as u32
316        } else {
317            0
318        }
319    };
320    Ok((hour, minute, second, nano))
321}
322
323/// Parse Utc date from YYYY MM DD hh mm ss
324pub(crate) fn parse_ymdhs(
325    year: i32,
326    month: u32,
327    day: u32,
328    hour: u32,
329    min: u32,
330    sec: u32,
331) -> Result<DateTime<Utc>, ParseError> {
332    parse_valid_utc(year, month, day, hour, min, sec, 0)
333}
334
335/// Using _opt on Utc. Will catch invalid Date (ex: month > 12).
336pub fn parse_valid_utc(
337    year: i32,
338    month: u32,
339    day: u32,
340    hour: u32,
341    min: u32,
342    sec: u32,
343    nano: u32,
344) -> Result<DateTime<Utc>, ParseError> {
345    let opt_utc = Utc
346        .ymd_opt(year, month, day)
347        .and_hms_nano_opt(hour, min, sec, nano);
348    match opt_utc {
349        chrono::LocalResult::Single(valid_utc) | chrono::LocalResult::Ambiguous(valid_utc, _) => {
350            Ok(valid_utc)
351        }
352        chrono::LocalResult::None => Err(format!(
353            "Failed to parse Utc Date from y:{} m:{} d:{} h:{} m:{} s:{}",
354            year, month, day, hour, min, sec
355        )
356        .into()),
357    }
358}
359
360/// A simple helper to pick a substring of length two from the given string.
361fn pick_s2(s: &str, i: usize) -> &str {
362    let end = i + 2;
363    s.get(i..end).unwrap_or("")
364}
365
366/// Parse latitude from two string.
367/// Argument `lat_string` expects format DDMM.MMM representing latitude.
368/// Argument `hemisphere` expects "N" for north or "S" for south. If `hemisphere` value
369/// is something else, north is quietly used as a fallback.
370pub(crate) fn parse_latitude_ddmm_mmm(
371    lat_string: &str,
372    hemisphere: &str,
373) -> Result<Option<f64>, ParseError> {
374    // DDMM.MMM
375    if lat_string.is_empty() {
376        return Ok(None);
377    }
378
379    // Validate: 4 digits, a decimal point, then 1 or more digits
380    let byte_string = lat_string.as_bytes();
381    if !(byte_string.iter().take(4).all(|c| c.is_ascii_digit())
382        && byte_string.get(4) == Some(&b'.')
383        && byte_string
384            .get(5)
385            .map(|c| c.is_ascii_digit())
386            .unwrap_or(false))
387    {
388        return Err(format!("Failed to parse latitude (DDMM.MMM) from {}", lat_string).into());
389    }
390    let end = 5 + byte_string
391        .iter()
392        .skip(5)
393        .take_while(|c| c.is_ascii_digit())
394        .count();
395
396    // Extract
397    let d = lat_string[0..2].parse::<f64>().unwrap_or(0.0);
398    let m = lat_string[2..end].parse::<f64>().unwrap_or(0.0);
399    let val = d + m / 60.0;
400    Ok(Some(match hemisphere {
401        "N" => val,
402        "S" => -val,
403        _ => val,
404    }))
405}
406
407/// Parse longitude from two string.
408/// Argument `lon_string` expects format DDDMM.MMM representing longitude.
409/// Argument `hemisphere` expects "E" for east or "W" for west. If `hemisphere` value is
410/// something else, east is quietly used as a fallback.
411pub(crate) fn parse_longitude_dddmm_mmm(
412    lon_string: &str,
413    hemisphere: &str,
414) -> Result<Option<f64>, String> {
415    // DDDMM.MMM
416    if lon_string.is_empty() {
417        return Ok(None);
418    }
419
420    // Validate: 5 digits, a decimal point, then 1 or more digits
421    let byte_string = lon_string.as_bytes();
422    if !(byte_string.iter().take(5).all(|c| c.is_ascii_digit())
423        && byte_string.get(5) == Some(&b'.')
424        && byte_string
425            .get(6)
426            .map(|c| c.is_ascii_digit())
427            .unwrap_or(false))
428    {
429        return Err(format!(
430            "Failed to parse longitude (DDDMM.MMM) from {}",
431            lon_string
432        ));
433    }
434    let end = 6 + byte_string
435        .iter()
436        .skip(6)
437        .take_while(|c| c.is_ascii_digit())
438        .count();
439
440    // Extract
441    let d = lon_string[0..3].parse::<f64>().unwrap_or(0.0);
442    let m = lon_string[3..end].parse::<f64>().unwrap_or(0.0);
443    let val = d + m / 60.0;
444    Ok(Some(match hemisphere {
445        "E" => val,
446        "W" => -val,
447        _ => val,
448    }))
449}
450
451/// Parse latitude from two string.
452/// Argument `lat_string` expects a latitude offset in minutes
453/// Argument `hemisphere` expects "N" for north or "S" for south. If `hemisphere` value
454/// is something else, north is quietly used as a fallback.
455pub(crate) fn parse_latitude_m_m(
456    lat_string: &str,
457    hemisphere: &str,
458) -> Result<Option<f64>, ParseError> {
459    if !lat_string.is_empty() {
460        match lat_string.parse::<f64>() {
461            Ok(lat) => match hemisphere {
462                "N" => Ok(Some(lat / 60.0)),
463                "S" => Ok(Some(-lat / 60.0)),
464                _ => Err(format!("Bad hemispehre: {}", hemisphere).into()),
465            },
466            Err(_) => Err(format!("Failed to parse float: {}", lat_string).into()),
467        }
468    } else {
469        Ok(None)
470    }
471}
472
473/// Parse longitude from two string.
474/// Argument `long_string` expects a longitude offset in minutes
475/// Argument `hemisphere` expects "E" for east or "W" for west. If `hemisphere` value is
476/// something else, east is quietly used as a fallback.
477pub(crate) fn parse_longitude_m_m(
478    lon_string: &str,
479    hemisphere: &str,
480) -> Result<Option<f64>, String> {
481    if !lon_string.is_empty() {
482        match lon_string.parse::<f64>() {
483            Ok(lon) => match hemisphere {
484                "E" => Ok(Some(lon / 60.0)),
485                "W" => Ok(Some(-lon / 60.0)),
486                _ => Err(format!("Bad hemispehre: {}", hemisphere)),
487            },
488            Err(_) => Err(format!("Failed to parse float: {}", lon_string)),
489        }
490    } else {
491        Ok(None)
492    }
493}
494
495// -------------------------------------------------------------------------------------------------
496
497#[cfg(test)]
498mod test {
499    use super::*;
500
501    #[test]
502    fn test_parse_payload() {
503        match parse_payload("w7b0P1") {
504            Ok(bv) => {
505                assert_eq!(
506                    bv,
507                    bits![
508                        1, 1, 1, 1, 1, 1, //
509                        0, 0, 0, 1, 1, 1, //
510                        1, 0, 1, 0, 1, 0, //
511                        0, 0, 0, 0, 0, 0, //
512                        1, 0, 0, 0, 0, 0, //
513                        0, 0, 0, 0, 0, 1, //
514                    ]
515                );
516            }
517            Err(e) => {
518                assert_eq!(e, "OK");
519            }
520        }
521    }
522
523    #[test]
524    fn test_pick_u64() {
525        let bv = bitvec![1, 0, 1, 1, 0, 1];
526        assert_eq!(pick_u64(&bv, 0, 2), 2);
527        assert_eq!(pick_u64(&bv, 2, 2), 3);
528        assert_eq!(pick_u64(&bv, 4, 2), 1);
529        assert_eq!(pick_u64(&bv, 0, 6), 45);
530        assert_eq!(pick_u64(&bv, 4, 4), 4);
531        assert_eq!(pick_u64(&bv, 6, 2), 0);
532    }
533
534    #[test]
535    fn test_pick_i64() {
536        assert_eq!(pick_i64(&bitvec![0, 1, 1, 1, 1, 1], 0, 6), 31);
537        assert_eq!(pick_i64(&bitvec![0, 0, 0, 0, 0, 1], 0, 6), 1);
538        assert_eq!(pick_i64(&bitvec![0, 0, 0, 0, 0, 0], 0, 6), 0);
539        assert_eq!(pick_i64(&bitvec![1, 1, 1, 1, 1, 1], 0, 6), -1);
540        assert_eq!(pick_i64(&bitvec![1, 0, 0, 0, 0, 0], 0, 6), -32);
541    }
542
543    #[test]
544    fn test_pick_string() {
545        let bv = bitvec![
546            1, 1, 1, 1, 1, 1, // ?
547            0, 0, 0, 0, 0, 1, // A
548            0, 0, 0, 1, 1, 1, // G
549            0, 1, 1, 1, 1, 1, // _
550            1, 1, 0, 1, 0, 0, // 4
551            1, 1, 1, 0, 1, 0, // :
552            1, 0, 0, 0, 0, 1, // !
553            0, 0, 0, 0, 0, 0, // @ (end of line char)
554            0, 0, 0, 0, 1, 0, // B (rubbish)
555        ];
556        assert_eq!(pick_string(&bv, 0, bv.len() / 6), "?AG_4:!");
557    }
558
559    #[test]
560    fn test_pick_eta() {
561        // Valid case
562        let bv = bitvec![
563            1, 0, 1, 0, // 10
564            0, 1, 0, 1, 1, // 11
565            1, 0, 1, 1, 0, // 22
566            1, 1, 1, 0, 0, 1, // 57
567        ];
568        let eta = pick_eta(&bv, 0).ok().unwrap();
569        assert_eq!(
570            eta,
571            Utc.with_ymd_and_hms(eta.unwrap().year(), 10, 11, 22, 57, 30)
572                .single()
573        );
574
575        // Invalid month
576        let bv = bitvec![
577            1, 1, 0, 1, // 13
578            0, 1, 0, 1, 1, // 11
579            1, 0, 1, 1, 0, // 22
580            1, 1, 1, 0, 0, 1, // 57
581        ];
582        assert!(!pick_eta(&bv, 0).is_ok());
583
584        // Invalid day
585        let bv = bitvec![
586            0, 0, 1, 0, // 2
587            1, 1, 1, 1, 1, // 31
588            1, 0, 1, 1, 0, // 22
589            1, 1, 1, 0, 0, 1, // 57
590        ];
591        assert!(!pick_eta(&bv, 0).is_ok());
592
593        // Invalid hour
594        let bv = bitvec![
595            1, 0, 1, 0, // 10
596            0, 1, 0, 1, 1, // 11
597            1, 1, 0, 0, 1, // 25
598            1, 1, 1, 0, 0, 1, // 57
599        ];
600        assert!(!pick_eta(&bv, 0).is_ok());
601
602        // Invalid minute
603        let bv = bitvec![
604            1, 0, 1, 0, // 10
605            0, 1, 0, 1, 1, // 11
606            1, 0, 1, 1, 0, // 22
607            1, 1, 1, 1, 0, 1, // 61
608        ];
609        assert!(!pick_eta(&bv, 0).is_ok());
610    }
611
612    #[test]
613    fn test_pick_eta_with_now() {
614        // February 28
615        let feb28 = bitvec![
616            0, 0, 1, 0, // 2
617            1, 1, 1, 0, 0, // 28
618            0, 0, 0, 0, 0, // 0
619            0, 0, 0, 0, 0, 0, // 0
620        ];
621
622        //February 29
623        let feb29 = bitvec![
624            0, 0, 1, 0, // 2
625            1, 1, 1, 0, 1, // 29
626            0, 0, 0, 0, 0, // 0
627            0, 0, 0, 0, 0, 0, // 0
628        ];
629
630        // Leap day case
631        let then = Utc
632            .with_ymd_and_hms(2020, 12, 31, 0, 0, 0)
633            .single()
634            .unwrap();
635        assert_eq!(
636            pick_eta_with_now(&feb29, 0, then).ok().unwrap(),
637            Utc.with_ymd_and_hms(2020, 2, 29, 0, 0, 30).single()
638        );
639
640        // Non leap day case
641        let then = Utc
642            .with_ymd_and_hms(2020, 12, 31, 0, 0, 0)
643            .single()
644            .unwrap();
645        assert_eq!(
646            pick_eta_with_now(&feb28, 0, then).ok().unwrap(),
647            Utc.with_ymd_and_hms(2021, 2, 28, 0, 0, 30).single()
648        );
649
650        // Non leap year invalid case
651        let then = Utc
652            .with_ymd_and_hms(2021, 12, 31, 0, 0, 0)
653            .single()
654            .unwrap();
655        assert_eq!(pick_eta_with_now(&feb29, 0, then).is_ok(), false);
656
657        // Non leap year valid case
658        let then = Utc
659            .with_ymd_and_hms(2021, 12, 31, 0, 0, 0)
660            .single()
661            .unwrap();
662        assert_eq!(pick_eta_with_now(&feb28, 0, then).is_ok(), true);
663
664        // One day late
665        let then = Utc.with_ymd_and_hms(2021, 3, 1, 0, 0, 0).single().unwrap();
666        assert_eq!(
667            pick_eta_with_now(&feb28, 0, then).ok().unwrap(),
668            Utc.with_ymd_and_hms(2021, 2, 28, 0, 0, 30).single()
669        );
670
671        // Six months late
672        let then = Utc.with_ymd_and_hms(2021, 8, 31, 0, 0, 0).single().unwrap();
673        assert_eq!(
674            pick_eta_with_now(&feb28, 0, then).ok().unwrap(),
675            Utc.with_ymd_and_hms(2022, 2, 28, 0, 0, 30).single()
676        );
677    }
678
679    #[test]
680    fn test_parse_valid_utc() {
681        assert!(parse_valid_utc(2020, 2, 29, 0, 0, 0, 0).is_ok());
682        assert!(!parse_valid_utc(2021, 2, 29, 0, 0, 0, 0).is_ok());
683    }
684
685    #[test]
686    fn test_pick_number_field() {
687        let s: Vec<&str> = "128,0,8.0,,xyz".split(',').collect();
688        assert_eq!(pick_number_field::<u8>(&s, 0).ok().unwrap().unwrap(), 128);
689        assert_eq!(pick_number_field::<u8>(&s, 1).ok().unwrap().unwrap(), 0);
690        assert_eq!(pick_number_field::<f64>(&s, 2).ok().unwrap().unwrap(), 8.0);
691        assert_eq!(pick_number_field::<u16>(&s, 3).ok().unwrap(), None);
692        assert!(!pick_number_field::<u32>(&s, 4).is_ok());
693        assert_eq!(pick_number_field::<u32>(&s, 5).ok().unwrap(), None);
694    }
695
696    #[test]
697    fn test_pick_hex_field() {
698        let s: Vec<&str> = "ff,0,,FFFF,8080808080808080".split(',').collect();
699        assert_eq!(pick_hex_field::<u8>(&s, 0).unwrap().unwrap(), 255);
700        assert_eq!(pick_hex_field::<u8>(&s, 1).unwrap().unwrap(), 0);
701        assert_eq!(pick_hex_field::<u8>(&s, 2).unwrap(), None);
702        assert_eq!(pick_hex_field::<u16>(&s, 3).unwrap().unwrap(), 65535);
703        assert_eq!(
704            pick_hex_field::<u64>(&s, 4).unwrap().unwrap(),
705            9259542123273814144
706        );
707    }
708
709    #[test]
710    fn test_parse_latitude_m_m() {
711        assert::close(
712            parse_latitude_m_m("3480", "N").ok().unwrap().unwrap_or(0.0),
713            58.0,
714            0.1,
715        );
716        assert::close(
717            parse_latitude_m_m("3480", "S").ok().unwrap().unwrap_or(0.0),
718            -58.0,
719            0.1,
720        );
721        assert!(!parse_latitude_m_m("3480", "X").is_ok());
722        assert!(!parse_latitude_m_m("ABCD", "N").is_ok());
723        assert!(parse_latitude_m_m("", "N").is_ok());
724        assert_eq!(parse_latitude_m_m("", "N").ok().unwrap(), None);
725    }
726
727    #[test]
728    fn test_parse_longitude_m_m() {
729        assert::close(
730            parse_longitude_m_m("1140", "E")
731                .ok()
732                .unwrap()
733                .unwrap_or(0.0),
734            19.0,
735            0.1,
736        );
737        assert::close(
738            parse_longitude_m_m("1140", "W")
739                .ok()
740                .unwrap()
741                .unwrap_or(0.0),
742            -19.0,
743            0.1,
744        );
745        assert!(!parse_longitude_m_m("1140", "X").is_ok());
746        assert!(!parse_longitude_m_m("ABCD", "E").is_ok());
747        assert!(parse_longitude_m_m("", "E").is_ok());
748        assert_eq!(parse_longitude_m_m("", "E").ok().unwrap(), None);
749    }
750
751    #[test]
752    fn test_pick_string_field() {
753        let s: Vec<&str> = "a,b,,dd,e".split(',').collect();
754        assert_eq!(pick_string_field(&s, 0), Some("a".into()));
755        assert_eq!(pick_string_field(&s, 1), Some("b".into()));
756        assert_eq!(pick_string_field(&s, 2), None);
757        assert_eq!(pick_string_field(&s, 3), Some("dd".into()));
758        assert_eq!(pick_string_field(&s, 4), Some("e".into()));
759        assert_eq!(pick_string_field(&s, 5), None);
760    }
761
762    #[test]
763    fn test_parse_time_with_fractions() {
764        assert_eq!(
765            parse_time_with_fractions("123456.987").unwrap_or((0, 0, 0, 0)),
766            (12, 34, 56, 987000000)
767        );
768        assert_eq!(
769            parse_time_with_fractions("123456").unwrap_or((0, 0, 0, 0)),
770            (12, 34, 56, 0)
771        );
772    }
773
774    #[test]
775    fn test_parse_hhmmss_ss() {
776        // Valid case with fractions
777        let then = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).single().unwrap();
778        assert_eq!(
779            parse_hhmmss_ss("123456.987", then).ok(),
780            Some(Utc.ymd(2000, 1, 1).and_hms_nano(12, 34, 56, 987000000))
781        );
782
783        // Valid case without fractions
784        let then = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).single().unwrap();
785        assert_eq!(
786            parse_hhmmss_ss("123456", then).ok(),
787            Some(Utc.ymd(2000, 1, 1).and_hms_nano(12, 34, 56, 0))
788        );
789
790        // Invalid case
791        assert_eq!(parse_hhmmss_ss("123456@", then).ok(), None);
792    }
793
794    #[test]
795    fn test_pick_date_with_fields() {
796        let s: Vec<&str> = "$GPZDA,072914.00,31,05,2018,+02,00".split(',').collect();
797        assert_eq!(
798            pick_date_with_fields(&s, 4, 3, 2, 0, 0, 0, 0).ok(),
799            Utc.with_ymd_and_hms(2018, 5, 31, 0, 0, 0).single()
800        )
801    }
802
803    #[test]
804    fn test_pick_timezone_with_fields() {
805        // Valid positive time zone
806        let s: Vec<&str> = ",,,,,+4,30".split(',').collect();
807        assert_eq!(
808            pick_timezone_with_fields(&s, 5, 6).ok(),
809            Some(FixedOffset::east(4 * 3600 + 30 * 60))
810        );
811
812        // Valid negative time zone
813        let s: Vec<&str> = ",,,,,-4,30".split(',').collect();
814        assert_eq!(
815            pick_timezone_with_fields(&s, 5, 6).ok(),
816            FixedOffset::east_opt(-4 * 3600 - 30 * 60)
817        );
818
819        // Invalid time zone
820        let s: Vec<&str> = ",,,,,+25,00".split(',').collect();
821        assert!(!pick_timezone_with_fields(&s, 5, 6).is_ok());
822    }
823}