@atomic-ehr/ucum
Version:
TypeScript implementation of UCUM (Unified Code for Units of Measure)
356 lines (355 loc) • 12.8 kB
JavaScript
import { parseUnit } from './parser/index.js';
import { units } from './units.js';
import { toCanonicalForm } from './canonical-form.js';
import { isArbitraryUnit } from './quantity.js';
// Comprehensive unit names database
const unitNames = {
// Base units
'm': { name: 'meter', plural: 'meters' },
'g': { name: 'gram', plural: 'grams' },
's': { name: 'second', plural: 'seconds' },
'A': { name: 'ampere', plural: 'amperes' },
'K': { name: 'kelvin', plural: 'kelvins' },
'mol': { name: 'mole', plural: 'moles' },
'cd': { name: 'candela', plural: 'candelas' },
'rad': { name: 'radian', plural: 'radians' },
'sr': { name: 'steradian', plural: 'steradians' },
// Time units
'min': { name: 'minute', plural: 'minutes' },
'h': { name: 'hour', plural: 'hours' },
'd': { name: 'day', plural: 'days' },
'wk': { name: 'week', plural: 'weeks' },
'mo': { name: 'month', plural: 'months' },
'a': { name: 'year', plural: 'years' },
// Volume units
'L': { name: 'liter', plural: 'liters' },
'mL': { name: 'milliliter', plural: 'milliliters' },
'dL': { name: 'deciliter', plural: 'deciliters' },
'µL': { name: 'microliter', plural: 'microliters' },
'uL': { name: 'microliter', plural: 'microliters' },
// Mass units
'kg': { name: 'kilogram', plural: 'kilograms' },
'mg': { name: 'milligram', plural: 'milligrams' },
'µg': { name: 'microgram', plural: 'micrograms' },
'ug': { name: 'microgram', plural: 'micrograms' },
'ng': { name: 'nanogram', plural: 'nanograms' },
// Derived SI units
'N': { name: 'newton', plural: 'newtons', symbol: 'N' },
'Pa': { name: 'pascal', plural: 'pascals', symbol: 'Pa' },
'J': { name: 'joule', plural: 'joules', symbol: 'J' },
'W': { name: 'watt', plural: 'watts', symbol: 'W' },
'C': { name: 'coulomb', plural: 'coulombs', symbol: 'C' },
'V': { name: 'volt', plural: 'volts', symbol: 'V' },
'F': { name: 'farad', plural: 'farads', symbol: 'F' },
'Ω': { name: 'ohm', plural: 'ohms', symbol: 'Ω' },
'Ohm': { name: 'ohm', plural: 'ohms', symbol: 'Ω' },
'S': { name: 'siemens', plural: 'siemens', symbol: 'S' },
'Wb': { name: 'weber', plural: 'webers', symbol: 'Wb' },
'T': { name: 'tesla', plural: 'teslas', symbol: 'T' },
'H': { name: 'henry', plural: 'henries', symbol: 'H' },
'Hz': { name: 'hertz', plural: 'hertz', symbol: 'Hz' },
'lm': { name: 'lumen', plural: 'lumens', symbol: 'lm' },
'lx': { name: 'lux', plural: 'lux', symbol: 'lx' },
'Bq': { name: 'becquerel', plural: 'becquerels', symbol: 'Bq' },
'Gy': { name: 'gray', plural: 'grays', symbol: 'Gy' },
'Sv': { name: 'sievert', plural: 'sieverts', symbol: 'Sv' },
'kat': { name: 'katal', plural: 'katals', symbol: 'kat' },
// Temperature units
'Cel': { name: 'degree Celsius', plural: 'degrees Celsius', symbol: '°C' },
'[degF]': { name: 'degree Fahrenheit', plural: 'degrees Fahrenheit', symbol: '°F' },
'[degR]': { name: 'degree Rankine', plural: 'degrees Rankine', symbol: '°R' },
'[degRe]': { name: 'degree Réaumur', plural: 'degrees Réaumur', symbol: '°Ré' },
// Clinical units
'[IU]': { name: 'international unit', plural: 'international units' },
'[pH]': { name: 'pH', plural: 'pH' },
'%': { name: 'percent', plural: 'percent' },
'[ppth]': { name: 'parts per thousand', plural: 'parts per thousand' },
'[ppm]': { name: 'parts per million', plural: 'parts per million' },
'[ppb]': { name: 'parts per billion', plural: 'parts per billion' },
'[pptr]': { name: 'parts per trillion', plural: 'parts per trillion' },
// Common non-SI units
'cal': { name: 'calorie', plural: 'calories' },
'kcal': { name: 'kilocalorie', plural: 'kilocalories' },
'eV': { name: 'electronvolt', plural: 'electronvolts' },
'u': { name: 'unified atomic mass unit', plural: 'unified atomic mass units' },
// Length units
'[in_i]': { name: 'inch', plural: 'inches' },
'[ft_i]': { name: 'foot', plural: 'feet' },
'[yd_i]': { name: 'yard', plural: 'yards' },
'[mi_i]': { name: 'mile', plural: 'miles' },
'[nmi_i]': { name: 'nautical mile', plural: 'nautical miles' },
// Pressure units
'bar': { name: 'bar', plural: 'bars' },
'atm': { name: 'atmosphere', plural: 'atmospheres' },
'mHg': { name: 'millimeter of mercury', plural: 'millimeters of mercury' },
'[psi]': { name: 'pound per square inch', plural: 'pounds per square inch' },
// Angle units
'deg': { name: 'degree', plural: 'degrees' },
"'": { name: 'arcminute', plural: 'arcminutes' },
'"': { name: 'arcsecond', plural: 'arcseconds' },
// Special units
'1': { name: 'one', plural: 'one' },
'{cells}': { name: 'cells', plural: 'cells' },
'{RBC}': { name: 'red blood cells', plural: 'red blood cells' },
'{WBC}': { name: 'white blood cells', plural: 'white blood cells' },
};
// Prefix names mapping
const prefixNames = {
'Y': 'yotta',
'Z': 'zetta',
'E': 'exa',
'P': 'peta',
'T': 'tera',
'G': 'giga',
'M': 'mega',
'k': 'kilo',
'h': 'hecto',
'da': 'deka',
'd': 'deci',
'c': 'centi',
'm': 'milli',
'µ': 'micro',
'u': 'micro',
'n': 'nano',
'p': 'pico',
'f': 'femto',
'a': 'atto',
'z': 'zepto',
'y': 'yocto',
};
/**
* Create enhanced unit information
*/
export function createUnitInfo(unit) {
// Parse the unit to check validity
const parseResult = parseUnit(unit);
if (parseResult.errors.length > 0) {
throw new Error(`Invalid unit: ${unit}`);
}
// Get canonical form
const canonical = toCanonicalForm(unit);
// Try to find unit data (works for simple units)
const unitData = units[unit];
// Determine type
let type = 'derived';
if (unitData?.isBaseUnit) {
type = 'base';
}
else if (canonical.specialFunction) {
type = 'special';
}
else if (unitData?.property === 'arbitrary' || isArbitraryUnit(unit)) {
type = 'arbitrary';
}
else if (Object.keys(canonical.dimension).length === 0) {
type = 'dimensionless';
}
// Generate name
let name;
if (unitData?.name) {
// Use database name if available
name = unitData.name;
}
else if (unitNames[unit]) {
// Use our enhanced names database
name = unitNames[unit].name;
}
else if (parseResult.ast) {
// Generate name from AST
name = generateNameFromAST(parseResult.ast);
}
else {
// Fallback to unit code
name = unit;
}
// Build info object
return {
type,
code: unit,
name,
printSymbol: unitData?.printSymbol,
isMetric: unitData?.isMetric ?? true,
isSpecial: !!canonical.specialFunction,
isArbitrary: unitData?.property === 'arbitrary' || isArbitraryUnit(unit),
isBase: unitData?.isBaseUnit ?? false,
class: unitData?.class,
property: unitData?.property,
dimension: canonical.dimension,
canonical,
definition: unitData?.value?.Unit !== '1' ? unitData?.value?.Unit : undefined
};
}
/**
* Display unit in human-readable format
*/
export function displayUnit(unit, options) {
// Strip annotations
const cleanUnit = unit.replace(/\{[^}]*\}/g, '');
// Handle different formats
if (options?.format === 'symbol' || !options?.format) {
return cleanUnit;
}
if (options.format === 'name' || options.format === 'long') {
// Try simple lookup first
if (unitNames[cleanUnit]) {
const name = unitNames[cleanUnit].name;
if (options.format === 'long' && units[cleanUnit]) {
const unitData = units[cleanUnit];
const definition = unitData?.value?.Unit;
if (definition && definition !== '1' && definition !== cleanUnit) {
return `${name} (${definition})`;
}
}
return name;
}
// Parse and generate name
const parseResult = parseUnit(cleanUnit);
if (parseResult.ast && parseResult.errors.length === 0) {
const name = generateNameFromAST(parseResult.ast);
if (options.format === 'long') {
// Try to add definition for known units
const unitData = units[cleanUnit];
if (unitData?.value?.Unit && unitData.value.Unit !== '1' && unitData.value.Unit !== cleanUnit) {
return `${name} (${unitData.value.Unit})`;
}
}
return name;
}
}
// Fallback to cleaned unit
return cleanUnit;
}
/**
* Generate human-readable name from AST
*/
function generateNameFromAST(ast) {
switch (ast.type) {
case 'unit':
return generateUnitName(ast);
case 'binary':
return generateBinaryName(ast);
case 'unary':
return generateUnaryName(ast);
case 'group':
return generateNameFromAST(ast.expression);
case 'factor':
if (ast.annotation) {
// Handle annotated factors
return ast.annotation;
}
return ast.value.toString();
default:
return '';
}
}
/**
* Generate name for a single unit
*/
function generateUnitName(unit) {
let baseName;
// Get base unit name
const unitNameData = unitNames[unit.atom];
if (unitNameData) {
baseName = unitNameData.name;
}
else {
const unitData = units[unit.atom];
if (unitData?.name) {
baseName = unitData.name;
}
else {
baseName = unit.atom;
}
}
// Handle prefix
if (unit.prefix) {
const prefixName = prefixNames[unit.prefix];
if (prefixName) {
// Special cases for common prefixed units
if (unit.prefix === 'k' && unit.atom === 'g') {
baseName = 'kilogram'; // Special case: kg is the base unit
}
else if (unit.atom === 'L') {
// For liter, combine prefix directly
baseName = prefixName + 'liter';
}
else if (unit.atom === 'g' && unit.prefix !== 'k') {
// For non-kilo gram prefixes
baseName = prefixName + 'gram';
}
else {
// General case
baseName = prefixName + baseName;
}
}
}
// Handle exponent
if (unit.exponent) {
const exp = typeof unit.exponent === 'string' ? parseInt(unit.exponent) : unit.exponent;
if (exp === 2) {
baseName = `square ${baseName}`;
}
else if (exp === 3) {
baseName = `cubic ${baseName}`;
}
else if (exp === -1) {
baseName = `per ${baseName}`;
}
else if (exp < 0) {
baseName = `per ${baseName} to the power of ${Math.abs(exp)}`;
}
else {
baseName = `${baseName} to the power of ${exp}`;
}
}
return baseName;
}
/**
* Generate name for binary operations
*/
function generateBinaryName(binary) {
const leftName = generateNameFromAST(binary.left);
const rightName = generateNameFromAST(binary.right);
if (binary.operator === '.') {
// Multiplication
return `${leftName} ${rightName}`;
}
else {
// Division
return `${leftName} per ${rightName}`;
}
}
/**
* Generate name for unary operations (leading division)
*/
function generateUnaryName(unary) {
const operandName = generateNameFromAST(unary.operand);
return `per ${operandName}`;
}
/**
* Get unit name considering prefixes
*/
function getUnitNameWithPrefix(prefix, unitAtom) {
// Check if we have a direct entry for the prefixed unit
const prefixedUnit = prefix ? prefix + unitAtom : unitAtom;
if (unitNames[prefixedUnit]) {
return unitNames[prefixedUnit].name;
}
// Build name from prefix and base unit
if (prefix && prefixNames[prefix]) {
const baseUnitName = unitNames[unitAtom]?.name || units[unitAtom]?.name || unitAtom;
// Special handling for certain combinations
if (unitAtom === 'g' && prefix === 'k') {
return 'kilogram';
}
else if (unitAtom === 'L') {
return prefixNames[prefix] + 'liter';
}
else if (unitAtom === 'g') {
return prefixNames[prefix] + 'gram';
}
return prefixNames[prefix] + baseUnitName;
}
// No prefix or unknown prefix
return unitNames[unitAtom]?.name || units[unitAtom]?.name || unitAtom;
}