noodles_cram/crai/
record.rs

1//! CRAM index record and fields.
2
3mod field;
4
5pub use self::field::Field;
6
7use std::{error, fmt, num, str::FromStr};
8
9use noodles_core::Position;
10
11const FIELD_DELIMITER: char = '\t';
12const MAX_FIELDS: usize = 6;
13
14/// A CRAM index record.
15#[derive(Clone, Debug, Default, Eq, PartialEq)]
16pub struct Record {
17    reference_sequence_id: Option<usize>,
18    alignment_start: Option<Position>,
19    alignment_span: usize,
20    offset: u64,
21    landmark: u64,
22    slice_length: u64,
23}
24
25impl Record {
26    /// Creates a CRAM index record.
27    ///
28    /// # Examples
29    ///
30    /// ```
31    /// use noodles_core::Position;
32    /// use noodles_cram::crai;
33    ///
34    /// let record = crai::Record::new(
35    ///     Some(0),
36    ///     Position::new(10946),
37    ///     6765,
38    ///     17711,
39    ///     233,
40    ///     317811,
41    /// );
42    /// ```
43    pub fn new(
44        reference_sequence_id: Option<usize>,
45        alignment_start: Option<Position>,
46        alignment_span: usize,
47        offset: u64,
48        landmark: u64,
49        slice_length: u64,
50    ) -> Self {
51        Self {
52            reference_sequence_id,
53            alignment_start,
54            alignment_span,
55            offset,
56            landmark,
57            slice_length,
58        }
59    }
60
61    /// Returns the reference sequence ID.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use noodles_core::Position;
67    /// use noodles_cram::crai;
68    ///
69    /// let record = crai::Record::new(
70    ///     Some(0),
71    ///     Position::new(10946),
72    ///     6765,
73    ///     17711,
74    ///     233,
75    ///     317811,
76    /// );
77    ///
78    /// assert_eq!(record.reference_sequence_id(), Some(0));
79    /// ```
80    pub fn reference_sequence_id(&self) -> Option<usize> {
81        self.reference_sequence_id
82    }
83
84    /// Returns the alignment start.
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use noodles_core::Position;
90    /// use noodles_cram::crai;
91    ///
92    /// let record = crai::Record::new(
93    ///     Some(0),
94    ///     Position::new(10946),
95    ///     6765,
96    ///     17711,
97    ///     233,
98    ///     317811,
99    /// );
100    ///
101    /// assert_eq!(record.alignment_start(), Position::new(10946));
102    /// ```
103    pub fn alignment_start(&self) -> Option<Position> {
104        self.alignment_start
105    }
106
107    /// Returns the alignment span.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use noodles_core::Position;
113    /// use noodles_cram::crai;
114    ///
115    /// let record = crai::Record::new(
116    ///     Some(0),
117    ///     Position::new(10946),
118    ///     6765,
119    ///     17711,
120    ///     233,
121    ///     317811,
122    /// );
123    ///
124    /// assert_eq!(record.alignment_span(), 6765);
125    /// ```
126    pub fn alignment_span(&self) -> usize {
127        self.alignment_span
128    }
129
130    /// Returns the offset of the container from the start of the stream.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use noodles_core::Position;
136    /// use noodles_cram::crai;
137    ///
138    /// let record = crai::Record::new(
139    ///     Some(0),
140    ///     Position::new(10946),
141    ///     6765,
142    ///     17711,
143    ///     233,
144    ///     317811,
145    /// );
146    ///
147    /// assert_eq!(record.offset(), 17711);
148    /// ```
149    pub fn offset(&self) -> u64 {
150        self.offset
151    }
152
153    /// Returns the offset of the slice from the start of the container.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use noodles_core::Position;
159    /// use noodles_cram::crai;
160    ///
161    /// let record = crai::Record::new(
162    ///     Some(0),
163    ///     Position::new(10946),
164    ///     6765,
165    ///     17711,
166    ///     233,
167    ///     317811,
168    /// );
169    ///
170    /// assert_eq!(record.landmark(), 233);
171    /// ```
172    pub fn landmark(&self) -> u64 {
173        self.landmark
174    }
175
176    /// Returns the size of the slice in bytes.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use noodles_core::Position;
182    /// use noodles_cram::crai;
183    ///
184    /// let record = crai::Record::new(
185    ///     Some(0),
186    ///     Position::new(10946),
187    ///     6765,
188    ///     17711,
189    ///     233,
190    ///     317811,
191    /// );
192    ///
193    /// assert_eq!(record.slice_length(), 317811);
194    /// ```
195    pub fn slice_length(&self) -> u64 {
196        self.slice_length
197    }
198}
199
200impl fmt::Display for Record {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        const UNMAPPED: i32 = -1;
203
204        if let Some(id) = self.reference_sequence_id() {
205            write!(f, "{id}\t")?;
206        } else {
207            write!(f, "{UNMAPPED}\t")?;
208        };
209
210        let alignment_start = self.alignment_start().map(usize::from).unwrap_or_default();
211
212        write!(
213            f,
214            "{}\t{}\t{}\t{}\t{}",
215            alignment_start, self.alignment_span, self.offset, self.landmark, self.slice_length
216        )
217    }
218}
219
220/// An error returned when a raw CRAM index record fails to parse.
221#[derive(Clone, Debug, Eq, PartialEq)]
222pub enum ParseError {
223    /// A field is missing.
224    Missing(Field),
225    /// A field is invalid.
226    Invalid(Field, std::num::ParseIntError),
227    /// The reference sequence ID is invalid.
228    InvalidReferenceSequenceId(num::TryFromIntError),
229}
230
231impl error::Error for ParseError {
232    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
233        match self {
234            Self::Missing(_) => None,
235            Self::Invalid(_, e) => Some(e),
236            Self::InvalidReferenceSequenceId(e) => Some(e),
237        }
238    }
239}
240
241impl fmt::Display for ParseError {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        match self {
244            Self::Missing(field) => write!(f, "missing field: {field:?}"),
245            Self::Invalid(field, _) => write!(f, "invalid field: {field:?}"),
246            Self::InvalidReferenceSequenceId(_) => f.write_str("invalid reference sequence ID"),
247        }
248    }
249}
250
251impl FromStr for Record {
252    type Err = ParseError;
253
254    fn from_str(s: &str) -> Result<Self, Self::Err> {
255        const UNMAPPED: i32 = -1;
256
257        let mut fields = s.splitn(MAX_FIELDS, FIELD_DELIMITER);
258
259        let reference_sequence_id =
260            parse_i32(&mut fields, Field::ReferenceSequenceId).and_then(|n| match n {
261                UNMAPPED => Ok(None),
262                _ => usize::try_from(n)
263                    .map(Some)
264                    .map_err(ParseError::InvalidReferenceSequenceId),
265            })?;
266
267        let alignment_start = parse_position(&mut fields, Field::AlignmentStart)?;
268        let alignment_span = parse_span(&mut fields, Field::AlignmentSpan)?;
269        let offset = parse_u64(&mut fields, Field::Offset)?;
270        let landmark = parse_u64(&mut fields, Field::Landmark)?;
271        let slice_length = parse_u64(&mut fields, Field::SliceLength)?;
272
273        Ok(Record::new(
274            reference_sequence_id,
275            alignment_start,
276            alignment_span,
277            offset,
278            landmark,
279            slice_length,
280        ))
281    }
282}
283
284fn parse_i32<'a, I>(fields: &mut I, field: Field) -> Result<i32, ParseError>
285where
286    I: Iterator<Item = &'a str>,
287{
288    fields
289        .next()
290        .ok_or(ParseError::Missing(field))
291        .and_then(|s| s.parse().map_err(|e| ParseError::Invalid(field, e)))
292}
293
294fn parse_u64<'a, I>(fields: &mut I, field: Field) -> Result<u64, ParseError>
295where
296    I: Iterator<Item = &'a str>,
297{
298    fields
299        .next()
300        .ok_or(ParseError::Missing(field))
301        .and_then(|s| s.parse().map_err(|e| ParseError::Invalid(field, e)))
302}
303
304fn parse_position<'a, I>(fields: &mut I, field: Field) -> Result<Option<Position>, ParseError>
305where
306    I: Iterator<Item = &'a str>,
307{
308    fields
309        .next()
310        .ok_or(ParseError::Missing(field))
311        .and_then(|s| s.parse().map_err(|e| ParseError::Invalid(field, e)))
312        .map(Position::new)
313}
314
315fn parse_span<'a, I>(fields: &mut I, field: Field) -> Result<usize, ParseError>
316where
317    I: Iterator<Item = &'a str>,
318{
319    fields
320        .next()
321        .ok_or(ParseError::Missing(field))
322        .and_then(|s| s.parse().map_err(|e| ParseError::Invalid(field, e)))
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_fmt() {
331        let record = Record::new(None, Position::new(10946), 6765, 17711, 233, 317811);
332        let actual = record.to_string();
333        let expected = "-1\t10946\t6765\t17711\t233\t317811";
334        assert_eq!(actual, expected);
335    }
336
337    #[test]
338    fn test_from_str() -> Result<(), Box<dyn std::error::Error>> {
339        let actual: Record = "0\t10946\t6765\t17711\t233\t317811".parse()?;
340
341        let expected = Record {
342            reference_sequence_id: Some(0),
343            alignment_start: Position::new(10946),
344            alignment_span: 6765,
345            offset: 17711,
346            landmark: 233,
347            slice_length: 317811,
348        };
349
350        assert_eq!(actual, expected);
351
352        Ok(())
353    }
354
355    #[test]
356    fn test_from_str_with_invalid_records() {
357        assert_eq!(
358            "0\t10946".parse::<Record>(),
359            Err(ParseError::Missing(Field::AlignmentSpan))
360        );
361
362        assert!(matches!(
363            "0\t10946\tnoodles".parse::<Record>(),
364            Err(ParseError::Invalid(Field::AlignmentSpan, _))
365        ));
366
367        assert!(matches!(
368            "-8\t10946\t6765\t17711\t233\t317811".parse::<Record>(),
369            Err(ParseError::InvalidReferenceSequenceId(_))
370        ));
371    }
372}