noodles_fasta/fai/
record.rs

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