1use std::fmt;
4use std::io::Write;
5use std::ops::Range;
6
7use crate::error::{Error, Result};
8use crate::format::Format;
9use crate::qual::{self, PHRED33};
10use crate::seq::{self, Alphabet, BaseCounts};
11
12#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
30pub struct Sequence {
31 pub id: String,
33 pub description: Option<String>,
35 pub seq: Vec<u8>,
37 pub quality: Option<Vec<u8>>,
39}
40
41impl Sequence {
42 pub fn fasta<I: Into<String>>(id: I, seq: impl Into<Vec<u8>>) -> Sequence {
44 Sequence {
45 id: id.into(),
46 description: None,
47 seq: seq.into(),
48 quality: None,
49 }
50 }
51
52 pub fn fastq<I: Into<String>>(
54 id: I,
55 seq: impl Into<Vec<u8>>,
56 quality: impl Into<Vec<u8>>,
57 ) -> Result<Sequence> {
58 let id = id.into();
59 let seq = seq.into();
60 let quality = quality.into();
61 if seq.len() != quality.len() {
62 return Err(Error::LengthMismatch {
63 id,
64 seq: seq.len(),
65 quality: quality.len(),
66 });
67 }
68 Ok(Sequence {
69 id,
70 description: None,
71 seq,
72 quality: Some(quality),
73 })
74 }
75
76 pub fn with_description<D: Into<String>>(mut self, description: D) -> Sequence {
78 let description = description.into();
79 self.description = if description.is_empty() {
80 None
81 } else {
82 Some(description)
83 };
84 self
85 }
86
87 pub fn len(&self) -> usize {
89 self.seq.len()
90 }
91
92 pub fn is_empty(&self) -> bool {
94 self.seq.is_empty()
95 }
96
97 pub fn has_quality(&self) -> bool {
99 self.quality.is_some()
100 }
101
102 pub fn format(&self) -> Format {
104 if self.has_quality() {
105 Format::Fastq
106 } else {
107 Format::Fasta
108 }
109 }
110
111 pub fn header(&self) -> String {
119 match &self.description {
120 Some(d) => format!("{} {}", self.id, d),
121 None => self.id.clone(),
122 }
123 }
124
125 pub fn seq_str(&self) -> Result<&str> {
127 std::str::from_utf8(&self.seq).map_err(|e| Error::InvalidByte {
128 id: self.id.clone(),
129 pos: e.valid_up_to(),
130 byte: self.seq[e.valid_up_to()],
131 })
132 }
133
134 pub fn clear(&mut self) {
138 self.id.clear();
139 self.description = None;
140 self.seq.clear();
141 if let Some(q) = self.quality.as_mut() {
142 q.clear();
143 }
144 }
145
146 pub fn base_counts(&self) -> BaseCounts {
148 BaseCounts::of(&self.seq)
149 }
150
151 pub fn gc_content(&self) -> Option<f64> {
153 seq::gc_content(&self.seq)
154 }
155
156 pub fn ambiguous_count(&self) -> u64 {
158 self.base_counts().n
159 }
160
161 pub fn reverse_complement(&self) -> Sequence {
171 Sequence {
172 id: self.id.clone(),
173 description: self.description.clone(),
174 seq: seq::reverse_complement(&self.seq),
175 quality: self
176 .quality
177 .as_ref()
178 .map(|q| q.iter().rev().copied().collect()),
179 }
180 }
181
182 pub fn reverse_complement_in_place(&mut self) {
184 seq::reverse_complement_in_place(&mut self.seq);
185 if let Some(q) = self.quality.as_mut() {
186 q.reverse();
187 }
188 }
189
190 pub fn make_uppercase(&mut self) {
192 self.seq.make_ascii_uppercase();
193 }
194
195 pub fn subseq(&self, range: Range<usize>) -> Result<Sequence> {
199 if range.start > range.end || range.end > self.seq.len() {
200 return Err(Error::OutOfBounds {
201 id: self.id.clone(),
202 start: range.start as u64,
203 end: range.end as u64,
204 length: self.seq.len() as u64,
205 });
206 }
207 Ok(Sequence {
208 id: self.id.clone(),
209 description: self.description.clone(),
210 seq: self.seq[range.clone()].to_vec(),
211 quality: self.quality.as_ref().map(|q| q[range].to_vec()),
212 })
213 }
214
215 pub fn trim_to(&mut self, range: Range<usize>) -> Result<()> {
217 if range.start > range.end || range.end > self.seq.len() {
218 return Err(Error::OutOfBounds {
219 id: self.id.clone(),
220 start: range.start as u64,
221 end: range.end as u64,
222 length: self.seq.len() as u64,
223 });
224 }
225 self.seq.truncate(range.end);
226 self.seq.drain(..range.start);
227 if let Some(q) = self.quality.as_mut() {
228 q.truncate(range.end);
229 q.drain(..range.start);
230 }
231 Ok(())
232 }
233
234 pub fn translate(&self, frame: usize, stop_at_stop: bool) -> Sequence {
236 Sequence {
237 id: self.id.clone(),
238 description: self.description.clone(),
239 seq: seq::translate(&self.seq, frame, stop_at_stop),
240 quality: None,
241 }
242 }
243
244 pub fn kmers(&self, k: usize) -> impl Iterator<Item = &[u8]> {
246 seq::kmers(&self.seq, k)
247 }
248
249 pub fn quality_scores(&self) -> Option<Vec<u8>> {
251 self.quality.as_ref().map(|q| qual::scores(q, PHRED33))
252 }
253
254 pub fn quality_scores_with(&self, offset: u8) -> Option<Vec<u8>> {
256 self.quality.as_ref().map(|q| qual::scores(q, offset))
257 }
258
259 pub fn mean_quality(&self) -> Option<f64> {
261 qual::mean_quality(self.quality.as_deref()?, PHRED33)
262 }
263
264 pub fn expected_errors(&self) -> Option<f64> {
266 Some(qual::expected_errors(self.quality.as_deref()?, PHRED33))
267 }
268
269 pub fn convert_quality_offset(&mut self, from: u8, to: u8) {
271 if let Some(q) = self.quality.as_mut() {
272 for c in q.iter_mut() {
273 *c = qual::encode(qual::score(*c, from), to);
274 }
275 }
276 }
277
278 pub fn into_fasta(mut self) -> Sequence {
280 self.quality = None;
281 self
282 }
283
284 pub fn validate(&self, alphabet: Alphabet) -> Result<()> {
292 if self.id.is_empty() {
293 return Err(Error::Parse {
294 line: 0,
295 kind: crate::error::ParseError::EmptyId,
296 });
297 }
298 if let Some(pos) = self.id.bytes().position(|b| b.is_ascii_whitespace()) {
299 return Err(Error::InvalidByte {
300 id: self.id.clone(),
301 pos,
302 byte: self.id.as_bytes()[pos],
303 });
304 }
305 if let Some(d) = &self.description {
306 if let Some(pos) = d.bytes().position(|b| b == b'\n' || b == b'\r') {
307 return Err(Error::InvalidByte {
308 id: self.id.clone(),
309 pos,
310 byte: d.as_bytes()[pos],
311 });
312 }
313 }
314 alphabet.validate_named(&self.seq, &self.id)?;
315 if let Some(q) = &self.quality {
316 if q.len() != self.seq.len() {
317 return Err(Error::LengthMismatch {
318 id: self.id.clone(),
319 seq: self.seq.len(),
320 quality: q.len(),
321 });
322 }
323 if let Some(pos) = q.iter().position(|&b| !(33..=126).contains(&b)) {
324 return Err(Error::InvalidByte {
325 id: self.id.clone(),
326 pos,
327 byte: q[pos],
328 });
329 }
330 }
331 Ok(())
332 }
333
334 pub fn write_fasta<W: Write>(&self, out: &mut W, line_width: Option<usize>) -> Result<()> {
342 check_writable_residues(&self.seq, &self.id)?;
343 out.write_all(b">")?;
344 self.write_header(out)?;
345 match line_width.filter(|w| *w > 0) {
346 None => {
347 out.write_all(&self.seq)?;
348 out.write_all(b"\n")?;
349 }
350 Some(width) => {
351 for chunk in self.seq.chunks(width) {
352 out.write_all(chunk)?;
353 out.write_all(b"\n")?;
354 }
355 if self.seq.is_empty() {
356 out.write_all(b"\n")?;
357 }
358 }
359 }
360 Ok(())
361 }
362
363 pub fn write_fastq<W: Write>(&self, out: &mut W) -> Result<()> {
366 let quality = self.quality.as_ref().ok_or_else(|| Error::MissingQuality {
367 id: self.id.clone(),
368 })?;
369 if quality.len() != self.seq.len() {
370 return Err(Error::LengthMismatch {
371 id: self.id.clone(),
372 seq: self.seq.len(),
373 quality: quality.len(),
374 });
375 }
376 check_writable_residues(&self.seq, &self.id)?;
377 check_writable_fastq_sequence(&self.seq, &self.id)?;
378 check_writable_quality(quality, &self.id)?;
379 out.write_all(b"@")?;
380 self.write_header(out)?;
381 out.write_all(&self.seq)?;
382 out.write_all(b"\n+\n")?;
383 out.write_all(quality)?;
384 out.write_all(b"\n")?;
385 Ok(())
386 }
387
388 fn write_header<W: Write>(&self, out: &mut W) -> Result<()> {
389 out.write_all(self.id.as_bytes())?;
390 if let Some(d) = &self.description {
391 out.write_all(b" ")?;
392 out.write_all(d.as_bytes())?;
393 }
394 out.write_all(b"\n")?;
395 Ok(())
396 }
397
398 pub fn to_string_in(&self, format: Format) -> Result<String> {
400 let mut buf = Vec::with_capacity(self.seq.len() * 2 + 64);
401 match format {
402 Format::Fasta => self.write_fasta(&mut buf, Some(60))?,
403 Format::Fastq => self.write_fastq(&mut buf)?,
404 }
405 Ok(String::from_utf8_lossy(&buf).into_owned())
406 }
407
408 pub(crate) fn set_header(&mut self, header: &[u8]) {
416 let header = trim_ascii_end(header);
417 let id = header_id(header);
418 self.id.push_str(&String::from_utf8_lossy(id));
419 let description = trim_ascii_start(&header[id.len()..]);
420 self.description = if description.is_empty() {
421 None
422 } else {
423 Some(String::from_utf8_lossy(description).into_owned())
424 };
425 }
426}
427
428pub(crate) fn header_id(header: &[u8]) -> &[u8] {
436 let end = header
437 .iter()
438 .position(|b| b.is_ascii_whitespace())
439 .unwrap_or(header.len());
440 &header[..end]
441}
442
443pub(crate) fn check_writable_residues(seq: &[u8], id: &str) -> Result<()> {
458 match memchr::memchr3(b'\n', b'\r', b'>', seq) {
459 None => Ok(()),
460 Some(pos) => Err(Error::InvalidByte {
461 id: id.to_string(),
462 pos,
463 byte: seq[pos],
464 }),
465 }
466}
467
468pub(crate) fn check_writable_fastq_sequence(seq: &[u8], id: &str) -> Result<()> {
470 if seq.first() == Some(&b'+') {
471 return Err(Error::InvalidByte {
472 id: id.to_string(),
473 pos: 0,
474 byte: b'+',
475 });
476 }
477 Ok(())
478}
479
480pub(crate) fn check_writable_quality(quality: &[u8], id: &str) -> Result<()> {
482 match memchr::memchr2(b'\n', b'\r', quality) {
483 None => Ok(()),
484 Some(pos) => Err(Error::InvalidByte {
485 id: id.to_string(),
486 pos,
487 byte: quality[pos],
488 }),
489 }
490}
491
492fn trim_ascii_start(mut bytes: &[u8]) -> &[u8] {
494 while let [first, rest @ ..] = bytes {
495 if first.is_ascii_whitespace() {
496 bytes = rest;
497 } else {
498 break;
499 }
500 }
501 bytes
502}
503
504fn trim_ascii_end(mut bytes: &[u8]) -> &[u8] {
505 while let [rest @ .., last] = bytes {
506 if last.is_ascii_whitespace() {
507 bytes = rest;
508 } else {
509 break;
510 }
511 }
512 bytes
513}
514
515impl fmt::Display for Sequence {
516 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518 let mut buf = Vec::new();
519 let rendered = match self.format() {
520 Format::Fasta => self.write_fasta(&mut buf, Some(60)),
521 Format::Fastq => self.write_fastq(&mut buf),
522 };
523 rendered.map_err(|_| fmt::Error)?;
524 f.write_str(&String::from_utf8_lossy(&buf))
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
533 fn fastq_requires_matching_lengths() {
534 assert!(Sequence::fastq("r", b"ACGT", b"III").is_err());
535 assert!(Sequence::fastq("r", b"ACGT", b"IIII").is_ok());
536 }
537
538 #[test]
539 fn header_splitting() {
540 let mut s = Sequence::default();
541 s.set_header(b"chr1 human chromosome 1");
542 assert_eq!(s.id, "chr1");
543 assert_eq!(s.description.as_deref(), Some("human chromosome 1"));
544
545 let mut s = Sequence::default();
546 s.set_header(b"lonely");
547 assert_eq!(s.id, "lonely");
548 assert_eq!(s.description, None);
549
550 let mut s = Sequence::default();
551 s.set_header(b"tabbed\tdesc with spaces ");
552 assert_eq!(s.id, "tabbed");
553 assert_eq!(s.description.as_deref(), Some("desc with spaces"));
554 }
555
556 #[test]
557 fn header_splitting_handles_non_ascii() {
558 let mut s = Sequence::default();
561 s.set_header("chr\u{a0}1 description".as_bytes());
562 assert_eq!(s.id, "chr\u{a0}1");
563 assert_eq!(s.description.as_deref(), Some("description"));
564
565 let mut s = Sequence::default();
567 s.set_header(&[b'i', b'd', 0xff, b' ', b'd']);
568 assert!(s.id.starts_with("id"));
569 assert_eq!(s.description.as_deref(), Some("d"));
570
571 let mut s = Sequence::default();
574 s.set_header(b" \t ");
575 assert!(s.id.is_empty());
576 }
577
578 #[test]
579 fn fasta_wrapping() {
580 let s = Sequence::fasta("x", b"AAAAACCCCC".to_vec());
581 let mut out = Vec::new();
582 s.write_fasta(&mut out, Some(5)).unwrap();
583 assert_eq!(out, b">x\nAAAAA\nCCCCC\n");
584
585 let mut out = Vec::new();
586 s.write_fasta(&mut out, None).unwrap();
587 assert_eq!(out, b">x\nAAAAACCCCC\n");
588
589 let mut out = Vec::new();
591 Sequence::fasta("empty", Vec::new())
592 .write_fasta(&mut out, Some(60))
593 .unwrap();
594 assert_eq!(out, b">empty\n\n");
595 }
596
597 #[test]
598 fn fastq_output_and_missing_quality() {
599 let r = Sequence::fastq("r", b"ACGT", b"IIII")
600 .unwrap()
601 .with_description("d");
602 let mut out = Vec::new();
603 r.write_fastq(&mut out).unwrap();
604 assert_eq!(out, b"@r d\nACGT\n+\nIIII\n");
605
606 let mut out = Vec::new();
607 assert!(matches!(
608 Sequence::fasta("r", b"ACGT".to_vec()).write_fastq(&mut out),
609 Err(Error::MissingQuality { .. })
610 ));
611 }
612
613 #[test]
614 fn trimming_keeps_quality_aligned() {
615 let mut r = Sequence::fastq("r", b"AACCGGTT", b"01234567").unwrap();
616 r.trim_to(2..6).unwrap();
617 assert_eq!(r.seq, b"CCGG");
618 assert_eq!(r.quality.as_deref(), Some(&b"2345"[..]));
619 assert!(r.trim_to(0..99).is_err());
620 }
621
622 #[test]
623 fn subseq_bounds() {
624 let r = Sequence::fastq("r", b"AACCGG", b"012345").unwrap();
625 let sub = r.subseq(1..3).unwrap();
626 assert_eq!(sub.seq, b"AC");
627 assert_eq!(sub.quality.as_deref(), Some(&b"12"[..]));
628 #[allow(clippy::reversed_empty_ranges)] {
630 assert!(r.subseq(4..2).is_err());
631 }
632 assert!(r.subseq(0..7).is_err());
633 }
634
635 #[test]
636 fn validation_catches_bad_records() {
637 let mut r = Sequence::fasta("ok", b"ACGT".to_vec());
638 assert!(r.validate(Alphabet::Dna).is_ok());
639 r.id = "has space".into();
640 assert!(r.validate(Alphabet::Dna).is_err());
641 r.id = String::new();
642 assert!(r.validate(Alphabet::Dna).is_err());
643
644 let mut r = Sequence::fastq("r", b"ACGT", b"IIII").unwrap();
645 r.quality = Some(b"II".to_vec());
646 assert!(matches!(
647 r.validate(Alphabet::Dna),
648 Err(Error::LengthMismatch { .. })
649 ));
650 r.quality = Some(b"II\nI".to_vec());
651 assert!(matches!(
652 r.validate(Alphabet::Dna),
653 Err(Error::InvalidByte { .. })
654 ));
655 }
656
657 #[test]
658 fn refuses_to_write_unrepresentable_residues() {
659 let mut out = Vec::new();
663 for bad in [&b"AC\r"[..], b"AC\nGT", b"AC>GT", b">AC"] {
664 let record = Sequence::fasta("x", bad.to_vec());
665 assert!(
666 matches!(
667 record.write_fasta(&mut out, Some(60)),
668 Err(Error::InvalidByte { .. })
669 ),
670 "{:?} should be rejected",
671 String::from_utf8_lossy(bad)
672 );
673 }
674 for good in [&b"ACGTN"[..], b"acgt-n.", b"MEEPQSDPSV*"] {
676 let record = Sequence::fasta("x", good.to_vec());
677 assert!(record.write_fasta(&mut out, Some(60)).is_ok());
678 }
679 }
680
681 #[test]
682 fn refuses_to_write_unrepresentable_fastq() {
683 let mut out = Vec::new();
684
685 let record = Sequence::fastq("x", b"+CGT", b"IIII").unwrap();
687 assert!(matches!(
688 record.write_fastq(&mut out),
689 Err(Error::InvalidByte {
690 pos: 0,
691 byte: b'+',
692 ..
693 })
694 ));
695 assert!(Sequence::fastq("x", b"A+GT", b"IIII")
697 .unwrap()
698 .write_fastq(&mut out)
699 .is_ok());
700
701 let record = Sequence::fastq("x", b"ACGT", b"II\nI").unwrap();
703 assert!(matches!(
704 record.write_fastq(&mut out),
705 Err(Error::InvalidByte { byte: b'\n', .. })
706 ));
707 assert!(Sequence::fastq("x", b"ACGT", b"@@@@")
709 .unwrap()
710 .write_fastq(&mut out)
711 .is_ok());
712 }
713
714 #[test]
715 fn quality_offset_conversion() {
716 let mut r = Sequence::fastq("r", b"ACGT", b"hhhh").unwrap();
717 r.convert_quality_offset(crate::qual::PHRED64, PHRED33);
718 assert_eq!(r.quality.as_deref(), Some(&b"IIII"[..]));
719 }
720
721 #[test]
722 fn clear_keeps_capacity() {
723 let mut r = Sequence::fastq("r", b"ACGT", b"IIII").unwrap();
724 let cap = r.seq.capacity();
725 r.clear();
726 assert!(r.id.is_empty() && r.seq.is_empty());
727 assert_eq!(r.quality.as_deref(), Some(&b""[..]));
728 assert_eq!(r.seq.capacity(), cap);
729 }
730}