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 _ => Ok(Timestamp::Unsupported(b.to_vec())),
49 }
50 }
51
52 pub fn encode(&self, out: &mut Vec<u8>) {
54 match self {
55 Timestamp::Ddhhmm(d, h, m) => {
56 out.extend_from_slice(format!("{:02}{:02}{:02}z", d, h, m).as_bytes());
57 }
58 Timestamp::Hhmmss(h, m, s) => {
59 out.extend_from_slice(format!("{:02}{:02}{:02}h", h, m, s).as_bytes());
60 }
61 Timestamp::Unsupported(raw) => out.extend_from_slice(raw),
62 }
63 }
64}
65
66fn validate_ddhhmm(day: u8, hour: u8, minute: u8, raw: &[u8]) -> Result<(), AprsError> {
67 if day == 0 || day > 31 {
68 return Err(AprsError::TimestampDayOutOfRange { day });
69 }
70 if hour > 23 {
71 return Err(AprsError::TimestampHourOutOfRange { hour });
72 }
73 if minute > 59 {
74 return Err(AprsError::TimestampMinuteOutOfRange { minute });
75 }
76 let _ = raw; Ok(())
78}
79
80fn validate_hhmmss(hour: u8, minute: u8, second: u8, raw: &[u8]) -> Result<(), AprsError> {
81 if hour > 23 {
82 return Err(AprsError::TimestampHourOutOfRange { hour });
83 }
84 if minute > 59 {
85 return Err(AprsError::TimestampMinuteOutOfRange { minute });
86 }
87 if second > 59 {
88 return Err(AprsError::TimestampSecondOutOfRange { second });
89 }
90 let _ = raw;
91 Ok(())
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn parse_ddhhmm() {
100 assert_eq!(
101 Timestamp::parse(b"092345z").unwrap(),
102 Timestamp::Ddhhmm(9, 23, 45)
103 );
104 }
105
106 #[test]
107 fn parse_hhmmss() {
108 assert_eq!(
109 Timestamp::parse(b"074849h").unwrap(),
110 Timestamp::Hhmmss(7, 48, 49)
111 );
112 }
113
114 #[test]
115 fn parse_local_unsupported() {
116 assert!(matches!(
117 Timestamp::parse(b"092345/").unwrap(),
118 Timestamp::Unsupported(_)
119 ));
120 }
121
122 #[test]
123 fn parse_nonstandard_designator_preserved() {
124 let ts = Timestamp::parse(b"291500#").unwrap();
127 assert!(matches!(ts, Timestamp::Unsupported(_)));
128 let mut out = Vec::new();
129 ts.encode(&mut out);
130 assert_eq!(out, b"291500#");
131 }
132
133 #[test]
134 fn day_zero_invalid() {
135 assert!(Timestamp::parse(b"002345z").is_err());
136 }
137
138 #[test]
139 fn day_32_invalid() {
140 assert!(Timestamp::parse(b"322345z").is_err());
141 }
142
143 #[test]
144 fn hour_24_invalid() {
145 assert!(Timestamp::parse(b"092445z").is_err());
146 }
147
148 #[test]
149 fn minute_60_invalid() {
150 assert!(Timestamp::parse(b"092360z").is_err());
151 }
152
153 #[test]
154 fn second_60_invalid() {
155 assert!(Timestamp::parse(b"074860h").is_err());
156 }
157
158 #[test]
159 fn encode_round_trip_ddhhmm() {
160 let ts = Timestamp::Ddhhmm(9, 23, 45);
161 let mut out = Vec::new();
162 ts.encode(&mut out);
163 assert_eq!(Timestamp::parse(&out).unwrap(), ts);
164 }
165
166 #[test]
167 fn encode_round_trip_hhmmss() {
168 let ts = Timestamp::Hhmmss(7, 48, 49);
169 let mut out = Vec::new();
170 ts.encode(&mut out);
171 assert_eq!(Timestamp::parse(&out).unwrap(), ts);
172 }
173}