adif/
parser.rs

1use chrono::{NaiveDate, NaiveTime};
2use indexmap::IndexMap;
3use regex::Regex;
4
5use crate::data::{AdifFile, AdifHeader, AdifRecord, AdifType};
6
7const TOKEN_RE: &str = r"(?:<([A-Za-z_]+):(\d+)(?::([A-Za-z]))?>([^<]*))";
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10struct Token {
11    pub key: String,
12    pub len: usize,
13    pub ty: Option<char>,
14    pub value: String,
15}
16
17fn parse_line_to_tokens(line: &str) -> Vec<Token> {
18    Regex::new(TOKEN_RE)
19        .unwrap()
20        .captures_iter(line)
21        .map(|cap| Token {
22            key: cap[1].to_string().to_uppercase(),
23            len: cap[2].parse().expect("Length is not an integer"),
24            ty: cap
25                .get(3)
26                .map(|val| val.as_str().chars().next().unwrap().to_ascii_uppercase()),
27            value: cap[4].trim_end().to_string(),
28        })
29        .collect()
30}
31
32fn create_token_map(tokens: Vec<Token>) -> IndexMap<String, AdifType> {
33    // Build a map
34    let mut map = IndexMap::new();
35
36    // Handle every token
37    for token in tokens {
38        map.insert(
39            token.key.clone(),
40            match token.ty {
41                Some(ty) => match ty {
42                    'B' => AdifType::Boolean(token.value.to_uppercase() == "Y"),
43                    'N' => AdifType::Number(
44                        lexical::parse(&token.value)
45                            .expect("Found a number value that cannot be parsed"),
46                    ),
47                    'D' => AdifType::Date(
48                        NaiveDate::parse_from_str(token.value.as_str(), "%Y%m%d").unwrap(),
49                    ),
50                    'T' => AdifType::Time(
51                        NaiveTime::parse_from_str(token.value.as_str(), "%H%M%S").unwrap(),
52                    ),
53                    _ => AdifType::Str(token.value),
54                },
55                None => AdifType::Str(token.value),
56            },
57        );
58    }
59    map
60}
61
62fn parse_tokens_to_record(tokens: Vec<Token>) -> AdifRecord {
63    create_token_map(tokens).into()
64}
65
66fn parse_tokens_to_header(tokens: Vec<Token>) -> AdifHeader {
67    create_token_map(tokens).into()
68}
69
70/// Parse the contents of an ADIF (`.adi`) file into a struct representation
71pub fn parse_adif(data: &str) -> AdifFile {
72    // Clean up EOH and EOR tokens
73    let data = data.replace("<eoh>", "<EOH>").replace("<eor>", "<EOR>");
74    let data = data.split("<EOH>");
75    let data = data.collect::<Vec<&str>>();
76
77    // Split file into a header and body
78    let body_raw = data.last().unwrap_or(&"");
79
80    // Parse the header
81    let header = match data.len() {
82        2 => {
83            let header_raw = data.first().unwrap_or(&"");
84            let header_tokens = parse_line_to_tokens(header_raw);
85            parse_tokens_to_header(header_tokens)
86        }
87        1 => {
88            // <EOH> not found; insert empty header
89            let i: IndexMap<String, AdifType> = IndexMap::new();
90            i.into()
91        }
92        _ => {
93            panic!("cannot parse ADIF: multiple <EOH> tokens found")
94        }
95    };
96
97    // Create the file
98    let file = AdifFile {
99        header,
100        body: body_raw
101            .split_terminator("<EOR>")
102            .collect::<Vec<&str>>()
103            .iter()
104            .map(|record_line| {
105                // Parse the record
106                let record_tokens = parse_line_to_tokens(record_line);
107                parse_tokens_to_record(record_tokens)
108            })
109            .collect(),
110    };
111
112    // Return
113    file
114}
115
116#[cfg(test)]
117mod tokenization_tests {
118    use super::*;
119
120    #[test]
121    pub fn test_line_to_tokens() {
122        let result = parse_line_to_tokens(
123            "<CALL:4>VA3ZZA <BAND:3>40m <MODE:2>CW <NAME:12>Evan Pratten <eor>",
124        );
125
126        assert_eq!(result.len(), 4);
127        assert_eq!(result[0].key, "CALL");
128        assert_eq!(result[0].value, "VA3ZZA");
129        assert_eq!(result[3].key, "NAME");
130        assert_eq!(result[3].value, "Evan Pratten");
131    }
132
133    #[test]
134    pub fn test_tokens_to_record() {
135        let tokens = parse_line_to_tokens("<CALL:4>VA3ZZA<A_NUMBER:3:N>401<BOOL:1:B>N<eor>");
136        let record = parse_tokens_to_record(tokens);
137
138        assert_eq!(
139            record.get("CALL"),
140            Some(&AdifType::Str("VA3ZZA".to_string()))
141        );
142        assert_eq!(record.get("A_NUMBER"), Some(&AdifType::Number(401.0)));
143        assert_eq!(record.get("BOOL"), Some(&AdifType::Boolean(false)));
144    }
145}