Skip to main content

aprs_decode/types/
timestamp.rs

1use crate::error::AprsError;
2use crate::util::parse_bytes;
3
4/// An APRS timestamp parsed from the packet header.
5///
6/// APRS101 defines three formats; the local-time format (`/`) is represented
7/// as `Unsupported` since it is deprecated and ambiguous.
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum Timestamp {
11    /// Day-of-month (1–31), Hour (0–23), Minute (0–59) in UTC. Suffix `z`.
12    Ddhhmm(u8, u8, u8),
13    /// Hour (0–23), Minute (0–59), Second (0–59) in UTC. Suffix `h`.
14    Hhmmss(u8, u8, u8),
15    /// Local-time format (deprecated, suffix `/`). Stored as raw bytes.
16    Unsupported(Vec<u8>),
17}
18
19impl Timestamp {
20    /// Parse a 7-byte timestamp field.
21    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    /// Write the timestamp in its original wire format.
49    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; // suppress unused warning
73    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}