1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn normalized_token(value: &str) -> String {
8 value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
9}
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum BiodiversityValueError {
13 Negative,
14 NonFinite,
15}
16
17impl fmt::Display for BiodiversityValueError {
18 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 Self::Negative => formatter.write_str("biodiversity value cannot be negative"),
21 Self::NonFinite => formatter.write_str("biodiversity value must be finite"),
22 }
23 }
24}
25
26impl Error for BiodiversityValueError {}
27
28#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub struct SpeciesRichness(u64);
30
31impl SpeciesRichness {
32 pub const fn new(value: i64) -> Result<Self, BiodiversityValueError> {
35 if value < 0 {
36 Err(BiodiversityValueError::Negative)
37 } else {
38 Ok(Self(value.cast_unsigned()))
39 }
40 }
41
42 #[must_use]
43 pub const fn get(self) -> u64 {
44 self.0
45 }
46}
47
48impl fmt::Display for SpeciesRichness {
49 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50 self.0.fmt(formatter)
51 }
52}
53
54#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
55pub enum DiversityIndexKind {
56 SpeciesRichness,
57 Shannon,
58 Simpson,
59 Evenness,
60 BetaDiversity,
61 PhylogeneticDiversity,
62 Unknown,
63 Custom(String),
64}
65
66impl fmt::Display for DiversityIndexKind {
67 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68 formatter.write_str(match self {
69 Self::SpeciesRichness => "species-richness",
70 Self::Shannon => "shannon",
71 Self::Simpson => "simpson",
72 Self::Evenness => "evenness",
73 Self::BetaDiversity => "beta-diversity",
74 Self::PhylogeneticDiversity => "phylogenetic-diversity",
75 Self::Unknown => "unknown",
76 Self::Custom(value) => value.as_str(),
77 })
78 }
79}
80
81impl FromStr for DiversityIndexKind {
82 type Err = DiversityIndexKindParseError;
83
84 fn from_str(value: &str) -> Result<Self, Self::Err> {
85 let trimmed = value.trim();
86
87 if trimmed.is_empty() {
88 return Err(DiversityIndexKindParseError::Empty);
89 }
90
91 Ok(match normalized_token(trimmed).as_str() {
92 "species-richness" => Self::SpeciesRichness,
93 "shannon" => Self::Shannon,
94 "simpson" => Self::Simpson,
95 "evenness" => Self::Evenness,
96 "beta-diversity" => Self::BetaDiversity,
97 "phylogenetic-diversity" => Self::PhylogeneticDiversity,
98 "unknown" => Self::Unknown,
99 _ => Self::Custom(trimmed.to_string()),
100 })
101 }
102}
103
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub enum DiversityIndexKindParseError {
106 Empty,
107}
108
109impl fmt::Display for DiversityIndexKindParseError {
110 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 Self::Empty => formatter.write_str("diversity index kind cannot be empty"),
113 }
114 }
115}
116
117impl Error for DiversityIndexKindParseError {}
118
119#[derive(Clone, Debug, PartialEq, PartialOrd)]
120pub struct DiversityIndex {
121 kind: DiversityIndexKind,
122 value: f64,
123}
124
125impl DiversityIndex {
126 pub fn new(kind: DiversityIndexKind, value: f64) -> Result<Self, BiodiversityValueError> {
129 if !value.is_finite() {
130 return Err(BiodiversityValueError::NonFinite);
131 }
132
133 Ok(Self { kind, value })
134 }
135
136 #[must_use]
137 pub const fn kind(&self) -> &DiversityIndexKind {
138 &self.kind
139 }
140
141 #[must_use]
142 pub const fn value(&self) -> f64 {
143 self.value
144 }
145}
146
147impl fmt::Display for DiversityIndex {
148 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
149 write!(formatter, "{}: {}", self.kind, self.value)
150 }
151}
152
153#[derive(Clone, Debug, PartialEq)]
154pub enum BiodiversityMeasure {
155 SpeciesRichness(SpeciesRichness),
156 DiversityIndex(DiversityIndex),
157}
158
159impl fmt::Display for BiodiversityMeasure {
160 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161 match self {
162 Self::SpeciesRichness(value) => write!(formatter, "species-richness: {value}"),
163 Self::DiversityIndex(value) => value.fmt(formatter),
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::{
171 BiodiversityMeasure, BiodiversityValueError, DiversityIndex, DiversityIndexKind,
172 SpeciesRichness,
173 };
174
175 #[test]
176 fn valid_species_richness() -> Result<(), BiodiversityValueError> {
177 let richness = SpeciesRichness::new(12)?;
178
179 assert_eq!(richness.get(), 12);
180 Ok(())
181 }
182
183 #[test]
184 fn negative_species_richness_rejected() {
185 assert_eq!(
186 SpeciesRichness::new(-1),
187 Err(BiodiversityValueError::Negative)
188 );
189 }
190
191 #[test]
192 fn diversity_index_kind_display_parse() {
193 assert_eq!(
194 "shannon".parse::<DiversityIndexKind>(),
195 Ok(DiversityIndexKind::Shannon)
196 );
197 assert_eq!(
198 DiversityIndexKind::BetaDiversity.to_string(),
199 "beta-diversity"
200 );
201 }
202
203 #[test]
204 fn custom_diversity_index_kind() {
205 assert_eq!(
206 "functional-diversity".parse::<DiversityIndexKind>(),
207 Ok(DiversityIndexKind::Custom(
208 "functional-diversity".to_string()
209 ))
210 );
211 }
212
213 #[test]
214 fn diversity_index_construction() -> Result<(), BiodiversityValueError> {
215 let index = DiversityIndex::new(DiversityIndexKind::Shannon, 2.3)?;
216
217 assert_eq!(index.kind(), &DiversityIndexKind::Shannon);
218 assert!((index.value() - 2.3).abs() < f64::EPSILON);
219 assert_eq!(
220 BiodiversityMeasure::DiversityIndex(index).to_string(),
221 "shannon: 2.3"
222 );
223 Ok(())
224 }
225}