wfdb_rust/
header.rs

1use regex::Regex;
2
3const DEFREQ: f32 = 250_f32;
4
5#[allow(non_camel_case_types)]
6#[derive(Copy, Clone, PartialEq, Debug)]
7pub enum StorageFormat {
8    _8bit_first_difference = 8,
9    _16bit_twos_complement = 16,
10    _24bit_twos_complement_lsb = 24,
11    _32bit_twos_complement_lsb = 32,
12    _16bit_twos_complement_msb = 61,
13    _8bit_offset_binary = 80,
14    _16bit_offset_binary = 160,
15    _12bit_twos_complement = 212,
16    _10bit_twos_complement_sets_of_11 = 310,
17    _10bit_twos_complement_sets_of_4 = 311,
18}
19
20#[derive(PartialEq, Debug)]
21pub struct RecordLine {
22    pub record_name: String,
23    pub number_of_segments: Option<u32>,
24    pub number_of_signals: u32,
25    pub sampling_frequency: Option<f32>,
26    pub counter_frequency: Option<f32>,
27    pub base_counter_value: Option<f32>,
28    pub samples_per_signal: Option<u32>,
29    pub base_time: Option<String>,
30    pub base_date: Option<String>,
31}
32
33#[derive(PartialEq, Debug)]
34pub struct SignalSpecLine {
35    pub filename: String,
36    pub format: StorageFormat,
37    pub samples_per_frame: Option<u32>,
38    pub skew: Option<u32>,
39    pub byte_offset: Option<u32>,
40    pub adc_gain: Option<f32>,
41    pub baseline: Option<u32>,
42    pub units: Option<String>,
43    pub adc_resolution: Option<u32>,
44    pub adc_zero: Option<u32>,
45    pub initial_value: Option<u32>,
46    pub checksum: Option<i16>,
47    pub block_size: Option<u32>,
48    pub description: Option<String>,
49}
50
51#[derive(PartialEq, Debug)]
52pub struct Header {
53    pub record: RecordLine,
54    pub signal_specs: Vec<SignalSpecLine>,
55}
56
57pub fn parse_record_line(record_line: &str) -> RecordLine {
58    let tokens: Vec<&str> = record_line.split_whitespace().collect();
59    let record_name;
60    let mut number_of_segments = None;
61
62    let record_name_tokens: Vec<&str> = tokens[0].split('/').collect();
63    if record_name_tokens.len() > 1 {
64        record_name = String::from(record_name_tokens[0]);
65        number_of_segments = record_name_tokens[1].parse::<u32>().ok();
66    } else {
67        record_name = String::from(record_name_tokens[0]);
68    }
69
70    let number_of_signals = tokens[1]
71        .parse::<u32>()
72        .expect("Invalid header: cannot parse number of signals.");
73
74    let mut sampling_frequency = Some(DEFREQ);
75    let mut counter_frequency = None;
76    let mut base_counter_value = None;
77    if tokens.len() > 2 {
78        let frequency_tokens: Vec<&str> = tokens[2].split('/').collect();
79        if frequency_tokens.len() > 1 {
80            let counter_regex = Regex::new(r"(\d+)\((\d+)\)").unwrap();
81            if counter_regex.is_match(frequency_tokens[1]) {
82                let captures = counter_regex.captures(frequency_tokens[1]).unwrap();
83                counter_frequency = Some(
84                    captures[1]
85                        .parse::<f32>()
86                        .expect("Invalid header: counter frequency specified, but not parseable."),
87                );
88                base_counter_value = Some(
89                    captures[2]
90                        .parse::<f32>()
91                        .expect("Invalid header: base counter value specified, but not parseable"),
92                );
93            } else {
94                counter_frequency = Some(
95                    frequency_tokens[0]
96                        .parse::<f32>()
97                        .expect("Invalid header: counter frequency specified, but not parseable."),
98                );
99            }
100        }
101        sampling_frequency = Some(
102            frequency_tokens[0]
103                .parse::<f32>()
104                .expect("Invalid header: Sampling frequency field present, but not parseable"),
105        );
106    }
107
108    let mut samples_per_signal = None;
109    if tokens.len() > 3 {
110        samples_per_signal = Some(
111            tokens[3]
112                .parse::<u32>()
113                .expect("Invalid header: samples per signal specified, but not parseable"),
114        );
115    }
116
117    // TODO: parse date and time
118
119    RecordLine {
120        record_name,
121        number_of_segments,
122        number_of_signals,
123        sampling_frequency,
124        counter_frequency: if counter_frequency == None {
125            sampling_frequency
126        } else {
127            counter_frequency
128        },
129        base_counter_value: if base_counter_value == None {
130            Some(0_f32)
131        } else {
132            base_counter_value
133        },
134        samples_per_signal,
135        base_time: None,
136        base_date: None,
137    }
138}
139
140pub fn parse_signal_line(signal_line: &str) -> SignalSpecLine {
141    let tokens: Vec<&str> = signal_line.split_whitespace().collect();
142    if tokens.len() < 2 {
143        panic!("Invalid header: signal specification line missing required fields.");
144    }
145
146    let filename = String::from(tokens[0]);
147    let format;
148    let mut samples_per_frame = None;
149    let mut skew = None;
150    let mut byte_offset = None;
151    let format_regex = Regex::new(r"(\d+)(?:x(\d+)(?::(\d+)(?:\+(\d+)))?)?").unwrap();
152    if format_regex.is_match(tokens[1]) {
153        let format_captures = format_regex.captures(tokens[1]).unwrap();
154        format = match &format_captures[1] {
155            "8" => StorageFormat::_8bit_first_difference,
156            "16" => StorageFormat::_16bit_twos_complement,
157            "24" => StorageFormat::_24bit_twos_complement_lsb,
158            "32" => StorageFormat::_32bit_twos_complement_lsb,
159            "61" => StorageFormat::_16bit_twos_complement_msb,
160            "80" => StorageFormat::_8bit_offset_binary,
161            "160" => StorageFormat::_16bit_offset_binary,
162            "212" => StorageFormat::_12bit_twos_complement,
163            "310" => StorageFormat::_10bit_twos_complement_sets_of_11,
164            "311" => StorageFormat::_10bit_twos_complement_sets_of_4,
165            _ => panic!("Unknown storage format!"),
166        };
167        if format_captures.get(2) != None {
168            samples_per_frame =
169                Some(format_captures[2].parse::<u32>().expect(
170                    "Invalid header: samples per frame specified, but could not be parsed",
171                ));
172        }
173        if format_captures.get(3) != None {
174            skew = Some(
175                format_captures[3]
176                    .parse::<u32>()
177                    .expect("Invalid header: skew specified, but could not be parsed"),
178            );
179        }
180        if format_captures.get(4) != None {
181            byte_offset = Some(
182                format_captures[4]
183                    .parse::<u32>()
184                    .expect("Invalid header: byte offset specified, but could not be parsed"),
185            );
186        }
187    } else {
188        panic!("Invalid header: signal format not properly specified.");
189    }
190
191    let mut adc_gain = None;
192    let mut baseline = None;
193    let mut units = Some(String::from("mV"));
194    //  TODO: default adc_resolution value depends on storage format.
195    //  This is the default for amplitude-format signals (most common)
196    let mut adc_resolution = Some(12);
197    let mut adc_zero = Some(0);
198    let mut initial_value = None;
199    let mut checksum = None;
200    let mut block_size = None;
201    let mut description = None;
202    if tokens.len() > 2 {
203        let adc_regex = Regex::new(r"(\d+)(?:\((?P<baseline>\d+)\))?(?:/(?P<units>\S+))?").unwrap();
204        let adc_tokens = adc_regex.captures(tokens[2]).unwrap();
205        adc_gain = Some(
206            adc_tokens[1]
207                .parse::<f32>()
208                .expect("Invalid header: adc gain specified, but not parseable"),
209        );
210        if adc_tokens.name("baseline") != None {
211            baseline = Some(
212                adc_tokens
213                    .name("baseline")
214                    .unwrap()
215                    .as_str()
216                    .parse::<u32>()
217                    .expect("Invalid header: baseline specified, but could not be parsed"),
218            );
219        }
220        if adc_tokens.name("units") != None {
221            units = Some(String::from(adc_tokens.name("units").unwrap().as_str()));
222        }
223
224        if tokens.len() > 3 {
225            adc_resolution = Some(
226                tokens[3]
227                    .parse::<u32>()
228                    .expect("Invalid header: ADC resolution specified but not parseable."),
229            );
230        }
231
232        if tokens.len() > 4 {
233            adc_zero = Some(
234                tokens[4]
235                    .parse::<u32>()
236                    .expect("Invalid header: ADC zero specified but not parseable."),
237            );
238        }
239
240        if tokens.len() > 5 {
241            initial_value = Some(
242                tokens[5]
243                    .parse::<u32>()
244                    .expect("Invalid header: initial value specified but not parseable."),
245            );
246        }
247
248        if tokens.len() > 6 {
249            checksum = Some(
250                tokens[6]
251                    .parse::<i16>()
252                    .expect("Invalid header: checksum specified but not parseable."),
253            );
254        }
255
256        if tokens.len() > 7 {
257            block_size = Some(
258                tokens[7]
259                    .parse::<u32>()
260                    .expect("Invalid header: block size specified but not parseable."),
261            );
262        }
263
264        if tokens.len() > 8 {
265            description = Some(String::from(tokens[8]));
266        }
267    }
268
269    SignalSpecLine {
270        filename,
271        format,
272        samples_per_frame,
273        skew,
274        byte_offset,
275        adc_gain,
276        baseline: if baseline == None { adc_zero } else { baseline },
277        units,
278        adc_resolution,
279        adc_zero,
280        initial_value: if initial_value == None {
281            adc_zero
282        } else {
283            initial_value
284        },
285        checksum,
286        block_size,
287        description,
288    }
289}
290
291pub fn read_header(header_string: &str) -> Header {
292    let mut header_lines = header_string.lines().filter(|&line| !line.starts_with("#"));
293    Header {
294        record: parse_record_line(header_lines.next().unwrap()),
295        signal_specs: header_lines.map(parse_signal_line).collect(),
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    #[test]
303    fn test_basic_header() {
304        let basic_header = "100 2 360 650000
305            100.dat 212 200 11 1024 995 -22131 0 MLII
306            100.dat 212 200 11 1024 1011 20052 0 V5";
307        let parsed = read_header(basic_header);
308        assert_eq!(
309            parsed,
310            Header {
311                record: RecordLine {
312                    record_name: String::from("100"),
313                    number_of_segments: None,
314                    number_of_signals: 2,
315                    sampling_frequency: Some(360_f32),
316                    counter_frequency: Some(360_f32),
317                    base_counter_value: Some(0.0),
318                    samples_per_signal: Some(650000),
319                    base_time: None,
320                    base_date: None,
321                },
322                signal_specs: vec![
323                    SignalSpecLine {
324                        filename: String::from("100.dat"),
325                        format: StorageFormat::_12bit_twos_complement,
326                        samples_per_frame: None,
327                        skew: None,
328                        byte_offset: None,
329                        adc_gain: Some(200_f32),
330                        baseline: Some(1024),
331                        units: Some(String::from("mV")),
332                        adc_resolution: Some(11),
333                        adc_zero: Some(1024),
334                        initial_value: Some(995),
335                        checksum: Some(-22131),
336                        block_size: Some(0),
337                        description: Some(String::from("MLII")),
338                    },
339                    SignalSpecLine {
340                        filename: String::from("100.dat"),
341                        format: StorageFormat::_12bit_twos_complement,
342                        samples_per_frame: None,
343                        skew: None,
344                        byte_offset: None,
345                        adc_gain: Some(200_f32),
346                        baseline: Some(1024),
347                        units: Some(String::from("mV")),
348                        adc_resolution: Some(11),
349                        adc_zero: Some(1024),
350                        initial_value: Some(1011),
351                        checksum: Some(20052),
352                        block_size: Some(0),
353                        description: Some(String::from("V5")),
354                    },
355                ],
356            }
357        )
358    }
359
360    #[test]
361    fn test_mit_record_line() {
362        let basic_record_line = "100 2 360 650000 0:0:0 0/0/0";
363        let parsed = parse_record_line(basic_record_line);
364        assert_eq!(
365            parsed,
366            RecordLine {
367                record_name: String::from("100"),
368                number_of_segments: None,
369                number_of_signals: 2,
370                sampling_frequency: Some(360_f32),
371                counter_frequency: Some(360_f32),
372                base_counter_value: Some(0.0),
373                samples_per_signal: Some(650000),
374                base_time: None,
375                base_date: None,
376            }
377        )
378    }
379
380    #[test]
381    fn test_custom_mit_record_line() {
382        let basic_record_line = "100/4 2 360/24(5) 650000 0:0:0 0/0/0";
383        let parsed = parse_record_line(basic_record_line);
384        assert_eq!(
385            parsed,
386            RecordLine {
387                record_name: String::from("100"),
388                number_of_segments: Some(4),
389                number_of_signals: 2,
390                sampling_frequency: Some(360.0),
391                counter_frequency: Some(24.0),
392                base_counter_value: Some(5.0),
393                samples_per_signal: Some(650000),
394                base_time: None,
395                base_date: None,
396            }
397        )
398    }
399
400    #[test]
401    fn test_aha_record_line() {
402        let basic_record_line = "7001 2 250 525000";
403        let parsed = parse_record_line(basic_record_line);
404        assert_eq!(
405            parsed,
406            RecordLine {
407                record_name: String::from("7001"),
408                number_of_segments: None,
409                number_of_signals: 2,
410                sampling_frequency: Some(250_f32),
411                counter_frequency: Some(250_f32),
412                base_counter_value: Some(0.0),
413                samples_per_signal: Some(525000),
414                base_time: None,
415                base_date: None,
416            }
417        )
418    }
419
420    #[test]
421    fn test_mit_signal_spec_line() {
422        let signal_line = "100.dat 212 200 11 1024 995 -22131 0 MLII";
423        assert_eq!(
424            parse_signal_line(signal_line),
425            SignalSpecLine {
426                filename: String::from("100.dat"),
427                format: StorageFormat::_12bit_twos_complement,
428                samples_per_frame: None,
429                skew: None,
430                byte_offset: None,
431                adc_gain: Some(200_f32),
432                baseline: Some(1024),
433                units: Some(String::from("mV")),
434                adc_resolution: Some(11),
435                adc_zero: Some(1024),
436                initial_value: Some(995),
437                checksum: Some(-22131),
438                block_size: Some(0),
439                description: Some(String::from("MLII")),
440            }
441        )
442    }
443
444    #[test]
445    fn test_custom_mit_signal_spec_line() {
446        let signal_line = "100.dat 212x3:2+53 200(2)/cm 11 1024 995 -22131 0 MLII";
447        assert_eq!(
448            parse_signal_line(signal_line),
449            SignalSpecLine {
450                filename: String::from("100.dat"),
451                format: StorageFormat::_12bit_twos_complement,
452                samples_per_frame: Some(3),
453                skew: Some(2),
454                byte_offset: Some(53),
455                adc_gain: Some(200.0),
456                baseline: Some(2),
457                units: Some(String::from("cm")),
458                adc_resolution: Some(11),
459                adc_zero: Some(1024),
460                initial_value: Some(995),
461                checksum: Some(-22131),
462                block_size: Some(0),
463                description: Some(String::from("MLII")),
464            }
465        )
466    }
467}