1use std::collections::HashMap;
23
24use thiserror::Error;
25
26use crate::P_ATM_STANDARD_KPA;
27
28#[derive(Debug, Error)]
30pub enum RegistryError {
31 #[error("unknown unit: {0}")]
32 UnknownUnit(String),
33 #[error("unknown dimension: {0}")]
34 UnknownDimension(String),
35 #[error("unit `{0}` already defined (pass overwrite=true to replace)")]
36 AlreadyDefined(String),
37 #[error("dimension `{0}` already defined")]
38 DimensionAlreadyDefined(String),
39 #[error("dimension mismatch: expected {expected:?}, found {found:?}")]
40 DimensionMismatch {
41 expected: DimensionVector,
42 found: DimensionVector,
43 },
44 #[error("non-positive absolute pressure ({0} kPa) — gauge value is below vacuum")]
45 NonPositivePressure(f64),
46 #[error("invalid unit string: {0}")]
47 ParseError(String),
48 #[error("atmospheric pressure must be > 0; got {0} kPa")]
49 BadAtmosphericPressure(f64),
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub struct DimensionVector(pub [i8; 7]);
57
58impl DimensionVector {
59 pub const fn new(exps: [i8; 7]) -> Self {
60 DimensionVector(exps)
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum Dimension {
68 Temperature, TemperatureDiff, Pressure, MolarEnergy, MolarEntropy, MolarVolume, Amount, Custom(&'static str), }
77
78impl Dimension {
79 pub fn name(&self) -> &str {
80 match self {
81 Dimension::Temperature => "temperature",
82 Dimension::TemperatureDiff => "temperature_diff",
83 Dimension::Pressure => "pressure",
84 Dimension::MolarEnergy => "molar_energy",
85 Dimension::MolarEntropy => "molar_entropy",
86 Dimension::MolarVolume => "molar_volume",
87 Dimension::Amount => "amount",
88 Dimension::Custom(s) => s,
89 }
90 }
91
92 pub fn vector(&self) -> DimensionVector {
93 match self {
94 Dimension::Temperature | Dimension::TemperatureDiff => {
95 DimensionVector::new([0, 0, 0, 0, 1, 0, 0])
96 }
97 Dimension::Pressure => DimensionVector::new([-1, 1, -2, 0, 0, 0, 0]),
98 Dimension::MolarEnergy => DimensionVector::new([2, 1, -2, 0, 0, -1, 0]),
99 Dimension::MolarEntropy => DimensionVector::new([2, 1, -2, 0, -1, -1, 0]),
100 Dimension::MolarVolume => DimensionVector::new([3, 0, 0, 0, 0, -1, 0]),
101 Dimension::Amount => DimensionVector::new([0, 0, 0, 0, 0, 1, 0]),
102 Dimension::Custom(_) => DimensionVector::new([0; 7]),
103 }
104 }
105
106 pub fn from_name(name: &str) -> Option<Dimension> {
109 Some(match name {
110 "temperature" => Dimension::Temperature,
111 "temperature_diff" => Dimension::TemperatureDiff,
112 "pressure" => Dimension::Pressure,
113 "molar_energy" => Dimension::MolarEnergy,
114 "molar_entropy" => Dimension::MolarEntropy,
115 "molar_volume" => Dimension::MolarVolume,
116 "amount" => Dimension::Amount,
117 _ => return None,
118 })
119 }
120}
121
122#[derive(Debug, Clone, Copy)]
124enum OffsetSource {
125 Constant(f64),
127 GaugePAtm,
130}
131
132#[derive(Debug, Clone)]
134pub struct UnitDef {
135 pub name: String,
136 pub dimension_name: String,
137 pub dimension_vector: DimensionVector,
138 pub scale: f64,
140 offset: OffsetSource,
141}
142
143impl UnitDef {
144 pub fn is_gauge(&self) -> bool {
145 matches!(self.offset, OffsetSource::GaugePAtm)
146 }
147
148 pub fn constant_offset(&self) -> Option<f64> {
149 match self.offset {
150 OffsetSource::Constant(o) => Some(o),
151 OffsetSource::GaugePAtm => None,
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
158pub struct UnitRegistry {
159 units: HashMap<String, UnitDef>,
160 dimensions: HashMap<String, DimensionVector>,
163 p_atm_kpa: f64,
164}
165
166impl UnitRegistry {
167 pub fn new() -> Self {
170 UnitRegistry {
171 units: HashMap::new(),
172 dimensions: HashMap::new(),
173 p_atm_kpa: P_ATM_STANDARD_KPA,
174 }
175 }
176
177 pub fn with_vle_defaults() -> Self {
180 let mut r = Self::new();
181 r.install_defaults();
182 r
183 }
184
185 fn install_defaults(&mut self) {
186 const DEFAULTS_TOML: &str = include_str!("data/defaults.toml");
190 self.load_from_toml_str(DEFAULTS_TOML)
191 .expect("built-in defaults.toml must parse and apply cleanly");
192 }
193
194 fn put(&mut self, u: UnitDef) {
195 self.units.insert(u.name.clone(), u);
196 }
197
198 pub fn atmospheric_pressure_kpa(&self) -> f64 {
205 self.p_atm_kpa
206 }
207
208 pub fn set_atmospheric_pressure(&mut self, p_atm_kpa: f64) -> Result<(), RegistryError> {
216 if !(p_atm_kpa.is_finite() && p_atm_kpa > 0.0) {
219 return Err(RegistryError::BadAtmosphericPressure(p_atm_kpa));
220 }
221 self.p_atm_kpa = p_atm_kpa;
222 Ok(())
223 }
224
225 pub fn define(
235 &mut self,
236 name: &str,
237 dimension: Dimension,
238 scale: f64,
239 offset: f64,
240 ) -> Result<(), RegistryError> {
241 self.define_with_dimension_name(name, dimension.name(), dimension.vector(), scale, offset)
242 }
243
244 pub fn define_gauge(
247 &mut self,
248 name: &str,
249 scale_kpa_per_unit: f64,
250 ) -> Result<(), RegistryError> {
251 if self.units.contains_key(name) {
252 return Err(RegistryError::AlreadyDefined(name.into()));
253 }
254 self.put(UnitDef {
255 name: name.into(),
256 dimension_name: "pressure".into(),
257 dimension_vector: Dimension::Pressure.vector(),
258 scale: scale_kpa_per_unit,
259 offset: OffsetSource::GaugePAtm,
260 });
261 Ok(())
262 }
263
264 pub fn define_dimension(
266 &mut self,
267 name: &str,
268 vector: DimensionVector,
269 ) -> Result<(), RegistryError> {
270 if Dimension::from_name(name).is_some() || self.dimensions.contains_key(name) {
271 return Err(RegistryError::DimensionAlreadyDefined(name.into()));
272 }
273 self.dimensions.insert(name.into(), vector);
274 Ok(())
275 }
276
277 pub fn define_with_dimension(
279 &mut self,
280 name: &str,
281 dimension_name: &str,
282 scale: f64,
283 offset: f64,
284 ) -> Result<(), RegistryError> {
285 let vector = self
286 .lookup_dimension(dimension_name)
287 .ok_or_else(|| RegistryError::UnknownDimension(dimension_name.into()))?;
288 self.define_with_dimension_name(name, dimension_name, vector, scale, offset)
289 }
290
291 fn define_with_dimension_name(
292 &mut self,
293 name: &str,
294 dim_name: &str,
295 vector: DimensionVector,
296 scale: f64,
297 offset: f64,
298 ) -> Result<(), RegistryError> {
299 if self.units.contains_key(name) {
300 return Err(RegistryError::AlreadyDefined(name.into()));
301 }
302 self.put(UnitDef {
303 name: name.into(),
304 dimension_name: dim_name.into(),
305 dimension_vector: vector,
306 scale,
307 offset: OffsetSource::Constant(offset),
308 });
309 Ok(())
310 }
311
312 fn lookup_dimension(&self, name: &str) -> Option<DimensionVector> {
313 Dimension::from_name(name)
314 .map(|d| d.vector())
315 .or_else(|| self.dimensions.get(name).copied())
316 }
317
318 pub fn get(&self, name: &str) -> Result<&UnitDef, RegistryError> {
321 self.units
322 .get(name)
323 .ok_or_else(|| RegistryError::UnknownUnit(name.into()))
324 }
325
326 pub fn unit_names(&self) -> impl Iterator<Item = &str> {
328 self.units.keys().map(|s| s.as_str())
329 }
330
331 fn offset_value(&self, u: &UnitDef) -> f64 {
334 match u.offset {
335 OffsetSource::Constant(o) => o,
336 OffsetSource::GaugePAtm => self.p_atm_kpa,
337 }
338 }
339
340 pub fn to_canonical(&self, value: f64, unit_name: &str) -> Result<f64, RegistryError> {
343 let u = self.get(unit_name)?;
344 let result = value * u.scale + self.offset_value(u);
345 if u.is_gauge() && result <= 0.0 {
347 return Err(RegistryError::NonPositivePressure(result));
348 }
349 Ok(result)
350 }
351
352 pub fn from_canonical(
355 &self,
356 value_canonical: f64,
357 unit_name: &str,
358 ) -> Result<f64, RegistryError> {
359 let u = self.get(unit_name)?;
360 Ok((value_canonical - self.offset_value(u)) / u.scale)
361 }
362
363 pub fn parse(&self, s: &str) -> Result<Quantity, RegistryError> {
366 let (value, unit) = crate::parser::split_value_unit(s)?;
367 let canonical = self.to_canonical(value, unit)?;
368 let u = self.get(unit)?;
369 Ok(Quantity {
370 canonical,
371 dimension: u.dimension_name.clone(),
372 })
373 }
374
375 pub fn format(&self, q: &Quantity, unit_name: &str) -> Result<String, RegistryError> {
377 let u = self.get(unit_name)?;
378 if u.dimension_name != q.dimension {
379 return Err(RegistryError::DimensionMismatch {
380 expected: self
381 .lookup_dimension(&q.dimension)
382 .unwrap_or(DimensionVector::new([0; 7])),
383 found: u.dimension_vector,
384 });
385 }
386 let v = self.from_canonical(q.canonical, unit_name)?;
387 Ok(format!("{v} {unit_name}"))
388 }
389}
390
391impl Default for UnitRegistry {
392 fn default() -> Self {
393 Self::with_vle_defaults()
394 }
395}
396
397#[derive(Debug, Clone, PartialEq)]
399pub struct Quantity {
400 pub canonical: f64,
401 pub dimension: String,
402}
403
404impl Quantity {
405 pub fn value_kpa(&self) -> f64 {
406 debug_assert_eq!(self.dimension, "pressure");
407 self.canonical
408 }
409 pub fn value_kelvin(&self) -> f64 {
410 debug_assert_eq!(self.dimension, "temperature");
411 self.canonical
412 }
413}