use std::collections::HashMap;
use thiserror::Error;
use crate::P_ATM_STANDARD_KPA;
#[derive(Debug, Error)]
pub enum RegistryError {
#[error("unknown unit: {0}")]
UnknownUnit(String),
#[error("unknown dimension: {0}")]
UnknownDimension(String),
#[error("unit `{0}` already defined (pass overwrite=true to replace)")]
AlreadyDefined(String),
#[error("dimension `{0}` already defined")]
DimensionAlreadyDefined(String),
#[error("dimension mismatch: expected {expected:?}, found {found:?}")]
DimensionMismatch {
expected: DimensionVector,
found: DimensionVector,
},
#[error("non-positive absolute pressure ({0} kPa) — gauge value is below vacuum")]
NonPositivePressure(f64),
#[error("invalid unit string: {0}")]
ParseError(String),
#[error("atmospheric pressure must be > 0; got {0} kPa")]
BadAtmosphericPressure(f64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DimensionVector(pub [i8; 7]);
impl DimensionVector {
pub const fn new(exps: [i8; 7]) -> Self {
DimensionVector(exps)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dimension {
Temperature, TemperatureDiff, Pressure, MolarEnergy, MolarEntropy, MolarVolume, Amount, Custom(&'static str), }
impl Dimension {
pub fn name(&self) -> &str {
match self {
Dimension::Temperature => "temperature",
Dimension::TemperatureDiff => "temperature_diff",
Dimension::Pressure => "pressure",
Dimension::MolarEnergy => "molar_energy",
Dimension::MolarEntropy => "molar_entropy",
Dimension::MolarVolume => "molar_volume",
Dimension::Amount => "amount",
Dimension::Custom(s) => s,
}
}
pub fn vector(&self) -> DimensionVector {
match self {
Dimension::Temperature | Dimension::TemperatureDiff => {
DimensionVector::new([0, 0, 0, 0, 1, 0, 0])
}
Dimension::Pressure => DimensionVector::new([-1, 1, -2, 0, 0, 0, 0]),
Dimension::MolarEnergy => DimensionVector::new([2, 1, -2, 0, 0, -1, 0]),
Dimension::MolarEntropy => DimensionVector::new([2, 1, -2, 0, -1, -1, 0]),
Dimension::MolarVolume => DimensionVector::new([3, 0, 0, 0, 0, -1, 0]),
Dimension::Amount => DimensionVector::new([0, 0, 0, 0, 0, 1, 0]),
Dimension::Custom(_) => DimensionVector::new([0; 7]),
}
}
pub fn from_name(name: &str) -> Option<Dimension> {
Some(match name {
"temperature" => Dimension::Temperature,
"temperature_diff" => Dimension::TemperatureDiff,
"pressure" => Dimension::Pressure,
"molar_energy" => Dimension::MolarEnergy,
"molar_entropy" => Dimension::MolarEntropy,
"molar_volume" => Dimension::MolarVolume,
"amount" => Dimension::Amount,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy)]
enum OffsetSource {
Constant(f64),
GaugePAtm,
}
#[derive(Debug, Clone)]
pub struct UnitDef {
pub name: String,
pub dimension_name: String,
pub dimension_vector: DimensionVector,
pub scale: f64,
offset: OffsetSource,
}
impl UnitDef {
pub fn is_gauge(&self) -> bool {
matches!(self.offset, OffsetSource::GaugePAtm)
}
pub fn constant_offset(&self) -> Option<f64> {
match self.offset {
OffsetSource::Constant(o) => Some(o),
OffsetSource::GaugePAtm => None,
}
}
}
#[derive(Debug, Clone)]
pub struct UnitRegistry {
units: HashMap<String, UnitDef>,
dimensions: HashMap<String, DimensionVector>,
p_atm_kpa: f64,
}
impl UnitRegistry {
pub fn new() -> Self {
UnitRegistry {
units: HashMap::new(),
dimensions: HashMap::new(),
p_atm_kpa: P_ATM_STANDARD_KPA,
}
}
pub fn with_vle_defaults() -> Self {
let mut r = Self::new();
r.install_defaults();
r
}
fn install_defaults(&mut self) {
const DEFAULTS_TOML: &str = include_str!("data/defaults.toml");
self.load_from_toml_str(DEFAULTS_TOML)
.expect("built-in defaults.toml must parse and apply cleanly");
}
fn put(&mut self, u: UnitDef) {
self.units.insert(u.name.clone(), u);
}
pub fn atmospheric_pressure_kpa(&self) -> f64 {
self.p_atm_kpa
}
pub fn set_atmospheric_pressure(&mut self, p_atm_kpa: f64) -> Result<(), RegistryError> {
if !(p_atm_kpa.is_finite() && p_atm_kpa > 0.0) {
return Err(RegistryError::BadAtmosphericPressure(p_atm_kpa));
}
self.p_atm_kpa = p_atm_kpa;
Ok(())
}
pub fn define(
&mut self,
name: &str,
dimension: Dimension,
scale: f64,
offset: f64,
) -> Result<(), RegistryError> {
self.define_with_dimension_name(name, dimension.name(), dimension.vector(), scale, offset)
}
pub fn define_gauge(
&mut self,
name: &str,
scale_kpa_per_unit: f64,
) -> Result<(), RegistryError> {
if self.units.contains_key(name) {
return Err(RegistryError::AlreadyDefined(name.into()));
}
self.put(UnitDef {
name: name.into(),
dimension_name: "pressure".into(),
dimension_vector: Dimension::Pressure.vector(),
scale: scale_kpa_per_unit,
offset: OffsetSource::GaugePAtm,
});
Ok(())
}
pub fn define_dimension(
&mut self,
name: &str,
vector: DimensionVector,
) -> Result<(), RegistryError> {
if Dimension::from_name(name).is_some() || self.dimensions.contains_key(name) {
return Err(RegistryError::DimensionAlreadyDefined(name.into()));
}
self.dimensions.insert(name.into(), vector);
Ok(())
}
pub fn define_with_dimension(
&mut self,
name: &str,
dimension_name: &str,
scale: f64,
offset: f64,
) -> Result<(), RegistryError> {
let vector = self
.lookup_dimension(dimension_name)
.ok_or_else(|| RegistryError::UnknownDimension(dimension_name.into()))?;
self.define_with_dimension_name(name, dimension_name, vector, scale, offset)
}
fn define_with_dimension_name(
&mut self,
name: &str,
dim_name: &str,
vector: DimensionVector,
scale: f64,
offset: f64,
) -> Result<(), RegistryError> {
if self.units.contains_key(name) {
return Err(RegistryError::AlreadyDefined(name.into()));
}
self.put(UnitDef {
name: name.into(),
dimension_name: dim_name.into(),
dimension_vector: vector,
scale,
offset: OffsetSource::Constant(offset),
});
Ok(())
}
fn lookup_dimension(&self, name: &str) -> Option<DimensionVector> {
Dimension::from_name(name)
.map(|d| d.vector())
.or_else(|| self.dimensions.get(name).copied())
}
pub fn get(&self, name: &str) -> Result<&UnitDef, RegistryError> {
self.units
.get(name)
.ok_or_else(|| RegistryError::UnknownUnit(name.into()))
}
pub fn unit_names(&self) -> impl Iterator<Item = &str> {
self.units.keys().map(|s| s.as_str())
}
fn offset_value(&self, u: &UnitDef) -> f64 {
match u.offset {
OffsetSource::Constant(o) => o,
OffsetSource::GaugePAtm => self.p_atm_kpa,
}
}
pub fn to_canonical(&self, value: f64, unit_name: &str) -> Result<f64, RegistryError> {
let u = self.get(unit_name)?;
let result = value * u.scale + self.offset_value(u);
if u.is_gauge() && result <= 0.0 {
return Err(RegistryError::NonPositivePressure(result));
}
Ok(result)
}
pub fn from_canonical(
&self,
value_canonical: f64,
unit_name: &str,
) -> Result<f64, RegistryError> {
let u = self.get(unit_name)?;
Ok((value_canonical - self.offset_value(u)) / u.scale)
}
pub fn parse(&self, s: &str) -> Result<Quantity, RegistryError> {
let (value, unit) = crate::parser::split_value_unit(s)?;
let canonical = self.to_canonical(value, unit)?;
let u = self.get(unit)?;
Ok(Quantity {
canonical,
dimension: u.dimension_name.clone(),
})
}
pub fn format(&self, q: &Quantity, unit_name: &str) -> Result<String, RegistryError> {
let u = self.get(unit_name)?;
if u.dimension_name != q.dimension {
return Err(RegistryError::DimensionMismatch {
expected: self
.lookup_dimension(&q.dimension)
.unwrap_or(DimensionVector::new([0; 7])),
found: u.dimension_vector,
});
}
let v = self.from_canonical(q.canonical, unit_name)?;
Ok(format!("{v} {unit_name}"))
}
}
impl Default for UnitRegistry {
fn default() -> Self {
Self::with_vle_defaults()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Quantity {
pub canonical: f64,
pub dimension: String,
}
impl Quantity {
pub fn value_kpa(&self) -> f64 {
debug_assert_eq!(self.dimension, "pressure");
self.canonical
}
pub fn value_kelvin(&self) -> f64 {
debug_assert_eq!(self.dimension, "temperature");
self.canonical
}
}