1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum Rank {
13 Unclassified,
15 Root,
17 Domain,
19 Kingdom,
21 Phylum,
23 Class,
25 Order,
27 Family,
29 Genus,
31 Species,
33}
34
35impl Rank {
36 #[must_use]
38 pub fn code(self) -> char {
39 match self {
40 Self::Unclassified => 'U',
41 Self::Root => 'R',
42 Self::Domain => 'D',
43 Self::Kingdom => 'K',
44 Self::Phylum => 'P',
45 Self::Class => 'C',
46 Self::Order => 'O',
47 Self::Family => 'F',
48 Self::Genus => 'G',
49 Self::Species => 'S',
50 }
51 }
52}
53
54impl fmt::Display for Rank {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{}", self.code())
57 }
58}
59
60impl FromStr for Rank {
61 type Err = anyhow::Error;
62
63 fn from_str(s: &str) -> Result<Self, Self::Err> {
64 match s {
65 "U" => Ok(Self::Unclassified),
66 "R" => Ok(Self::Root),
67 "D" => Ok(Self::Domain),
68 "K" => Ok(Self::Kingdom),
69 "P" => Ok(Self::Phylum),
70 "C" => Ok(Self::Class),
71 "O" => Ok(Self::Order),
72 "F" => Ok(Self::Family),
73 "G" => Ok(Self::Genus),
74 "S" => Ok(Self::Species),
75 _ => anyhow::bail!("invalid rank code: {s:?}"),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
89pub struct TaxonomicRank {
90 rank: Rank,
91 depth: Option<u32>,
92}
93
94impl TaxonomicRank {
95 #[must_use]
97 pub fn rank(&self) -> Rank {
98 self.rank
99 }
100
101 #[must_use]
103 pub fn depth(&self) -> Option<u32> {
104 self.depth
105 }
106
107 #[must_use]
109 pub fn is_standard(&self) -> bool {
110 self.depth.is_none()
111 }
112}
113
114impl fmt::Display for TaxonomicRank {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 write!(f, "{}", self.rank.code())?;
117 if let Some(d) = self.depth {
118 write!(f, "{d}")?;
119 }
120 Ok(())
121 }
122}
123
124impl FromStr for TaxonomicRank {
125 type Err = anyhow::Error;
126
127 fn from_str(s: &str) -> Result<Self, Self::Err> {
128 anyhow::ensure!(!s.is_empty(), "empty rank string");
129
130 let code = &s[..1];
131 let rank = Rank::from_str(code)?;
132 let suffix = &s[1..];
133
134 if suffix.is_empty() {
135 return Ok(Self { rank, depth: None });
136 }
137
138 let depth: u32 =
139 suffix.parse().map_err(|_| anyhow::anyhow!("invalid rank depth suffix: {s:?}"))?;
140 anyhow::ensure!(depth > 0, "rank depth suffix must be > 0, got: {s:?}");
141 Ok(Self { rank, depth: Some(depth) })
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_rank_round_trip_all_standard() {
151 let cases = [
152 (Rank::Unclassified, "U"),
153 (Rank::Root, "R"),
154 (Rank::Domain, "D"),
155 (Rank::Kingdom, "K"),
156 (Rank::Phylum, "P"),
157 (Rank::Class, "C"),
158 (Rank::Order, "O"),
159 (Rank::Family, "F"),
160 (Rank::Genus, "G"),
161 (Rank::Species, "S"),
162 ];
163
164 for (rank, code) in cases {
165 assert_eq!(rank.to_string(), code, "Display failed for {rank:?}");
166 assert_eq!(Rank::from_str(code).unwrap(), rank, "FromStr failed for {code}");
167 assert_eq!(rank.code(), code.chars().next().unwrap());
168 }
169 }
170
171 #[test]
172 fn test_rank_invalid() {
173 assert!(Rank::from_str("X").is_err());
174 assert!(Rank::from_str("").is_err());
175 assert!(Rank::from_str("GG").is_err());
176 }
177
178 #[test]
179 fn test_taxonomic_rank_standard_round_trip() {
180 for code in ["U", "R", "D", "K", "P", "C", "O", "F", "G", "S"] {
181 let tr = TaxonomicRank::from_str(code).unwrap();
182 assert!(tr.is_standard());
183 assert_eq!(tr.depth(), None);
184 assert_eq!(tr.to_string(), code);
185 }
186 }
187
188 #[test]
189 fn test_taxonomic_rank_non_standard_parse() {
190 let tr = TaxonomicRank::from_str("G2").unwrap();
191 assert_eq!(tr.rank(), Rank::Genus);
192 assert_eq!(tr.depth(), Some(2));
193 assert!(!tr.is_standard());
194 assert_eq!(tr.to_string(), "G2");
195 }
196
197 #[test]
198 fn test_taxonomic_rank_domain_depth() {
199 let tr = TaxonomicRank::from_str("D1").unwrap();
200 assert_eq!(tr.rank(), Rank::Domain);
201 assert_eq!(tr.depth(), Some(1));
202 }
203
204 #[test]
205 fn test_taxonomic_rank_display_non_standard() {
206 let tr = TaxonomicRank::from_str("S3").unwrap();
207 assert_eq!(tr.rank(), Rank::Species);
208 assert_eq!(tr.depth(), Some(3));
209 assert_eq!(tr.to_string(), "S3");
210 }
211
212 #[test]
213 fn test_taxonomic_rank_invalid_code() {
214 assert!(TaxonomicRank::from_str("X").is_err());
215 assert!(TaxonomicRank::from_str("X1").is_err());
216 }
217
218 #[test]
219 fn test_taxonomic_rank_zero_depth() {
220 assert!(TaxonomicRank::from_str("G0").is_err());
221 }
222
223 #[test]
224 fn test_taxonomic_rank_empty() {
225 assert!(TaxonomicRank::from_str("").is_err());
226 }
227
228 #[test]
229 fn test_taxonomic_rank_non_numeric_suffix() {
230 assert!(TaxonomicRank::from_str("Gx").is_err());
231 }
232}