ogn_parser/
timestamp.rs

1use chrono::{Duration, prelude::*};
2use std::fmt::{Display, Formatter};
3use std::str::FromStr;
4
5use crate::AprsError;
6use serde::Serialize;
7
8#[derive(Eq, PartialEq, Debug, Clone)]
9pub enum Timestamp {
10    /// Day of month, Hour and Minute in UTC
11    DDHHMM(u8, u8, u8),
12    /// Hour, Minute and Second in UTC
13    HHMMSS(u8, u8, u8),
14    /// Unsupported timestamp format
15    Unsupported(String),
16}
17
18impl FromStr for Timestamp {
19    type Err = AprsError;
20
21    fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
22        let b = s.as_bytes();
23
24        if b.len() != 7 {
25            return Err(AprsError::InvalidTimestamp(s.to_owned()));
26        }
27
28        let one = s[0..2]
29            .parse::<u8>()
30            .map_err(|_| AprsError::InvalidTimestamp(s.to_owned()))?;
31        let two = s[2..4]
32            .parse::<u8>()
33            .map_err(|_| AprsError::InvalidTimestamp(s.to_owned()))?;
34        let three = s[4..6]
35            .parse::<u8>()
36            .map_err(|_| AprsError::InvalidTimestamp(s.to_owned()))?;
37
38        Ok(match (b[6] as char, one, two, three) {
39            ('z', 0..=31, 0..=23, 0..=59) => Timestamp::DDHHMM(one, two, three),
40            ('h', 0..=23, 0..=59, 0..=59) => Timestamp::HHMMSS(one, two, three),
41            ('/', _, _, _) => Timestamp::Unsupported(s.to_owned()),
42            _ => return Err(AprsError::InvalidTimestamp(s.to_owned())),
43        })
44    }
45}
46
47impl Display for Timestamp {
48    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
49        match self {
50            Self::DDHHMM(d, h, m) => write!(f, "{d:02}{h:02}{m:02}z"),
51            Self::HHMMSS(h, m, s) => write!(f, "{h:02}{m:02}{s:02}h"),
52            Self::Unsupported(s) => write!(f, "{s}"),
53        }
54    }
55}
56
57impl Serialize for Timestamp {
58    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
59    where
60        S: serde::Serializer,
61    {
62        serializer.serialize_str(&format!("{self}"))
63    }
64}
65
66impl Timestamp {
67    pub fn to_datetime(&self, reference: &DateTime<Utc>) -> Result<DateTime<Utc>, AprsError> {
68        match self {
69            Timestamp::HHMMSS(h, m, s) => {
70                let time = NaiveTime::from_hms_opt(*h as u32, *m as u32, *s as u32).unwrap();
71                let base_date = reference.date_naive();
72                let naive = NaiveDateTime::new(base_date, time);
73                let datetime: DateTime<Utc> = Utc.from_utc_datetime(&naive);
74
75                match (datetime - reference).num_hours() {
76                    -25..=-23 => Ok(datetime + Duration::days(1)),
77                    -1..=1 => Ok(datetime),
78                    23..=25 => Ok(datetime - Duration::days(1)),
79                    _ => Err(AprsError::TimestampOutOfRange(format!(
80                        "{datetime} {reference}"
81                    ))),
82                }
83            }
84            Timestamp::DDHHMM(_d, h, m) => {
85                // FIXME: d is currently not considered. We always use the day of reference
86                let time = NaiveTime::from_hms_opt(*h as u32, *m as u32, 0).unwrap();
87                let base_date = reference.date_naive();
88                let naive = NaiveDateTime::new(base_date, time);
89                let datetime: DateTime<Utc> = Utc.from_utc_datetime(&naive);
90
91                match (datetime - reference).num_hours() {
92                    -25..=-23 => Ok(datetime + Duration::days(1)),
93                    -1..=1 => Ok(datetime),
94                    23..=25 => Ok(datetime - Duration::days(1)),
95                    _ => Err(AprsError::TimestampOutOfRange(format!(
96                        "{datetime} {reference}"
97                    ))),
98                }
99            }
100            Timestamp::Unsupported(_s) => {
101                todo!()
102            }
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use csv::WriterBuilder;
110    use std::io::stdout;
111
112    use super::*;
113
114    #[test]
115    fn parse_ddhhmm() {
116        assert_eq!("311234z".parse(), Ok(Timestamp::DDHHMM(31, 12, 34)));
117    }
118
119    #[test]
120    fn parse_hhmmss() {
121        assert_eq!("123456h".parse(), Ok(Timestamp::HHMMSS(12, 34, 56)));
122    }
123
124    #[test]
125    fn parse_local_time() {
126        assert_eq!(
127            "123456/".parse::<Timestamp>(),
128            Ok(Timestamp::Unsupported("123456/".to_owned()))
129        );
130    }
131
132    #[test]
133    fn invalid_timestamp() {
134        assert_eq!(
135            "1234567".parse::<Timestamp>(),
136            Err(AprsError::InvalidTimestamp("1234567".to_owned()))
137        );
138    }
139
140    #[test]
141    fn invalid_timestamp2() {
142        assert_eq!(
143            "123a56z".parse::<Timestamp>(),
144            Err(AprsError::InvalidTimestamp("123a56z".to_owned()))
145        );
146    }
147
148    #[test]
149    fn invalid_ddhhmm() {
150        assert_eq!(
151            "322460z".parse::<Timestamp>(),
152            Err(AprsError::InvalidTimestamp("322460z".to_owned()))
153        );
154    }
155
156    #[test]
157    fn invalid_hhmmss() {
158        assert_eq!(
159            "246060h".parse::<Timestamp>(),
160            Err(AprsError::InvalidTimestamp("246060h".to_owned()))
161        );
162    }
163
164    #[test]
165    fn test_serialize() {
166        let timestamp: Timestamp = "311234z".parse().unwrap();
167        let mut wtr = WriterBuilder::new().from_writer(stdout());
168        wtr.serialize(timestamp).unwrap();
169        wtr.flush().unwrap();
170    }
171
172    #[test]
173    fn test_hhmmss_within_1h() {
174        let reference = Utc.with_ymd_and_hms(2025, 4, 25, 23, 55, 7).unwrap();
175        let timestamp = Timestamp::HHMMSS(23, 50, 0);
176        let target = Utc.with_ymd_and_hms(2025, 4, 25, 23, 50, 0).unwrap();
177        assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
178    }
179
180    #[test]
181    fn test_hhmmss_within_1h_daychange() {
182        let reference = Utc.with_ymd_and_hms(2025, 4, 10, 23, 55, 7).unwrap();
183        let timestamp = Timestamp::HHMMSS(0, 5, 20);
184        let target = Utc.with_ymd_and_hms(2025, 4, 11, 0, 5, 20).unwrap();
185        assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
186
187        let reference = Utc.with_ymd_and_hms(2025, 4, 10, 0, 10, 7).unwrap();
188        let timestamp = Timestamp::HHMMSS(23, 49, 20);
189        let target = Utc.with_ymd_and_hms(2025, 4, 9, 23, 49, 20).unwrap();
190        assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
191    }
192
193    #[test]
194    fn test_hhmmss_within_1h_monthchange() {
195        let reference = Utc.with_ymd_and_hms(2025, 3, 31, 23, 55, 7).unwrap();
196        let timestamp = Timestamp::HHMMSS(0, 10, 20);
197        let target = Utc.with_ymd_and_hms(2025, 4, 1, 0, 10, 20).unwrap();
198        assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
199
200        let reference = Utc.with_ymd_and_hms(2025, 4, 1, 0, 10, 7).unwrap();
201        let timestamp = Timestamp::HHMMSS(23, 55, 20);
202        let target = Utc.with_ymd_and_hms(2025, 3, 31, 23, 55, 20).unwrap();
203        assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
204    }
205
206    #[test]
207    fn test_hhmmss_bad_time_range() {
208        let reference = Utc.with_ymd_and_hms(2025, 4, 10, 12, 10, 7).unwrap();
209        let timestamp = Timestamp::HHMMSS(23, 49, 20);
210        assert!(timestamp.to_datetime(&reference).is_err());
211    }
212
213    #[test]
214    fn test_ddhhmm_within_1h() {
215        let reference = Utc.with_ymd_and_hms(2025, 4, 10, 23, 55, 7).unwrap();
216        let timestamp = Timestamp::DDHHMM(10, 23, 50);
217        let target = Utc.with_ymd_and_hms(2025, 4, 10, 23, 50, 0).unwrap();
218
219        assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
220    }
221
222    #[test]
223    fn test_ddhhmm_within_1h_monthchange() {
224        let reference = Utc.with_ymd_and_hms(2025, 3, 31, 23, 55, 0).unwrap();
225        let timestamp = Timestamp::DDHHMM(1, 0, 10);
226        let target = Utc.with_ymd_and_hms(2025, 4, 1, 0, 10, 0).unwrap();
227        assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
228
229        let reference = Utc.with_ymd_and_hms(2025, 4, 1, 0, 10, 0).unwrap();
230        let timestamp = Timestamp::DDHHMM(31, 23, 55);
231        let target = Utc.with_ymd_and_hms(2025, 3, 31, 23, 55, 0).unwrap();
232        assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
233    }
234}