use std::sync::OnceLock;
use bicmath_core::error::{EngineError, ErrorCode};
use bicmath_core::value::Dimension;
use num_bigint::BigInt;
use num_rational::BigRational;
use num_traits::ToPrimitive;
pub const REGISTRY_VERSION: &str = "1.0.0";
#[derive(Clone, Debug)]
pub enum Factor {
Exact(BigRational),
Approx(f64),
}
impl Factor {
pub fn as_exact(&self) -> Option<&BigRational> {
match self {
Factor::Exact(value) => Some(value),
Factor::Approx(_) => None,
}
}
pub fn to_f64(&self) -> f64 {
match self {
Factor::Exact(value) => value.to_f64().unwrap_or(f64::NAN),
Factor::Approx(value) => *value,
}
}
}
#[derive(Clone, Debug)]
pub struct Unit {
pub id: &'static str,
pub symbol: &'static str,
pub name: &'static str,
pub kind: &'static str,
pub dimension: Dimension,
pub factor: Factor,
pub offset: Option<BigRational>,
pub exact_factor: bool,
pub aliases: &'static [&'static str],
pub affine: bool,
pub notes: &'static str,
}
fn exact(numer: i128, denom: i128) -> Factor {
Factor::Exact(BigRational::new(BigInt::from(numer), BigInt::from(denom)))
}
fn exact_decimal(mantissa: i128, scale: u32) -> Factor {
Factor::Exact(BigRational::new(
BigInt::from(mantissa),
BigInt::from(10i128.pow(scale)),
))
}
fn approx(value: f64) -> Factor {
Factor::Approx(value)
}
fn affine_offset(numer: i128, denom: i128) -> Option<BigRational> {
Some(BigRational::new(BigInt::from(numer), BigInt::from(denom)))
}
#[allow(clippy::too_many_arguments)]
fn unit(
id: &'static str,
symbol: &'static str,
name: &'static str,
kind: &'static str,
dimension: Dimension,
factor: Factor,
offset: Option<BigRational>,
exact_factor: bool,
aliases: &'static [&'static str],
affine: bool,
notes: &'static str,
) -> Unit {
Unit {
id,
symbol,
name,
kind,
dimension,
factor,
offset,
exact_factor,
aliases,
affine,
notes,
}
}
fn build_registry() -> Vec<Unit> {
let length = Dimension::new([1, 0, 0, 0, 0, 0, 0, 0]);
let mass = Dimension::new([0, 1, 0, 0, 0, 0, 0, 0]);
let time = Dimension::new([0, 0, 1, 0, 0, 0, 0, 0]);
let current = Dimension::new([0, 0, 0, 1, 0, 0, 0, 0]);
let temperature = Dimension::new([0, 0, 0, 0, 1, 0, 0, 0]);
let amount = Dimension::new([0, 0, 0, 0, 0, 1, 0, 0]);
let luminous = Dimension::new([0, 0, 0, 0, 0, 0, 1, 0]);
let angle = Dimension::new([0, 0, 0, 0, 0, 0, 0, 1]);
let area = Dimension::new([2, 0, 0, 0, 0, 0, 0, 0]);
let volume = Dimension::new([3, 0, 0, 0, 0, 0, 0, 0]);
let speed = Dimension::new([1, 0, -1, 0, 0, 0, 0, 0]);
let pressure = Dimension::new([-1, 1, -2, 0, 0, 0, 0, 0]);
let energy = Dimension::new([2, 1, -2, 0, 0, 0, 0, 0]);
let power = Dimension::new([2, 1, -3, 0, 0, 0, 0, 0]);
let illuminance = Dimension::new([-2, 0, 0, 0, 0, 0, 1, 0]);
vec![
unit(
"meter",
"m",
"meter",
"length",
length,
exact(1, 1),
None,
true,
&["metre", "meters", "metres"],
false,
"SI base unit of length.",
),
unit(
"kilometer",
"km",
"kilometer",
"length",
length,
exact(1000, 1),
None,
true,
&["kilometre", "kilometers", "kilometres"],
false,
"",
),
unit(
"centimeter",
"cm",
"centimeter",
"length",
length,
exact(1, 100),
None,
true,
&["centimetre", "centimeters", "centimetres"],
false,
"",
),
unit(
"millimeter",
"mm",
"millimeter",
"length",
length,
exact(1, 1000),
None,
true,
&["millimetre", "millimeters", "millimetres"],
false,
"",
),
unit(
"micrometer",
"um",
"micrometer",
"length",
length,
exact(1, 1_000_000),
None,
true,
&[
"µm",
"micrometre",
"micrometers",
"micrometres",
"micron",
"microns",
],
false,
"",
),
unit(
"nanometer",
"nm",
"nanometer",
"length",
length,
exact(1, 1_000_000_000),
None,
true,
&["nanometre", "nanometers", "nanometres"],
false,
"",
),
unit(
"mile",
"mi",
"mile",
"length",
length,
exact_decimal(1609344, 3),
None,
true,
&["statute_mile", "miles"],
false,
"",
),
unit(
"yard",
"yd",
"yard",
"length",
length,
exact_decimal(9144, 4),
None,
true,
&["yards"],
false,
"",
),
unit(
"foot",
"ft",
"foot",
"length",
length,
exact_decimal(3048, 4),
None,
true,
&["feet"],
false,
"",
),
unit(
"inch",
"in",
"inch",
"length",
length,
exact_decimal(254, 4),
None,
true,
&["inches"],
false,
"",
),
unit(
"nautical_mile",
"nmi",
"nautical mile",
"length",
length,
exact(1852, 1),
None,
true,
&["nautical_miles", "NM"],
false,
"",
),
unit(
"angstrom",
"angstrom",
"angstrom",
"length",
length,
exact(1, 10_000_000_000),
None,
true,
&["Ã…", "angstroms"],
false,
"",
),
unit(
"kilogram",
"kg",
"kilogram",
"mass",
mass,
exact(1, 1),
None,
true,
&["kilograms", "kilogramme", "kilogrammes"],
false,
"SI base unit of mass.",
),
unit(
"gram",
"g",
"gram",
"mass",
mass,
exact(1, 1000),
None,
true,
&["grams", "gramme", "grammes"],
false,
"",
),
unit(
"milligram",
"mg",
"milligram",
"mass",
mass,
exact(1, 1_000_000),
None,
true,
&["milligrams"],
false,
"",
),
unit(
"tonne",
"t",
"tonne",
"mass",
mass,
exact(1000, 1),
None,
true,
&["metric_ton", "metric_tons", "tonnes"],
false,
"Metric tonne. The unqualified identifier \"ton\" is ambiguous and rejected.",
),
unit(
"pound",
"lb",
"pound",
"mass",
mass,
exact_decimal(45359237, 8),
None,
true,
&["lbs", "pounds", "pound_mass"],
false,
"",
),
unit(
"ounce",
"oz",
"ounce",
"mass",
mass,
exact_decimal(28349523125, 12),
None,
true,
&["ounces"],
false,
"Avoirdupois mass ounce. The unqualified \"fluid_ounce\" is ambiguous; use us_fluid_ounce or imperial_fluid_ounce.",
),
unit(
"stone",
"st",
"stone",
"mass",
mass,
exact_decimal(635029318, 8),
None,
true,
&["stones"],
false,
"",
),
unit(
"second",
"s",
"second",
"time",
time,
exact(1, 1),
None,
true,
&["sec", "seconds"],
false,
"SI base unit of time.",
),
unit(
"millisecond",
"ms",
"millisecond",
"time",
time,
exact(1, 1000),
None,
true,
&["milliseconds"],
false,
"",
),
unit(
"microsecond",
"us",
"microsecond",
"time",
time,
exact(1, 1_000_000),
None,
true,
&["µs", "microseconds"],
false,
"",
),
unit(
"minute",
"min",
"minute",
"time",
time,
exact(60, 1),
None,
true,
&["minutes"],
false,
"",
),
unit(
"hour",
"h",
"hour",
"time",
time,
exact(3600, 1),
None,
true,
&["hr", "hrs", "hours"],
false,
"",
),
unit(
"day",
"d",
"day",
"time",
time,
exact(86400, 1),
None,
true,
&["days"],
false,
"",
),
unit(
"week",
"week",
"week",
"time",
time,
exact(604800, 1),
None,
true,
&["weeks", "wk"],
false,
"",
),
unit(
"ampere",
"A",
"ampere",
"current",
current,
exact(1, 1),
None,
true,
&["amp", "amps", "amperes"],
false,
"SI base unit of electric current.",
),
unit(
"milliampere",
"mA",
"milliampere",
"current",
current,
exact(1, 1000),
None,
true,
&["milliamps", "milliamperes"],
false,
"",
),
unit(
"kelvin",
"K",
"kelvin",
"temperature_absolute",
temperature,
exact(1, 1),
affine_offset(0, 1),
true,
&["degK", "kelvins"],
false,
"SI base unit of thermodynamic temperature. Absolute but not affine: the offset is zero.",
),
unit(
"degree_celsius",
"degC",
"degree Celsius",
"temperature_absolute",
temperature,
exact(1, 1),
affine_offset(5463, 20),
true,
&["°C", "celsius", "degrees_celsius"],
true,
"Absolute temperature (affine, offset 273.15 K). Differences use units.temperature_difference, which ignores the offset (kind temperature_delta).",
),
unit(
"degree_fahrenheit",
"degF",
"degree Fahrenheit",
"temperature_absolute",
temperature,
exact(5, 9),
affine_offset(45967, 180),
true,
&["°F", "fahrenheit", "degrees_fahrenheit"],
true,
"Absolute temperature (affine, factor 5/9 and offset 459.67*5/9 K). Differences use units.temperature_difference, which ignores the offset (kind temperature_delta).",
),
unit(
"mole",
"mol",
"mole",
"amount",
amount,
exact(1, 1),
None,
true,
&["moles"],
false,
"SI base unit of amount of substance.",
),
unit(
"millimole",
"mmol",
"millimole",
"amount",
amount,
exact(1, 1000),
None,
true,
&["millimoles"],
false,
"",
),
unit(
"candela",
"cd",
"candela",
"luminous",
luminous,
exact(1, 1),
None,
true,
&["candelas"],
false,
"SI base unit of luminous intensity.",
),
unit(
"lumen",
"lm",
"lumen",
"luminous",
luminous,
exact(1, 1),
None,
true,
&["lumens"],
false,
"Luminous flux based on the candela. This catalog omits the steradian, so lumen shares the luminous dimension with candela.",
),
unit(
"lux",
"lx",
"lux",
"luminous",
illuminance,
exact(1, 1),
None,
true,
&["luxes"],
false,
"Illuminance: lumen per square meter (luminous*length^-2).",
),
unit(
"radian",
"rad",
"radian",
"angle",
angle,
exact(1, 1),
None,
true,
&["radians"],
false,
"SI coherent unit of plane angle.",
),
unit(
"degree",
"deg",
"degree",
"angle",
angle,
approx(std::f64::consts::PI / 180.0),
None,
false,
&["degrees"],
false,
"Plane angle. pi/180 is irrational; the factor is a float64 approximation with exact_factor=false.",
),
unit(
"gradian",
"grad",
"gradian",
"angle",
angle,
approx(std::f64::consts::PI / 200.0),
None,
false,
&["gradians", "gon"],
false,
"Plane angle. pi/200 is irrational; the factor is a float64 approximation with exact_factor=false.",
),
unit(
"turn",
"turn",
"turn",
"angle",
angle,
approx(2.0 * std::f64::consts::PI),
None,
false,
&["turns", "revolution", "revolutions", "rev"],
false,
"One full revolution. 2*pi is irrational; the factor is a float64 approximation with exact_factor=false.",
),
unit(
"square_meter",
"m2",
"square meter",
"area",
area,
exact(1, 1),
None,
true,
&[
"m^2",
"sqm",
"square_meters",
"square_metre",
"square_metres",
],
false,
"",
),
unit(
"square_kilometer",
"km2",
"square kilometer",
"area",
area,
exact(1_000_000, 1),
None,
true,
&["km^2", "square_kilometers"],
false,
"",
),
unit(
"hectare",
"ha",
"hectare",
"area",
area,
exact(10_000, 1),
None,
true,
&["hectares"],
false,
"",
),
unit(
"acre",
"acre",
"acre",
"area",
area,
exact_decimal(40468564224, 7),
None,
true,
&["acres"],
false,
"",
),
unit(
"square_foot",
"ft2",
"square foot",
"area",
area,
exact_decimal(9290304, 8),
None,
true,
&["ft^2", "sqft", "square_feet"],
false,
"",
),
unit(
"square_mile",
"mi2",
"square mile",
"area",
area,
exact_decimal(2589988110336, 6),
None,
true,
&["mi^2", "square_miles"],
false,
"",
),
unit(
"cubic_meter",
"m3",
"cubic meter",
"volume",
volume,
exact(1, 1),
None,
true,
&["m^3", "cubic_meters"],
false,
"",
),
unit(
"liter",
"L",
"liter",
"volume",
volume,
exact(1, 1000),
None,
true,
&["l", "liters", "litre", "litres"],
false,
"",
),
unit(
"milliliter",
"mL",
"milliliter",
"volume",
volume,
exact(1, 1_000_000),
None,
true,
&["ml", "milliliters", "millilitre", "millilitres"],
false,
"",
),
unit(
"cubic_centimeter",
"cm3",
"cubic centimeter",
"volume",
volume,
exact(1, 1_000_000),
None,
true,
&["cm^3", "cc", "cubic_centimeters"],
false,
"",
),
unit(
"us_gallon",
"gal_us",
"US liquid gallon",
"volume",
volume,
exact_decimal(3785411784, 12),
None,
true,
&["us_gal", "us_gallons"],
false,
"US liquid gallon, exactly 231 cubic inches.",
),
unit(
"us_quart",
"qt_us",
"US liquid quart",
"volume",
volume,
exact_decimal(946352946, 12),
None,
true,
&["us_quarts"],
false,
"",
),
unit(
"us_pint",
"pt_us",
"US liquid pint",
"volume",
volume,
exact_decimal(473176473, 12),
None,
true,
&["us_pints"],
false,
"",
),
unit(
"us_cup",
"cup_us",
"US customary cup",
"volume",
volume,
exact_decimal(2365882365, 13),
None,
true,
&["us_cups"],
false,
"",
),
unit(
"us_fluid_ounce",
"floz_us",
"US fluid ounce",
"volume",
volume,
exact_decimal(295735295625, 16),
None,
true,
&["us_fl_oz", "us_fluid_ounces"],
false,
"US customary fluid ounce.",
),
unit(
"us_tablespoon",
"tbsp_us",
"US tablespoon",
"volume",
volume,
exact_decimal(1478676478125, 17),
None,
true,
&["us_tablespoons"],
false,
"",
),
unit(
"us_teaspoon",
"tsp_us",
"US teaspoon",
"volume",
volume,
exact_decimal(492892159375, 17),
None,
true,
&["us_teaspoons"],
false,
"",
),
unit(
"imperial_gallon",
"gal_uk",
"imperial gallon",
"volume",
volume,
exact_decimal(454609, 8),
None,
true,
&["uk_gallon", "imperial_gallons"],
false,
"Imperial gallon, exactly 4.54609 liters.",
),
unit(
"imperial_fluid_ounce",
"floz_uk",
"imperial fluid ounce",
"volume",
volume,
exact_decimal(284130625, 13),
None,
true,
&["uk_fluid_ounce", "imperial_fluid_ounces"],
false,
"Imperial fluid ounce.",
),
unit(
"cubic_inch",
"in3",
"cubic inch",
"volume",
volume,
exact_decimal(16387064, 12),
None,
true,
&["in^3", "cubic_inches"],
false,
"",
),
unit(
"cubic_foot",
"ft3",
"cubic foot",
"volume",
volume,
exact_decimal(28316846592, 12),
None,
true,
&["ft^3", "cubic_feet"],
false,
"",
),
unit(
"meter_per_second",
"m/s",
"meter per second",
"speed",
speed,
exact(1, 1),
None,
true,
&["mps", "meters_per_second"],
false,
"",
),
unit(
"kilometer_per_hour",
"km/h",
"kilometer per hour",
"speed",
speed,
exact(5, 18),
None,
true,
&["kph", "kilometers_per_hour"],
false,
"",
),
unit(
"mile_per_hour",
"mph",
"mile per hour",
"speed",
speed,
exact_decimal(44704, 5),
None,
true,
&["miles_per_hour"],
false,
"",
),
unit(
"knot",
"kn",
"knot",
"speed",
speed,
exact(463, 900),
None,
true,
&["kt", "knots"],
false,
"One nautical mile per hour.",
),
unit(
"pascal",
"Pa",
"pascal",
"pressure",
pressure,
exact(1, 1),
None,
true,
&["pascals"],
false,
"SI coherent unit of pressure.",
),
unit(
"kilopascal",
"kPa",
"kilopascal",
"pressure",
pressure,
exact(1000, 1),
None,
true,
&["kilopascals"],
false,
"",
),
unit(
"bar",
"bar",
"bar",
"pressure",
pressure,
exact(100_000, 1),
None,
true,
&["bars"],
false,
"",
),
unit(
"atmosphere",
"atm",
"standard atmosphere",
"pressure",
pressure,
exact(101325, 1),
None,
true,
&["atmospheres", "standard_atmosphere"],
false,
"",
),
unit(
"psi",
"psi",
"pound per square inch",
"pressure",
pressure,
exact_decimal(6894757293168, 9),
None,
true,
&["PSI", "pounds_per_square_inch"],
false,
"Pound-force per square inch.",
),
unit(
"millimeter_mercury",
"mmHg",
"millimeter of mercury",
"pressure",
pressure,
exact_decimal(133322387415, 9),
None,
true,
&["millimeters_mercury", "mm_hg"],
false,
"",
),
unit(
"torr",
"torr",
"torr",
"pressure",
pressure,
exact(101325, 760),
None,
true,
&["Torr", "torrs"],
false,
"",
),
unit(
"joule",
"J",
"joule",
"energy",
energy,
exact(1, 1),
None,
true,
&["joules"],
false,
"SI coherent unit of energy.",
),
unit(
"kilojoule",
"kJ",
"kilojoule",
"energy",
energy,
exact(1000, 1),
None,
true,
&["kilojoules"],
false,
"",
),
unit(
"calorie",
"cal",
"calorie",
"energy",
energy,
exact_decimal(4184, 3),
None,
true,
&["calories"],
false,
"Thermochemical calorie, exactly 4.184 J.",
),
unit(
"kilocalorie",
"kcal",
"kilocalorie",
"energy",
energy,
exact(4184, 1),
None,
true,
&["kilocalories", "Calorie", "Calories"],
false,
"",
),
unit(
"watt_hour",
"Wh",
"watt hour",
"energy",
energy,
exact(3600, 1),
None,
true,
&["watt_hours"],
false,
"",
),
unit(
"kilowatt_hour",
"kWh",
"kilowatt hour",
"energy",
energy,
exact(3_600_000, 1),
None,
true,
&["kilowatt_hours"],
false,
"",
),
unit(
"british_thermal_unit",
"BTU",
"British thermal unit",
"energy",
energy,
exact_decimal(105505585262, 8),
None,
true,
&["BTUs", "btu", "btus"],
false,
"",
),
unit(
"electronvolt",
"eV",
"electronvolt",
"energy",
energy,
exact_decimal(1602176634, 28),
None,
true,
&["electronvolts", "electron_volts"],
false,
"Defined exactly as 1.602176634e-19 J.",
),
unit(
"watt",
"W",
"watt",
"power",
power,
exact(1, 1),
None,
true,
&["watts"],
false,
"SI coherent unit of power.",
),
unit(
"kilowatt",
"kW",
"kilowatt",
"power",
power,
exact(1000, 1),
None,
true,
&["kilowatts"],
false,
"",
),
unit(
"megawatt",
"MW",
"megawatt",
"power",
power,
exact(1_000_000, 1),
None,
true,
&["megawatts"],
false,
"",
),
unit(
"horsepower",
"hp",
"mechanical horsepower",
"power",
power,
exact_decimal(7456998715822702, 13),
None,
true,
&["mechanical_horsepower"],
false,
"",
),
]
}
pub fn registry() -> &'static [Unit] {
static REGISTRY: OnceLock<Vec<Unit>> = OnceLock::new();
REGISTRY.get_or_init(build_registry)
}
const AMBIGUOUS: &[(&str, &[&str])] = &[
("gallon", &["us_gallon", "imperial_gallon"]),
("gal", &["us_gallon", "imperial_gallon"]),
("ton", &["tonne"]),
("fluid_ounce", &["us_fluid_ounce", "imperial_fluid_ounce"]),
("floz", &["us_fluid_ounce", "imperial_fluid_ounce"]),
("fl_oz", &["us_fluid_ounce", "imperial_fluid_ounce"]),
];
pub fn find_unit(id: &str) -> Result<&'static Unit, EngineError> {
let key = id.trim();
if let Some((_, alternatives)) = AMBIGUOUS.iter().find(|(name, _)| *name == key) {
return Err(EngineError::new(
ErrorCode::DomainViolation,
format!(
"unit identifier {key:?} is ambiguous; use one of: {}",
alternatives.join(", ")
),
)
.with_details(serde_json::json!({
"unit": key,
"alternatives": alternatives,
})));
}
if let Some(found) = registry().iter().find(|candidate| {
candidate.id == key || candidate.symbol == key || candidate.aliases.contains(&key)
}) {
return Ok(found);
}
let known: Vec<&str> = registry().iter().map(|candidate| candidate.id).collect();
Err(EngineError::new(
ErrorCode::DomainViolation,
format!("unknown unit {key:?}; use units.list_units to see the registry"),
)
.with_details(serde_json::json!({
"unit": key,
"known_units": known,
})))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ids_symbols_and_aliases_are_unambiguous() {
let mut seen: Vec<(&str, &str)> = Vec::new();
for candidate in registry() {
assert!(!candidate.id.is_empty());
assert!(candidate.factor.as_exact().is_some() == candidate.exact_factor);
for key in std::iter::once(candidate.id)
.chain(std::iter::once(candidate.symbol))
.chain(candidate.aliases.iter().copied())
{
if let Some((_, owner)) = seen.iter().find(|(existing, _)| *existing == key) {
assert_eq!(
*owner, candidate.id,
"unit key {key:?} is registered by both {owner} and {}",
candidate.id
);
}
seen.push((key, candidate.id));
}
}
}
#[test]
fn affine_units_have_offsets() {
for candidate in registry() {
if candidate.affine {
assert!(
candidate.offset.is_some(),
"affine unit {} needs an offset",
candidate.id
);
}
}
}
}