1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn non_empty_text(value: impl AsRef<str>) -> Result<String, ProcessTextError> {
8 let original = value.as_ref();
9
10 if original.trim().is_empty() {
11 Err(ProcessTextError::Empty)
12 } else {
13 Ok(original.to_string())
14 }
15}
16
17fn normalized_token(value: &str) -> String {
18 let mut normalized = String::with_capacity(value.len());
19 let mut previous_separator = false;
20
21 for character in value.trim().chars() {
22 if character.is_ascii_alphanumeric() {
23 normalized.push(character.to_ascii_lowercase());
24 previous_separator = false;
25 } else if (character.is_whitespace() || character == '-' || character == '_')
26 && !previous_separator
27 && !normalized.is_empty()
28 {
29 normalized.push('-');
30 previous_separator = true;
31 }
32 }
33
34 if normalized.ends_with('-') {
35 let _ = normalized.pop();
36 }
37
38 normalized
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum ProcessTextError {
43 Empty,
44}
45
46impl fmt::Display for ProcessTextError {
47 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Self::Empty => formatter.write_str("geologic process text cannot be empty"),
50 }
51 }
52}
53
54impl Error for ProcessTextError {}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum ProcessParseError {
58 Empty,
59}
60
61impl fmt::Display for ProcessParseError {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Empty => formatter.write_str("geologic process vocabulary cannot be empty"),
65 }
66 }
67}
68
69impl Error for ProcessParseError {}
70
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum ProcessRateError {
73 InvalidFormat,
74 InvalidNumber,
75 NonFiniteValue,
76 EmptyUnit,
77}
78
79impl fmt::Display for ProcessRateError {
80 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Self::InvalidFormat => {
83 formatter.write_str("process rate must be in '<value> <unit>' format")
84 },
85 Self::InvalidNumber => formatter.write_str("process rate value must be a valid number"),
86 Self::NonFiniteValue => formatter.write_str("process rate value must be finite"),
87 Self::EmptyUnit => formatter.write_str("process rate unit cannot be empty"),
88 }
89 }
90}
91
92impl Error for ProcessRateError {}
93
94#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
95pub struct GeologicProcess(String);
96
97impl GeologicProcess {
98 pub fn new(value: impl AsRef<str>) -> Result<Self, ProcessTextError> {
104 non_empty_text(value).map(Self)
105 }
106
107 #[must_use]
108 pub fn as_str(&self) -> &str {
109 &self.0
110 }
111}
112
113impl AsRef<str> for GeologicProcess {
114 fn as_ref(&self) -> &str {
115 self.as_str()
116 }
117}
118
119impl fmt::Display for GeologicProcess {
120 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121 formatter.write_str(self.as_str())
122 }
123}
124
125impl FromStr for GeologicProcess {
126 type Err = ProcessTextError;
127
128 fn from_str(value: &str) -> Result<Self, Self::Err> {
129 Self::new(value)
130 }
131}
132
133#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub enum ProcessKind {
135 Weathering,
136 Erosion,
137 Deposition,
138 Lithification,
139 Metamorphism,
140 Melting,
141 Uplift,
142 Subsidence,
143 Volcanism,
144 Unknown,
145 Custom(String),
146}
147
148impl fmt::Display for ProcessKind {
149 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150 match self {
151 Self::Weathering => formatter.write_str("weathering"),
152 Self::Erosion => formatter.write_str("erosion"),
153 Self::Deposition => formatter.write_str("deposition"),
154 Self::Lithification => formatter.write_str("lithification"),
155 Self::Metamorphism => formatter.write_str("metamorphism"),
156 Self::Melting => formatter.write_str("melting"),
157 Self::Uplift => formatter.write_str("uplift"),
158 Self::Subsidence => formatter.write_str("subsidence"),
159 Self::Volcanism => formatter.write_str("volcanism"),
160 Self::Unknown => formatter.write_str("unknown"),
161 Self::Custom(value) => formatter.write_str(value),
162 }
163 }
164}
165
166impl FromStr for ProcessKind {
167 type Err = ProcessParseError;
168
169 fn from_str(value: &str) -> Result<Self, Self::Err> {
170 let trimmed = value.trim();
171
172 if trimmed.is_empty() {
173 return Err(ProcessParseError::Empty);
174 }
175
176 match normalized_token(trimmed).as_str() {
177 "weathering" => Ok(Self::Weathering),
178 "erosion" => Ok(Self::Erosion),
179 "deposition" => Ok(Self::Deposition),
180 "lithification" => Ok(Self::Lithification),
181 "metamorphism" => Ok(Self::Metamorphism),
182 "melting" => Ok(Self::Melting),
183 "uplift" => Ok(Self::Uplift),
184 "subsidence" => Ok(Self::Subsidence),
185 "volcanism" => Ok(Self::Volcanism),
186 "unknown" => Ok(Self::Unknown),
187 _ => Ok(Self::Custom(trimmed.to_string())),
188 }
189 }
190}
191
192#[derive(Clone, Debug, PartialEq)]
193pub struct ProcessRate {
194 value: f64,
195 unit: String,
196}
197
198impl ProcessRate {
199 pub fn new(value: f64, unit: impl AsRef<str>) -> Result<Self, ProcessRateError> {
206 if !value.is_finite() {
207 return Err(ProcessRateError::NonFiniteValue);
208 }
209
210 let unit = unit.as_ref();
211 if unit.trim().is_empty() {
212 return Err(ProcessRateError::EmptyUnit);
213 }
214
215 Ok(Self {
216 value,
217 unit: unit.to_string(),
218 })
219 }
220
221 #[must_use]
222 pub const fn value(&self) -> f64 {
223 self.value
224 }
225
226 #[must_use]
227 pub fn unit(&self) -> &str {
228 &self.unit
229 }
230}
231
232impl fmt::Display for ProcessRate {
233 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
234 write!(formatter, "{} {}", self.value, self.unit)
235 }
236}
237
238impl FromStr for ProcessRate {
239 type Err = ProcessRateError;
240
241 fn from_str(value: &str) -> Result<Self, Self::Err> {
242 let trimmed = value.trim();
243 let (numeric_value, unit) = trimmed
244 .split_once(char::is_whitespace)
245 .ok_or(ProcessRateError::InvalidFormat)?;
246
247 let numeric_value = numeric_value
248 .parse::<f64>()
249 .map_err(|_| ProcessRateError::InvalidNumber)?;
250
251 Self::new(numeric_value, unit.trim())
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::{
258 GeologicProcess, ProcessKind, ProcessParseError, ProcessRate, ProcessRateError,
259 ProcessTextError,
260 };
261
262 #[test]
263 fn geologic_process_wrapper() -> Result<(), ProcessTextError> {
264 let process = GeologicProcess::new("delta progradation")?;
265
266 assert_eq!(process.as_str(), "delta progradation");
267 Ok(())
268 }
269
270 #[test]
271 fn process_kind_display_parse() -> Result<(), ProcessParseError> {
272 assert_eq!(ProcessKind::Weathering.to_string(), "weathering");
273 assert_eq!("erosion".parse::<ProcessKind>()?, ProcessKind::Erosion);
274 Ok(())
275 }
276
277 #[test]
278 fn custom_process_kind() -> Result<(), ProcessParseError> {
279 assert_eq!(
280 "diagenesis".parse::<ProcessKind>()?,
281 ProcessKind::Custom("diagenesis".to_string())
282 );
283 Ok(())
284 }
285
286 #[test]
287 fn process_rate_construction() -> Result<(), ProcessRateError> {
288 let rate = ProcessRate::new(0.2, "mm/yr")?;
289
290 assert!((rate.value() - 0.2).abs() < f64::EPSILON);
291 assert_eq!(rate.unit(), "mm/yr");
292 Ok(())
293 }
294
295 #[test]
296 fn process_rate_display_is_stable() -> Result<(), ProcessRateError> {
297 let rate = "1.5 mm/yr".parse::<ProcessRate>()?;
298
299 assert_eq!(rate.to_string(), "1.5 mm/yr");
300 Ok(())
301 }
302}