use crate::Error;
use rand::Rng;
use rand::distr::{Distribution, StandardUniform};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ReadState {
#[default]
#[serde(rename = "primary_forward")]
PrimaryFwd,
#[serde(rename = "primary_reverse")]
PrimaryRev,
#[serde(rename = "secondary_forward")]
SecondaryFwd,
#[serde(rename = "secondary_reverse")]
SecondaryRev,
#[serde(rename = "supplementary_forward")]
SupplementaryFwd,
#[serde(rename = "supplementary_reverse")]
SupplementaryRev,
#[serde(rename = "unmapped")]
Unmapped,
}
impl Distribution<ReadState> for StandardUniform {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> ReadState {
match rng.random_range(0..7) {
0 => ReadState::PrimaryFwd,
1 => ReadState::PrimaryRev,
2 => ReadState::SecondaryFwd,
3 => ReadState::SecondaryRev,
4 => ReadState::SupplementaryFwd,
5 => ReadState::SupplementaryRev,
6 => ReadState::Unmapped,
_ => unreachable!(),
}
}
}
impl From<ReadState> for u16 {
fn from(value: ReadState) -> u16 {
match value {
ReadState::PrimaryFwd => 0,
ReadState::Unmapped => 4,
ReadState::PrimaryRev => 16,
ReadState::SecondaryFwd => 256,
ReadState::SecondaryRev => 272,
ReadState::SupplementaryFwd => 2048,
ReadState::SupplementaryRev => 2064,
}
}
}
impl TryFrom<u16> for ReadState {
type Error = Error;
fn try_from(value: u16) -> Result<ReadState, Error> {
match value {
0 => Ok(ReadState::PrimaryFwd),
4 => Ok(ReadState::Unmapped),
16 => Ok(ReadState::PrimaryRev),
256 => Ok(ReadState::SecondaryFwd),
272 => Ok(ReadState::SecondaryRev),
2048 => Ok(ReadState::SupplementaryFwd),
2064 => Ok(ReadState::SupplementaryRev),
v => Err(Error::UnknownAlignState(format!(
"BAM flag {v} cannot be converted to our `ReadState` variants"
))),
}
}
}
impl FromStr for ReadState {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"primary_forward" => Ok(ReadState::PrimaryFwd),
"primary_reverse" => Ok(ReadState::PrimaryRev),
"secondary_forward" => Ok(ReadState::SecondaryFwd),
"secondary_reverse" => Ok(ReadState::SecondaryRev),
"supplementary_forward" => Ok(ReadState::SupplementaryFwd),
"supplementary_reverse" => Ok(ReadState::SupplementaryRev),
"unmapped" => Ok(ReadState::Unmapped),
v => Err(Error::UnknownAlignState(format!(
"{v} cannot be converted to `ReadState` variant"
))),
}
}
}
impl fmt::Display for ReadState {
#[expect(
clippy::pattern_type_mismatch,
reason = "simple function, notation cleaner without *"
)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ReadState::PrimaryFwd => "primary_forward",
ReadState::SecondaryFwd => "secondary_forward",
ReadState::SupplementaryFwd => "supplementary_forward",
ReadState::PrimaryRev => "primary_reverse",
ReadState::SecondaryRev => "secondary_reverse",
ReadState::SupplementaryRev => "supplementary_reverse",
ReadState::Unmapped => "unmapped",
}
.fmt(f)
}
}
impl ReadState {
#[expect(
clippy::pattern_type_mismatch,
reason = "simple function, notation cleaner without *"
)]
#[must_use]
pub fn is_unmapped(&self) -> bool {
match self {
ReadState::Unmapped => true,
ReadState::PrimaryFwd
| ReadState::PrimaryRev
| ReadState::SecondaryFwd
| ReadState::SecondaryRev
| ReadState::SupplementaryFwd
| ReadState::SupplementaryRev => false,
}
}
#[expect(
clippy::pattern_type_mismatch,
reason = "simple function, notation cleaner without *"
)]
#[must_use]
pub fn strand(&self) -> char {
match self {
ReadState::Unmapped => '.',
ReadState::PrimaryFwd | ReadState::SecondaryFwd | ReadState::SupplementaryFwd => '+',
ReadState::PrimaryRev | ReadState::SecondaryRev | ReadState::SupplementaryRev => '-',
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn readstate_u16_conversion_roundtrip() {
let states = vec![
ReadState::PrimaryFwd,
ReadState::PrimaryRev,
ReadState::SecondaryFwd,
ReadState::SecondaryRev,
ReadState::SupplementaryFwd,
ReadState::SupplementaryRev,
ReadState::Unmapped,
];
for state in states {
let flag: u16 = state.into();
let recovered_state: ReadState =
flag.try_into().expect("conversion from u16 should work");
assert_eq!(state, recovered_state);
}
}
#[test]
fn readstate_specific_flag_values() {
assert_eq!(u16::from(ReadState::PrimaryFwd), 0);
assert_eq!(u16::from(ReadState::Unmapped), 4);
assert_eq!(u16::from(ReadState::PrimaryRev), 16);
assert_eq!(u16::from(ReadState::SecondaryFwd), 256);
assert_eq!(u16::from(ReadState::SecondaryRev), 272);
assert_eq!(u16::from(ReadState::SupplementaryFwd), 2048);
assert_eq!(u16::from(ReadState::SupplementaryRev), 2064);
}
#[test]
fn readstate_invalid_u16_values() {
let invalid_flags = vec![1, 2, 8, 32, 64, 128, 512, 1024, 4096, 8192];
for flag in invalid_flags {
assert!(matches!(
ReadState::try_from(flag),
Err(Error::UnknownAlignState(_))
));
}
}
#[test]
fn readstate_string_consistency() {
let states = vec![
ReadState::PrimaryFwd,
ReadState::PrimaryRev,
ReadState::SecondaryFwd,
ReadState::SecondaryRev,
ReadState::SupplementaryFwd,
ReadState::SupplementaryRev,
ReadState::Unmapped,
];
for state in states {
let string_repr = format!("{state}");
let parsed_state = ReadState::from_str(&string_repr).expect("should parse");
assert_eq!(state, parsed_state);
}
}
#[test]
#[should_panic(expected = "UnknownAlignState")]
fn readstate_from_str_invalid_state() {
let _result: ReadState = ReadState::from_str("invalid_state").unwrap();
}
#[test]
#[should_panic(expected = "UnknownAlignState")]
fn readstate_from_str_empty_string() {
let _result: ReadState = ReadState::from_str("").unwrap();
}
#[test]
#[should_panic(expected = "UnknownAlignState")]
fn readstate_from_str_incomplete_string() {
let _result: ReadState = ReadState::from_str("primary").unwrap();
}
#[test]
fn readstate_random_generation_all_variants() {
let mut rng = rand::rng();
let mut generated_states = std::collections::HashSet::new();
for _ in 0..1000 {
let state: ReadState = rng.random();
let _: bool = generated_states.insert(state);
}
assert_eq!(generated_states.len(), 7);
assert!(generated_states.contains(&ReadState::PrimaryFwd));
assert!(generated_states.contains(&ReadState::PrimaryRev));
assert!(generated_states.contains(&ReadState::SecondaryFwd));
assert!(generated_states.contains(&ReadState::SecondaryRev));
assert!(generated_states.contains(&ReadState::SupplementaryFwd));
assert!(generated_states.contains(&ReadState::SupplementaryRev));
assert!(generated_states.contains(&ReadState::Unmapped));
}
}