use std::collections::HashMap;
use std::sync::OnceLock;
use crate::types::F;
use super::dimension::Dimension;
use super::error::UnitsError;
use super::quantity::Quantity;
use super::unit::Unit;
#[derive(Clone, Debug, PartialEq)]
pub struct UnitDef {
pub name: String,
pub aliases: Vec<String>,
pub symbol: String,
pub factor: F,
pub offset: F,
pub dimension: Dimension,
pub prefixable: bool,
}
const PREFIXES: &[(&str, F)] = &[
("da", 1e1),
("y", 1e-24),
("z", 1e-21),
("a", 1e-18),
("f", 1e-15),
("p", 1e-12),
("n", 1e-9),
("u", 1e-6),
("µ", 1e-6),
("m", 1e-3),
("c", 1e-2),
("d", 1e-1),
("h", 1e2),
("k", 1e3),
("M", 1e6),
("G", 1e9),
("T", 1e12),
("P", 1e15),
("E", 1e18),
("Z", 1e21),
("Y", 1e24),
];
pub struct UnitRegistry {
defs: Vec<UnitDef>,
index: HashMap<String, usize>,
}
static GLOBAL_REGISTRY: OnceLock<UnitRegistry> = OnceLock::new();
impl UnitRegistry {
pub fn new() -> UnitRegistry {
let mut r = UnitRegistry::empty();
for def in md_defs() {
r.define(def)
.expect("preloaded MD unit table must be collision-free");
}
r
}
pub fn empty() -> UnitRegistry {
let mut r = UnitRegistry {
defs: Vec::new(),
index: HashMap::new(),
};
for def in base_defs() {
r.define(def)
.expect("SI base unit table must be collision-free");
}
r
}
pub fn global() -> &'static UnitRegistry {
GLOBAL_REGISTRY.get_or_init(UnitRegistry::new)
}
pub fn define(&mut self, def: UnitDef) -> Result<(), UnitsError> {
let mut keys: Vec<&str> = Vec::with_capacity(2 + def.aliases.len());
keys.push(&def.name);
if def.symbol != def.name {
keys.push(&def.symbol);
}
for alias in &def.aliases {
if !keys.contains(&alias.as_str()) {
keys.push(alias);
}
}
for key in &keys {
if self.index.contains_key(*key) {
return Err(UnitsError::Redefinition {
name: (*key).to_string(),
});
}
}
let idx = self.defs.len();
let keys: Vec<String> = keys.into_iter().map(str::to_string).collect();
self.defs.push(def);
for key in keys {
self.index.insert(key, idx);
}
Ok(())
}
pub fn parse(&self, expr: &str) -> Result<Unit, UnitsError> {
super::parse::parse_expr(self, expr)
}
pub fn quantity(&self, value: F, expr: &str) -> Result<Quantity, UnitsError> {
Ok(Quantity::new(value, self.parse(expr)?))
}
pub fn definitions(&self) -> impl Iterator<Item = &UnitDef> {
self.defs.iter()
}
pub(crate) fn resolve_atom(&self, atom: &str) -> Option<(F, F, Dimension)> {
if let Some(&idx) = self.index.get(atom) {
let def = &self.defs[idx];
return Some((def.factor, def.offset, def.dimension));
}
for (prefix, scale) in PREFIXES {
let Some(rest) = atom.strip_prefix(prefix) else {
continue;
};
if rest.is_empty() {
continue;
}
if let Some(&idx) = self.index.get(rest) {
let def = &self.defs[idx];
if def.prefixable && def.offset == 0.0 {
return Some((scale * def.factor, 0.0, def.dimension));
}
}
}
None
}
}
fn def(
name: &str,
symbol: &str,
aliases: &[&str],
factor: F,
offset: F,
dimension: Dimension,
prefixable: bool,
) -> UnitDef {
UnitDef {
name: name.to_string(),
aliases: aliases.iter().map(|s| s.to_string()).collect(),
symbol: symbol.to_string(),
factor,
offset,
dimension,
prefixable,
}
}
fn base_defs() -> Vec<UnitDef> {
const LUMINOSITY: Dimension = Dimension::from_exponents([0, 0, 0, 0, 0, 0, 1]);
vec![
def("meter", "m", &[], 1.0, 0.0, Dimension::LENGTH, true),
def("kilogram", "kg", &[], 1.0, 0.0, Dimension::MASS, false),
def("gram", "g", &[], 1e-3, 0.0, Dimension::MASS, true),
def("second", "s", &["sec"], 1.0, 0.0, Dimension::TIME, true),
def("ampere", "A", &[], 1.0, 0.0, Dimension::CURRENT, true),
def("kelvin", "K", &[], 1.0, 0.0, Dimension::TEMPERATURE, true),
def("mole", "mol", &[], 1.0, 0.0, Dimension::AMOUNT, true),
def("candela", "cd", &[], 1.0, 0.0, LUMINOSITY, true),
]
}
fn md_defs() -> Vec<UnitDef> {
const CHARGE_LENGTH: Dimension = Dimension::from_exponents([1, 0, 1, 1, 0, 0, 0]);
let l = Dimension::LENGTH;
let e = Dimension::ENERGY;
vec![
def("angstrom", "Å", &["ang"], 1e-10, 0.0, l, false),
def("bohr", "bohr", &["a0"], 5.291_772_109_03e-11, 0.0, l, false),
def("joule", "J", &[], 1.0, 0.0, e, true),
def("calorie", "cal", &[], 4.184, 0.0, e, true),
def("electron_volt", "eV", &[], 1.602_176_634e-19, 0.0, e, true),
def("hartree", "Eh", &[], 4.359_744_722_207_1e-18, 0.0, e, false),
def("newton", "N", &[], 1.0, 0.0, Dimension::FORCE, true),
def("pascal", "Pa", &[], 1.0, 0.0, Dimension::PRESSURE, true),
def("bar", "bar", &[], 1e5, 0.0, Dimension::PRESSURE, false),
def(
"atmosphere",
"atm",
&[],
101_325.0,
0.0,
Dimension::PRESSURE,
false,
),
def("minute", "min", &[], 60.0, 0.0, Dimension::TIME, false),
def("hour", "h", &[], 3600.0, 0.0, Dimension::TIME, false),
def(
"dalton",
"Da",
&["amu"],
1.660_539_066_60e-27,
0.0,
Dimension::MASS,
true,
),
def(
"degC",
"degC",
&["celsius", "°C"],
1.0,
273.15,
Dimension::TEMPERATURE,
false,
),
def(
"radian",
"rad",
&[],
1.0,
0.0,
Dimension::DIMENSIONLESS,
true,
),
def(
"degree",
"deg",
&[],
std::f64::consts::PI / 180.0,
0.0,
Dimension::DIMENSIONLESS,
false,
),
def("coulomb", "C", &[], 1.0, 0.0, Dimension::CHARGE, true),
def(
"elementary_charge",
"e",
&[],
1.602_176_634e-19,
0.0,
Dimension::CHARGE,
false,
),
def(
"debye",
"D",
&[],
3.335_640_951_98e-30,
0.0,
CHARGE_LENGTH,
false,
),
]
}
impl Default for UnitRegistry {
fn default() -> UnitRegistry {
UnitRegistry::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn custom_def(name: &str, aliases: &[&str]) -> UnitDef {
UnitDef {
name: name.to_string(),
aliases: aliases.iter().map(|s| s.to_string()).collect(),
symbol: name.to_string(),
factor: 1.0,
offset: 0.0,
dimension: Dimension::LENGTH,
prefixable: false,
}
}
#[test]
fn define_new_unit_ok() {
let mut r = UnitRegistry::empty();
assert!(r.define(custom_def("smoot", &["sm"])).is_ok());
assert!(r.parse("smoot").is_ok());
}
#[test]
fn redefinition_is_error() {
let mut r = UnitRegistry::empty();
r.define(custom_def("smoot", &[])).unwrap();
let err = r.define(custom_def("smoot", &[])).unwrap_err();
assert!(matches!(err, UnitsError::Redefinition { .. }));
}
#[test]
fn alias_collision_is_error() {
let mut r = UnitRegistry::empty();
r.define(custom_def("smoot", &["sm"])).unwrap();
let err = r.define(custom_def("widget", &["sm"])).unwrap_err();
assert!(matches!(err, UnitsError::Redefinition { .. }));
}
#[test]
fn custom_registry_independent_of_global() {
let mut r = UnitRegistry::empty();
r.define(custom_def("smoot", &[])).unwrap();
assert!(UnitRegistry::global().parse("smoot").is_err());
assert!(UnitRegistry::new().parse("smoot").is_err());
}
#[test]
fn empty_has_si_base_only() {
let r = UnitRegistry::empty();
assert!(r.parse("meter").is_ok());
assert!(r.parse("kg").is_ok());
assert!(r.parse("s").is_ok());
assert!(r.parse("mol").is_ok());
assert!(r.parse("K").is_ok());
assert!(r.parse("kcal").is_err());
assert!(r.parse("angstrom").is_err());
assert!(r.parse("eV").is_err());
}
#[test]
fn new_has_md_units() {
let r = UnitRegistry::new();
assert!(r.parse("kcal").is_ok());
assert!(r.parse("angstrom").is_ok());
assert!(r.parse("eV").is_ok());
assert!(r.parse("hartree").is_ok());
assert!(r.parse("bohr").is_ok());
assert!(r.parse("atm").is_ok());
}
#[test]
fn quantity_convenience() {
let r = UnitRegistry::new();
let q = r.quantity(1.5, "kcal").unwrap();
assert_eq!(q.value(), 1.5);
}
#[test]
fn defined_unit_converts_correctly() {
let mut r = UnitRegistry::empty();
let mut def = custom_def("smoot", &[]);
def.factor = 1.7018;
r.define(def).unwrap();
let smoot = r.parse("smoot").unwrap();
let meter = r.parse("m").unwrap();
let factor = smoot.factor_to(&meter).unwrap();
assert!(
((factor - 1.7018) / 1.7018).abs() <= 1e-15,
"smoot->m factor = {factor}"
);
let q = r.quantity(2.0, "smoot").unwrap().to(&meter).unwrap();
assert!(
((q.value() - 3.4036) / 3.4036).abs() <= 1e-15,
"2 smoot = {} m",
q.value()
);
}
#[test]
fn defined_prefixable_unit_accepts_prefix() {
let mut r = UnitRegistry::empty();
let mut def = custom_def("blip", &[]);
def.factor = 2.0;
def.dimension = Dimension::TIME;
def.prefixable = true;
r.define(def).unwrap();
let kblip = r.parse("kblip").unwrap();
let blip = r.parse("blip").unwrap();
assert_eq!(kblip.factor_to(&blip).unwrap(), 1000.0);
assert_eq!(kblip.dimension(), Dimension::TIME);
}
#[test]
fn definitions_iterates_all_defs() {
let r = UnitRegistry::empty();
let base_count = r.definitions().count();
assert!(base_count >= 8, "SI base set missing: {base_count}");
let mut r = UnitRegistry::empty();
r.define(custom_def("smoot", &[])).unwrap();
assert!(r.definitions().any(|d| d.name == "smoot"));
assert!(UnitRegistry::new().definitions().count() > base_count);
}
}