use std::collections::HashMap;
use crate::conversion::Conversion;
use crate::error::{Error, Result};
use crate::expr::{Expr, is_valid_symbol};
use crate::kind::Kind;
use crate::prefix::{Prefix, PrefixMode, all_prefixes};
use crate::quantity::Quantity;
use crate::ratio::Ratio;
use crate::unit::{AmbiguousDef, Unit, UnitDef, UnitExpr};
const PREFIX_SOURCE: &str =
"SI Brochure, 9th ed. (2019), SI prefixes; IEC 80000-13 (binary prefixes)";
#[derive(Debug, Clone, Default)]
pub struct Registry {
units: Vec<Unit>,
by_symbol: HashMap<String, usize>,
ambiguous: HashMap<String, Vec<String>>,
lowered: HashMap<String, Vec<String>>,
}
impl Registry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn from_defs(units: &[UnitDef], ambiguous: &[AmbiguousDef]) -> Result<Self> {
let mut reg = Self::new();
for a in ambiguous {
reg.declare_ambiguous(a)?;
}
for def in units {
reg.register(Unit::from_def(def)?)?;
}
Ok(reg)
}
#[must_use]
pub fn len(&self) -> usize {
self.units.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.units.is_empty()
}
#[must_use]
pub fn units(&self) -> &[Unit] {
&self.units
}
#[must_use]
pub fn name_is_free(&self, name: &str) -> bool {
if self.by_symbol.contains_key(name) {
return false;
}
self.ambiguous.contains_key(name)
|| matches!(self.resolve_symbol(name), Err(Error::UnknownUnit { .. }))
}
#[must_use]
pub fn prefix_clash(&self, name: &str, mode: PrefixMode) -> Option<String> {
all_prefixes()
.filter(|p| mode.accepts(p.family))
.map(|p| format!("{}{name}", p.symbol))
.find(|c| self.by_symbol.contains_key(c) || self.ambiguous.contains_key(c))
}
pub fn add_alias(&mut self, alias: &str, target: &str) -> Result<()> {
let bad = |symbol: &str, message: String| Error::InvalidDefinition {
symbol: symbol.to_string(),
message,
};
if !is_valid_symbol(alias) {
return Err(bad(
alias,
format!("'{alias}' is not a well-formed unit name"),
));
}
if !self.name_is_free(alias) {
return Err(bad(
alias,
format!("'{alias}' already means a unit and cannot be redefined"),
));
}
let Some(&index) = self.by_symbol.get(target) else {
return Err(bad(
alias,
format!(
"'{target}' is not a unit symbol or alias known at this point (a target must be a table unit, a [[unit]], or a name mapped earlier)"
),
));
};
if let Some(clash) = self
.units
.get(index)
.and_then(|u| self.prefix_clash(alias, u.prefixes()))
{
return Err(bad(
alias,
format!("'{alias}' would hide '{clash}', which already means something else"),
));
}
self.lowered
.entry(alias.to_lowercase())
.or_default()
.push(alias.to_string());
self.ambiguous.remove(alias);
self.by_symbol.insert(alias.to_string(), index);
Ok(())
}
pub fn declare_ambiguous(&mut self, def: &AmbiguousDef) -> Result<()> {
if !is_valid_symbol(def.symbol) {
return Err(Error::InvalidDefinition {
symbol: def.symbol.to_string(),
message: "ambiguous symbol is not well-formed".to_string(),
});
}
if self.by_symbol.contains_key(def.symbol) {
return Err(Error::DuplicateSymbol {
symbol: def.symbol.to_string(),
});
}
let mut candidates: Vec<String> = def.candidates.iter().map(|c| (*c).to_string()).collect();
candidates.extend(def.planned.iter().map(|p| format!("{p} (planned)")));
self.lowered
.entry(def.symbol.to_lowercase())
.or_default()
.push(def.symbol.to_string());
self.ambiguous.insert(def.symbol.to_string(), candidates);
Ok(())
}
pub fn register(&mut self, unit: Unit) -> Result<()> {
let names: Vec<&str> = core::iter::once(unit.symbol())
.chain(unit.aliases().iter().map(String::as_str))
.collect();
for (i, name) in names.iter().enumerate() {
if self.ambiguous.contains_key(*name) {
return Err(Error::InvalidDefinition {
symbol: (*name).to_string(),
message: "this bare symbol is declared ambiguous and cannot name a unit"
.to_string(),
});
}
let repeated = names.iter().take(i).any(|n| n == name);
if repeated || self.by_symbol.contains_key(*name) {
return Err(Error::DuplicateSymbol {
symbol: (*name).to_string(),
});
}
}
let index = self.units.len();
for name in names {
self.by_symbol.insert(name.to_string(), index);
self.lowered
.entry(name.to_lowercase())
.or_default()
.push(name.to_string());
}
self.units.push(unit);
Ok(())
}
#[must_use]
pub fn get(&self, symbol: &str) -> Option<&Unit> {
self.by_symbol.get(symbol).and_then(|&i| self.units.get(i))
}
#[must_use]
pub fn ambiguous_candidates(&self, symbol: &str) -> Option<&[String]> {
self.ambiguous.get(symbol).map(Vec::as_slice)
}
fn unit_expr(&self, text: &str, unit: &Unit, prefix: Option<Prefix>) -> Result<UnitExpr> {
let (factor, mut sources) = match prefix {
Some(p) => (
unit.factor().checked_mul(p.factor()?)?,
vec![PREFIX_SOURCE.to_string()],
),
None => (unit.factor(), Vec::new()),
};
if !unit.source().is_empty() {
sources.insert(0, unit.source().to_string());
}
let difference = if prefix.is_none() && unit.kind() == Kind::AbsoluteTemperature {
self.get(&format!("delta_{}", unit.symbol()))
.filter(|d| {
d.kind() == Kind::TemperatureDifference
&& d.dim() == unit.dim()
&& d.factor() == unit.factor()
})
.map(|d| d.symbol().to_string())
} else {
None
};
Ok(UnitExpr::from_parts(
Expr::Unit(text.to_string()),
unit.dim(),
unit.kind(),
factor,
unit.offset(),
unit.exactness(),
sources,
)
.with_difference(difference))
}
fn suggestions(&self, symbol: &str) -> Vec<String> {
let mut out: Vec<String> = self
.lowered
.get(&symbol.to_lowercase())
.map(|names| {
names
.iter()
.filter(|n| n.as_str() != symbol)
.cloned()
.collect()
})
.unwrap_or_default();
out.sort();
out.dedup();
out.truncate(5);
out
}
pub fn resolve_symbol(&self, symbol: &str) -> Result<UnitExpr> {
if let Some(c) = self.ambiguous.get(symbol) {
return Err(Error::AmbiguousSymbol {
symbol: symbol.to_string(),
candidates: c.clone(),
});
}
if let Some(unit) = self.get(symbol) {
return self.unit_expr(symbol, unit, None);
}
let mut hits: Vec<(Prefix, &Unit)> = Vec::new();
let mut ambiguous_rest: Vec<(Prefix, &Vec<String>)> = Vec::new();
for p in all_prefixes() {
let Some(rest) = symbol.strip_prefix(p.symbol).filter(|r| !r.is_empty()) else {
continue;
};
if let Some(c) = self.ambiguous.get(rest) {
ambiguous_rest.push((p, c));
}
if let Some(unit) = self.get(rest).filter(|u| u.prefixes().accepts(p.family)) {
hits.push((p, unit));
}
}
if let (false, Some((p, cands))) = (hits.is_empty(), ambiguous_rest.first()) {
return Err(Error::AmbiguousSymbol {
symbol: symbol.to_string(),
candidates: cands.iter().map(|c| format!("{}{c}", p.symbol)).collect(),
});
}
match hits.as_slice() {
[(p, unit)] => self.unit_expr(symbol, unit, Some(*p)),
[] => {
if let Some((p, cands)) = ambiguous_rest.first() {
return Err(Error::AmbiguousSymbol {
symbol: symbol.to_string(),
candidates: cands.iter().map(|c| format!("{}{c}", p.symbol)).collect(),
});
}
Err(Error::UnknownUnit {
symbol: symbol.to_string(),
suggestions: self.suggestions(symbol),
})
}
many => Err(Error::AmbiguousSymbol {
symbol: symbol.to_string(),
candidates: many
.iter()
.map(|(p, u)| format!("{} + {}", p.symbol, u.symbol()))
.collect(),
}),
}
}
pub fn resolve(&self, expr: &Expr) -> Result<UnitExpr> {
match expr {
Expr::One => Ok(UnitExpr::unity()),
Expr::Unit(s) => self.resolve_symbol(s),
Expr::Mul(a, b) => self.resolve(a)?.mul(&self.resolve(b)?),
Expr::Div(a, b) => self.resolve(a)?.div(&self.resolve(b)?),
Expr::Pow(a, n) => self.resolve(a)?.pow(*n),
}
}
pub fn parse(&self, input: &str) -> Result<UnitExpr> {
self.resolve(&Expr::parse(input)?)
}
pub fn conversion(&self, from: &str, to: &str) -> Result<Conversion> {
Conversion::between(&self.parse(from)?, &self.parse(to)?)
}
pub fn quantity(&self, input: &str) -> Result<Quantity> {
let (value, rest) = Ratio::parse_prefix(input.trim())?;
let rest = rest.trim();
let unit = if rest.is_empty() {
UnitExpr::unity()
} else {
self.parse(rest)?
};
Ok(Quantity::new(value, unit))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dim::Dim;
use crate::kind::Kind;
use crate::prefix::PrefixMode;
fn reg() -> Registry {
let mut r = Registry::new();
let si = PrefixMode::Si;
let add = |r: &mut Registry, u: Unit| r.register(u).unwrap();
add(
&mut r,
Unit::linear("m", Dim::M, Ratio::ONE)
.unwrap()
.with_prefixes(si),
);
add(
&mut r,
Unit::linear("s", Dim::S, Ratio::ONE)
.unwrap()
.with_prefixes(si),
);
add(
&mut r,
Unit::linear("g", Dim::KG, Ratio::new(1, 1000).unwrap())
.unwrap()
.with_prefixes(si),
);
add(
&mut r,
Unit::linear("h", Dim::S, Ratio::from_int(3600)).unwrap(),
);
add(
&mut r,
Unit::linear("min", Dim::S, Ratio::from_int(60)).unwrap(),
);
add(
&mut r,
Unit::linear("mi", Dim::M, Ratio::new(1_609_344, 1000).unwrap()).unwrap(),
);
add(
&mut r,
Unit::linear("t", Dim::KG, Ratio::from_int(1000))
.unwrap()
.with_prefixes(si),
);
add(
&mut r,
Unit::linear("ft", Dim::M, Ratio::new(381, 1250).unwrap()).unwrap(),
);
add(
&mut r,
Unit::linear(
"cal_th",
Dim::new([1, 2, -2, 0, 0, 0, 0]),
Ratio::new(523, 125).unwrap(),
)
.unwrap()
.with_prefixes(si),
);
add(
&mut r,
Unit::linear("byte", Dim::NONE, Ratio::from_int(8))
.unwrap()
.with_prefixes(PrefixMode::SiAndBinary),
);
add(
&mut r,
Unit::new(
"K",
Dim::K,
Kind::AbsoluteTemperature,
Ratio::ONE,
Ratio::ZERO,
)
.unwrap()
.with_prefixes(si),
);
add(
&mut r,
Unit::new(
"degC",
Dim::K,
Kind::AbsoluteTemperature,
Ratio::ONE,
Ratio::new(5463, 20).unwrap(),
)
.unwrap(),
);
r.declare_ambiguous(&AmbiguousDef {
symbol: "cal",
candidates: &["cal_th", "cal_IT"],
planned: &[],
note: "",
})
.unwrap();
r.declare_ambiguous(&AmbiguousDef {
symbol: "pt",
candidates: &["pt_us"],
planned: &["pt_imp"],
note: "",
})
.unwrap();
r
}
#[test]
fn exact_symbols_beat_prefix_splits() {
let r = reg();
assert_eq!(r.parse("min").unwrap().factor(), Ratio::from_int(60));
assert_eq!(r.parse("mi").unwrap().dim(), Dim::M);
assert_eq!(
r.parse("ft").unwrap().factor(),
Ratio::new(381, 1250).unwrap()
);
assert_eq!(r.parse("kg").unwrap().factor(), Ratio::ONE);
assert_eq!(
r.parse("mg").unwrap().factor(),
Ratio::new(1, 1_000_000).unwrap()
);
}
#[test]
fn all_si_prefixes_resolve_on_metre() {
let r = reg();
for p in crate::prefix::SI_PREFIXES {
let e = r.parse(&format!("{}m", p.symbol)).unwrap();
assert_eq!(e.factor(), p.factor().unwrap(), "{}", p.name);
}
assert_eq!(
r.parse("Qm").unwrap().factor(),
Ratio::from_int(10_i128.pow(30))
);
assert_eq!(
r.parse("qm").unwrap().factor(),
Ratio::new(1, 10_i128.pow(30)).unwrap()
);
assert_eq!(
r.parse("um").unwrap().factor(),
r.parse("\u{b5}m").unwrap().factor()
);
}
#[test]
fn binary_prefixes() {
let r = reg();
assert!(r.parse("KiB").is_err()); assert_eq!(
r.parse("Kibyte").unwrap().factor(),
Ratio::from_int(8 * 1024)
);
assert_eq!(r.parse("kbyte").unwrap().factor(), Ratio::from_int(8000));
assert!(
r.parse("Kim").is_err(),
"metre does not accept binary prefixes"
);
}
#[test]
fn ambiguous_bare_symbols_are_rejected_with_candidates() {
let r = reg();
match r.parse("cal") {
Err(Error::AmbiguousSymbol { symbol, candidates }) => {
assert_eq!(symbol, "cal");
assert_eq!(candidates, ["cal_th", "cal_IT"]);
}
other => panic!("{other:?}"),
}
match r.parse("kcal") {
Err(Error::AmbiguousSymbol { candidates, .. }) => {
assert_eq!(candidates, ["kcal_th", "kcal_IT"]);
}
other => panic!("{other:?}"),
}
assert_eq!(
r.parse("kcal_th").unwrap().factor(),
Ratio::new(523, 125)
.unwrap()
.checked_mul(Ratio::from_int(1000))
.unwrap()
);
let e = r.parse("pt").unwrap_err();
assert_eq!(e.candidates(), ["pt_us", "pt_imp (planned)"]);
assert_eq!(e.code(), "ambiguous_symbol");
assert!(r.parse("m/cal").is_err());
}
#[test]
fn unknown_units_suggest_case_fixes() {
let r = reg();
assert_eq!(r.parse("KG").unwrap_err().code(), "unknown_unit");
match r.parse("H") {
Err(Error::UnknownUnit {
symbol,
suggestions,
}) => {
assert_eq!(symbol, "H");
assert_eq!(suggestions, ["h"]);
}
other => panic!("{other:?}"),
}
assert_eq!(r.parse("zork").unwrap_err().code(), "unknown_unit");
assert_eq!(r.parse("Mm").unwrap().factor(), Ratio::from_int(1_000_000));
}
#[test]
fn compound_units_compose_once() {
let r = reg();
let c = r.conversion("km/h", "m/s").unwrap();
assert_eq!(c.factor(), Ratio::new(5, 18).unwrap());
let c = r.conversion("m/s", "km/h").unwrap();
assert_eq!(c.factor(), Ratio::new(18, 5).unwrap());
assert_eq!(c.apply(10.0), 36.0);
assert_eq!(
r.parse("m2").unwrap().dim(),
Dim::new([0, 2, 0, 0, 0, 0, 0])
);
assert_eq!(
r.conversion("km2", "m^2").unwrap().factor(),
Ratio::from_int(1_000_000)
);
assert_eq!(
r.conversion("1/min", "s-1").unwrap().factor(),
Ratio::new(1, 60).unwrap()
);
assert_eq!(
r.conversion("g/(m.s)", "kg/(m.s)").unwrap().factor(),
Ratio::new(1, 1000).unwrap()
);
}
#[test]
fn dimension_and_kind_mismatch() {
let r = reg();
assert_eq!(
r.conversion("m", "s").unwrap_err().code(),
"dimension_mismatch"
);
assert_eq!(
r.conversion("K", "m").unwrap_err().code(),
"dimension_mismatch"
);
}
#[test]
fn affine_units_cannot_join_compounds() {
let r = reg();
assert_eq!(r.parse("degC/s").unwrap_err().code(), "refused");
assert_eq!(r.parse("degC2").unwrap_err().code(), "refused");
assert!(r.parse("degC").is_ok());
assert!(r.parse("degC^1").is_ok());
assert!(r.parse("1.degC").is_ok());
}
#[test]
fn temperature_conversion_is_affine_and_exact() {
let r = reg();
let c = r.conversion("degC", "K").unwrap();
assert_eq!(
c.apply_exact(Ratio::ZERO).unwrap(),
Ratio::new(5463, 20).unwrap()
);
let back = r.conversion("K", "degC").unwrap();
assert_eq!(
back.apply_exact(Ratio::new(5463, 20).unwrap()).unwrap(),
Ratio::ZERO
);
assert_eq!(
c.then(&back)
.unwrap()
.apply_exact(Ratio::from_int(37))
.unwrap(),
Ratio::from_int(37)
);
}
#[test]
fn register_rejects_duplicates_and_ambiguous_names() {
let mut r = reg();
assert_eq!(
r.register(Unit::linear("m", Dim::M, Ratio::ONE).unwrap())
.unwrap_err()
.code(),
"duplicate_symbol"
);
assert_eq!(
r.register(Unit::linear("cal", Dim::M, Ratio::ONE).unwrap())
.unwrap_err()
.code(),
"invalid_definition"
);
let u = Unit::linear("zz", Dim::M, Ratio::ONE)
.unwrap()
.with_aliases(&["zz"])
.unwrap();
assert_eq!(r.register(u).unwrap_err().code(), "duplicate_symbol");
let before = r.len();
let u = Unit::linear("yy", Dim::M, Ratio::ONE)
.unwrap()
.with_aliases(&["ft"])
.unwrap();
assert!(r.register(u).is_err());
assert_eq!(
r.len(),
before,
"failed registration must not leave partial state"
);
assert!(r.get("yy").is_none());
}
#[test]
fn quantities_parse() {
let r = reg();
let q = r.quantity(" 1.1 km ").unwrap();
assert_eq!(q.value(), Ratio::new(11, 10).unwrap());
assert!(r.quantity("5").unwrap().unit().is_unity());
assert!(r.quantity("km").is_err());
}
#[test]
fn overflow_in_compound_units_is_an_error_not_a_wrap() {
let r = reg();
assert_eq!(r.parse("Qm2").unwrap_err().code(), "overflow");
assert!(r.parse("Qm").is_ok());
assert_eq!(r.parse("m^200").unwrap_err().code(), "overflow");
}
}