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, PopulationTextError> {
12 let trimmed = value.as_ref().trim();
13
14 if trimmed.is_empty() {
15 Err(PopulationTextError::Empty)
16 } else {
17 Ok(trimmed.to_string())
18 }
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum PopulationTextError {
23 Empty,
24}
25
26impl fmt::Display for PopulationTextError {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::Empty => formatter.write_str("population text cannot be empty"),
30 }
31 }
32}
33
34impl Error for PopulationTextError {}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum PopulationValueError {
38 Negative,
39 NonFinite,
40}
41
42impl fmt::Display for PopulationValueError {
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::Negative => formatter.write_str("population value cannot be negative"),
46 Self::NonFinite => formatter.write_str("population value must be finite"),
47 }
48 }
49}
50
51impl Error for PopulationValueError {}
52
53#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub struct PopulationId(String);
55
56impl PopulationId {
57 pub fn new(value: impl AsRef<str>) -> Result<Self, PopulationTextError> {
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 PopulationId {
70 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71 formatter.write_str(self.as_str())
72 }
73}
74
75impl FromStr for PopulationId {
76 type Err = PopulationTextError;
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 PopulationName(String);
85
86impl PopulationName {
87 pub fn new(value: impl AsRef<str>) -> Result<Self, PopulationTextError> {
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 PopulationName {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 formatter.write_str(self.as_str())
102 }
103}
104
105impl FromStr for PopulationName {
106 type Err = PopulationTextError;
107
108 fn from_str(value: &str) -> Result<Self, Self::Err> {
109 Self::new(value)
110 }
111}
112
113#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
114pub struct PopulationSize(u64);
115
116impl PopulationSize {
117 pub const fn new(value: i64) -> Result<Self, PopulationValueError> {
120 if value < 0 {
121 Err(PopulationValueError::Negative)
122 } else {
123 Ok(Self(value.cast_unsigned()))
124 }
125 }
126
127 #[must_use]
128 pub const fn get(self) -> u64 {
129 self.0
130 }
131}
132
133impl fmt::Display for PopulationSize {
134 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135 self.0.fmt(formatter)
136 }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
140pub struct PopulationDensity(f64);
141
142impl PopulationDensity {
143 pub fn new(value: f64) -> Result<Self, PopulationValueError> {
147 if !value.is_finite() {
148 return Err(PopulationValueError::NonFinite);
149 }
150
151 if value < 0.0 {
152 return Err(PopulationValueError::Negative);
153 }
154
155 Ok(Self(value))
156 }
157
158 #[must_use]
159 pub const fn get(self) -> f64 {
160 self.0
161 }
162}
163
164impl fmt::Display for PopulationDensity {
165 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
166 self.0.fmt(formatter)
167 }
168}
169
170#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
171pub enum PopulationTrend {
172 Increasing,
173 Decreasing,
174 Stable,
175 Fluctuating,
176 Unknown,
177 Custom(String),
178}
179
180impl fmt::Display for PopulationTrend {
181 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
182 formatter.write_str(match self {
183 Self::Increasing => "increasing",
184 Self::Decreasing => "decreasing",
185 Self::Stable => "stable",
186 Self::Fluctuating => "fluctuating",
187 Self::Unknown => "unknown",
188 Self::Custom(value) => value.as_str(),
189 })
190 }
191}
192
193impl FromStr for PopulationTrend {
194 type Err = PopulationTrendParseError;
195
196 fn from_str(value: &str) -> Result<Self, Self::Err> {
197 let trimmed = value.trim();
198
199 if trimmed.is_empty() {
200 return Err(PopulationTrendParseError::Empty);
201 }
202
203 Ok(match normalized_token(trimmed).as_str() {
204 "increasing" => Self::Increasing,
205 "decreasing" => Self::Decreasing,
206 "stable" => Self::Stable,
207 "fluctuating" => Self::Fluctuating,
208 "unknown" => Self::Unknown,
209 _ => Self::Custom(trimmed.to_string()),
210 })
211 }
212}
213
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub enum PopulationTrendParseError {
216 Empty,
217}
218
219impl fmt::Display for PopulationTrendParseError {
220 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
221 match self {
222 Self::Empty => formatter.write_str("population trend cannot be empty"),
223 }
224 }
225}
226
227impl Error for PopulationTrendParseError {}
228
229#[cfg(test)]
230mod tests {
231 use super::{
232 PopulationDensity, PopulationId, PopulationSize, PopulationTextError, PopulationTrend,
233 PopulationValueError,
234 };
235
236 #[test]
237 fn valid_population_id() -> Result<(), PopulationTextError> {
238 let id = PopulationId::new("delta-cranes")?;
239
240 assert_eq!(id.as_str(), "delta-cranes");
241 Ok(())
242 }
243
244 #[test]
245 fn empty_population_id_rejected() {
246 assert_eq!(PopulationId::new(" "), Err(PopulationTextError::Empty));
247 }
248
249 #[test]
250 fn valid_population_size() -> Result<(), PopulationValueError> {
251 let size = PopulationSize::new(42)?;
252
253 assert_eq!(size.get(), 42);
254 Ok(())
255 }
256
257 #[test]
258 fn negative_population_size_rejected() {
259 assert_eq!(PopulationSize::new(-1), Err(PopulationValueError::Negative));
260 }
261
262 #[test]
263 fn population_trend_display_parse() {
264 assert_eq!(
265 "stable".parse::<PopulationTrend>(),
266 Ok(PopulationTrend::Stable)
267 );
268 assert_eq!(PopulationTrend::Fluctuating.to_string(), "fluctuating");
269 }
270
271 #[test]
272 fn valid_population_density() -> Result<(), PopulationValueError> {
273 let density = PopulationDensity::new(3.5)?;
274
275 assert!((density.get() - 3.5).abs() < f64::EPSILON);
276 Ok(())
277 }
278}