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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum SpeciesNameError {
20 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub struct GenusName(String);
37
38impl GenusName {
39 pub fn new(value: impl AsRef<str>) -> Result<Self, SpeciesNameError> {
47 non_empty_text(value).map(Self)
48 }
49
50 #[must_use]
52 pub fn as_str(&self) -> &str {
53 &self.0
54 }
55
56 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
85pub struct SpecificEpithet(String);
86
87impl SpecificEpithet {
88 pub fn new(value: impl AsRef<str>) -> Result<Self, SpeciesNameError> {
94 non_empty_text(value).map(Self)
95 }
96
97 #[must_use]
99 pub fn as_str(&self) -> &str {
100 &self.0
101 }
102
103 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub struct SubspecificEpithet(String);
133
134impl SubspecificEpithet {
135 pub fn new(value: impl AsRef<str>) -> Result<Self, SpeciesNameError> {
141 non_empty_text(value).map(Self)
142 }
143
144 #[must_use]
146 pub fn as_str(&self) -> &str {
147 &self.0
148 }
149
150 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
179pub struct BinomialName {
180 genus: GenusName,
181 specific_epithet: SpecificEpithet,
182}
183
184impl BinomialName {
185 #[must_use]
187 pub const fn new(genus: GenusName, specific_epithet: SpecificEpithet) -> Self {
188 Self {
189 genus,
190 specific_epithet,
191 }
192 }
193
194 #[must_use]
196 pub const fn genus(&self) -> &GenusName {
197 &self.genus
198 }
199
200 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
215pub struct TrinomialName {
216 binomial: BinomialName,
217 subspecific_epithet: SubspecificEpithet,
218}
219
220impl TrinomialName {
221 #[must_use]
223 pub const fn new(binomial: BinomialName, subspecific_epithet: SubspecificEpithet) -> Self {
224 Self {
225 binomial,
226 subspecific_epithet,
227 }
228 }
229
230 #[must_use]
232 pub const fn binomial(&self) -> &BinomialName {
233 &self.binomial
234 }
235
236 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
251pub enum SpeciesName {
252 Binomial(BinomialName),
254 Trinomial(TrinomialName),
256 Custom(String),
258}
259
260impl SpeciesName {
261 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}