Skip to main content

use_trophic_level/
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, TrophicTextError> {
12    let trimmed = value.as_ref().trim();
13
14    if trimmed.is_empty() {
15        Err(TrophicTextError::Empty)
16    } else {
17        Ok(trimmed.to_string())
18    }
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum TrophicTextError {
23    Empty,
24}
25
26impl fmt::Display for TrophicTextError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("trophic text cannot be empty"),
30        }
31    }
32}
33
34impl Error for TrophicTextError {}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum TrophicValueError {
38    Negative,
39    NonFinite,
40}
41
42impl fmt::Display for TrophicValueError {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Negative => formatter.write_str("trophic value cannot be negative"),
46            Self::NonFinite => formatter.write_str("trophic value must be finite"),
47        }
48    }
49}
50
51impl Error for TrophicValueError {}
52
53#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub enum TrophicLevel {
55    PrimaryProducer,
56    PrimaryConsumer,
57    SecondaryConsumer,
58    TertiaryConsumer,
59    QuaternaryConsumer,
60    Decomposer,
61    Detritivore,
62    Omnivore,
63    Unknown,
64    Custom(String),
65}
66
67impl fmt::Display for TrophicLevel {
68    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69        formatter.write_str(match self {
70            Self::PrimaryProducer => "primary-producer",
71            Self::PrimaryConsumer => "primary-consumer",
72            Self::SecondaryConsumer => "secondary-consumer",
73            Self::TertiaryConsumer => "tertiary-consumer",
74            Self::QuaternaryConsumer => "quaternary-consumer",
75            Self::Decomposer => "decomposer",
76            Self::Detritivore => "detritivore",
77            Self::Omnivore => "omnivore",
78            Self::Unknown => "unknown",
79            Self::Custom(value) => value.as_str(),
80        })
81    }
82}
83
84impl FromStr for TrophicLevel {
85    type Err = TrophicLevelParseError;
86
87    fn from_str(value: &str) -> Result<Self, Self::Err> {
88        let trimmed = value.trim();
89
90        if trimmed.is_empty() {
91            return Err(TrophicLevelParseError::Empty);
92        }
93
94        Ok(match normalized_token(trimmed).as_str() {
95            "primary-producer" => Self::PrimaryProducer,
96            "primary-consumer" => Self::PrimaryConsumer,
97            "secondary-consumer" => Self::SecondaryConsumer,
98            "tertiary-consumer" => Self::TertiaryConsumer,
99            "quaternary-consumer" => Self::QuaternaryConsumer,
100            "decomposer" => Self::Decomposer,
101            "detritivore" => Self::Detritivore,
102            "omnivore" => Self::Omnivore,
103            "unknown" => Self::Unknown,
104            _ => Self::Custom(trimmed.to_string()),
105        })
106    }
107}
108
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum TrophicLevelParseError {
111    Empty,
112}
113
114impl fmt::Display for TrophicLevelParseError {
115    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            Self::Empty => formatter.write_str("trophic level cannot be empty"),
118        }
119    }
120}
121
122impl Error for TrophicLevelParseError {}
123
124#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
125pub struct TrophicRole(String);
126
127impl TrophicRole {
128    /// # Errors
129    /// Returns `TrophicTextError::Empty` when `value` is blank.
130    pub fn new(value: impl AsRef<str>) -> Result<Self, TrophicTextError> {
131        non_empty_text(value).map(Self)
132    }
133
134    #[must_use]
135    pub fn as_str(&self) -> &str {
136        &self.0
137    }
138}
139
140impl fmt::Display for TrophicRole {
141    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
142        formatter.write_str(self.as_str())
143    }
144}
145
146impl FromStr for TrophicRole {
147    type Err = TrophicTextError;
148
149    fn from_str(value: &str) -> Result<Self, Self::Err> {
150        Self::new(value)
151    }
152}
153
154#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
155pub struct TrophicPosition(f64);
156
157impl TrophicPosition {
158    /// # Errors
159    /// Returns `TrophicValueError::NonFinite` when `value` is not finite.
160    /// Returns `TrophicValueError::Negative` when `value` is less than zero.
161    pub fn new(value: f64) -> Result<Self, TrophicValueError> {
162        if !value.is_finite() {
163            return Err(TrophicValueError::NonFinite);
164        }
165
166        if value < 0.0 {
167            return Err(TrophicValueError::Negative);
168        }
169
170        Ok(Self(value))
171    }
172
173    #[must_use]
174    pub const fn get(self) -> f64 {
175        self.0
176    }
177}
178
179impl fmt::Display for TrophicPosition {
180    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
181        self.0.fmt(formatter)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::{TrophicLevel, TrophicPosition, TrophicRole, TrophicTextError, TrophicValueError};
188
189    #[test]
190    fn trophic_level_display_parse() {
191        assert_eq!(
192            "primary-consumer".parse::<TrophicLevel>(),
193            Ok(TrophicLevel::PrimaryConsumer)
194        );
195        assert_eq!(TrophicLevel::Decomposer.to_string(), "decomposer");
196    }
197
198    #[test]
199    fn custom_trophic_level() {
200        assert_eq!(
201            "suspension-feeder".parse::<TrophicLevel>(),
202            Ok(TrophicLevel::Custom("suspension-feeder".to_string()))
203        );
204    }
205
206    #[test]
207    fn valid_trophic_position() -> Result<(), TrophicValueError> {
208        let position = TrophicPosition::new(2.5)?;
209
210        assert!((position.get() - 2.5).abs() < f64::EPSILON);
211        Ok(())
212    }
213
214    #[test]
215    fn negative_trophic_position_rejected() {
216        assert_eq!(TrophicPosition::new(-1.0), Err(TrophicValueError::Negative));
217    }
218
219    #[test]
220    fn trophic_role_construction() -> Result<(), TrophicTextError> {
221        let role = TrophicRole::new("reef grazer")?;
222
223        assert_eq!(role.to_string(), "reef grazer");
224        Ok(())
225    }
226}