wfdb/header/
common.rs

1use std::io::BufRead;
2
3use crate::{Error, Result};
4
5use super::{Metadata, SegmentInfo, SignalInfo};
6
7/// Header specifications containing either signal or segment data.
8///
9/// A WFDB header contains either signal specifications (for single-segment)
10/// or segment specifications (for multi-segment). These two are mutually exclusive.
11#[derive(Debug, Clone, PartialEq)]
12pub enum Specifications {
13    /// Single-segment record with signal specifications.
14    SingleSegment {
15        /// Signal specifications for each signal in the record.
16        signals: Vec<SignalInfo>,
17    },
18    /// Multi-segment record with segment specifications.
19    MultiSegment {
20        /// Segment specifications for each segment in the record.
21        segments: Vec<SegmentInfo>,
22    },
23}
24
25impl Specifications {
26    /// Get the signal specifications if this is a single-segment record.
27    ///
28    /// Returns `Some` for single-segment records, `None` for multi-segment records.
29    #[must_use]
30    pub fn signals(&self) -> Option<&[SignalInfo]> {
31        if let Self::SingleSegment { signals } = self {
32            Some(signals)
33        } else {
34            None
35        }
36    }
37
38    /// Get the segment specifications if this is a multi-segment record.
39    ///
40    /// Returns `Some` for multi-segment records, `None` for single-segment records.
41    #[must_use]
42    pub fn segments(&self) -> Option<&[SegmentInfo]> {
43        if let Self::MultiSegment { segments } = self {
44            Some(segments)
45        } else {
46            None
47        }
48    }
49
50    /// Check if this is a multi-segment record.
51    #[must_use]
52    pub const fn is_multi_segment(&self) -> bool {
53        matches!(self, Self::MultiSegment { .. })
54    }
55
56    /// Check if this is a single-segment record.
57    #[must_use]
58    pub const fn is_single_segment(&self) -> bool {
59        matches!(self, Self::SingleSegment { .. })
60    }
61}
62
63/// Parsed WFDB header file content.
64///
65/// A header file specifies the record metadata, and either signal specifications
66/// (for single-segment records) or segment specifications (for multi-segment records).
67/// These two types are mutually exclusive.
68///
69/// # Examples
70///
71/// Here are a few examples of header file structures:
72///
73/// ## Single-segment record (MIT-BIH Database record 100):
74/// ```text
75/// 100 2 360 650000 0:0:0 0/0/0
76/// 100.dat 212 200 11 1024 995 -22131 0 MLII
77/// 100.dat 212 200 11 1024 1011 20052 0 V5
78/// # 69 M 1085 1629 x1
79/// # Aldomet, Inderal
80/// ```
81///
82/// ## Multi-segment record:
83/// ```text
84/// multi/3 2 360 45000
85/// 100s 21600
86/// null 1800
87/// 100s 21600
88/// ```
89#[derive(Debug, Clone, PartialEq)]
90pub struct Header {
91    /// Record metadata from the record line.
92    pub metadata: Metadata,
93    /// Specifications (signals for single-segment, segments for multi-segment).
94    pub specifications: Specifications,
95    /// Info strings (comments following signal/segment specifications).
96    ///
97    /// Each string represents the content of one comment line (without the '#' prefix).
98    pub info_strings: Vec<String>,
99}
100
101impl Header {
102    // [Header decoding functions]
103
104    /// Parse a WFDB header from a buffered reader.
105    ///
106    /// # Format
107    ///
108    /// Header files contain ASCII text with the following structure:
109    /// 1. Optional comment lines (starting with '#')
110    /// 2. Record line (required)
111    /// 3. Signal specification lines (for single-segment records) OR
112    ///    Segment specification lines (for multi-segment records)
113    /// 4. Optional info strings (comment lines after specifications)
114    ///
115    /// # Errors
116    ///
117    /// Will return an error if:
118    /// - The record line is missing or invalid
119    /// - Signal/segment specifications are missing or invalid
120    /// - The number of specifications doesn't match the record line
121    pub fn from_reader<R: BufRead>(reader: &mut R) -> Result<Self> {
122        // Use iterator-based approach with proper line handling
123        let lines: Vec<String> = reader.lines().collect::<std::io::Result<Vec<String>>>()?;
124
125        Self::from_lines(&lines)
126    }
127
128    /// Parse a WFDB header from a slice of lines.
129    ///
130    /// This is the internal parsing function used by `from_reader`.
131    fn from_lines(lines: &[String]) -> Result<Self> {
132        // Find the first non-empty, non-comment line (record line)
133        let record_line_idx = lines
134            .iter()
135            .position(|line| {
136                let trimmed = line.trim();
137                !trimmed.is_empty() && !trimmed.starts_with('#')
138            })
139            .ok_or_else(|| Error::InvalidHeader("Missing record line in header".to_string()))?;
140
141        // Parse the record line
142        let metadata = Metadata::from_record_line(&lines[record_line_idx])?;
143        let mut line_idx = record_line_idx + 1;
144
145        // Determine if this is a multi-segment record
146        let is_multi_segment = metadata.num_segments.is_some();
147
148        // Parse signal or segment specifications
149        let (signals, segments) = if is_multi_segment {
150            // Parse segment specifications
151            #[allow(clippy::expect_used)]
152            let num_segments = metadata.num_segments.expect("num_segments should be Some");
153            let mut segment_specs = Vec::new();
154
155            while line_idx < lines.len() && segment_specs.len() < num_segments {
156                let line = lines[line_idx].trim();
157
158                // Skip comments (but they shouldn't appear before all segments are read)
159                if line.is_empty() || line.starts_with('#') {
160                    line_idx += 1;
161                    continue;
162                }
163
164                segment_specs.push(SegmentInfo::from_segment_line(line)?);
165                line_idx += 1;
166            }
167
168            if segment_specs.len() < num_segments {
169                return Err(Error::InvalidHeader(format!(
170                    "Expected {} segment specifications, found {}",
171                    num_segments,
172                    segment_specs.len()
173                )));
174            }
175
176            (None, Some(segment_specs))
177        } else {
178            // Parse signal specifications
179            let num_signals = metadata.num_signals;
180            let mut signal_specs = Vec::new();
181
182            while line_idx < lines.len() && signal_specs.len() < num_signals {
183                let line = lines[line_idx].trim();
184
185                // Skip comments
186                if line.is_empty() || line.starts_with('#') {
187                    line_idx += 1;
188                    continue;
189                }
190
191                signal_specs.push(SignalInfo::from_signal_line(line)?);
192                line_idx += 1;
193            }
194
195            if signal_specs.len() < num_signals {
196                return Err(Error::InvalidHeader(format!(
197                    "Expected {} signal specifications, found {}",
198                    num_signals,
199                    signal_specs.len()
200                )));
201            }
202
203            (Some(signal_specs), None)
204        };
205
206        // Parse info strings
207        let mut info_strings = Vec::new();
208
209        while line_idx < lines.len() {
210            let line = lines[line_idx].trim();
211
212            if line.starts_with('#') {
213                // Remove the '#' prefix and collect as info string
214                let info = line.trim_start_matches('#').to_string();
215                info_strings.push(info);
216            }
217
218            line_idx += 1;
219        }
220
221        #[allow(clippy::expect_used)]
222        let specifications = match (signals, segments) {
223            (Some(signals), None) => Specifications::SingleSegment { signals },
224            (None, Some(segments)) => Specifications::MultiSegment { segments },
225            _ => unreachable!("Either signals or segments should be Some, but not both"),
226        };
227
228        Ok(Self {
229            metadata,
230            specifications,
231            info_strings,
232        })
233    }
234
235    // [Accessors]
236
237    /// Get the record metadata.
238    #[must_use]
239    pub const fn metadata(&self) -> &Metadata {
240        &self.metadata
241    }
242
243    /// Get the specifications (signals or segments).
244    #[must_use]
245    pub const fn specifications(&self) -> &Specifications {
246        &self.specifications
247    }
248
249    /// Get the signal specifications.
250    ///
251    /// Returns `Some` for single-segment records, `None` for multi-segment records.
252    #[must_use]
253    pub fn signals(&self) -> Option<&[SignalInfo]> {
254        self.specifications.signals()
255    }
256
257    /// Get the segment specifications.
258    ///
259    /// Returns `Some` for multi-segment records, `None` for single-segment records.
260    #[must_use]
261    pub fn segments(&self) -> Option<&[SegmentInfo]> {
262        self.specifications.segments()
263    }
264
265    /// Get the info strings.
266    #[must_use]
267    pub fn info_strings(&self) -> &[String] {
268        &self.info_strings
269    }
270
271    /// Check if this is a multi-segment record.
272    #[must_use]
273    pub const fn is_multi_segment(&self) -> bool {
274        self.specifications.is_multi_segment()
275    }
276
277    /// Check if this is a single-segment record.
278    #[must_use]
279    pub const fn is_single_segment(&self) -> bool {
280        self.specifications.is_single_segment()
281    }
282
283    /// Get the number of signals.
284    #[must_use]
285    pub const fn num_signals(&self) -> usize {
286        self.metadata.num_signals
287    }
288
289    /// Get the number of segments (for multi-segment records).
290    ///
291    /// Returns `Some` for multi-segment records, `None` for single-segment records.
292    #[must_use]
293    pub const fn num_segments(&self) -> Option<usize> {
294        self.metadata.num_segments
295    }
296}