Skip to main content

use_niche/
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
11fn non_empty_text(value: impl AsRef<str>) -> Result<String, NicheTextError> {
12    let trimmed = value.as_ref().trim();
13
14    if trimmed.is_empty() {
15        Err(NicheTextError::Empty)
16    } else {
17        Ok(trimmed.to_string())
18    }
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum NicheTextError {
23    Empty,
24}
25
26impl fmt::Display for NicheTextError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("niche text cannot be empty"),
30        }
31    }
32}
33
34impl Error for NicheTextError {}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum NicheValueError {
38    Negative,
39    NonFinite,
40}
41
42impl fmt::Display for NicheValueError {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Negative => formatter.write_str("niche value cannot be negative"),
46            Self::NonFinite => formatter.write_str("niche value must be finite"),
47        }
48    }
49}
50
51impl Error for NicheValueError {}
52
53#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub struct NicheName(String);
55
56impl NicheName {
57    /// # Errors
58    /// Returns `NicheTextError::Empty` when `value` is blank.
59    pub fn new(value: impl AsRef<str>) -> Result<Self, NicheTextError> {
60        non_empty_text(value).map(Self)
61    }
62
63    #[must_use]
64    pub fn as_str(&self) -> &str {
65        &self.0
66    }
67}
68
69impl fmt::Display for NicheName {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        formatter.write_str(self.as_str())
72    }
73}
74
75impl FromStr for NicheName {
76    type Err = NicheTextError;
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)]
84pub struct ResourceUse(String);
85
86impl ResourceUse {
87    /// # Errors
88    /// Returns `NicheTextError::Empty` when `value` is blank.
89    pub fn new(value: impl AsRef<str>) -> Result<Self, NicheTextError> {
90        non_empty_text(value).map(Self)
91    }
92
93    #[must_use]
94    pub fn as_str(&self) -> &str {
95        &self.0
96    }
97}
98
99impl fmt::Display for ResourceUse {
100    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101        formatter.write_str(self.as_str())
102    }
103}
104
105impl FromStr for ResourceUse {
106    type Err = NicheTextError;
107
108    fn from_str(value: &str) -> Result<Self, Self::Err> {
109        Self::new(value)
110    }
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
114pub struct NicheBreadth(f64);
115
116impl NicheBreadth {
117    /// # Errors
118    /// Returns `NicheValueError::NonFinite` when `value` is not finite.
119    /// Returns `NicheValueError::Negative` when `value` is less than zero.
120    pub fn new(value: f64) -> Result<Self, NicheValueError> {
121        if !value.is_finite() {
122            return Err(NicheValueError::NonFinite);
123        }
124
125        if value < 0.0 {
126            return Err(NicheValueError::Negative);
127        }
128
129        Ok(Self(value))
130    }
131
132    #[must_use]
133    pub const fn get(self) -> f64 {
134        self.0
135    }
136}
137
138impl fmt::Display for NicheBreadth {
139    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140        self.0.fmt(formatter)
141    }
142}
143
144#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
145pub enum NicheKind {
146    Fundamental,
147    Realized,
148    Trophic,
149    Spatial,
150    Temporal,
151    Functional,
152    Unknown,
153    Custom(String),
154}
155
156impl fmt::Display for NicheKind {
157    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158        formatter.write_str(match self {
159            Self::Fundamental => "fundamental",
160            Self::Realized => "realized",
161            Self::Trophic => "trophic",
162            Self::Spatial => "spatial",
163            Self::Temporal => "temporal",
164            Self::Functional => "functional",
165            Self::Unknown => "unknown",
166            Self::Custom(value) => value.as_str(),
167        })
168    }
169}
170
171impl FromStr for NicheKind {
172    type Err = NicheKindParseError;
173
174    fn from_str(value: &str) -> Result<Self, Self::Err> {
175        let trimmed = value.trim();
176
177        if trimmed.is_empty() {
178            return Err(NicheKindParseError::Empty);
179        }
180
181        Ok(match normalized_token(trimmed).as_str() {
182            "fundamental" => Self::Fundamental,
183            "realized" => Self::Realized,
184            "trophic" => Self::Trophic,
185            "spatial" => Self::Spatial,
186            "temporal" => Self::Temporal,
187            "functional" => Self::Functional,
188            "unknown" => Self::Unknown,
189            _ => Self::Custom(trimmed.to_string()),
190        })
191    }
192}
193
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
195pub enum NicheKindParseError {
196    Empty,
197}
198
199impl fmt::Display for NicheKindParseError {
200    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
201        match self {
202            Self::Empty => formatter.write_str("niche kind cannot be empty"),
203        }
204    }
205}
206
207impl Error for NicheKindParseError {}
208
209#[cfg(test)]
210mod tests {
211    use super::{NicheBreadth, NicheKind, NicheName, NicheTextError, NicheValueError};
212
213    #[test]
214    fn valid_niche_name() -> Result<(), NicheTextError> {
215        let name = NicheName::new("canopy pollinator")?;
216
217        assert_eq!(name.as_str(), "canopy pollinator");
218        Ok(())
219    }
220
221    #[test]
222    fn empty_niche_name_rejected() {
223        assert_eq!(NicheName::new(""), Err(NicheTextError::Empty));
224    }
225
226    #[test]
227    fn niche_kind_display_parse() {
228        assert_eq!("trophic".parse::<NicheKind>(), Ok(NicheKind::Trophic));
229        assert_eq!(NicheKind::Spatial.to_string(), "spatial");
230    }
231
232    #[test]
233    fn custom_niche_kind() {
234        assert_eq!(
235            "edge-specialist".parse::<NicheKind>(),
236            Ok(NicheKind::Custom("edge-specialist".to_string()))
237        );
238    }
239
240    #[test]
241    fn valid_niche_breadth() -> Result<(), NicheValueError> {
242        let breadth = NicheBreadth::new(1.25)?;
243
244        assert!((breadth.get() - 1.25).abs() < f64::EPSILON);
245        Ok(())
246    }
247
248    #[test]
249    fn negative_niche_breadth_rejected() {
250        assert_eq!(NicheBreadth::new(-0.1), Err(NicheValueError::Negative));
251    }
252}