use std::{collections::HashMap, hash::Hash, u64, vec}; use crate::SignalFormat; /// Holds the essential header information about the record, like the amount of /// signals, and the sampling frequency. /// /// Only the `name`, `signal_count`, and `sampling_freq` values are required by /// default. #[derive(Debug, Clone)] struct Record { name: String, seg_num: Option, signal_count: usize, sampling_freq: u64, counter_freq: Option, base_counter_val: Option, sample_num: Option, basetime: Option, // I dont thing we really need to care much about these basedate: Option } impl Record { /// Attempts to generate the record information from a string of the argument /// line that's provided by the WFDB header file /// /// Returns a `Result`, with the error field potentially /// describing why the parsing operation failed pub fn from_str(argument_line: &str) -> Result { let args: Vec<&str> = argument_line.split(' ').collect(); if args.len() < 2 { return Err("error: header file provided lacks sufficient record arguments."); } let name = args[0].to_string(); let seg_num: Option; let sig_count: usize; let sampling_freq: u64; // Everything else is initialized with None, this is for sake of me not getting a stroke let mut counter_freq: Option = None; let mut base_counter_val: Option = None; let mut sample_num: Option = None; let mut basetime: Option = None; let mut basedate: Option = None; // Signals and segments are kept in a single argument organized as signal/segment // segments are optional and we need to check this too { let seg_sig: Vec<&str> = args[1].split('/').collect(); if seg_sig.len() == 2 { match seg_sig[1].parse::() { Ok(value) => { seg_num = Some(value); } Err(_) => { return Err("error: failed to parse segment number from header."); } } } else { seg_num = None; } match seg_sig[0].parse::() { Ok(value) => { sig_count = value; } Err(_) => { return Err("error: failed to parse signal number from header."); } } } // Parse everything else, if present loop { // This is apparently the way to get safe correct goto-like behaviour if args.len() <= 2 { // Sampling frequency and counter frequency sampling_freq = 250; // Default value break; } { let freq_dual: Vec<&str> = args[2].split('/').collect(); if freq_dual.len() == 2 { match freq_dual[1].parse::() { Ok(value) => { counter_freq = Some(value); } Err(_) => { return Err("error: failed to parse counter frequency from header."); } } } match freq_dual[0].parse::() { Ok(value) => { sampling_freq = value; } Err(_) => { return Err("error: failed to parse sampling frequency from header."); } } } if args.len() <= 3 {break;} match args[3].parse::() { Ok(value) => { base_counter_val = Some(value); } Err(_) => {} } if args.len() <= 4 {break;} match args[4].parse::() { Ok(value) => { sample_num = Some(value); } Err(_) => {} } if args.len() <= 5 {break;} basetime = Some(args[5].to_string()); if args.len() <= 6 {break;} basedate = Some(args[6].to_string()); break; } Ok(Record { name: name, seg_num: seg_num, signal_count: sig_count, sampling_freq: sampling_freq, counter_freq: counter_freq, base_counter_val: base_counter_val, sample_num: sample_num, basetime: basetime, basedate: basedate }) } } /// Keys for all the data that can possibly be parsed from the values appended /// to the ADC block #[derive(Debug, Eq, Hash, PartialEq)] enum AdcBlockKeys { Gain, Baseline, Units } /// Holds the information relevant to individual signals and their specifications /// /// By minimum, only carries the `filename`, `format`, and `adc_zero` attributes #[derive(Debug, Clone)] pub struct SignalSpec { filename: String, format: SignalFormat, samples_frame: Option, skew: Option, offset: Option, adc_gain: Option, baseline: Option, units: Option, adc_resolution: Option, adc_zero: i64, initial_val: Option, checksum: Option, blocksize: Option, desc: Option } impl SignalSpec { /// Attempts to generate a valid signal specification struct from a supplied /// string. /// Returns a `Result` containing possible error information /// relevant to parsing failures. /// /// ## Arguments /// - `argument_line` - argument line for the signal specification, taken /// from header file pub fn from_str(argument_line: &str) -> Result { let args: Vec<&str> = argument_line.split(' ').collect(); if args.len() < 2 { return Err("error: signal provided by header file lacks sufficient arguments."); } let name = args[0].to_string(); let sigformat: SignalFormat; // Optional args let samples_frame: Option = None; let skew: Option = None; let offset: Option = None; let mut adc_gain: Option = None; let mut baseline: Option = None; let mut units: Option = None; let mut adc_resolution: Option = None; let mut adc_zero:i64 = 0; let mut initial_val: Option = None; let mut checksum: Option = None; let mut blocksize: Option = None; let mut desc: Option = None; // TODO: implement samplesperframe, skew, and offset match args[1].parse::() { Ok(value) => { sigformat = SignalSpec::parse_format(value); }, Err(_) => { return Err("error: unable to parse format from signal"); } } loop { if args.len() <= 2 { break; } { let mut results: HashMap = HashMap::new(); let mut buffer: String = String::new(); let mut found_baseline = false; let mut found_units = false; // Parse through all the characters and get the individual parts for character in args[2].chars() { loop { if !found_baseline && !found_units && character == '(' { found_baseline = true; results.insert(AdcBlockKeys::Gain, buffer); buffer = String::new(); break; } else if found_baseline && character == ')' { results.insert(AdcBlockKeys::Baseline, buffer); buffer = String::new(); break; } else if !found_units && character == '/' { found_units = true; if !found_baseline { results.insert(AdcBlockKeys::Gain, buffer); } buffer = String::new(); break; } buffer.push(character); break; } } if found_units { results.insert(AdcBlockKeys::Units, buffer); } // Try and get the actual values out of the results match results.get(&AdcBlockKeys::Gain) { Some(value ) => { match value.parse::() { Ok(parsed) => { adc_gain = Some(parsed); }, Err(_) => { return Err("error: Signal contains adc gain block with invalid entry"); } } }, None => { return Err("error: Signal contains adc gain block but the block is empty."); } } match results.get(&AdcBlockKeys::Baseline) { Some(value) => { match value.parse::() { Ok(parsed) => { baseline = Some(parsed); }, Err(_) => { return Err("error: Unable to parse baseline from signal"); } } }, None => {} } match results.get(&AdcBlockKeys::Units) { Some(value) => { units = Some(value.clone()); }, None => {} } } if args.len() <= 3 { break; } match args[3].parse::() { Ok(value) => { adc_resolution = Some(value); }, Err(_) => { return Err("error: unable to parse resolution from signal"); } } if args.len() <= 4 { break; } match args[4].parse::() { Ok(value) => { adc_zero = value; }, Err(_) => { adc_zero = 0; // Standard: If this field is missing, a value of zero is assumed. } } if args.len() <= 5 { break; } match args[5].parse::() { Ok(value) => { initial_val = Some(value); }, // Standard: If this field is missing, // the adc_zero value is presumed. // We do this on the data parsing part. Err(_) => {} } if args.len() <= 6 { break; } match args[6].parse::() { Ok(value) => { checksum = Some(value); }, Err(_) => { return Err("error: unable to parse checksum from signal"); } } if args.len() <= 7 { break; } match args[7].parse::() { Ok(value) => { blocksize = Some(value); }, Err(_) => { return Err("error: unable to parse block size from signal"); } } if args.len() <= 8 { break; } desc = Some(args[8].to_string()); break; } Ok( SignalSpec { filename: name, format: sigformat, samples_frame: samples_frame, skew: skew, offset: offset, adc_gain: adc_gain, baseline: baseline, units: units, adc_resolution: adc_resolution, adc_zero: adc_zero, initial_val: initial_val, checksum: checksum, blocksize: blocksize, desc: desc } ) } /// Parses the WFDB signal format from the format number, if it exists and /// is implemented fn parse_format(formatnum: u64) -> SignalFormat { match formatnum { 16 => SignalFormat::Format16, 212 => SignalFormat::Format212, 0..=u64::MAX => SignalFormat::Unimpl } } } /// Holds all the possible data from the WFDB header file, those being the record /// line data and all the possible signal specifications present on the following /// lines #[derive(Debug, Clone)] pub struct Header { record: Option, signal_specs: Vec } impl Header { /// Creates a completely empty, not fully initialized header /// /// This is a workaround because I couldn't figure out how to make the compiler /// accept non-initializing the value for the first pass of a for loop in /// `parse_header()` fn new() -> Header { Header { record: None, signal_specs: vec![] } } /// Creates a Header from a supplied Record struct. /// /// Initializes the signal_specs vector with the correct capacity provided /// by the record. fn with_record(record: Record) -> Header { let capacity = record.signal_count; Header { record: Some(record), signal_specs: Vec::with_capacity(capacity) } } fn add_signal_spec(&mut self, spec: SignalSpec) { self.signal_specs.push(spec); } pub fn is_empty(&self) -> bool { match self.record { Some(_) => false, None => true } } pub fn signals_per_file(&self) -> HashMap { let mut map: HashMap = HashMap::new(); for signal in &self.signal_specs { map.entry(signal.filename.clone()).and_modify(|val: &mut u64| *val += 1 ).or_insert(1); } return map; } } /// Attempts to parse the header file. /// /// Returns a Result either containing the `Header` struct, or the /// error string describing why we couldn't parse the header. pub fn parse_header(header_data: &str) -> Result { let header_lines: Vec<&str> = header_data.split("\n").collect(); let mut found_record: bool = false; let mut header: Header = Header::new(); let mut specs_read: usize = 0; let mut specs_max: usize = 0; for line in header_lines { // Ignore commented lines if line.starts_with("#") { continue; } if !found_record { let possible_record = Record::from_str(line); match possible_record { Ok(rec) => { specs_max = rec.signal_count; header = Header::with_record(rec); found_record = true; continue; } Err(e) => { return Err(e) } } } let possible_spec = SignalSpec::from_str(line); match possible_spec { Ok(spec) => { header.add_signal_spec(spec); } Err(e) => { return Err(e); } } specs_read += 1; if specs_read >= specs_max { break; } } if header.is_empty() { return Err("Unable to parse valid header information"); } Ok(header) }