Skip to main content

use_taxonomy/
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, TaxonomyNameError> {
8    let trimmed = value.as_ref().trim();
9
10    if trimmed.is_empty() {
11        Err(TaxonomyNameError::Empty)
12    } else {
13        Ok(trimmed.to_string())
14    }
15}
16
17fn normalized_key(value: &str) -> String {
18    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
19}
20
21/// Error returned when taxonomy names are empty.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum TaxonomyNameError {
24    /// The supplied name was empty after trimming surrounding whitespace.
25    Empty,
26}
27
28impl fmt::Display for TaxonomyNameError {
29    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::Empty => formatter.write_str("taxonomy name cannot be empty"),
32        }
33    }
34}
35
36impl Error for TaxonomyNameError {}
37
38/// A non-empty taxon name.
39#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct TaxonName(String);
41
42impl TaxonName {
43    /// Creates a taxon name from non-empty text.
44    ///
45    /// Surrounding whitespace is trimmed; the remaining text and casing are preserved.
46    ///
47    /// # Errors
48    ///
49    /// Returns [`TaxonomyNameError::Empty`] when the trimmed name is empty.
50    pub fn new(value: impl AsRef<str>) -> Result<Self, TaxonomyNameError> {
51        non_empty_text(value).map(Self)
52    }
53
54    /// Returns the taxon name text.
55    #[must_use]
56    pub fn as_str(&self) -> &str {
57        &self.0
58    }
59
60    /// Consumes the name and returns the owned string.
61    #[must_use]
62    pub fn into_string(self) -> String {
63        self.0
64    }
65}
66
67impl AsRef<str> for TaxonName {
68    fn as_ref(&self) -> &str {
69        self.as_str()
70    }
71}
72
73impl fmt::Display for TaxonName {
74    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75        formatter.write_str(self.as_str())
76    }
77}
78
79impl FromStr for TaxonName {
80    type Err = TaxonomyNameError;
81
82    fn from_str(value: &str) -> Result<Self, Self::Err> {
83        Self::new(value)
84    }
85}
86
87/// A non-empty scientific name.
88#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
89pub struct ScientificName(String);
90
91impl ScientificName {
92    /// Creates a scientific name from non-empty text.
93    ///
94    /// Surrounding whitespace is trimmed; the remaining text and casing are preserved.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`TaxonomyNameError::Empty`] when the trimmed name is empty.
99    pub fn new(value: impl AsRef<str>) -> Result<Self, TaxonomyNameError> {
100        non_empty_text(value).map(Self)
101    }
102
103    /// Returns the scientific name text.
104    #[must_use]
105    pub fn as_str(&self) -> &str {
106        &self.0
107    }
108
109    /// Consumes the name and returns the owned string.
110    #[must_use]
111    pub fn into_string(self) -> String {
112        self.0
113    }
114}
115
116impl AsRef<str> for ScientificName {
117    fn as_ref(&self) -> &str {
118        self.as_str()
119    }
120}
121
122impl fmt::Display for ScientificName {
123    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
124        formatter.write_str(self.as_str())
125    }
126}
127
128impl FromStr for ScientificName {
129    type Err = TaxonomyNameError;
130
131    fn from_str(value: &str) -> Result<Self, Self::Err> {
132        Self::new(value)
133    }
134}
135
136/// A non-empty common name.
137#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
138pub struct CommonName(String);
139
140impl CommonName {
141    /// Creates a common name from non-empty text.
142    ///
143    /// Surrounding whitespace is trimmed; the remaining text and casing are preserved.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`TaxonomyNameError::Empty`] when the trimmed name is empty.
148    pub fn new(value: impl AsRef<str>) -> Result<Self, TaxonomyNameError> {
149        non_empty_text(value).map(Self)
150    }
151
152    /// Returns the common name text.
153    #[must_use]
154    pub fn as_str(&self) -> &str {
155        &self.0
156    }
157
158    /// Consumes the name and returns the owned string.
159    #[must_use]
160    pub fn into_string(self) -> String {
161        self.0
162    }
163}
164
165impl AsRef<str> for CommonName {
166    fn as_ref(&self) -> &str {
167        self.as_str()
168    }
169}
170
171impl fmt::Display for CommonName {
172    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173        formatter.write_str(self.as_str())
174    }
175}
176
177impl FromStr for CommonName {
178    type Err = TaxonomyNameError;
179
180    fn from_str(value: &str) -> Result<Self, Self::Err> {
181        Self::new(value)
182    }
183}
184
185/// Broad biological rank vocabulary.
186#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
187pub enum TaxonomicRank {
188    /// Domain rank.
189    Domain,
190    /// Kingdom rank.
191    Kingdom,
192    /// Phylum rank.
193    Phylum,
194    /// Class rank.
195    Class,
196    /// Order rank.
197    Order,
198    /// Family rank.
199    Family,
200    /// Genus rank.
201    Genus,
202    /// Species rank.
203    Species,
204    /// Subspecies rank.
205    Subspecies,
206    /// Variety rank.
207    Variety,
208    /// Strain rank.
209    Strain,
210    /// Clade label.
211    Clade,
212    /// Explicitly unranked classification.
213    Unranked,
214    /// Unknown rank.
215    Unknown,
216    /// Caller-defined rank text.
217    Custom(String),
218}
219
220impl fmt::Display for TaxonomicRank {
221    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
222        match self {
223            Self::Domain => formatter.write_str("domain"),
224            Self::Kingdom => formatter.write_str("kingdom"),
225            Self::Phylum => formatter.write_str("phylum"),
226            Self::Class => formatter.write_str("class"),
227            Self::Order => formatter.write_str("order"),
228            Self::Family => formatter.write_str("family"),
229            Self::Genus => formatter.write_str("genus"),
230            Self::Species => formatter.write_str("species"),
231            Self::Subspecies => formatter.write_str("subspecies"),
232            Self::Variety => formatter.write_str("variety"),
233            Self::Strain => formatter.write_str("strain"),
234            Self::Clade => formatter.write_str("clade"),
235            Self::Unranked => formatter.write_str("unranked"),
236            Self::Unknown => formatter.write_str("unknown"),
237            Self::Custom(value) => formatter.write_str(value),
238        }
239    }
240}
241
242impl FromStr for TaxonomicRank {
243    type Err = TaxonomicRankParseError;
244
245    fn from_str(value: &str) -> Result<Self, Self::Err> {
246        let trimmed = value.trim();
247
248        if trimmed.is_empty() {
249            return Err(TaxonomicRankParseError::Empty);
250        }
251
252        match normalized_key(trimmed).as_str() {
253            "domain" => Ok(Self::Domain),
254            "kingdom" => Ok(Self::Kingdom),
255            "phylum" | "division" => Ok(Self::Phylum),
256            "class" => Ok(Self::Class),
257            "order" => Ok(Self::Order),
258            "family" => Ok(Self::Family),
259            "genus" => Ok(Self::Genus),
260            "species" => Ok(Self::Species),
261            "subspecies" | "sub-species" => Ok(Self::Subspecies),
262            "variety" => Ok(Self::Variety),
263            "strain" => Ok(Self::Strain),
264            "clade" => Ok(Self::Clade),
265            "unranked" | "un-ranked" => Ok(Self::Unranked),
266            "unknown" => Ok(Self::Unknown),
267            _ => Ok(Self::Custom(trimmed.to_string())),
268        }
269    }
270}
271
272/// Error returned when parsing a taxonomic rank fails.
273#[derive(Clone, Copy, Debug, Eq, PartialEq)]
274pub enum TaxonomicRankParseError {
275    /// The rank text was empty after trimming surrounding whitespace.
276    Empty,
277}
278
279impl fmt::Display for TaxonomicRankParseError {
280    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
281        match self {
282            Self::Empty => formatter.write_str("taxonomic rank cannot be empty"),
283        }
284    }
285}
286
287impl Error for TaxonomicRankParseError {}
288
289/// A taxon represented by rank and validated name.
290#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
291pub struct Taxon {
292    rank: TaxonomicRank,
293    name: TaxonName,
294}
295
296impl Taxon {
297    /// Creates a taxon from rank and name.
298    #[must_use]
299    pub const fn new(rank: TaxonomicRank, name: TaxonName) -> Self {
300        Self { rank, name }
301    }
302
303    /// Returns the rank.
304    #[must_use]
305    pub const fn rank(&self) -> &TaxonomicRank {
306        &self.rank
307    }
308
309    /// Returns the name.
310    #[must_use]
311    pub const fn name(&self) -> &TaxonName {
312        &self.name
313    }
314}
315
316impl fmt::Display for Taxon {
317    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
318        write!(formatter, "{}: {}", self.rank, self.name)
319    }
320}
321
322/// An ordered taxonomic lineage.
323#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
324pub struct TaxonomicLineage {
325    taxa: Vec<Taxon>,
326}
327
328impl TaxonomicLineage {
329    /// Creates a lineage from caller-supplied taxon order.
330    #[must_use]
331    pub const fn new(taxa: Vec<Taxon>) -> Self {
332        Self { taxa }
333    }
334
335    /// Returns taxa in lineage order.
336    #[must_use]
337    pub fn taxa(&self) -> &[Taxon] {
338        &self.taxa
339    }
340
341    /// Returns the number of taxa.
342    #[must_use]
343    pub const fn len(&self) -> usize {
344        self.taxa.len()
345    }
346
347    /// Returns `true` when the lineage has no taxa.
348    #[must_use]
349    pub const fn is_empty(&self) -> bool {
350        self.taxa.is_empty()
351    }
352}
353
354impl From<Vec<Taxon>> for TaxonomicLineage {
355    fn from(taxa: Vec<Taxon>) -> Self {
356        Self::new(taxa)
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::{
363        CommonName, ScientificName, Taxon, TaxonName, TaxonomicLineage, TaxonomicRank,
364        TaxonomicRankParseError, TaxonomyNameError,
365    };
366
367    #[test]
368    fn constructs_valid_taxon_name() -> Result<(), TaxonomyNameError> {
369        let name = TaxonName::new("  Animalia  ")?;
370
371        assert_eq!(name.as_str(), "Animalia");
372        assert_eq!(name.to_string(), "Animalia");
373        Ok(())
374    }
375
376    #[test]
377    fn rejects_empty_taxon_name() {
378        assert_eq!(TaxonName::new("   "), Err(TaxonomyNameError::Empty));
379    }
380
381    #[test]
382    fn displays_and_parses_ranks() -> Result<(), TaxonomicRankParseError> {
383        assert_eq!(TaxonomicRank::Species.to_string(), "species");
384        assert_eq!("kingdom".parse::<TaxonomicRank>()?, TaxonomicRank::Kingdom);
385        assert_eq!(
386            "sub species".parse::<TaxonomicRank>()?,
387            TaxonomicRank::Subspecies
388        );
389        Ok(())
390    }
391
392    #[test]
393    fn parses_custom_rank() -> Result<(), TaxonomicRankParseError> {
394        assert_eq!(
395            "section".parse::<TaxonomicRank>()?,
396            TaxonomicRank::Custom("section".to_string())
397        );
398        assert_eq!(
399            "  ".parse::<TaxonomicRank>(),
400            Err(TaxonomicRankParseError::Empty)
401        );
402        Ok(())
403    }
404
405    #[test]
406    fn lineage_preserves_order() -> Result<(), TaxonomyNameError> {
407        let lineage = TaxonomicLineage::new(vec![
408            Taxon::new(TaxonomicRank::Kingdom, TaxonName::new("Animalia")?),
409            Taxon::new(TaxonomicRank::Phylum, TaxonName::new("Chordata")?),
410            Taxon::new(TaxonomicRank::Genus, TaxonName::new("Homo")?),
411        ]);
412
413        assert_eq!(lineage.len(), 3);
414        assert_eq!(lineage.taxa()[0].name().as_str(), "Animalia");
415        assert_eq!(lineage.taxa()[2].rank(), &TaxonomicRank::Genus);
416        assert!(!lineage.is_empty());
417        Ok(())
418    }
419
420    #[test]
421    fn constructs_scientific_and_common_names() -> Result<(), TaxonomyNameError> {
422        let scientific = ScientificName::new("Homo sapiens")?;
423        let common = CommonName::new("human")?;
424
425        assert_eq!(scientific.to_string(), "Homo sapiens");
426        assert_eq!(common.to_string(), "human");
427        Ok(())
428    }
429}