Skip to main content

use_alignment/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn non_empty_text(value: impl AsRef<str>) -> Result<String, AlignmentValueError> {
8    let trimmed = value.as_ref().trim();
9
10    if trimmed.is_empty() {
11        Err(AlignmentValueError::Empty)
12    } else {
13        Ok(value.as_ref().to_string())
14    }
15}
16
17/// Error returned by alignment vocabulary constructors.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum AlignmentValueError {
20    /// The supplied value was empty after trimming surrounding whitespace.
21    Empty,
22}
23
24impl fmt::Display for AlignmentValueError {
25    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::Empty => formatter.write_str("alignment value cannot be empty"),
28        }
29    }
30}
31
32impl Error for AlignmentValueError {}
33
34/// A non-empty alignment identifier.
35#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub struct AlignmentId(String);
37
38impl AlignmentId {
39    /// Creates an alignment identifier from non-empty text.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`AlignmentValueError::Empty`] when the trimmed identifier is empty.
44    pub fn new(value: impl AsRef<str>) -> Result<Self, AlignmentValueError> {
45        non_empty_text(value).map(Self)
46    }
47
48    /// Returns the identifier text.
49    #[must_use]
50    pub fn as_str(&self) -> &str {
51        &self.0
52    }
53}
54
55impl fmt::Display for AlignmentId {
56    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57        formatter.write_str(self.as_str())
58    }
59}
60
61impl FromStr for AlignmentId {
62    type Err = AlignmentValueError;
63
64    fn from_str(value: &str) -> Result<Self, Self::Err> {
65        Self::new(value)
66    }
67}
68
69/// A descriptive alignment kind.
70#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
71pub enum AlignmentKind {
72    /// Pairwise alignment.
73    Pairwise,
74    /// Multiple sequence alignment.
75    Multiple,
76    /// Local alignment.
77    Local,
78    /// Global alignment.
79    Global,
80    /// Semi-global alignment.
81    SemiGlobal,
82    /// Unknown alignment kind.
83    Unknown,
84    /// Domain-specific alignment kind.
85    Custom(String),
86}
87
88impl fmt::Display for AlignmentKind {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            Self::Pairwise => formatter.write_str("pairwise"),
92            Self::Multiple => formatter.write_str("multiple"),
93            Self::Local => formatter.write_str("local"),
94            Self::Global => formatter.write_str("global"),
95            Self::SemiGlobal => formatter.write_str("semi-global"),
96            Self::Unknown => formatter.write_str("unknown"),
97            Self::Custom(kind) => formatter.write_str(kind),
98        }
99    }
100}
101
102impl FromStr for AlignmentKind {
103    type Err = core::convert::Infallible;
104
105    fn from_str(value: &str) -> Result<Self, Self::Err> {
106        let kind = match value.trim().to_ascii_lowercase().as_str() {
107            "pairwise" => Self::Pairwise,
108            "multiple" => Self::Multiple,
109            "local" => Self::Local,
110            "global" => Self::Global,
111            "semi-global" | "semiglobal" | "semi_global" => Self::SemiGlobal,
112            "unknown" | "" => Self::Unknown,
113            _ => Self::Custom(value.to_string()),
114        };
115
116        Ok(kind)
117    }
118}
119
120/// A numeric alignment score.
121#[derive(Clone, Copy, Debug, PartialEq)]
122pub struct AlignmentScore(f64);
123
124impl AlignmentScore {
125    /// Creates an alignment score.
126    #[must_use]
127    pub const fn new(value: f64) -> Self {
128        Self(value)
129    }
130
131    /// Returns the score value.
132    #[must_use]
133    pub const fn value(self) -> f64 {
134        self.0
135    }
136}
137
138/// A non-empty aligned sequence string that may contain gaps.
139#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
140pub struct AlignedSequence(String);
141
142impl AlignedSequence {
143    /// Creates an aligned sequence from non-empty text.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`AlignmentValueError::Empty`] when the trimmed text is empty.
148    pub fn new(value: impl AsRef<str>) -> Result<Self, AlignmentValueError> {
149        non_empty_text(value).map(Self)
150    }
151
152    /// Returns the aligned sequence text.
153    #[must_use]
154    pub fn as_str(&self) -> &str {
155        &self.0
156    }
157
158    /// Returns the number of Unicode scalar values in the aligned text.
159    #[must_use]
160    pub fn aligned_len(&self) -> usize {
161        self.0.chars().count()
162    }
163
164    /// Returns true when the aligned text is empty.
165    #[must_use]
166    pub const fn is_empty(&self) -> bool {
167        self.0.is_empty()
168    }
169}
170
171impl fmt::Display for AlignedSequence {
172    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173        formatter.write_str(self.as_str())
174    }
175}
176
177impl FromStr for AlignedSequence {
178    type Err = AlignmentValueError;
179
180    fn from_str(value: &str) -> Result<Self, Self::Err> {
181        Self::new(value)
182    }
183}
184
185/// Summary metadata for an alignment.
186#[derive(Clone, Debug, PartialEq)]
187pub struct AlignmentSummary {
188    kind: AlignmentKind,
189    score: Option<AlignmentScore>,
190    sequences: Vec<AlignedSequence>,
191}
192
193impl AlignmentSummary {
194    /// Creates an alignment summary with no score or sequences.
195    #[must_use]
196    pub const fn new(kind: AlignmentKind) -> Self {
197        Self {
198            kind,
199            score: None,
200            sequences: Vec::new(),
201        }
202    }
203
204    /// Sets the alignment score.
205    #[must_use]
206    pub const fn with_score(mut self, score: AlignmentScore) -> Self {
207        self.score = Some(score);
208        self
209    }
210
211    /// Adds an aligned sequence to the summary.
212    #[must_use]
213    pub fn with_sequence(mut self, sequence: AlignedSequence) -> Self {
214        self.sequences.push(sequence);
215        self
216    }
217
218    /// Returns the alignment kind.
219    #[must_use]
220    pub const fn kind(&self) -> &AlignmentKind {
221        &self.kind
222    }
223
224    /// Returns the optional alignment score.
225    #[must_use]
226    pub const fn score(&self) -> Option<AlignmentScore> {
227        self.score
228    }
229
230    /// Returns the aligned sequences.
231    #[must_use]
232    pub fn sequences(&self) -> &[AlignedSequence] {
233        &self.sequences
234    }
235
236    /// Returns the number of aligned sequences.
237    #[must_use]
238    pub const fn sequence_count(&self) -> usize {
239        self.sequences.len()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::{
246        AlignedSequence, AlignmentKind, AlignmentScore, AlignmentSummary, AlignmentValueError,
247    };
248    use core::str::FromStr;
249
250    #[test]
251    fn alignment_kind_displays_and_parses() {
252        assert_eq!(AlignmentKind::SemiGlobal.to_string(), "semi-global");
253        assert_eq!(
254            AlignmentKind::from_str("pairwise"),
255            Ok(AlignmentKind::Pairwise)
256        );
257    }
258
259    #[test]
260    fn creates_valid_aligned_sequence() {
261        let sequence = AlignedSequence::new("ACG-T").expect("valid aligned sequence");
262
263        assert_eq!(sequence.as_str(), "ACG-T");
264    }
265
266    #[test]
267    fn aligned_length_helper_counts_symbols() {
268        let sequence = AlignedSequence::new("ACG-T").expect("valid aligned sequence");
269
270        assert_eq!(sequence.aligned_len(), 5);
271    }
272
273    #[test]
274    fn rejects_empty_aligned_sequence() {
275        assert_eq!(AlignedSequence::new(" "), Err(AlignmentValueError::Empty));
276    }
277
278    #[test]
279    fn constructs_alignment_score() {
280        let score = AlignmentScore::new(42.5);
281
282        assert!((score.value() - 42.5).abs() < f64::EPSILON);
283    }
284
285    #[test]
286    fn supports_custom_alignment_kind() {
287        assert_eq!(
288            AlignmentKind::from_str("chain"),
289            Ok(AlignmentKind::Custom("chain".into()))
290        );
291    }
292
293    #[test]
294    fn alignment_summary_stores_metadata_only() {
295        let summary = AlignmentSummary::new(AlignmentKind::Pairwise)
296            .with_score(AlignmentScore::new(1.0))
297            .with_sequence(AlignedSequence::new("A-C").expect("valid aligned sequence"));
298
299        assert_eq!(summary.kind(), &AlignmentKind::Pairwise);
300        assert_eq!(summary.score(), Some(AlignmentScore::new(1.0)));
301        assert_eq!(summary.sequence_count(), 1);
302    }
303}