Skip to main content

fastx/
borrowed.rs

1//! Records borrowed from the reader's buffer, for passes that never keep them.
2//!
3//! [`crate::FastxReader::read_into`] copies each record's bytes into a
4//! [`Sequence`] you own. That copy is what lets you keep the record, pass it
5//! between threads or hold several at once — and on a large file it is also
6//! measurably a third of the parsing time.
7//!
8//! [`SequenceRef`] skips it. The reader guarantees a whole record is in its
9//! buffer and hands out slices into that buffer, so the record is valid only
10//! until the next one is read. For the common shape — FASTQ, or FASTA on a single
11//! line — nothing is copied at all.
12//!
13//! ```
14//! use fastx::FastxReader;
15//!
16//! let data = b"@r1 sample\nACGTACGT\n+\nIIIIIIII\n@r2\nTTTT\n+\n!!!!\n";
17//! let mut reader = FastxReader::new(&data[..]);
18//!
19//! let mut bases = 0;
20//! reader.for_each_ref(|record| {
21//!     bases += record.len();
22//!     Ok(())
23//! })?;
24//! assert_eq!(bases, 12);
25//! # Ok::<(), fastx::Error>(())
26//! ```
27
28use std::borrow::Cow;
29
30use crate::error::Result;
31use crate::format::Format;
32use crate::qual::{self, PHRED33};
33use crate::record::Sequence;
34use crate::seq::{self, BaseCounts};
35
36/// One FASTA or FASTQ record, borrowed from the reader that produced it.
37///
38/// Valid only until the next record is read. Call [`SequenceRef::to_owned`] to
39/// keep one.
40///
41/// Identifiers are `&[u8]` rather than `&str` deliberately: validating UTF-8 is
42/// work this type exists to avoid, and sequence identifiers are ASCII in
43/// practice. [`SequenceRef::id_str`] converts when you need text, borrowing
44/// rather than allocating whenever the bytes are already valid UTF-8.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct SequenceRef<'a> {
47    id: &'a [u8],
48    description: Option<&'a [u8]>,
49    seq: &'a [u8],
50    quality: Option<&'a [u8]>,
51}
52
53impl<'a> SequenceRef<'a> {
54    /// Build a borrowed record from parts. The reader uses this; you rarely will.
55    pub(crate) fn new(
56        id: &'a [u8],
57        description: Option<&'a [u8]>,
58        seq: &'a [u8],
59        quality: Option<&'a [u8]>,
60    ) -> SequenceRef<'a> {
61        SequenceRef {
62            id,
63            description,
64            seq,
65            quality,
66        }
67    }
68
69    /// The identifier: the header up to the first ASCII whitespace.
70    pub fn id(&self) -> &'a [u8] {
71        self.id
72    }
73
74    /// The identifier as text, replacing anything that is not UTF-8.
75    ///
76    /// Borrows when the bytes are already valid UTF-8, which is the normal case.
77    pub fn id_str(&self) -> Cow<'a, str> {
78        String::from_utf8_lossy(self.id)
79    }
80
81    /// Everything after the first whitespace in the header, if any.
82    pub fn description(&self) -> Option<&'a [u8]> {
83        self.description
84    }
85
86    /// The description as text, replacing anything that is not UTF-8.
87    pub fn description_str(&self) -> Option<Cow<'a, str>> {
88        self.description.map(String::from_utf8_lossy)
89    }
90
91    /// The residues, without line breaks.
92    pub fn seq(&self) -> &'a [u8] {
93        self.seq
94    }
95
96    /// The Phred quality characters, FASTQ only, always Phred+33.
97    pub fn quality(&self) -> Option<&'a [u8]> {
98        self.quality
99    }
100
101    /// Number of residues.
102    pub fn len(&self) -> usize {
103        self.seq.len()
104    }
105
106    /// True when the record has no residues.
107    pub fn is_empty(&self) -> bool {
108        self.seq.is_empty()
109    }
110
111    /// True when the record carries quality scores.
112    pub fn has_quality(&self) -> bool {
113        self.quality.is_some()
114    }
115
116    /// The format this record can be written as losslessly.
117    pub fn format(&self) -> Format {
118        if self.has_quality() {
119            Format::Fastq
120        } else {
121            Format::Fasta
122        }
123    }
124
125    /// Per-base counts.
126    pub fn base_counts(&self) -> BaseCounts {
127        BaseCounts::of(self.seq)
128    }
129
130    /// GC fraction over unambiguous bases, `None` when there are none.
131    pub fn gc_content(&self) -> Option<f64> {
132        seq::gc_content(self.seq)
133    }
134
135    /// Error-probability-weighted mean quality (Phred+33).
136    pub fn mean_quality(&self) -> Option<f64> {
137        qual::mean_quality(self.quality?, PHRED33)
138    }
139
140    /// Expected number of sequencing errors in the read (Phred+33).
141    pub fn expected_errors(&self) -> Option<f64> {
142        Some(qual::expected_errors(self.quality?, PHRED33))
143    }
144
145    /// Iterator over overlapping k-mers.
146    pub fn kmers(&self, k: usize) -> impl Iterator<Item = &'a [u8]> {
147        seq::kmers(self.seq, k)
148    }
149
150    /// Copy into an owned [`Sequence`], for the records you want to keep.
151    pub fn to_owned(&self) -> Sequence {
152        Sequence {
153            id: self.id_str().into_owned(),
154            description: self.description_str().map(Cow::into_owned),
155            seq: self.seq.to_vec(),
156            quality: self.quality.map(<[u8]>::to_vec),
157        }
158    }
159
160    /// Write this record out in `format`.
161    ///
162    /// FASTA lines are wrapped at `line_width`; `None` writes one line.
163    pub fn write<W: std::io::Write>(
164        &self,
165        out: &mut W,
166        format: Format,
167        line_width: Option<usize>,
168    ) -> Result<()> {
169        match format {
170            Format::Fasta => {
171                out.write_all(b">")?;
172                self.write_header(out)?;
173                match line_width.filter(|w| *w > 0) {
174                    None => {
175                        out.write_all(self.seq)?;
176                        out.write_all(b"\n")?;
177                    }
178                    Some(width) => {
179                        if self.seq.is_empty() {
180                            out.write_all(b"\n")?;
181                        }
182                        for chunk in self.seq.chunks(width) {
183                            out.write_all(chunk)?;
184                            out.write_all(b"\n")?;
185                        }
186                    }
187                }
188            }
189            Format::Fastq => {
190                let quality = self.quality.ok_or_else(|| crate::Error::MissingQuality {
191                    id: self.id_str().into_owned(),
192                })?;
193                out.write_all(b"@")?;
194                self.write_header(out)?;
195                out.write_all(self.seq)?;
196                out.write_all(b"\n+\n")?;
197                out.write_all(quality)?;
198                out.write_all(b"\n")?;
199            }
200        }
201        Ok(())
202    }
203
204    fn write_header<W: std::io::Write>(&self, out: &mut W) -> Result<()> {
205        out.write_all(self.id)?;
206        if let Some(description) = self.description {
207            out.write_all(b" ")?;
208            out.write_all(description)?;
209        }
210        out.write_all(b"\n")?;
211        Ok(())
212    }
213}
214
215impl PartialEq<Sequence> for SequenceRef<'_> {
216    /// Compare against an owned record, so tests can assert the two read paths
217    /// agree without copying first.
218    fn eq(&self, other: &Sequence) -> bool {
219        self.id == other.id.as_bytes()
220            && self.description == other.description.as_deref().map(str::as_bytes)
221            && self.seq == other.seq.as_slice()
222            && self.quality == other.quality.as_deref()
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn accessors_and_conversion() {
232        let record = SequenceRef::new(b"r1", Some(b"a sample"), b"ACGTN", Some(b"IIII!"));
233        assert_eq!(record.id(), b"r1");
234        assert_eq!(record.id_str(), "r1");
235        assert_eq!(record.description_str().unwrap(), "a sample");
236        assert_eq!(record.len(), 5);
237        assert!(record.has_quality());
238        assert_eq!(record.format(), Format::Fastq);
239        assert_eq!(record.gc_content(), Some(0.5));
240        assert_eq!(record.kmers(4).count(), 2);
241
242        let owned = record.to_owned();
243        assert_eq!(owned.id, "r1");
244        assert_eq!(owned.description.as_deref(), Some("a sample"));
245        assert_eq!(owned.seq, b"ACGTN");
246        assert_eq!(owned.quality.as_deref(), Some(&b"IIII!"[..]));
247        assert!(record == owned);
248    }
249
250    #[test]
251    fn non_utf8_ids_survive_as_bytes() {
252        // The owned path is lossy here; the borrowed one keeps the bytes intact
253        // and only converts on request.
254        let record = SequenceRef::new(&[b'i', 0xff], None, b"AC", None);
255        assert_eq!(record.id(), &[b'i', 0xff]);
256        assert_eq!(record.id_str(), "i\u{fffd}");
257    }
258
259    #[test]
260    fn writes_both_formats() {
261        let record = SequenceRef::new(b"r", Some(b"d"), b"ACGTAC", Some(b"IIIIII"));
262        let mut out = Vec::new();
263        record.write(&mut out, Format::Fastq, None).unwrap();
264        assert_eq!(out, b"@r d\nACGTAC\n+\nIIIIII\n");
265
266        let mut out = Vec::new();
267        record.write(&mut out, Format::Fasta, Some(4)).unwrap();
268        assert_eq!(out, b">r d\nACGT\nAC\n");
269
270        // FASTA without quality cannot be written as FASTQ.
271        let fasta = SequenceRef::new(b"r", None, b"AC", None);
272        let mut out = Vec::new();
273        assert!(fasta.write(&mut out, Format::Fastq, None).is_err());
274    }
275}