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}