1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum ResidueError {
10 EmptySymbol,
12 MultipleSymbols,
14}
15
16impl fmt::Display for ResidueError {
17 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::EmptySymbol => formatter.write_str("residue symbol cannot be empty"),
20 Self::MultipleSymbols => formatter.write_str("residue symbol must be one character"),
21 }
22 }
23}
24
25impl Error for ResidueError {}
26
27#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub enum ResidueKind {
30 Nucleotide,
32 AminoAcid,
34 Gap,
36 Ambiguous,
38 Unknown,
40 Custom(String),
42}
43
44impl fmt::Display for ResidueKind {
45 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Self::Nucleotide => formatter.write_str("nucleotide"),
48 Self::AminoAcid => formatter.write_str("amino-acid"),
49 Self::Gap => formatter.write_str("gap"),
50 Self::Ambiguous => formatter.write_str("ambiguous"),
51 Self::Unknown => formatter.write_str("unknown"),
52 Self::Custom(kind) => formatter.write_str(kind),
53 }
54 }
55}
56
57impl FromStr for ResidueKind {
58 type Err = core::convert::Infallible;
59
60 fn from_str(value: &str) -> Result<Self, Self::Err> {
61 let kind = match value.trim().to_ascii_lowercase().as_str() {
62 "nucleotide" => Self::Nucleotide,
63 "amino-acid" | "amino_acid" | "amino acid" => Self::AminoAcid,
64 "gap" => Self::Gap,
65 "ambiguous" => Self::Ambiguous,
66 "unknown" | "" => Self::Unknown,
67 _ => Self::Custom(value.to_string()),
68 };
69
70 Ok(kind)
71 }
72}
73
74#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
76pub struct ResidueSymbol(char);
77
78impl ResidueSymbol {
79 #[must_use]
81 pub const fn from_char(value: char) -> Self {
82 Self(value)
83 }
84
85 pub fn new(value: impl AsRef<str>) -> Result<Self, ResidueError> {
92 let mut chars = value.as_ref().chars();
93 let Some(symbol) = chars.next() else {
94 return Err(ResidueError::EmptySymbol);
95 };
96
97 if chars.next().is_some() {
98 Err(ResidueError::MultipleSymbols)
99 } else {
100 Ok(Self(symbol))
101 }
102 }
103
104 #[must_use]
106 pub const fn as_char(self) -> char {
107 self.0
108 }
109}
110
111impl fmt::Display for ResidueSymbol {
112 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113 write!(formatter, "{}", self.0)
114 }
115}
116
117impl FromStr for ResidueSymbol {
118 type Err = ResidueError;
119
120 fn from_str(value: &str) -> Result<Self, Self::Err> {
121 Self::new(value)
122 }
123}
124
125#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct Residue {
128 symbol: ResidueSymbol,
129 kind: ResidueKind,
130}
131
132impl Residue {
133 #[must_use]
135 pub const fn new(symbol: char, kind: ResidueKind) -> Self {
136 Self {
137 symbol: ResidueSymbol::from_char(symbol),
138 kind,
139 }
140 }
141
142 #[must_use]
144 pub const fn gap() -> Self {
145 Self::new('-', ResidueKind::Gap)
146 }
147
148 #[must_use]
150 pub const fn ambiguous(symbol: char) -> Self {
151 Self::new(symbol, ResidueKind::Ambiguous)
152 }
153
154 #[must_use]
156 pub const fn symbol(&self) -> ResidueSymbol {
157 self.symbol
158 }
159
160 #[must_use]
162 pub const fn kind(&self) -> &ResidueKind {
163 &self.kind
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::{Residue, ResidueError, ResidueKind, ResidueSymbol};
170 use core::str::FromStr;
171
172 #[test]
173 fn creates_valid_residue_symbol() {
174 let symbol = ResidueSymbol::new("A").expect("valid symbol");
175
176 assert_eq!(symbol.as_char(), 'A');
177 assert_eq!(symbol.to_string(), "A");
178 }
179
180 #[test]
181 fn rejects_empty_or_multiple_symbol_text() {
182 assert_eq!(ResidueSymbol::new(""), Err(ResidueError::EmptySymbol));
183 assert_eq!(ResidueSymbol::new("AC"), Err(ResidueError::MultipleSymbols));
184 }
185
186 #[test]
187 fn creates_gap_residue() {
188 let residue = Residue::gap();
189
190 assert_eq!(residue.symbol().as_char(), '-');
191 assert_eq!(residue.kind(), &ResidueKind::Gap);
192 }
193
194 #[test]
195 fn creates_ambiguous_residue() {
196 let residue = Residue::ambiguous('N');
197
198 assert_eq!(residue.symbol().as_char(), 'N');
199 assert_eq!(residue.kind(), &ResidueKind::Ambiguous);
200 }
201
202 #[test]
203 fn residue_kind_displays_and_parses() {
204 assert_eq!(ResidueKind::AminoAcid.to_string(), "amino-acid");
205 assert_eq!(ResidueKind::from_str("gap"), Ok(ResidueKind::Gap));
206 }
207
208 #[test]
209 fn supports_custom_residue_kind() {
210 assert_eq!(
211 ResidueKind::from_str("modified"),
212 Ok(ResidueKind::Custom("modified".into()))
213 );
214 }
215}