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 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 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 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}