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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum TaxonomyNameError {
24 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct TaxonName(String);
41
42impl TaxonName {
43 pub fn new(value: impl AsRef<str>) -> Result<Self, TaxonomyNameError> {
51 non_empty_text(value).map(Self)
52 }
53
54 #[must_use]
56 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59
60 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
89pub struct ScientificName(String);
90
91impl ScientificName {
92 pub fn new(value: impl AsRef<str>) -> Result<Self, TaxonomyNameError> {
100 non_empty_text(value).map(Self)
101 }
102
103 #[must_use]
105 pub fn as_str(&self) -> &str {
106 &self.0
107 }
108
109 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
138pub struct CommonName(String);
139
140impl CommonName {
141 pub fn new(value: impl AsRef<str>) -> Result<Self, TaxonomyNameError> {
149 non_empty_text(value).map(Self)
150 }
151
152 #[must_use]
154 pub fn as_str(&self) -> &str {
155 &self.0
156 }
157
158 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
187pub enum TaxonomicRank {
188 Domain,
190 Kingdom,
192 Phylum,
194 Class,
196 Order,
198 Family,
200 Genus,
202 Species,
204 Subspecies,
206 Variety,
208 Strain,
210 Clade,
212 Unranked,
214 Unknown,
216 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
274pub enum TaxonomicRankParseError {
275 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
291pub struct Taxon {
292 rank: TaxonomicRank,
293 name: TaxonName,
294}
295
296impl Taxon {
297 #[must_use]
299 pub const fn new(rank: TaxonomicRank, name: TaxonName) -> Self {
300 Self { rank, name }
301 }
302
303 #[must_use]
305 pub const fn rank(&self) -> &TaxonomicRank {
306 &self.rank
307 }
308
309 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
324pub struct TaxonomicLineage {
325 taxa: Vec<Taxon>,
326}
327
328impl TaxonomicLineage {
329 #[must_use]
331 pub const fn new(taxa: Vec<Taxon>) -> Self {
332 Self { taxa }
333 }
334
335 #[must_use]
337 pub fn taxa(&self) -> &[Taxon] {
338 &self.taxa
339 }
340
341 #[must_use]
343 pub const fn len(&self) -> usize {
344 self.taxa.len()
345 }
346
347 #[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}