use crate::{
encoder::NmeaEncode,
macros::{write_byte, write_str},
message::NmeaMessageError,
parser::NmeaParse,
time::{NmeaDate, NmeaDateTime, NmeaDateTimeOffset, NmeaTime},
};
#[derive(Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Zda<'a> {
pub time: &'a str,
pub day: &'a str,
pub month: &'a str,
pub year: &'a str,
pub local_zone_hours: &'a str,
pub local_zone_minutes: &'a str,
}
impl<'a> NmeaParse<'a> for Zda<'a> {
fn parse(fields: &'a str) -> Result<Self, NmeaMessageError> {
let mut f = fields.splitn(6, ',');
Ok(Self {
time: f.next().ok_or(NmeaMessageError::MissingField)?,
day: f.next().ok_or(NmeaMessageError::MissingField)?,
month: f.next().ok_or(NmeaMessageError::MissingField)?,
year: f.next().ok_or(NmeaMessageError::MissingField)?,
local_zone_hours: f.next().ok_or(NmeaMessageError::MissingField)?,
local_zone_minutes: f.next().ok_or(NmeaMessageError::MissingField)?,
})
}
}
impl NmeaEncode for Zda<'_> {
fn encoded_len(&self) -> usize {
self.time.len()
+ self.day.len()
+ self.month.len()
+ self.year.len()
+ self.local_zone_hours.len()
+ self.local_zone_minutes.len()
+ 5
}
fn encode(&self, buf: &mut [u8]) -> usize {
let mut pos = 0;
write_str!(buf, pos, self.time);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.day);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.month);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.year);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.local_zone_hours);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.local_zone_minutes);
pos
}
}
impl Zda<'_> {
#[must_use]
pub fn time(&self) -> Option<NmeaTime> {
NmeaTime::parse(self.time)
}
#[must_use]
pub fn date(&self) -> Option<NmeaDate> {
let year = self.year.parse().ok()?;
let month = self.month.parse().ok()?;
if !(1..=12).contains(&month) {
return None;
}
let day = self.day.parse().ok()?;
if !(1..=31).contains(&day) {
return None;
}
Some(NmeaDate { year, month, day })
}
#[must_use]
pub fn date_time(&self) -> Option<NmeaDateTime> {
Some(NmeaDateTime {
date: self.date()?,
time: self.time()?,
})
}
#[must_use]
pub fn date_time_with_offset(&self) -> Option<NmeaDateTimeOffset> {
Some(NmeaDateTimeOffset {
datetime: self.date_time()?,
offset_hours: self.local_zone_hours.parse().ok()?,
offset_minutes: self.local_zone_minutes.parse().ok()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{encoder::NmeaEncode, parser::NmeaParse};
fn zda(fields: &str) -> Zda<'_> {
Zda::parse(fields).expect("valid parse")
}
#[test]
fn parse_typical() {
let s = zda("123519.00,14,03,2004,00,00");
assert_eq!(s.time, "123519.00");
assert_eq!(s.day, "14");
assert_eq!(s.month, "03");
assert_eq!(s.year, "2004");
assert_eq!(s.local_zone_hours, "00");
assert_eq!(s.local_zone_minutes, "00");
}
#[test]
fn parse_with_offset() {
let s = zda("010203.00,01,01,2000,05,30");
assert_eq!(s.local_zone_hours, "05");
assert_eq!(s.local_zone_minutes, "30");
}
#[test]
fn parse_empty_fields() {
let s = zda(",,,,, ");
assert_eq!(s.time, "");
assert_eq!(s.day, "");
assert_eq!(s.month, "");
assert_eq!(s.year, "");
assert_eq!(s.local_zone_hours, "");
assert_eq!(s.local_zone_minutes, " ");
}
#[test]
fn parse_missing_fields_returns_error() {
assert!(Zda::parse("123519.00,14,03,2004,00").is_err());
assert!(Zda::parse("123519.00").is_err());
assert!(Zda::parse("").is_err());
}
fn roundtrip(input: &str) {
let s = zda(input);
let len = s.encoded_len();
let mut buf = [0u8; 256];
let written = s.encode(&mut buf);
assert_eq!(
written, len,
"encode() returned {written} but encoded_len() said {len}"
);
assert_eq!(&buf[..written], input.as_bytes());
}
#[test]
fn encode_roundtrip_typical() {
roundtrip("123519.00,14,03,2004,00,00");
}
#[test]
fn encode_roundtrip_with_offset() {
roundtrip("010203.00,01,01,2000,05,30");
}
#[test]
fn encode_roundtrip_empty_fields() {
roundtrip(",,,,,");
}
#[test]
fn encoded_len_counts_five_commas() {
let s = zda(",,,,,");
assert_eq!(s.encoded_len(), 5);
}
#[test]
fn time_valid() {
let t = zda("123519.00,14,03,2004,00,00").time().unwrap();
assert_eq!(t.hours, 12);
assert_eq!(t.minutes, 35);
assert_eq!(t.seconds, 19);
}
#[test]
fn time_invalid_returns_none() {
assert!(zda("BADTIME,14,03,2004,00,00").time().is_none());
assert!(zda(",14,03,2004,00,00").time().is_none());
}
#[test]
fn date_valid() {
let d = zda("123519.00,14,03,2004,00,00").date().unwrap();
assert_eq!(d.year, 2004);
assert_eq!(d.month, 3);
assert_eq!(d.day, 14);
}
#[test]
fn date_month_zero_is_invalid() {
assert!(zda("120000.00,01,00,2000,00,00").date().is_none());
}
#[test]
fn date_month_thirteen_is_invalid() {
assert!(zda("120000.00,01,13,2000,00,00").date().is_none());
}
#[test]
fn date_day_zero_is_invalid() {
assert!(zda("120000.00,00,01,2000,00,00").date().is_none());
}
#[test]
fn date_day_thirty_two_is_invalid() {
assert!(zda("120000.00,32,01,2000,00,00").date().is_none());
}
#[test]
fn date_boundary_day_31_month_12() {
let d = zda("235959.00,31,12,1999,00,00").date().unwrap();
assert_eq!(d.day, 31);
assert_eq!(d.month, 12);
}
#[test]
fn date_non_numeric_fields_return_none() {
assert!(zda("120000.00,XX,03,2004,00,00").date().is_none());
assert!(zda("120000.00,14,YY,2004,00,00").date().is_none());
assert!(zda("120000.00,14,03,ZZZZ,00,00").date().is_none());
}
#[test]
fn date_empty_fields_return_none() {
assert!(zda("120000.00,,,2004,00,00").date().is_none());
}
#[test]
fn date_time_valid() {
let dt = zda("123519.00,14,03,2004,00,00").date_time().unwrap();
assert_eq!(dt.date.year, 2004);
assert_eq!(dt.date.month, 3);
assert_eq!(dt.date.day, 14);
assert_eq!(dt.time.hours, 12);
assert_eq!(dt.time.minutes, 35);
assert_eq!(dt.time.seconds, 19);
}
#[test]
fn date_time_bad_time_returns_none() {
assert!(zda("BADTIME,14,03,2004,00,00").date_time().is_none());
}
#[test]
fn date_time_bad_date_returns_none() {
assert!(zda("123519.00,14,13,2004,00,00").date_time().is_none());
}
#[test]
fn offset_valid_positive() {
let dto = zda("123519.00,14,03,2004,05,30")
.date_time_with_offset()
.unwrap();
assert_eq!(dto.offset_hours, 5);
assert_eq!(dto.offset_minutes, 30);
}
#[test]
fn offset_valid_negative() {
let dto = zda("123519.00,14,03,2004,-05,00")
.date_time_with_offset()
.unwrap();
assert_eq!(dto.offset_hours, -5);
assert_eq!(dto.offset_minutes, 0);
}
#[test]
fn offset_utc_zero() {
let dto = zda("000000.00,01,01,2000,00,00")
.date_time_with_offset()
.unwrap();
assert_eq!(dto.offset_hours, 0);
assert_eq!(dto.offset_minutes, 0);
}
#[test]
fn offset_non_numeric_returns_none() {
assert!(
zda("123519.00,14,03,2004,XX,00")
.date_time_with_offset()
.is_none()
);
assert!(
zda("123519.00,14,03,2004,05,YY")
.date_time_with_offset()
.is_none()
);
}
#[test]
fn offset_bad_datetime_returns_none() {
assert!(
zda("123519.00,14,13,2004,05,30")
.date_time_with_offset()
.is_none()
);
}
}