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.subschemas = Some(Box::new(SubschemaValidation {
33 one_of: Some(vec![
34 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::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 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 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 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 pub fn parsed(&self) -> &T {
183 &self.parsed
184 }
185
186 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
198pub type Ratio = Quantity<si::Ratio>;
200
201impl DefaultUnit for si::Ratio {
202 const DEFAULT_UNIT: &str = "";
203}
204
205pub type Area = Quantity<si::Area>;
207
208impl DefaultUnit for si::Area {
209 const DEFAULT_UNIT: &str = "km²";
210}
211
212pub type Compressibility = Quantity<si::Compressibility>;
214
215impl DefaultUnit for si::Compressibility {
216 const DEFAULT_UNIT: &str = "Pa⁻¹";
217}
218
219pub type HydraulicPermeability = Quantity<si::HydraulicPermeability>;
221
222impl DefaultUnit for si::HydraulicPermeability {
223 const DEFAULT_UNIT: &str = "mD";
224}
225
226pub type Length = Quantity<si::Length>;
228
229impl DefaultUnit for si::Length {
230 const DEFAULT_UNIT: &str = "km";
231}
232
233pub type Mass = Quantity<si::Mass>;
235
236impl DefaultUnit for si::Mass {
237 const DEFAULT_UNIT: &str = "g";
238}
239
240pub type Time = Quantity<si::Time>;
242impl DefaultUnit for si::Time {
243 const DEFAULT_UNIT: &'static str = "yr"; }
245
246pub type Temperature = Quantity<si::ThermodynamicTemperature>;
248
249impl DefaultUnit for si::ThermodynamicTemperature {
250 const DEFAULT_UNIT: &'static str = "°C";
251}
252
253pub type Pressure = Quantity<si::Pressure>;
255
256impl DefaultUnit for si::Pressure {
257 const DEFAULT_UNIT: &'static str = "Pa";
258}
259
260pub type Volume = Quantity<si::Volume>;
262
263impl DefaultUnit for si::Volume {
264 const DEFAULT_UNIT: &'static str = "m³";
265}
266
267pub 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"); attempt_length_parse("10 xyz"); attempt_length_parse(""); attempt_length_parse("reference m"); }
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}