Skip to main content

use_species/
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, SpeciesNameError> {
8    let trimmed = value.as_ref().trim();
9
10    if trimmed.is_empty() {
11        Err(SpeciesNameError::Empty)
12    } else {
13        Ok(trimmed.to_string())
14    }
15}
16
17/// Error returned when species name components are empty.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum SpeciesNameError {
20    /// The supplied value was empty after trimming surrounding whitespace.
21    Empty,
22}
23
24impl fmt::Display for SpeciesNameError {
25    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::Empty => formatter.write_str("species name component cannot be empty"),
28        }
29    }
30}
31
32impl Error for SpeciesNameError {}
33
34/// A non-empty genus name.
35#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub struct GenusName(String);
37
38impl GenusName {
39    /// Creates a genus name from non-empty text.
40    ///
41    /// Surrounding whitespace is trimmed; the remaining text and casing are preserved.
42    ///
43    /// # Errors
44    ///
45    /// Returns [`SpeciesNameError::Empty`] when the trimmed name is empty.
46    pub fn new(value: impl AsRef<str>) -> Result<Self, SpeciesNameError> {
47        non_empty_text(value).map(Self)
48    }
49
50    /// Returns the genus text.
51    #[must_use]
52    pub fn as_str(&self) -> &str {
53        &self.0
54    }
55
56    /// Consumes the name and returns the owned string.
57    #[must_use]
58    pub fn into_string(self) -> String {
59        self.0
60    }
61}
62
63impl AsRef<str> for GenusName {
64    fn as_ref(&self) -> &str {
65        self.as_str()
66    }
67}
68
69impl fmt::Display for GenusName {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        formatter.write_str(self.as_str())
72    }
73}
74
75impl FromStr for GenusName {
76    type Err = SpeciesNameError;
77
78    fn from_str(value: &str) -> Result<Self, Self::Err> {
79        Self::new(value)
80    }
81}
82
83/// A non-empty specific epithet.
84#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
85pub struct SpecificEpithet(String);
86
87impl SpecificEpithet {
88    /// Creates a specific epithet from non-empty text.
89    ///
90    /// # Errors
91    ///
92    /// Returns [`SpeciesNameError::Empty`] when the trimmed epithet is empty.
93    pub fn new(value: impl AsRef<str>) -> Result<Self, SpeciesNameError> {
94        non_empty_text(value).map(Self)
95    }
96
97    /// Returns the epithet text.
98    #[must_use]
99    pub fn as_str(&self) -> &str {
100        &self.0
101    }
102
103    /// Consumes the epithet and returns the owned string.
104    #[must_use]
105    pub fn into_string(self) -> String {
106        self.0
107    }
108}
109
110impl AsRef<str> for SpecificEpithet {
111    fn as_ref(&self) -> &str {
112        self.as_str()
113    }
114}
115
116impl fmt::Display for SpecificEpithet {
117    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118        formatter.write_str(self.as_str())
119    }
120}
121
122impl FromStr for SpecificEpithet {
123    type Err = SpeciesNameError;
124
125    fn from_str(value: &str) -> Result<Self, Self::Err> {
126        Self::new(value)
127    }
128}
129
130/// A non-empty subspecific epithet.
131#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub struct SubspecificEpithet(String);
133
134impl SubspecificEpithet {
135    /// Creates a subspecific epithet from non-empty text.
136    ///
137    /// # Errors
138    ///
139    /// Returns [`SpeciesNameError::Empty`] when the trimmed epithet is empty.
140    pub fn new(value: impl AsRef<str>) -> Result<Self, SpeciesNameError> {
141        non_empty_text(value).map(Self)
142    }
143
144    /// Returns the epithet text.
145    #[must_use]
146    pub fn as_str(&self) -> &str {
147        &self.0
148    }
149
150    /// Consumes the epithet and returns the owned string.
151    #[must_use]
152    pub fn into_string(self) -> String {
153        self.0
154    }
155}
156
157impl AsRef<str> for SubspecificEpithet {
158    fn as_ref(&self) -> &str {
159        self.as_str()
160    }
161}
162
163impl fmt::Display for SubspecificEpithet {
164    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
165        formatter.write_str(self.as_str())
166    }
167}
168
169impl FromStr for SubspecificEpithet {
170    type Err = SpeciesNameError;
171
172    fn from_str(value: &str) -> Result<Self, Self::Err> {
173        Self::new(value)
174    }
175}
176
177/// A descriptive binomial species name.
178#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
179pub struct BinomialName {
180    genus: GenusName,
181    specific_epithet: SpecificEpithet,
182}
183
184impl BinomialName {
185    /// Creates a binomial name from validated components.
186    #[must_use]
187    pub const fn new(genus: GenusName, specific_epithet: SpecificEpithet) -> Self {
188        Self {
189            genus,
190            specific_epithet,
191        }
192    }
193
194    /// Returns the genus component.
195    #[must_use]
196    pub const fn genus(&self) -> &GenusName {
197        &self.genus
198    }
199
200    /// Returns the specific epithet component.
201    #[must_use]
202    pub const fn specific_epithet(&self) -> &SpecificEpithet {
203        &self.specific_epithet
204    }
205}
206
207impl fmt::Display for BinomialName {
208    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
209        write!(formatter, "{} {}", self.genus, self.specific_epithet)
210    }
211}
212
213/// A descriptive trinomial species name.
214#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
215pub struct TrinomialName {
216    binomial: BinomialName,
217    subspecific_epithet: SubspecificEpithet,
218}
219
220impl TrinomialName {
221    /// Creates a trinomial name from validated components.
222    #[must_use]
223    pub const fn new(binomial: BinomialName, subspecific_epithet: SubspecificEpithet) -> Self {
224        Self {
225            binomial,
226            subspecific_epithet,
227        }
228    }
229
230    /// Returns the binomial portion.
231    #[must_use]
232    pub const fn binomial(&self) -> &BinomialName {
233        &self.binomial
234    }
235
236    /// Returns the subspecific epithet.
237    #[must_use]
238    pub const fn subspecific_epithet(&self) -> &SubspecificEpithet {
239        &self.subspecific_epithet
240    }
241}
242
243impl fmt::Display for TrinomialName {
244    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
245        write!(formatter, "{} {}", self.binomial, self.subspecific_epithet)
246    }
247}
248
249/// A species name represented as binomial, trinomial, or caller-defined descriptive text.
250#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
251pub enum SpeciesName {
252    /// A binomial species name.
253    Binomial(BinomialName),
254    /// A trinomial species name.
255    Trinomial(TrinomialName),
256    /// Caller-defined descriptive species text.
257    Custom(String),
258}
259
260impl SpeciesName {
261    /// Creates a custom descriptive species name from non-empty text.
262    ///
263    /// # Errors
264    ///
265    /// Returns [`SpeciesNameError::Empty`] when the trimmed text is empty.
266    pub fn custom(value: impl AsRef<str>) -> Result<Self, SpeciesNameError> {
267        non_empty_text(value).map(Self::Custom)
268    }
269}
270
271impl From<BinomialName> for SpeciesName {
272    fn from(name: BinomialName) -> Self {
273        Self::Binomial(name)
274    }
275}
276
277impl From<TrinomialName> for SpeciesName {
278    fn from(name: TrinomialName) -> Self {
279        Self::Trinomial(name)
280    }
281}
282
283impl fmt::Display for SpeciesName {
284    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
285        match self {
286            Self::Binomial(name) => name.fmt(formatter),
287            Self::Trinomial(name) => name.fmt(formatter),
288            Self::Custom(value) => formatter.write_str(value),
289        }
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::{
296        BinomialName, GenusName, SpeciesName, SpeciesNameError, SpecificEpithet,
297        SubspecificEpithet, TrinomialName,
298    };
299
300    #[test]
301    fn constructs_valid_binomial_name() -> Result<(), SpeciesNameError> {
302        let name = BinomialName::new(GenusName::new("Homo")?, SpecificEpithet::new("sapiens")?);
303
304        assert_eq!(name.genus().as_str(), "Homo");
305        assert_eq!(name.specific_epithet().as_str(), "sapiens");
306        assert_eq!(name.to_string(), "Homo sapiens");
307        Ok(())
308    }
309
310    #[test]
311    fn constructs_valid_trinomial_name() -> Result<(), SpeciesNameError> {
312        let binomial = BinomialName::new(GenusName::new("Canis")?, SpecificEpithet::new("lupus")?);
313        let trinomial = TrinomialName::new(binomial, SubspecificEpithet::new("familiaris")?);
314
315        assert_eq!(trinomial.to_string(), "Canis lupus familiaris");
316        Ok(())
317    }
318
319    #[test]
320    fn rejects_empty_genus() {
321        assert_eq!(GenusName::new("  "), Err(SpeciesNameError::Empty));
322    }
323
324    #[test]
325    fn rejects_empty_specific_epithet() {
326        assert_eq!(SpecificEpithet::new(""), Err(SpeciesNameError::Empty));
327    }
328
329    #[test]
330    fn displays_species_names() -> Result<(), SpeciesNameError> {
331        let binomial = BinomialName::new(
332            GenusName::new("Escherichia")?,
333            SpecificEpithet::new("coli")?,
334        );
335        let species = SpeciesName::from(binomial);
336
337        assert_eq!(species.to_string(), "Escherichia coli");
338        Ok(())
339    }
340
341    #[test]
342    fn constructs_custom_species_name() -> Result<(), SpeciesNameError> {
343        let species = SpeciesName::custom("unresolved species descriptor")?;
344
345        assert_eq!(species.to_string(), "unresolved species descriptor");
346        assert_eq!(SpeciesName::custom("   "), Err(SpeciesNameError::Empty));
347        Ok(())
348    }
349}