#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
fn non_empty_text(value: impl AsRef<str>) -> Result<String, AlignmentValueError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(AlignmentValueError::Empty)
} else {
Ok(value.as_ref().to_string())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AlignmentValueError {
Empty,
}
impl fmt::Display for AlignmentValueError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("alignment value cannot be empty"),
}
}
}
impl Error for AlignmentValueError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct AlignmentId(String);
impl AlignmentId {
pub fn new(value: impl AsRef<str>) -> Result<Self, AlignmentValueError> {
non_empty_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for AlignmentId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for AlignmentId {
type Err = AlignmentValueError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum AlignmentKind {
Pairwise,
Multiple,
Local,
Global,
SemiGlobal,
Unknown,
Custom(String),
}
impl fmt::Display for AlignmentKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Pairwise => formatter.write_str("pairwise"),
Self::Multiple => formatter.write_str("multiple"),
Self::Local => formatter.write_str("local"),
Self::Global => formatter.write_str("global"),
Self::SemiGlobal => formatter.write_str("semi-global"),
Self::Unknown => formatter.write_str("unknown"),
Self::Custom(kind) => formatter.write_str(kind),
}
}
}
impl FromStr for AlignmentKind {
type Err = core::convert::Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let kind = match value.trim().to_ascii_lowercase().as_str() {
"pairwise" => Self::Pairwise,
"multiple" => Self::Multiple,
"local" => Self::Local,
"global" => Self::Global,
"semi-global" | "semiglobal" | "semi_global" => Self::SemiGlobal,
"unknown" | "" => Self::Unknown,
_ => Self::Custom(value.to_string()),
};
Ok(kind)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AlignmentScore(f64);
impl AlignmentScore {
#[must_use]
pub const fn new(value: f64) -> Self {
Self(value)
}
#[must_use]
pub const fn value(self) -> f64 {
self.0
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct AlignedSequence(String);
impl AlignedSequence {
pub fn new(value: impl AsRef<str>) -> Result<Self, AlignmentValueError> {
non_empty_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn aligned_len(&self) -> usize {
self.0.chars().count()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Display for AlignedSequence {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for AlignedSequence {
type Err = AlignmentValueError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AlignmentSummary {
kind: AlignmentKind,
score: Option<AlignmentScore>,
sequences: Vec<AlignedSequence>,
}
impl AlignmentSummary {
#[must_use]
pub const fn new(kind: AlignmentKind) -> Self {
Self {
kind,
score: None,
sequences: Vec::new(),
}
}
#[must_use]
pub const fn with_score(mut self, score: AlignmentScore) -> Self {
self.score = Some(score);
self
}
#[must_use]
pub fn with_sequence(mut self, sequence: AlignedSequence) -> Self {
self.sequences.push(sequence);
self
}
#[must_use]
pub const fn kind(&self) -> &AlignmentKind {
&self.kind
}
#[must_use]
pub const fn score(&self) -> Option<AlignmentScore> {
self.score
}
#[must_use]
pub fn sequences(&self) -> &[AlignedSequence] {
&self.sequences
}
#[must_use]
pub const fn sequence_count(&self) -> usize {
self.sequences.len()
}
}
#[cfg(test)]
mod tests {
use super::{
AlignedSequence, AlignmentKind, AlignmentScore, AlignmentSummary, AlignmentValueError,
};
use core::str::FromStr;
#[test]
fn alignment_kind_displays_and_parses() {
assert_eq!(AlignmentKind::SemiGlobal.to_string(), "semi-global");
assert_eq!(
AlignmentKind::from_str("pairwise"),
Ok(AlignmentKind::Pairwise)
);
}
#[test]
fn creates_valid_aligned_sequence() {
let sequence = AlignedSequence::new("ACG-T").expect("valid aligned sequence");
assert_eq!(sequence.as_str(), "ACG-T");
}
#[test]
fn aligned_length_helper_counts_symbols() {
let sequence = AlignedSequence::new("ACG-T").expect("valid aligned sequence");
assert_eq!(sequence.aligned_len(), 5);
}
#[test]
fn rejects_empty_aligned_sequence() {
assert_eq!(AlignedSequence::new(" "), Err(AlignmentValueError::Empty));
}
#[test]
fn constructs_alignment_score() {
let score = AlignmentScore::new(42.5);
assert!((score.value() - 42.5).abs() < f64::EPSILON);
}
#[test]
fn supports_custom_alignment_kind() {
assert_eq!(
AlignmentKind::from_str("chain"),
Ok(AlignmentKind::Custom("chain".into()))
);
}
#[test]
fn alignment_summary_stores_metadata_only() {
let summary = AlignmentSummary::new(AlignmentKind::Pairwise)
.with_score(AlignmentScore::new(1.0))
.with_sequence(AlignedSequence::new("A-C").expect("valid aligned sequence"));
assert_eq!(summary.kind(), &AlignmentKind::Pairwise);
assert_eq!(summary.score(), Some(AlignmentScore::new(1.0)));
assert_eq!(summary.sequence_count(), 1);
}
}