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.length(), 8);
66    /// ```
67    pub fn length(&self) -> u64 {
68        self.length
69    }
70
71    /// Returns the offset from the start.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use noodles_fasta::fai;
77    /// let record = fai::Record::new("sq0", 8, 4, 80, 81);
78    /// assert_eq!(record.offset(), 4);
79    /// ```
80    pub fn offset(&self) -> u64 {
81        self.offset
82    }
83
84    /// Returns the number of bases in a line.
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use noodles_fasta::fai;
90    /// let record = fai::Record::new("sq0", 8, 4, 80, 81);
91    /// assert_eq!(record.line_bases(), 80);
92    /// ```
93    pub fn line_bases(&self) -> u64 {
94        self.line_bases
95    }
96
97    /// Returns the number of characters in a line.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use noodles_fasta::fai;
103    /// let record = fai::Record::new("sq0", 8, 4, 80, 81);
104    /// assert_eq!(record.line_width(), 81);
105    /// ```
106    pub fn line_width(&self) -> u64 {
107        self.line_width
108    }
109
110    /// Returns the start position of the given interval.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use noodles_core::{region::Interval, Position};
116    /// use noodles_fasta::fai;
117    ///
118    /// let record = fai::Record::new("sq0", 10946, 4, 80, 81);
119    /// let interval = Interval::from(..);
120    ///
121    /// assert_eq!(record.query(interval)?, 4);
122    /// Ok::<_, std::io::Error>(())
123    /// ```
124    pub fn query(&self, interval: Interval) -> io::Result<u64> {
125        let start = interval
126            .start()
127            .map(|position| usize::from(position) - 1)
128            .unwrap_or_default();
129
130        let start =
131            u64::try_from(start).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
132
133        let pos = self.offset()
134            + start / self.line_bases() * self.line_width()
135            + start % self.line_bases();
136
137        Ok(pos)
138    }
139}
140
141/// An error returned when a raw FASTA index record fails to parse.
142#[derive(Clone, Debug, Eq, PartialEq)]
143pub enum ParseError {
144    /// The input is empty.
145    Empty,
146    /// A field is missing.
147    MissingField(Field),
148    /// A field is invalid.
149    InvalidField(Field, std::num::ParseIntError),
150}
151
152impl error::Error for ParseError {
153    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
154        match self {
155            Self::InvalidField(_, e) => Some(e),
156            _ => None,
157        }
158    }
159}
160
161impl fmt::Display for ParseError {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::Empty => f.write_str("empty input"),
165            Self::MissingField(field) => write!(f, "missing field: {field:?}"),
166            Self::InvalidField(field, _) => write!(f, "invalid field: {field:?}"),
167        }
168    }
169}
170
171impl FromStr for Record {
172    type Err = ParseError;
173
174    fn from_str(s: &str) -> Result<Self, Self::Err> {
175        if s.is_empty() {
176            return Err(ParseError::Empty);
177        }
178
179        let mut fields = s.splitn(MAX_FIELDS, FIELD_DELIMITER);
180
181        let name = parse_string(&mut fields, Field::Name)?;
182        let len = parse_u64(&mut fields, Field::Length)?;
183        let offset = parse_u64(&mut fields, Field::Offset)?;
184        let line_bases = parse_u64(&mut fields, Field::LineBases)?;
185        let line_width = parse_u64(&mut fields, Field::LineWidth)?;
186
187        Ok(Self::new(name, len, offset, line_bases, line_width))
188    }
189}
190
191fn parse_string<'a, I>(fields: &mut I, field: Field) -> Result<String, ParseError>
192where
193    I: Iterator<Item = &'a str>,
194{
195    fields
196        .next()
197        .ok_or(ParseError::MissingField(field))
198        .map(|s| s.into())
199}
200
201fn parse_u64<'a, I>(fields: &mut I, field: Field) -> Result<u64, ParseError>
202where
203    I: Iterator<Item = &'a str>,
204{
205    fields
206        .next()
207        .ok_or(ParseError::MissingField(field))
208        .and_then(|s| s.parse().map_err(|e| ParseError::InvalidField(field, e)))
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_from_str() {
217        assert_eq!(
218            "sq0\t10946\t4\t80\t81".parse(),
219            Ok(Record::new("sq0", 10946, 4, 80, 81))
220        );
221
222        assert_eq!("".parse::<Record>(), Err(ParseError::Empty));
223
224        assert_eq!(
225            "sq0".parse::<Record>(),
226            Err(ParseError::MissingField(Field::Length))
227        );
228
229        assert!(matches!(
230            "sq0\tnoodles".parse::<Record>(),
231            Err(ParseError::InvalidField(Field::Length, _))
232        ));
233    }
234}