geode/parse/
quantity.rs

1#![allow(unused)]
2use super::unit::{format_unit, FormatUnitError};
3use super::RawRepr;
4use crate::StdError;
5use const_format::{concatcp, formatcp};
6use lazy_static::lazy_static;
7use regex::Regex;
8use schemars::schema::{
9    InstanceType, Schema, SchemaObject, SingleOrVec, StringValidation, SubschemaValidation,
10};
11use schemars::JsonSchema;
12use serde::{de::Error, Deserialize, Serialize};
13use std::fmt::{Debug, Display};
14use std::str::FromStr;
15use thiserror::Error;
16use uom::str::ParseQuantityError as UomParseError;
17
18#[derive(Debug, PartialEq, PartialOrd, Clone)]
19pub struct Quantity<L> {
20    raw: String,
21    parsed: L,
22}
23
24impl<T> JsonSchema for Quantity<T> {
25    fn schema_name() -> String {
26        String::from("Quantity")
27    }
28
29    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
30        let mut schema = SchemaObject::default();
31        //schema.instance_type = Some(SingleOrVec::Single(Box::new(InstanceType::String)));
32        schema.subschemas = Some(Box::new(SubschemaValidation {
33            one_of: Some(vec![
34                // Schema for string type
35                Schema::Object(SchemaObject {
36                    instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
37                    string: Some(Box::new(StringValidation {
38                        pattern: Some(NO_REF_QUANTITY_PATTERN.to_string()),
39                        ..Default::default()
40                    })),
41                    ..Default::default()
42                }),
43                // Schema for number type
44                Schema::Object(SchemaObject {
45                    instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Number))),
46                    ..Default::default()
47                }),
48            ]),
49            ..Default::default()
50        }));
51
52        Schema::Object(schema)
53    }
54}
55
56impl<T> Display for Quantity<T> {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{}", self.raw)
59    }
60}
61
62impl<L> RawRepr for Quantity<L> {
63    fn raw(&self) -> &str {
64        &self.raw
65    }
66}
67
68#[derive(Debug, Error)]
69pub enum ParseQuantityError {
70    #[error("invalid quantity format : '{0}', should be 'value [unit]'")]
71    InvalidFormat(String),
72    #[error("this quantity can't be a reference, please remove the 'ref' or 'reference' keyword")]
73    NoReference,
74    #[error("invalid unit format: {0}")]
75    InvalidUnitFormat(#[from] FormatUnitError),
76    #[error("quantity not recognized: '{0}'")]
77    Unrecognized(#[from] UomParseError),
78}
79
80impl<T> FromStr for Quantity<T>
81where
82    T: FromStr<Err = UomParseError> + DefaultUnit + Debug,
83{
84    type Err = ParseQuantityError;
85
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        Ok(Quantity::new(s)?)
88    }
89}
90
91impl<T> Serialize for Quantity<T> {
92    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
93    where
94        S: serde::Serializer,
95    {
96        // Only serialize the `raw` field as "value"
97        serializer.serialize_str(&self.raw)
98    }
99}
100
101impl<'de, T> Deserialize<'de> for Quantity<T>
102where
103    T: FromStr<Err = UomParseError> + Debug + DefaultUnit,
104{
105    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106    where
107        D: serde::Deserializer<'de>,
108    {
109        // First, deserialize the `raw` field as a string
110        let raw: &str = Deserialize::deserialize(deserializer)?;
111        Ok(Quantity::new(raw).map_err(|e| D::Error::custom(e))?)
112    }
113}
114
115const PARTIAL_QUANTITY_PATTERN: &str =
116    r"\s*([+-]?[\d_ ]*?\.?[\d_ ]+?(?:e(?:\+|-)?[.\d]+)?)[ \t]*([^\d\s.](?:.*?[^.])?)?\s*";
117
118pub const NO_REF_QUANTITY_PATTERN: &str = formatcp!("^{PARTIAL_QUANTITY_PATTERN}$");
119
120const PARTIAL_REFERENCE_PATTERN: &str = concatcp!(r"\s*(reference|ref)?", PARTIAL_QUANTITY_PATTERN);
121pub const QUANTITY_PATTERN: &str = formatcp!("^{PARTIAL_REFERENCE_PATTERN}$");
122
123pub const RANGE_PATTERN: &str =
124    formatcp!(r"^{PARTIAL_QUANTITY_PATTERN}\s*..\s*{PARTIAL_QUANTITY_PATTERN}$");
125
126lazy_static! {
127    pub static ref QUANTITY_RE: Regex = Regex::new(QUANTITY_PATTERN).unwrap();
128}
129
130pub fn get_unit(quantity: &str) -> Option<&str> {
131    Some(QUANTITY_RE.captures(quantity)?.get(3)?.as_str())
132}
133
134impl<T> Quantity<T>
135where
136    T: FromStr<Err = UomParseError> + Debug + DefaultUnit,
137{
138    // Constructor to create a new ParsedValue
139    pub fn new(raw: &str) -> Result<Self, ParseQuantityError> {
140        QUANTITY_RE.to_string();
141        if let Some(captures) = QUANTITY_RE.captures(raw) {
142            if captures.get(1).is_some() {
143                return Err(ParseQuantityError::NoReference);
144            }
145            let mut unit: String = T::DEFAULT_UNIT.to_string();
146            if let Some(u) = captures.get(3) {
147                unit = format_unit(u.as_str())?;
148            }
149
150            let value = &captures[2];
151            let mut pretty_value = String::with_capacity(value.len());
152            let mut prepped_value = String::with_capacity(value.len());
153
154            for c in value.chars() {
155                match c {
156                    ' ' => pretty_value.push(' '),
157                    '_' => pretty_value.push(' '),
158                    _ => {
159                        pretty_value.push(c);
160                        prepped_value.push(c);
161                    }
162                }
163            }
164
165            let prepped_raw = format!("{} {}", prepped_value, &unit);
166
167            Ok(Quantity {
168                parsed: prepped_raw.parse()?,
169                raw: format!(
170                    "{}{}{}",
171                    pretty_value,
172                    if unit.len() > 0 { " " } else { "" },
173                    &unit
174                ),
175            })
176        } else {
177            Err(ParseQuantityError::InvalidFormat(raw.to_string()))
178        }
179    }
180
181    /// Getter for the parsed quantity
182    pub fn parsed(&self) -> &T {
183        &self.parsed
184    }
185
186    /// Raw representation of the quantity as written by the user
187    pub fn raw(&self) -> &str {
188        &self.raw
189    }
190}
191
192use uom::si::f64 as si;
193
194pub trait DefaultUnit {
195    const DEFAULT_UNIT: &str;
196}
197
198/// Ratio (unit less value resulting from calculating the ratio of two quantities)
199pub type Ratio = Quantity<si::Ratio>;
200
201impl DefaultUnit for si::Ratio {
202    const DEFAULT_UNIT: &str = "";
203}
204
205/// Area (default: km²)
206pub type Area = Quantity<si::Area>;
207
208impl DefaultUnit for si::Area {
209    const DEFAULT_UNIT: &str = "km²";
210}
211
212/// Compressibility (default: Pa⁻¹)
213pub type Compressibility = Quantity<si::Compressibility>;
214
215impl DefaultUnit for si::Compressibility {
216    const DEFAULT_UNIT: &str = "Pa⁻¹";
217}
218
219/// HydraulicPermeability (default: darcy)
220pub type HydraulicPermeability = Quantity<si::HydraulicPermeability>;
221
222impl DefaultUnit for si::HydraulicPermeability {
223    const DEFAULT_UNIT: &str = "mD";
224}
225
226/// Length (default: kilometers, since distances in geoscience are often measured in km)
227pub type Length = Quantity<si::Length>;
228
229impl DefaultUnit for si::Length {
230    const DEFAULT_UNIT: &str = "km";
231}
232
233/// Mass (default: grams, since small mass quantities in geoscience, especially in analysis, use grams)
234pub type Mass = Quantity<si::Mass>;
235
236impl DefaultUnit for si::Mass {
237    const DEFAULT_UNIT: &str = "g";
238}
239
240/// Time (default: years, due to the typical timescales in geoscience, especially for geological processes)
241pub type Time = Quantity<si::Time>;
242impl DefaultUnit for si::Time {
243    const DEFAULT_UNIT: &'static str = "yr"; // Years are commonly used in geoscience
244}
245
246/// Temperature (default: Celsius, as temperature is often measured in Celsius in geoscience contexts)
247pub type Temperature = Quantity<si::ThermodynamicTemperature>;
248
249impl DefaultUnit for si::ThermodynamicTemperature {
250    const DEFAULT_UNIT: &'static str = "°C";
251}
252
253/// Pressure (default: pascal, as pressure is often measured in pascal in scientific contexts)
254pub type Pressure = Quantity<si::Pressure>;
255
256impl DefaultUnit for si::Pressure {
257    const DEFAULT_UNIT: &'static str = "Pa";
258}
259
260/// Volume (default: cubic meters, which is the SI unit for volume)
261pub type Volume = Quantity<si::Volume>;
262
263impl DefaultUnit for si::Volume {
264    const DEFAULT_UNIT: &'static str = "m³";
265}
266
267/// Molar Mass (default: grams per mole, as it's commonly used in geoscience)
268pub type MolarMass = Quantity<si::MolarMass>;
269
270impl DefaultUnit for si::MolarMass {
271    const DEFAULT_UNIT: &'static str = "g/mol";
272}
273
274#[cfg(test)]
275mod tests {
276    use std::str::FromStr;
277
278    use super::{DefaultUnit, Pressure, Quantity};
279    use std::fmt::Debug;
280    use uom::si::{f64::Length, length::*};
281
282    fn make_parsed<L>(raw: &str, parsed: L) -> Quantity<L>
283    where
284        L: FromStr + Debug + DefaultUnit,
285    {
286        Quantity {
287            raw: raw.to_string(),
288            parsed,
289        }
290    }
291
292    #[test]
293    fn parse_length_with_valid_input() {
294        fn make_length(raw: &str) -> super::Length {
295            super::Length::new(raw).unwrap()
296        }
297        assert_eq!(
298            make_length("10 m"),
299            make_parsed("10 m", Length::new::<meter>(10.))
300        );
301        assert_eq!(
302            make_length("10m"),
303            make_parsed("10 m", Length::new::<meter>(10.))
304        );
305        assert_eq!(
306            make_length("10     m"),
307            make_parsed("10 m", Length::new::<meter>(10.))
308        );
309        assert_eq!(
310            make_length("10"),
311            make_parsed("10 km", Length::new::<kilometer>(10.))
312        );
313        assert_eq!(
314            make_length("100 000 m"),
315            make_parsed("100 000 m", Length::new::<kilometer>(100.))
316        );
317        assert_eq!(
318            make_length("1 meter"),
319            make_parsed("1 meter", Length::new::<meter>(1.))
320        );
321        assert_eq!(
322            make_length("2 meters"),
323            make_parsed("2 meters", Length::new::<meter>(2.))
324        );
325        assert_eq!(
326            make_length("-1"),
327            make_parsed("-1 km", Length::new::<kilometer>(-1.))
328        );
329        assert_eq!(
330            super::Compressibility::new("1e-09 Pa-1").expect("Valid quantity should be parsed."),
331            Quantity {
332                parsed: uom::si::f64::Compressibility::new::<uom::si::compressibility::pascal>(
333                    1e-09
334                ),
335                raw: "1e-09 Pa⁻¹".to_string()
336            }
337        );
338    }
339    #[test]
340    fn parse_length_with_invalid_input() {
341        fn attempt_length_parse(raw: &str) {
342            let result = super::Length::new(raw);
343            assert!(result.is_err(), "Expected error for input '{}'", raw);
344        }
345
346        attempt_length_parse("ten m"); // Invalid number format
347        attempt_length_parse("10 xyz"); // Unrecognized unit
348        attempt_length_parse(""); // Empty string
349                                  //attempt_length_parse("10 10 m"); // Invalid format
350        attempt_length_parse("reference m"); // Missing number
351    }
352
353    #[test]
354    fn parse_reference_should_err() {
355        let raw = "reference 5 000 000";
356        assert!(raw.parse::<Pressure>().is_err());
357    }
358}