aprs_decode/types/
timestamp.rs1use crate::error::AprsError;
2use crate::util::parse_bytes;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum Timestamp {
11 Ddhhmm(u8, u8, u8),
13 Hhmmss(u8, u8, u8),
15 Unsupported(Vec<u8>),
17}
18
19impl Timestamp {
20 pub fn parse(b: &[u8]) -> Result<Self, AprsError> {
22 if b.len() != 7 {
23 return Err(AprsError::InvalidTimestampFormat { raw: b.to_vec() });
24 }
25 if b[6] == b'/' {
26 return Ok(Timestamp::Unsupported(b.to_vec()));
27 }
28 let f1: u8 = parse_bytes(&b[0..2])
29 .ok_or_else(|| AprsError::InvalidTimestampFormat { raw: b.to_vec() })?;
30 let f2: u8 = parse_bytes(&b[2..4])
31 .ok_or_else(|| AprsError::InvalidTimestampFormat { raw: b.to_vec() })?;
32 let f3: u8 = parse_bytes(&b[4..6])
33 .ok_or_else(|| AprsError::InvalidTimestampFormat { raw: b.to_vec() })?;
34
35 match b[6] {
36 b'z' | b'Z' => {
37 validate_ddhhmm(f1, f2, f3, b)?;
38 Ok(Timestamp::Ddhhmm(f1, f2, f3))
39 }
40 b'h' | b'H' => {
41 validate_hhmmss(f1, f2, f3, b)?;
42 Ok(Timestamp::Hhmmss(f1, f2, f3))
43 }
44 _ => Err(AprsError::InvalidTimestampFormat { raw: b.to_vec() }),
45 }
46 }
47
48 pub fn encode(&self, out: &mut Vec<u8>) {
50 match self {
51 Timestamp::Ddhhmm(d, h, m) => {
52 out.extend_from_slice(format!("{:02}{:02}{:02}z", d, h, m).as_bytes());
53 }
54 Timestamp::Hhmmss(h, m, s) => {
55 out.extend_from_slice(format!("{:02}{:02}{:02}h", h, m, s).as_bytes());
56 }
57 Timestamp::Unsupported(raw) => out.extend_from_slice(raw),
58 }
59 }
60}
61
62fn validate_ddhhmm(day: u8, hour: u8, minute: u8, raw: &[u8]) -> Result<(), AprsError> {
63 if day == 0 || day > 31 {
64 return Err(AprsError::TimestampDayOutOfRange { day });
65 }
66 if hour > 23 {
67 return Err(AprsError::TimestampHourOutOfRange { hour });
68 }
69 if minute > 59 {
70 return Err(AprsError::TimestampMinuteOutOfRange { minute });
71 }
72 let _ = raw; Ok(())
74}
75
76fn validate_hhmmss(hour: u8, minute: u8, second: u8, raw: &[u8]) -> Result<(), AprsError> {
77 if hour > 23 {
78 return Err(AprsError::TimestampHourOutOfRange { hour });
79 }
80 if minute > 59 {
81 return Err(AprsError::TimestampMinuteOutOfRange { minute });
82 }
83 if second > 59 {
84 return Err(AprsError::TimestampSecondOutOfRange { second });
85 }
86 let _ = raw;
87 Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn parse_ddhhmm() {
96 assert_eq!(
97 Timestamp::parse(b"092345z").unwrap(),
98 Timestamp::Ddhhmm(9, 23, 45)
99 );
100 }
101
102 #[test]
103 fn parse_hhmmss() {
104 assert_eq!(
105 Timestamp::parse(b"074849h").unwrap(),
106 Timestamp::Hhmmss(7, 48, 49)
107 );
108 }
109
110 #[test]
111 fn parse_local_unsupported() {
112 assert!(matches!(
113 Timestamp::parse(b"092345/").unwrap(),
114 Timestamp::Unsupported(_)
115 ));
116 }
117
118 #[test]
119 fn day_zero_invalid() {
120 assert!(Timestamp::parse(b"002345z").is_err());
121 }
122
123 #[test]
124 fn day_32_invalid() {
125 assert!(Timestamp::parse(b"322345z").is_err());
126 }
127
128 #[test]
129 fn hour_24_invalid() {
130 assert!(Timestamp::parse(b"092445z").is_err());
131 }
132
133 #[test]
134 fn minute_60_invalid() {
135 assert!(Timestamp::parse(b"092360z").is_err());
136 }
137
138 #[test]
139 fn second_60_invalid() {
140 assert!(Timestamp::parse(b"074860h").is_err());
141 }
142
143 #[test]
144 fn encode_round_trip_ddhhmm() {
145 let ts = Timestamp::Ddhhmm(9, 23, 45);
146 let mut out = Vec::new();
147 ts.encode(&mut out);
148 assert_eq!(Timestamp::parse(&out).unwrap(), ts);
149 }
150
151 #[test]
152 fn encode_round_trip_hhmmss() {
153 let ts = Timestamp::Hhmmss(7, 48, 49);
154 let mut out = Vec::new();
155 ts.encode(&mut out);
156 assert_eq!(Timestamp::parse(&out).unwrap(), ts);
157 }
158}