use crate::{
alignment::{Alignment, AlignmentStates, MaybeAligned, NextCiglet},
data::{
cigar::LenInAlignment,
err::ResultWithErrorContext,
types::cigar::{Cigar, CigarView, CigarViewMut},
},
iter_utils::ProcessResultsExt,
math::AnyInt,
prelude::*,
};
use std::{
fmt::{Display, Formatter},
hash::Hash,
};
mod reader;
mod sort_traits;
mod std_traits;
mod view_traits;
pub use reader::*;
pub use sort_traits::SamDataSort;
#[derive(Clone, Debug)]
pub struct SamData {
pub qname: String,
pub flag: u16,
pub rname: String,
pub pos: usize,
pub mapq: u8,
pub cigar: Cigar,
rnext: char,
pnext: u32,
tlen: i32,
pub seq: Nucleotides,
pub qual: QualityScores,
pub opt_fields: SamOptRaw,
}
impl PartialEq for SamData {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.qname == other.qname
&& self.flag == other.flag
&& self.rname == other.rname
&& self.pos == other.pos
&& self.mapq == other.mapq
&& self.cigar == other.cigar
&& self.rnext == other.rnext
&& self.pnext == other.pnext
&& self.tlen == other.tlen
&& self.seq == other.seq
&& self.qual == other.qual
}
}
impl Eq for SamData {}
impl Hash for SamData {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.qname.hash(state);
self.flag.hash(state);
self.rname.hash(state);
self.pos.hash(state);
self.mapq.hash(state);
self.cigar.hash(state);
self.rnext.hash(state);
self.pnext.hash(state);
self.tlen.hash(state);
self.seq.hash(state);
self.qual.hash(state);
}
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct SamDataView<'a> {
pub qname: &'a str,
pub flag: u16,
pub rname: &'a str,
pub pos: usize,
pub mapq: u8,
pub cigar: CigarView<'a>,
rnext: char,
pnext: u32,
tlen: i32,
pub seq: NucleotidesView<'a>,
pub qual: QualityScoresView<'a>,
}
#[derive(Eq, PartialEq, Hash, Debug)]
pub struct SamDataViewMut<'a> {
pub qname: &'a mut String,
pub flag: u16,
pub rname: &'a mut String,
pub pos: usize,
pub mapq: u8,
pub cigar: CigarViewMut<'a>,
rnext: char,
pnext: u32,
tlen: i32,
pub seq: NucleotidesViewMut<'a>,
pub qual: QualityScoresViewMut<'a>,
}
impl SamData {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
qname: String, flag: u16, rname: String, pos: usize, mapq: u8, cigar: Cigar, seq: Nucleotides, qual: QualityScores,
) -> Self {
SamData {
qname,
flag,
rname,
pos,
mapq,
cigar,
rnext: '*',
pnext: 0,
tlen: 0,
seq,
qual,
opt_fields: SamOptRaw::new(),
}
}
#[inline]
#[must_use]
pub fn unmapped(qname: &str, rname: &str) -> Self {
let seq = Nucleotides::from(b"*");
let qual = unsafe { QualityScores::from_vec_unchecked(b"*".to_vec()) };
Self::new(qname.to_string(), 4, rname.to_string(), 0, 255, Cigar::new(), seq, qual)
}
#[inline]
#[must_use]
pub fn from_alignment<T: AnyInt + Into<i64>>(
alignment: &Alignment<T>, qname: String, flag: u16, rname: String, mapq: u8, seq: Nucleotides, qual: QualityScores,
) -> Self {
let pos = alignment.ref_range.start + 1;
let cigar = alignment.states.to_cigar_unchecked();
let opt_fields = SamOptRaw::new_with_score(alignment.score);
SamData {
qname,
flag,
rname,
pos,
mapq,
cigar,
rnext: '*',
pnext: 0,
tlen: 0,
seq,
qual,
opt_fields,
}
}
#[inline]
pub fn to_alignment<T: AnyInt>(&self, score: T, ref_len: usize) -> std::io::Result<MaybeAligned<Alignment<T>>> {
if self.is_unmapped() {
return Ok(MaybeAligned::Unmapped);
}
if is_missing_sam_field(&self.seq) {
return Err(std::io::Error::other(
"The seq field in the SAM data record was not populated.",
));
}
let query_len = self.seq.len();
let ref_range_start = self.pos - 1;
let ref_range_end = ref_range_start + self.cigar.ref_len_in_alignment();
let ref_range = ref_range_start..ref_range_end;
let mut ciglets = self.cigar.iter();
ciglets.next_ciglet_if_op(|op| op == b'H');
let soft_clipping_front = ciglets.next_ciglet_if_op(|op| op == b'S').map_or(0, |c| c.inc);
ciglets.next_ciglet_back_if_op(|op| op == b'H');
let soft_clipping_back = ciglets.next_ciglet_back_if_op(|op| op == b'S').map_or(0, |c| c.inc);
let soft_clipping = soft_clipping_front + soft_clipping_back;
let query_range_start = soft_clipping_front;
let query_range_end = query_range_start + (query_len - soft_clipping);
let query_range = query_range_start..query_range_end;
let states = AlignmentStates::try_from(&self.cigar).map_err(std::io::Error::other)?;
Ok(MaybeAligned::Some(Alignment {
score,
ref_range,
query_range,
states,
ref_len,
query_len,
}))
}
#[inline]
#[must_use]
pub fn is_unmapped(&self) -> bool {
self.flag & 0x4 != 0 || self.cigar.ref_len_in_alignment() == 0
}
}
impl<'a> SamDataView<'a> {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn new(
qname: &'a str, flag: u16, rname: &'a str, pos: usize, mapq: u8, cigar: CigarView<'a>, seq: NucleotidesView<'a>,
qual: QualityScoresView<'a>,
) -> Self {
SamDataView {
qname,
flag,
rname,
pos,
mapq,
cigar,
rnext: '*',
pnext: 0,
tlen: 0,
seq,
qual,
}
}
#[inline]
#[must_use]
pub fn unmapped(qname: &'a str, rname: &'a str) -> Self {
let seq = NucleotidesView::from(b"*");
let qual = unsafe { QualityScoresView::from_bytes_unchecked(b"*") };
Self::new(qname, 4, rname, 0, 255, CigarView::new(), seq, qual)
}
}
impl<'a> SamDataViewMut<'a> {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn new(
qname: &'a mut String, flag: u16, rname: &'a mut String, pos: usize, mapq: u8, cigar: CigarViewMut<'a>,
seq: NucleotidesViewMut<'a>, qual: QualityScoresViewMut<'a>,
) -> Self {
SamDataViewMut {
qname,
flag,
rname,
pos,
mapq,
cigar,
rnext: '*',
pnext: 0,
tlen: 0,
seq,
qual,
}
}
}
pub(crate) fn is_missing_sam_field(field: impl AsRef<[u8]>) -> bool {
let field = field.as_ref();
field.is_empty() || field == b"*"
}
#[derive(Clone, Debug, Default)]
pub struct SamOptRaw(Vec<String>);
impl SamOptRaw {
#[inline]
#[must_use]
pub fn new() -> Self {
SamOptRaw(Vec::new())
}
#[inline]
#[must_use]
pub fn new_with_score<T: AnyInt + Into<i64>>(score: T) -> Self {
let mut inner = Vec::with_capacity(1);
inner.push(format!("AS:i:{score}", score = score.into()));
SamOptRaw(inner)
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = std::io::Result<SamOptField>> {
self.0.iter().map(|field| {
let inv_opt_err_msg = || std::io::Error::other(format!("Invalid optional field {field}"));
let (tag_text, rest) = field.split_once(':').ok_or_else(inv_opt_err_msg)?;
let (type_text, string_value) = rest.split_once(':').ok_or_else(inv_opt_err_msg)?;
let tag = SamOptField::parse_tag(tag_text)?;
let type_code = SamOptField::parse_type(type_text)?;
let opt_field = SamOptField::parse_value(tag, type_code, string_value)
.with_context(format!("Failed to parse field '{field}'"))?;
Ok(opt_field)
})
}
pub fn get(&self, tag: &str) -> std::io::Result<Option<SamOptField>> {
for field in &self.0 {
let inv_opt_err_msg = || std::io::Error::other(format!("Invalid optional field {field}"));
let (this_tag, rest) = field.split_once(':').ok_or_else(inv_opt_err_msg)?;
if this_tag == tag {
let (type_text, string_value) = rest.split_once(':').ok_or_else(inv_opt_err_msg)?;
let tag = SamOptField::parse_tag(this_tag)?;
let type_code = SamOptField::parse_type(type_text)?;
let opt_field = match SamOptField::parse_value(tag, type_code, string_value) {
Ok(opt_field) => opt_field,
Err(e) => {
return Err(std::io::Error::other(format!(
"Failed to parse field '{field}' due to error: {e}"
)));
}
};
return Ok(Some(opt_field));
}
}
Ok(None)
}
#[inline]
pub fn push(&mut self, tag: &str, data: &SamOptValue) {
self.0.push(format!("{tag}:{data}"));
}
}
impl FromIterator<String> for SamOptRaw {
#[inline]
fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
SamOptRaw(Vec::from_iter(iter))
}
}
#[derive(Clone, Debug)]
pub struct SamOptField {
pub tag: [u8; 2],
pub value: SamOptValue,
}
impl SamOptField {
fn parse_tag(tag: &str) -> std::io::Result<[u8; 2]> {
let bytes = tag.as_bytes();
if bytes.len() != 2 || !bytes[0].is_ascii_alphabetic() || !bytes[1].is_ascii_alphanumeric() {
return Err(std::io::Error::other(format!("Invalid SAM optional tag: {tag}")));
}
Ok([bytes[0], bytes[1]])
}
fn parse_type(type_text: &str) -> std::io::Result<char> {
let mut type_chars = type_text.chars();
let Some(typ) = type_chars.next() else {
return Err(std::io::Error::other("Missing optional field type"));
};
if type_chars.next().is_some() {
return Err(std::io::Error::other(format!("Invalid optional field type {type_text}")));
}
Ok(typ)
}
fn parse_value(tag: [u8; 2], type_code: char, string_value: &str) -> std::io::Result<SamOptField> {
match type_code {
'A' => {
let mut chars = string_value.chars();
let Some(c) = chars.next() else {
return Err(std::io::Error::other("'A' field has empty value"));
};
if chars.next().is_some() {
return Err(std::io::Error::other("'A' field must contain exactly one character"));
}
if !c.is_ascii() {
return Err(std::io::Error::other("'A' field must be ASCII"));
}
Ok(SamOptField {
tag,
value: SamOptValue::Char(c as u8),
})
}
'i' => {
let parsed = string_value.parse::<i64>().with_context("Error parsing 'i' field")?;
Ok(SamOptField {
tag,
value: SamOptValue::Int(parsed),
})
}
'f' => {
let parsed = string_value.parse::<f32>().with_context("Error parsing 'f' field")?;
Ok(SamOptField {
tag,
value: SamOptValue::Float(parsed),
})
}
'Z' => Ok(SamOptField {
tag,
value: SamOptValue::String(String::from(string_value)),
}),
'H' => {
if !string_value.len().is_multiple_of(2) {
return Err(std::io::Error::other(format!(
"'H' field must contain an even number digits. Found {}",
string_value.len()
)));
}
if !string_value.as_bytes().iter().all(u8::is_ascii_hexdigit) {
return Err(std::io::Error::other("'H' field must contain hexadecimal digits"));
}
Ok(SamOptField {
tag,
value: SamOptValue::Hex(string_value.to_ascii_uppercase()),
})
}
'B' => Ok(SamOptField {
tag,
value: SamOptValue::parse_opt_array(string_value).with_context("Failed to parse 'B' array")?,
}),
_ => Err(std::io::Error::other(format!(
"Unsupported SAM optional field type {type_code}"
))),
}
}
#[inline]
#[must_use]
pub fn char(self) -> Option<u8> {
match self.value {
SamOptValue::Char(c) => Some(c),
_ => None,
}
}
#[inline]
#[must_use]
pub fn int(self) -> Option<i64> {
match self.value {
SamOptValue::Int(i) => Some(i),
_ => None,
}
}
#[inline]
#[must_use]
pub fn float(self) -> Option<f32> {
match self.value {
SamOptValue::Float(f) => Some(f),
_ => None,
}
}
#[inline]
#[must_use]
pub fn string(self) -> Option<String> {
match self.value {
SamOptValue::String(f) => Some(f),
_ => None,
}
}
#[inline]
#[must_use]
pub fn hex(self) -> Option<String> {
match self.value {
SamOptValue::Hex(f) => Some(f),
_ => None,
}
}
#[inline]
#[must_use]
pub fn array(self) -> Option<OptArray> {
match self.value {
SamOptValue::Array(f) => Some(f),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub enum SamOptValue {
Char(u8),
Int(i64),
Float(f32),
String(String),
Hex(String),
Array(OptArray),
}
impl SamOptValue {
fn parse_opt_array(string_value: &str) -> std::io::Result<Self> {
let mut pieces = string_value.split(',');
let Some(subtype) = pieces.next() else {
return Err(std::io::Error::other("Missing subtype"));
};
match subtype {
"c" => {
let values = pieces
.map(str::parse::<i8>)
.process_results(|iter| iter.collect())
.with_context("Error parsing 'c' subtype (`i8`)")?;
Ok(Self::Array(OptArray::I8(values)))
}
"C" => {
let values = pieces
.map(str::parse::<u8>)
.process_results(|iter| iter.collect())
.with_context("Error parsing 'C' subtype (`u8`)")?;
Ok(Self::Array(OptArray::U8(values)))
}
"s" => {
let values = pieces
.map(str::parse::<i16>)
.process_results(|iter| iter.collect())
.with_context("Error parsing 's' subtype (`i16`)")?;
Ok(Self::Array(OptArray::I16(values)))
}
"S" => {
let values = pieces
.map(str::parse::<u16>)
.process_results(|iter| iter.collect())
.with_context("Error parsing 'S' subtype (`u16`)")?;
Ok(Self::Array(OptArray::U16(values)))
}
"i" => {
let values = pieces
.map(str::parse::<i32>)
.process_results(|iter| iter.collect())
.with_context("Error parsing 'i' subtype (`i32`)")?;
Ok(Self::Array(OptArray::I32(values)))
}
"I" => {
let values = pieces
.map(str::parse::<u32>)
.process_results(|iter| iter.collect())
.with_context("Error parsing 'I' subtype (`u32`)")?;
Ok(Self::Array(OptArray::U32(values)))
}
"f" => {
let values = pieces
.map(str::parse::<f32>)
.process_results(|iter| iter.collect())
.with_context("Error parsing 'f' subtype (`f32`)")?;
Ok(Self::Array(OptArray::F32(values)))
}
_ => Err(std::io::Error::other(format!("Unsupported subtype {subtype}"))),
}
}
}
impl Display for SamOptValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SamOptValue::Char(val) => write!(f, "A:{val}", val = *val as char),
SamOptValue::Int(val) => write!(f, "i:{val}"),
SamOptValue::Float(val) => write!(f, "f:{val}"),
SamOptValue::String(val) => write!(f, "Z:{val}"),
SamOptValue::Hex(val) => write!(f, "H:{val}"),
SamOptValue::Array(val) => match val {
OptArray::I8(vec) => OptArray::fmt_opt_array(f, 'c', vec),
OptArray::U8(vec) => OptArray::fmt_opt_array(f, 'C', vec),
OptArray::I16(vec) => OptArray::fmt_opt_array(f, 's', vec),
OptArray::U16(vec) => OptArray::fmt_opt_array(f, 'S', vec),
OptArray::I32(vec) => OptArray::fmt_opt_array(f, 'i', vec),
OptArray::U32(vec) => OptArray::fmt_opt_array(f, 'I', vec),
OptArray::F32(vec) => OptArray::fmt_opt_array(f, 'f', vec),
},
}
}
}
#[derive(Debug, Clone)]
pub enum OptArray {
I8(Vec<i8>),
U8(Vec<u8>),
I16(Vec<i16>),
U16(Vec<u16>),
I32(Vec<i32>),
U32(Vec<u32>),
F32(Vec<f32>),
}
impl OptArray {
fn fmt_opt_array<T: Display>(f: &mut Formatter<'_>, arr_type: char, vals: &[T]) -> std::fmt::Result {
write!(f, "B:{arr_type}")?;
let mut iter = vals.iter();
if let Some(first) = iter.next() {
write!(f, "{first}")?;
}
for v in iter {
write!(f, ",{v}")?;
}
Ok(())
}
}