Skip to main content

use_biodiversity/
lib.rs

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    /// # Errors
33    /// Returns `BiodiversityValueError::Negative` when `value` is less than zero.
34    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    /// # Errors
127    /// Returns `BiodiversityValueError::NonFinite` when `value` is not finite.
128    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}