noodles_fasta/fai/
record.rs

1mod field;
2
3use std::{error, fmt, io, str::FromStr};
4
5use bstr::{BStr, BString};
6use noodles_core::region::Interval;
7
8use self::field::Field;
9
10const FIELD_DELIMITER: char = '\t';
11const MAX_FIELDS: usize = 5;
12
13/// A FASTA index record.
14#[derive(Clone, Debug, Default, Eq, PartialEq)]
15pub struct Record {
16    name: BString,
17    length: u64,
18    offset: u64,
19    line_bases: u64,
20    line_width: u64,
21}
22
23impl Record {
24    /// Creates a FASTA index record.
25    ///
26    /// # Examples
27    ///
28    /// ```
29    /// use noodles_fasta::fai;
30    /// let record = fai::Record::new("sq0", 8, 4, 80, 81);
31    /// ```
32    pub fn new<N>(name: N, length: u64, offset: u64, line_bases: u64, line_width: u64) -> Self
33    where
34        N: Into<BString>,
35    {
36        Self {
37            name: name.into(),
38            length,
39            offset,
40            line_bases,
41            line_width,
42        }
43    }
44
45    /// Returns the record name.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use noodles_fasta::fai;
51    /// let record = fai::Record::new("sq0", 8, 4, 80, 81);
52    /// assert_eq!(record.name(), b"sq0");
53    /// ```
54    pub fn name(&self) -> &BStr {
55        self.name.as_ref()
56    }
57
58    /// Returns the length of the sequence.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use noodles_fasta::fai;
64    /// let record = fai::Record::new("sq0", 8, 4, 80, 81);
65    /// assert_eq!(record.len(), 8);
66    /// ```
67    #[allow(clippy::len_without_is_empty)]
68    #[deprecated(since = "0.23.0", note = "Use `Record::length` instead.")]
69    pub fn len(&self) -> u64 {
70        self.length()
71    }
72
73    /// Returns the length of the sequence.
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// use noodles_fasta::fai;
79    /// let record = fai::Record::new("sq0", 8, 4, 80, 81);
80    /// assert_eq!(record.length(), 8);
81    /// ```
82    pub fn length(&self) -> u64 {
83        self.length
84    }
85
86    /// Returns the offset from the start.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use noodles_fasta::fai;
92    /// let record = fai::Record::new("sq0", 8, 4, 80, 81);
93    /// assert_eq!(record.offset(), 4);
94    /// ```
95    pub fn offset(&self) -> u64 {
96        self.offset
97    }
98
99    /// Returns the number of bases in a line.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use noodles_fasta::fai;
105    /// let record = fai::Record::new("sq0", 8, 4, 80, 81);
106    /// assert_eq!(record.line_bases(), 80);
107    /// ```
108    pub fn line_bases(&self) -> u64 {
109        self.line_bases
110    }
111
112    /// Returns the number of characters in a line.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use noodles_fasta::fai;
118    /// let record = fai::Record::new("sq0", 8, 4, 80, 81);
119    /// assert_eq!(record.line_width(), 81);
120    /// ```
121    pub fn line_width(&self) -> u64 {
122        self.line_width
123    }
124
125    /// Returns the start position of the given interval.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use noodles_core::{region::Interval, Position};
131    /// use noodles_fasta::fai;
132    ///
133    /// let record = fai::Record::new("sq0", 10946, 4, 80, 81);
134    /// let interval = Interval::from(..);
135    ///
136    /// assert_eq!(record.query(interval)?, 4);
137    /// Ok::<_, std::io::Error>(())
138    /// ```
139    pub fn query(&self, interval: Interval) -> io::Result<u64> {
140        let start = interval
141            .start()
142            .map(|position| usize::from(position) - 1)
143            .unwrap_or_default();
144
145        let start =
146            u64::try_from(start).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
147
148        let pos = self.offset()
149            + start / self.line_bases() * self.line_width()
150            + start % self.line_bases();
151
152        Ok(pos)
153    }
154}
155
156/// An error returned when a raw FASTA index record fails to parse.
157#[derive(Clone, Debug, Eq, PartialEq)]
158pub enum ParseError {
159    /// The input is empty.
160    Empty,
161    /// A field is missing.
162    MissingField(Field),
163    /// A field is invalid.
164    InvalidField(Field, std::num::ParseIntError),
165}
166
167impl error::Error for ParseError {
168    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
169        match self {
170            Self::InvalidField(_, e) => Some(e),
171            _ => None,
172        }
173    }
174}
175
176impl fmt::Display for ParseError {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match self {
179            Self::Empty => f.write_str("empty input"),
180            Self::MissingField(field) => write!(f, "missing field: {field:?}"),
181            Self::InvalidField(field, _) => write!(f, "invalid field: {field:?}"),
182        }
183    }
184}
185
186impl FromStr for Record {
187    type Err = ParseError;
188
189    fn from_str(s: &str) -> Result<Self, Self::Err> {
190        if s.is_empty() {
191            return Err(ParseError::Empty);
192        }
193
194        let mut fields = s.splitn(MAX_FIELDS, FIELD_DELIMITER);
195
196        let name = parse_string(&mut fields, Field::Name)?;
197        let len = parse_u64(&mut fields, Field::Length)?;
198        let offset = parse_u64(&mut fields, Field::Offset)?;
199        let line_bases = parse_u64(&mut fields, Field::LineBases)?;
200        let line_width = parse_u64(&mut fields, Field::LineWidth)?;
201
202        Ok(Self::new(name, len, offset, line_bases, line_width))
203    }
204}
205
206fn parse_string<'a, I>(fields: &mut I, field: Field) -> Result<String, ParseError>
207where
208    I: Iterator<Item = &'a str>,
209{
210    fields
211        .next()
212        .ok_or(ParseError::MissingField(field))
213        .map(|s| s.into())
214}
215
216fn parse_u64<'a, I>(fields: &mut I, field: Field) -> Result<u64, ParseError>
217where
218    I: Iterator<Item = &'a str>,
219{
220    fields
221        .next()
222        .ok_or(ParseError::MissingField(field))
223        .and_then(|s| s.parse().map_err(|e| ParseError::InvalidField(field, e)))
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_from_str() {
232        assert_eq!(
233            "sq0\t10946\t4\t80\t81".parse(),
234            Ok(Record::new("sq0", 10946, 4, 80, 81))
235        );
236
237        assert_eq!("".parse::<Record>(), Err(ParseError::Empty));
238
239        assert_eq!(
240            "sq0".parse::<Record>(),
241            Err(ParseError::MissingField(Field::Length))
242        );
243
244        assert!(matches!(
245            "sq0\tnoodles".parse::<Record>(),
246            Err(ParseError::InvalidField(Field::Length, _))
247        ));
248    }
249}