#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResidueError {
EmptySymbol,
MultipleSymbols,
}
impl fmt::Display for ResidueError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptySymbol => formatter.write_str("residue symbol cannot be empty"),
Self::MultipleSymbols => formatter.write_str("residue symbol must be one character"),
}
}
}
impl Error for ResidueError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ResidueKind {
Nucleotide,
AminoAcid,
Gap,
Ambiguous,
Unknown,
Custom(String),
}
impl fmt::Display for ResidueKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Nucleotide => formatter.write_str("nucleotide"),
Self::AminoAcid => formatter.write_str("amino-acid"),
Self::Gap => formatter.write_str("gap"),
Self::Ambiguous => formatter.write_str("ambiguous"),
Self::Unknown => formatter.write_str("unknown"),
Self::Custom(kind) => formatter.write_str(kind),
}
}
}
impl FromStr for ResidueKind {
type Err = core::convert::Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let kind = match value.trim().to_ascii_lowercase().as_str() {
"nucleotide" => Self::Nucleotide,
"amino-acid" | "amino_acid" | "amino acid" => Self::AminoAcid,
"gap" => Self::Gap,
"ambiguous" => Self::Ambiguous,
"unknown" | "" => Self::Unknown,
_ => Self::Custom(value.to_string()),
};
Ok(kind)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ResidueSymbol(char);
impl ResidueSymbol {
#[must_use]
pub const fn from_char(value: char) -> Self {
Self(value)
}
pub fn new(value: impl AsRef<str>) -> Result<Self, ResidueError> {
let mut chars = value.as_ref().chars();
let Some(symbol) = chars.next() else {
return Err(ResidueError::EmptySymbol);
};
if chars.next().is_some() {
Err(ResidueError::MultipleSymbols)
} else {
Ok(Self(symbol))
}
}
#[must_use]
pub const fn as_char(self) -> char {
self.0
}
}
impl fmt::Display for ResidueSymbol {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0)
}
}
impl FromStr for ResidueSymbol {
type Err = ResidueError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Residue {
symbol: ResidueSymbol,
kind: ResidueKind,
}
impl Residue {
#[must_use]
pub const fn new(symbol: char, kind: ResidueKind) -> Self {
Self {
symbol: ResidueSymbol::from_char(symbol),
kind,
}
}
#[must_use]
pub const fn gap() -> Self {
Self::new('-', ResidueKind::Gap)
}
#[must_use]
pub const fn ambiguous(symbol: char) -> Self {
Self::new(symbol, ResidueKind::Ambiguous)
}
#[must_use]
pub const fn symbol(&self) -> ResidueSymbol {
self.symbol
}
#[must_use]
pub const fn kind(&self) -> &ResidueKind {
&self.kind
}
}
#[cfg(test)]
mod tests {
use super::{Residue, ResidueError, ResidueKind, ResidueSymbol};
use core::str::FromStr;
#[test]
fn creates_valid_residue_symbol() {
let symbol = ResidueSymbol::new("A").expect("valid symbol");
assert_eq!(symbol.as_char(), 'A');
assert_eq!(symbol.to_string(), "A");
}
#[test]
fn rejects_empty_or_multiple_symbol_text() {
assert_eq!(ResidueSymbol::new(""), Err(ResidueError::EmptySymbol));
assert_eq!(ResidueSymbol::new("AC"), Err(ResidueError::MultipleSymbols));
}
#[test]
fn creates_gap_residue() {
let residue = Residue::gap();
assert_eq!(residue.symbol().as_char(), '-');
assert_eq!(residue.kind(), &ResidueKind::Gap);
}
#[test]
fn creates_ambiguous_residue() {
let residue = Residue::ambiguous('N');
assert_eq!(residue.symbol().as_char(), 'N');
assert_eq!(residue.kind(), &ResidueKind::Ambiguous);
}
#[test]
fn residue_kind_displays_and_parses() {
assert_eq!(ResidueKind::AminoAcid.to_string(), "amino-acid");
assert_eq!(ResidueKind::from_str("gap"), Ok(ResidueKind::Gap));
}
#[test]
fn supports_custom_residue_kind() {
assert_eq!(
ResidueKind::from_str("modified"),
Ok(ResidueKind::Custom("modified".into()))
);
}
}