adif/
data.rs

1use std::{
2    fmt::Display,
3    ops::{Deref, DerefMut},
4};
5
6use chrono::{Datelike, Timelike};
7use indexmap::IndexMap;
8
9#[derive(Debug)]
10pub struct SerializeError {
11    pub message: String,
12    pub offender: String,
13}
14
15impl Display for SerializeError {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        write!(f, "{}. Offending value: {}", self.message, self.offender)
18    }
19}
20
21/// Supported datatypes for representing ADIF data
22#[derive(Debug, Clone, PartialEq)]
23pub enum AdifType {
24    /// Basic string type
25    Str(String),
26
27    /// Basic boolean type
28    Boolean(bool),
29
30    /// Basic number type
31    Number(f64),
32
33    /// 8 Digits representing a UTC date in `YYYYMMDD` format, where
34    ///  - YYYY is a 4-Digit year specifier, where 1930 <= YYYY
35    ///  - MM is a 2-Digit month specifier, where 1 <= MM <= 12
36    ///  - DD is a 2-Digit day specifier, where 1 <= DD <= DaysInMonth(MM)
37    Date(chrono::NaiveDate),
38
39    /// 6 Digits representing a UTC time in HHMMSS format
40    /// or 4 Digits representing a time in HHMM format, where:
41    ///  - HH is a 2-Digit hour specifier, where 0 <= HH <= 23
42    ///  - MM is a 2-Digit minute specifier, where 0 <= MM <= 59
43    ///  - SS is a 2-Digit second specifier, where 0 <= SS <= 59
44    Time(chrono::NaiveTime),
45}
46
47impl AdifType {
48    /// Get the single-char indicator used to specify a type
49    pub fn get_data_type_indicator(&self) -> Option<char> {
50        match self {
51            AdifType::Str(_) => None,
52            AdifType::Boolean(_) => Some('B'),
53            AdifType::Number(_) => Some('N'),
54            AdifType::Date(_) => Some('D'),
55            AdifType::Time(_) => Some('T'),
56        }
57    }
58
59    /// Serialize into a single value
60    pub fn serialize(&self, key_name: &str) -> Result<String, SerializeError> {
61        // Convert enum value into a usable string
62        let value: Result<String, SerializeError> = match self {
63            AdifType::Str(val) => {
64                // String cannot contain linebreaks, and must be ASCII
65                if val.contains('\n') {
66                    return Err(SerializeError {
67                        message: "String cannot contain linebreaks".to_string(),
68                        offender: val.to_string(),
69                    });
70                }
71                if !val.is_ascii() {
72                    return Err(SerializeError {
73                        message: "String must be ASCII".to_string(),
74                        offender: val.to_string(),
75                    });
76                }
77
78                Ok(val.to_string())
79            }
80            AdifType::Boolean(val) => Ok(match val {
81                true => "Y",
82                false => "N",
83            }
84            .to_string()),
85            AdifType::Number(val) => Ok(val.to_string()),
86            AdifType::Date(val) => {
87                // Date must be after 1929
88                if val.year() < 1930 {
89                    return Err(SerializeError {
90                        message: "Date must be >= 1930".to_string(),
91                        offender: val.to_string(),
92                    });
93                }
94
95                Ok(format!("{}{:02}{:02}", val.year(), val.month(), val.day()))
96            }
97            AdifType::Time(val) => Ok(format!(
98                "{:02}{:02}{:02}",
99                val.hour(),
100                val.minute(),
101                val.second()
102            )),
103        };
104        let value: &str = &(value?);
105
106        // Format the result
107        Ok(format!(
108            "<{}:{}{}>{}",
109            key_name.to_uppercase().replace(' ', "_"),
110            value.len(),
111            match self.get_data_type_indicator() {
112                Some(id) => format!(":{}", id),
113                None => String::new(),
114            },
115            value
116        ))
117    }
118}
119
120impl Display for AdifType {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "{:?}", self)
123    }
124}
125
126/// A single ADIF record, consisting of many values
127#[derive(Debug, Clone, PartialEq)]
128pub struct AdifRecord(IndexMap<String, AdifType>);
129
130impl AdifRecord {
131    /// Serialize into a full record string
132    pub fn serialize(&self) -> Result<String, SerializeError> {
133        let mut output = self
134            .0
135            .iter()
136            .map(|(key, value)| value.serialize(key))
137            .collect::<Result<Vec<String>, SerializeError>>()?
138            .join("");
139        output.push_str("<eor>");
140        Ok(output)
141    }
142}
143
144impl From<IndexMap<String, AdifType>> for AdifRecord {
145    fn from(map: IndexMap<String, AdifType>) -> Self {
146        Self(map)
147    }
148}
149
150impl<'a> From<IndexMap<&'a str, AdifType>> for AdifRecord {
151    fn from(map: IndexMap<&'a str, AdifType>) -> Self {
152        Self(
153            map.iter()
154                .map(|(key, value)| (key.to_string(), value.clone()))
155                .collect(),
156        )
157    }
158}
159
160impl Display for AdifRecord {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        write!(f, "{:?}", self.serialize())
163    }
164}
165
166impl Deref for AdifRecord {
167    type Target = IndexMap<String, AdifType>;
168
169    fn deref(&self) -> &Self::Target {
170        &self.0
171    }
172}
173
174impl DerefMut for AdifRecord {
175    fn deref_mut(&mut self) -> &mut Self::Target {
176        &mut self.0
177    }
178}
179
180/// An ADIF file header, consisting of many values
181#[derive(Debug, Clone, PartialEq)]
182pub struct AdifHeader(IndexMap<String, AdifType>);
183
184impl AdifHeader {
185    /// Serialize into a full header string
186    pub fn serialize(&self) -> Result<String, SerializeError> {
187        let mut output = String::new();
188        output.push_str(&format!(
189            "Generated {} (UTC)\n\n",
190            chrono::Utc::now().format("%Y-%m-%d %H:%M:%S")
191        ));
192
193        let header_tags = self
194            .0
195            .iter()
196            .map(|(key, value)| value.serialize(key))
197            .collect::<Result<Vec<String>, SerializeError>>()?
198            .join("\n");
199        output.push_str(&header_tags);
200
201        output.push('\n');
202        output.push_str("<EOH>");
203
204        Ok(output)
205    }
206}
207
208impl From<IndexMap<String, AdifType>> for AdifHeader {
209    fn from(map: IndexMap<String, AdifType>) -> Self {
210        Self(map)
211    }
212}
213
214impl<'a> From<IndexMap<&'a str, AdifType>> for AdifHeader {
215    fn from(map: IndexMap<&'a str, AdifType>) -> Self {
216        Self(
217            map.iter()
218                .map(|(key, value)| (key.to_string(), value.clone()))
219                .collect(),
220        )
221    }
222}
223
224impl Display for AdifHeader {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        write!(f, "{:?}", self.serialize())
227    }
228}
229
230impl Deref for AdifHeader {
231    type Target = IndexMap<String, AdifType>;
232
233    fn deref(&self) -> &Self::Target {
234        &self.0
235    }
236}
237
238impl DerefMut for AdifHeader {
239    fn deref_mut(&mut self) -> &mut Self::Target {
240        &mut self.0
241    }
242}
243
244/// Defines an entire file of ADIF data
245#[derive(Debug, Clone, PartialEq)]
246pub struct AdifFile {
247    pub header: AdifHeader,
248    pub body: Vec<AdifRecord>,
249}
250
251impl AdifFile {
252    /// Serialize into text data to be written to a file
253    pub fn serialize(&self) -> Result<String, SerializeError> {
254        Ok(format!(
255            "{}\n{}",
256            self.header.serialize()?,
257            self.body
258                .iter()
259                .map(|record| record.serialize())
260                .collect::<Result<Vec<String>, SerializeError>>()?
261                .join("\n")
262        ))
263    }
264}
265
266#[cfg(test)]
267mod types_tests {
268    use chrono::{NaiveDate, NaiveTime};
269
270    use super::*;
271
272    #[test]
273    pub fn test_ser_string() {
274        assert_eq!(
275            AdifType::Str("Hello, world!".to_string())
276                .serialize("test")
277                .unwrap(),
278            "<TEST:13>Hello, world!"
279        );
280    }
281
282    #[test]
283    pub fn test_ser_bool() {
284        assert_eq!(
285            AdifType::Boolean(true).serialize("test").unwrap(),
286            "<TEST:1:B>Y"
287        );
288        assert_eq!(
289            AdifType::Boolean(false).serialize("test").unwrap(),
290            "<TEST:1:B>N"
291        );
292    }
293
294    #[test]
295    pub fn test_ser_num() {
296        assert_eq!(
297            AdifType::Number(3.5).serialize("test").unwrap(),
298            "<TEST:3:N>3.5"
299        );
300        assert_eq!(
301            AdifType::Number(-3.5).serialize("test").unwrap(),
302            "<TEST:4:N>-3.5"
303        );
304        assert_eq!(
305            AdifType::Number(-12.0).serialize("test").unwrap(),
306            "<TEST:3:N>-12"
307        );
308    }
309
310    #[test]
311    pub fn test_ser_date() {
312        assert_eq!(
313            AdifType::Date(NaiveDate::from_ymd_opt(2020, 2, 24).unwrap())
314                .serialize("test")
315                .unwrap(),
316            "<TEST:8:D>20200224"
317        );
318        assert!(AdifType::Date(NaiveDate::from_ymd_opt(1910, 2, 2).unwrap())
319            .serialize("test")
320            .is_err());
321    }
322
323    #[test]
324    pub fn test_ser_time() {
325        assert_eq!(
326            AdifType::Time(NaiveTime::from_hms_opt(23, 2, 5).unwrap())
327                .serialize("test")
328                .unwrap(),
329            "<TEST:6:T>230205"
330        );
331    }
332}
333
334#[cfg(test)]
335mod record_tests {
336
337    use indexmap::indexmap;
338
339    use super::*;
340
341    #[test]
342    pub fn test_ser_record() {
343        let test_record: AdifRecord = indexmap! {
344            "a number" => AdifType::Number(15.5),
345            "test string" => AdifType::Str("Heyo rusty friends!".to_string()),
346        }
347        .into();
348
349        assert_eq!(
350            test_record.serialize().unwrap(),
351            "<A_NUMBER:4:N>15.5<TEST_STRING:19>Heyo rusty friends!<eor>"
352        );
353    }
354
355    #[test]
356    pub fn test_ser_header() {
357        let test_header: AdifHeader = indexmap! {
358            "a number" => AdifType::Number(15.5),
359            "test string" => AdifType::Str("Heyo rusty friends!".to_string()),
360        }
361        .into();
362
363        let serialized_lines = test_header.serialize().unwrap();
364        let mut serialized_lines = serialized_lines.lines();
365
366        // Skip the "generated" line
367        serialized_lines.next();
368        serialized_lines.next();
369
370        // Test the header lines
371        assert_eq!(serialized_lines.next(), Some("<A_NUMBER:4:N>15.5"));
372        assert_eq!(
373            serialized_lines.next(),
374            Some("<TEST_STRING:19>Heyo rusty friends!")
375        );
376        assert_eq!(serialized_lines.next(), Some("<EOH>"));
377    }
378}