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 let mut map = IndexMap::new();
35
36 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
70pub fn parse_adif(data: &str) -> AdifFile {
72 let data = data.replace("<eoh>", "<EOH>").replace("<eor>", "<EOR>");
74 let data = data.split("<EOH>");
75 let data = data.collect::<Vec<&str>>();
76
77 let body_raw = data.last().unwrap_or(&"");
79
80 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 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 let file = AdifFile {
99 header,
100 body: body_raw
101 .split_terminator("<EOR>")
102 .collect::<Vec<&str>>()
103 .iter()
104 .map(|record_line| {
105 let record_tokens = parse_line_to_tokens(record_line);
107 parse_tokens_to_record(record_tokens)
108 })
109 .collect(),
110 };
111
112 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}