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 DDHHMM(u8, u8, u8),
12 HHMMSS(u8, u8, u8),
14 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 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!("230000h".parse::<Timestamp>().is_ok(), "23 is a valid hour");
159 assert!(
160 "005900h".parse::<Timestamp>().is_ok(),
161 "59 is a valid minute"
162 );
163 assert!(
164 "000059h".parse::<Timestamp>().is_ok(),
165 "59 is a valid second"
166 );
167
168 assert!(
169 "240000h".parse::<Timestamp>().is_err(),
170 "24 is not a valid hour"
171 );
172 assert!(
173 "006000h".parse::<Timestamp>().is_err(),
174 "60 is not a valid minute"
175 );
176 assert!(
177 "000060h".parse::<Timestamp>().is_err(),
178 "60 is not a valid second"
179 );
180 }
181
182 #[test]
183 fn test_serialize() {
184 let timestamp: Timestamp = "311234z".parse().unwrap();
185 let mut wtr = WriterBuilder::new().from_writer(stdout());
186 wtr.serialize(timestamp).unwrap();
187 wtr.flush().unwrap();
188 }
189
190 #[test]
191 fn test_hhmmss_within_1h() {
192 let reference = Utc.with_ymd_and_hms(2025, 4, 25, 23, 55, 7).unwrap();
193 let timestamp = Timestamp::HHMMSS(23, 50, 0);
194 let target = Utc.with_ymd_and_hms(2025, 4, 25, 23, 50, 0).unwrap();
195 assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
196 }
197
198 #[test]
199 fn test_hhmmss_within_1h_daychange() {
200 let reference = Utc.with_ymd_and_hms(2025, 4, 10, 23, 55, 7).unwrap();
201 let timestamp = Timestamp::HHMMSS(0, 5, 20);
202 let target = Utc.with_ymd_and_hms(2025, 4, 11, 0, 5, 20).unwrap();
203 assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
204
205 let reference = Utc.with_ymd_and_hms(2025, 4, 10, 0, 10, 7).unwrap();
206 let timestamp = Timestamp::HHMMSS(23, 49, 20);
207 let target = Utc.with_ymd_and_hms(2025, 4, 9, 23, 49, 20).unwrap();
208 assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
209 }
210
211 #[test]
212 fn test_hhmmss_within_1h_monthchange() {
213 let reference = Utc.with_ymd_and_hms(2025, 3, 31, 23, 55, 7).unwrap();
214 let timestamp = Timestamp::HHMMSS(0, 10, 20);
215 let target = Utc.with_ymd_and_hms(2025, 4, 1, 0, 10, 20).unwrap();
216 assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
217
218 let reference = Utc.with_ymd_and_hms(2025, 4, 1, 0, 10, 7).unwrap();
219 let timestamp = Timestamp::HHMMSS(23, 55, 20);
220 let target = Utc.with_ymd_and_hms(2025, 3, 31, 23, 55, 20).unwrap();
221 assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
222 }
223
224 #[test]
225 fn test_hhmmss_bad_time_range() {
226 let reference = Utc.with_ymd_and_hms(2025, 4, 10, 12, 10, 7).unwrap();
227 let timestamp = Timestamp::HHMMSS(23, 49, 20);
228 assert!(timestamp.to_datetime(&reference).is_err());
229 }
230
231 #[test]
232 fn test_ddhhmm_within_1h() {
233 let reference = Utc.with_ymd_and_hms(2025, 4, 10, 23, 55, 7).unwrap();
234 let timestamp = Timestamp::DDHHMM(10, 23, 50);
235 let target = Utc.with_ymd_and_hms(2025, 4, 10, 23, 50, 0).unwrap();
236
237 assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
238 }
239
240 #[test]
241 fn test_ddhhmm_within_1h_monthchange() {
242 let reference = Utc.with_ymd_and_hms(2025, 3, 31, 23, 55, 0).unwrap();
243 let timestamp = Timestamp::DDHHMM(1, 0, 10);
244 let target = Utc.with_ymd_and_hms(2025, 4, 1, 0, 10, 0).unwrap();
245 assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
246
247 let reference = Utc.with_ymd_and_hms(2025, 4, 1, 0, 10, 0).unwrap();
248 let timestamp = Timestamp::DDHHMM(31, 23, 55);
249 let target = Utc.with_ymd_and_hms(2025, 3, 31, 23, 55, 0).unwrap();
250 assert_eq!(timestamp.to_datetime(&reference).unwrap(), target);
251 }
252}