#[cfg_attr(feature = "c", repr(C))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NmeaDate {
pub year: u16,
pub month: u8,
pub day: u8,
}
#[cfg_attr(feature = "c", repr(C))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NmeaTime {
pub hours: u8,
pub minutes: u8,
pub seconds: u8,
pub subseconds: u16,
}
impl NmeaTime {
#[must_use]
pub fn parse(raw: &str) -> Option<Self> {
if raw.len() < 6 {
return None;
}
let hours: u8 = raw[0..2].parse().ok()?;
if hours > 23 {
return None;
}
let minutes: u8 = raw[2..4].parse().ok()?;
if minutes > 59 {
return None;
}
let seconds: u8 = raw[4..6].parse().ok()?;
if seconds > 60 {
return None;
}
let subseconds: u16 = if raw.len() >= 8 {
raw[7..].parse().ok()?
} else {
0
};
if subseconds > 99 {
return None;
}
Some(NmeaTime {
hours,
minutes,
seconds,
subseconds,
})
}
}
#[cfg_attr(feature = "c", repr(C))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NmeaDateTime {
pub date: NmeaDate,
pub time: NmeaTime,
}
#[cfg_attr(feature = "c", repr(C))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NmeaDateTimeOffset {
pub datetime: NmeaDateTime,
pub offset_hours: i8,
pub offset_minutes: i8,
}
#[cfg(feature = "time")]
#[derive(Debug, thiserror::Error)]
pub enum NmeaTimeConversionError {
#[error(
"invalid time: hours={hours}, minutes={minutes}, seconds={seconds}, subseconds={subseconds}"
)]
InvalidTime {
hours: u8,
minutes: u8,
seconds: u8,
subseconds: u16,
},
#[error("invalid date: year={year}, month={month}, day={day}")]
InvalidDate { year: u16, month: u8, day: u8 },
#[error("invalid UTC offset: hours={offset_hours}, minutes={offset_minutes}")]
InvalidOffset {
offset_hours: i8,
offset_minutes: i8,
},
}
#[cfg(feature = "time")]
impl TryFrom<NmeaTime> for time::Time {
type Error = NmeaTimeConversionError;
fn try_from(value: NmeaTime) -> Result<Self, Self::Error> {
time::Time::from_hms_milli(value.hours, value.minutes, value.seconds, value.subseconds)
.map_err(|_| NmeaTimeConversionError::InvalidTime {
hours: value.hours,
minutes: value.minutes,
seconds: value.seconds,
subseconds: value.subseconds,
})
}
}
#[cfg(feature = "time")]
impl TryFrom<NmeaDate> for time::Date {
type Error = NmeaTimeConversionError;
fn try_from(value: NmeaDate) -> Result<Self, Self::Error> {
let month = time::Month::try_from(value.month).map_err(|_| {
NmeaTimeConversionError::InvalidDate {
year: value.year,
month: value.month,
day: value.day,
}
})?;
time::Date::from_calendar_date(value.year.into(), month, value.day).map_err(|_| {
NmeaTimeConversionError::InvalidDate {
year: value.year,
month: value.month,
day: value.day,
}
})
}
}
#[cfg(feature = "time")]
impl TryFrom<NmeaDateTime> for time::PrimitiveDateTime {
type Error = NmeaTimeConversionError;
fn try_from(value: NmeaDateTime) -> Result<Self, Self::Error> {
Ok(time::PrimitiveDateTime::new(
value.date.try_into()?,
value.time.try_into()?,
))
}
}
#[cfg(feature = "time")]
impl TryFrom<NmeaDateTimeOffset> for time::OffsetDateTime {
type Error = NmeaTimeConversionError;
fn try_from(value: NmeaDateTimeOffset) -> Result<Self, Self::Error> {
let dt: time::PrimitiveDateTime = value.datetime.try_into()?;
let offset = time::UtcOffset::from_hms(value.offset_hours, value.offset_minutes, 0)
.map_err(|_| NmeaTimeConversionError::InvalidOffset {
offset_hours: value.offset_hours,
offset_minutes: value.offset_minutes,
})?;
Ok(dt.assume_offset(offset))
}
}
#[cfg(feature = "chrono")]
#[derive(Debug, thiserror::Error)]
pub enum NmeaChronoConversionError {
#[error(
"invalid time: hours={hours}, minutes={minutes}, seconds={seconds}, subseconds={subseconds}"
)]
InvalidTime {
hours: u8,
minutes: u8,
seconds: u8,
subseconds: u16,
},
#[error("invalid date: year={year}, month={month}, day={day}")]
InvalidDate { year: u16, month: u8, day: u8 },
#[error("invalid UTC offset: hours={offset_hours}, minutes={offset_minutes}")]
InvalidOffset {
offset_hours: i8,
offset_minutes: i8,
},
}
#[cfg(feature = "chrono")]
impl TryFrom<NmeaTime> for chrono::NaiveTime {
type Error = NmeaChronoConversionError;
fn try_from(value: NmeaTime) -> Result<Self, Self::Error> {
chrono::NaiveTime::from_hms_milli_opt(
value.hours.into(),
value.minutes.into(),
value.seconds.into(),
value.subseconds.into(),
)
.ok_or(NmeaChronoConversionError::InvalidTime {
hours: value.hours,
minutes: value.minutes,
seconds: value.seconds,
subseconds: value.subseconds,
})
}
}
#[cfg(feature = "chrono")]
impl TryFrom<NmeaDate> for chrono::NaiveDate {
type Error = NmeaChronoConversionError;
fn try_from(value: NmeaDate) -> Result<Self, Self::Error> {
chrono::NaiveDate::from_ymd_opt(value.year.into(), value.month.into(), value.day.into())
.ok_or(NmeaChronoConversionError::InvalidDate {
year: value.year,
month: value.month,
day: value.day,
})
}
}
#[cfg(feature = "chrono")]
impl TryFrom<NmeaDateTime> for chrono::NaiveDateTime {
type Error = NmeaChronoConversionError;
fn try_from(value: NmeaDateTime) -> Result<Self, Self::Error> {
Ok(chrono::NaiveDateTime::new(
value.date.try_into()?,
value.time.try_into()?,
))
}
}
#[cfg(feature = "chrono")]
impl TryFrom<NmeaDateTimeOffset> for chrono::DateTime<chrono::FixedOffset> {
type Error = NmeaChronoConversionError;
fn try_from(value: NmeaDateTimeOffset) -> Result<Self, Self::Error> {
let dt: chrono::NaiveDateTime = value.datetime.try_into()?;
let offset_secs =
i32::from(value.offset_hours) * 3600 + i32::from(value.offset_minutes) * 60;
let offset = chrono::FixedOffset::east_opt(offset_secs).ok_or(
NmeaChronoConversionError::InvalidOffset {
offset_hours: value.offset_hours,
offset_minutes: value.offset_minutes,
},
)?;
Ok(chrono::DateTime::from_naive_utc_and_offset(dt, offset))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(unused)]
fn date() -> NmeaDate {
NmeaDate {
year: 2004,
month: 3,
day: 14,
}
}
#[allow(unused)]
fn time() -> NmeaTime {
NmeaTime {
hours: 12,
minutes: 35,
seconds: 19,
subseconds: 42,
}
}
#[allow(unused)]
fn datetime() -> NmeaDateTime {
NmeaDateTime {
date: date(),
time: time(),
}
}
#[allow(unused)]
fn datetime_offset() -> NmeaDateTimeOffset {
NmeaDateTimeOffset {
datetime: datetime(),
offset_hours: 5,
offset_minutes: 30,
}
}
#[cfg(feature = "time")]
mod time_tests {
use super::*;
#[test]
fn time_converts() {
let t: time::Time = time().try_into().unwrap();
assert_eq!(t.hour(), 12);
assert_eq!(t.minute(), 35);
assert_eq!(t.second(), 19);
assert_eq!(t.millisecond(), 42);
}
#[test]
fn time_invalid_hours_errors() {
let t = NmeaTime {
hours: 24,
minutes: 0,
seconds: 0,
subseconds: 0,
};
assert!(time::Time::try_from(t).is_err());
}
#[test]
fn time_invalid_minutes_errors() {
let t = NmeaTime {
hours: 0,
minutes: 60,
seconds: 0,
subseconds: 0,
};
assert!(time::Time::try_from(t).is_err());
}
#[test]
fn time_invalid_seconds_errors() {
let t = NmeaTime {
hours: 0,
minutes: 0,
seconds: 61,
subseconds: 0,
};
assert!(time::Time::try_from(t).is_err());
}
#[test]
fn date_converts() {
let d: time::Date = date().try_into().unwrap();
assert_eq!(d.year(), 2004);
assert_eq!(d.month(), time::Month::March);
assert_eq!(d.day(), 14);
}
#[test]
fn date_invalid_month_errors() {
let d = NmeaDate {
year: 2004,
month: 13,
day: 1,
};
assert!(time::Date::try_from(d).is_err());
}
#[test]
fn date_invalid_day_errors() {
let d = NmeaDate {
year: 2004,
month: 1,
day: 32,
};
assert!(time::Date::try_from(d).is_err());
}
#[test]
fn datetime_converts() {
let dt: time::PrimitiveDateTime = datetime().try_into().unwrap();
assert_eq!(dt.year(), 2004);
assert_eq!(dt.month(), time::Month::March);
assert_eq!(dt.day(), 14);
assert_eq!(dt.hour(), 12);
assert_eq!(dt.minute(), 35);
assert_eq!(dt.second(), 19);
}
#[test]
fn datetime_offset_converts() {
let dt: time::OffsetDateTime = datetime_offset().try_into().unwrap();
assert_eq!(dt.offset().whole_hours(), 5);
assert_eq!(dt.offset().minutes_past_hour(), 30);
}
#[test]
fn datetime_offset_invalid_errors() {
let dto = NmeaDateTimeOffset {
datetime: datetime(),
offset_hours: 26,
offset_minutes: 0,
};
assert!(time::OffsetDateTime::try_from(dto).is_err());
}
#[test]
fn datetime_utc_offset_zero() {
let dto = NmeaDateTimeOffset {
datetime: datetime(),
offset_hours: 0,
offset_minutes: 0,
};
let dt: time::OffsetDateTime = dto.try_into().unwrap();
assert_eq!(dt.offset().whole_hours(), 0);
assert_eq!(dt.offset().minutes_past_hour(), 0);
}
#[test]
fn datetime_negative_offset() {
let dto = NmeaDateTimeOffset {
datetime: datetime(),
offset_hours: -5,
offset_minutes: 0,
};
let dt: time::OffsetDateTime = dto.try_into().unwrap();
assert_eq!(dt.offset().whole_hours(), -5);
}
}
#[cfg(feature = "chrono")]
mod chrono_tests {
use super::*;
use chrono::Datelike;
use chrono::Timelike;
#[test]
fn time_converts() {
let t: chrono::NaiveTime = time().try_into().unwrap();
assert_eq!(t.hour(), 12);
assert_eq!(t.minute(), 35);
assert_eq!(t.second(), 19);
assert_eq!(t.nanosecond(), 42_000_000);
}
#[test]
fn time_invalid_hours_errors() {
let t = NmeaTime {
hours: 24,
minutes: 0,
seconds: 0,
subseconds: 0,
};
assert!(chrono::NaiveTime::try_from(t).is_err());
}
#[test]
fn time_invalid_minutes_errors() {
let t = NmeaTime {
hours: 0,
minutes: 60,
seconds: 0,
subseconds: 0,
};
assert!(chrono::NaiveTime::try_from(t).is_err());
}
#[test]
fn date_converts() {
let d: chrono::NaiveDate = date().try_into().unwrap();
assert_eq!(d.year(), 2004);
assert_eq!(d.month(), 3);
assert_eq!(d.day(), 14);
}
#[test]
fn date_invalid_month_errors() {
let d = NmeaDate {
year: 2004,
month: 13,
day: 1,
};
assert!(chrono::NaiveDate::try_from(d).is_err());
}
#[test]
fn date_invalid_day_errors() {
let d = NmeaDate {
year: 2004,
month: 1,
day: 32,
};
assert!(chrono::NaiveDate::try_from(d).is_err());
}
#[test]
fn datetime_converts() {
let dt: chrono::NaiveDateTime = datetime().try_into().unwrap();
assert_eq!(dt.date().year(), 2004);
assert_eq!(dt.date().month(), 3);
assert_eq!(dt.date().day(), 14);
assert_eq!(dt.time().hour(), 12);
assert_eq!(dt.time().minute(), 35);
assert_eq!(dt.time().second(), 19);
}
#[test]
fn datetime_offset_converts() {
let dt: chrono::DateTime<chrono::FixedOffset> = datetime_offset().try_into().unwrap();
assert_eq!(dt.offset().local_minus_utc(), 5 * 3600 + 30 * 60);
}
#[test]
fn datetime_offset_invalid_errors() {
let dto = NmeaDateTimeOffset {
datetime: datetime(),
offset_hours: 24,
offset_minutes: 0,
};
assert!(chrono::DateTime::<chrono::FixedOffset>::try_from(dto).is_err());
}
#[test]
fn datetime_utc_offset_zero() {
let dto = NmeaDateTimeOffset {
datetime: datetime(),
offset_hours: 0,
offset_minutes: 0,
};
let dt: chrono::DateTime<chrono::FixedOffset> = dto.try_into().unwrap();
assert_eq!(dt.offset().local_minus_utc(), 0);
}
#[test]
fn datetime_negative_offset() {
let dto = NmeaDateTimeOffset {
datetime: datetime(),
offset_hours: -5,
offset_minutes: 0,
};
let dt: chrono::DateTime<chrono::FixedOffset> = dto.try_into().unwrap();
assert_eq!(dt.offset().local_minus_utc(), -5 * 3600);
}
}
}