@atomic-ehr/ucum
Version:
TypeScript implementation of UCUM (Unified Code for Units of Measure)
221 lines (220 loc) • 7.62 kB
JavaScript
import { units, baseUnits } from './units.js';
import { prefixes } from './prefixes.js';
import { parseUnit } from './parser/index.js';
import { Dimension } from './dimension.js';
// Helper functions
function isBaseUnit(unit) {
return unit in baseUnits;
}
function getPrefixValue(prefix) {
return prefixes[prefix]?.value ?? 1;
}
function getUnitDefinition(unit) {
const unitData = units[unit];
if (!unitData || unitData.isBaseUnit)
return null;
// Special units have function notation like "cel(1 K)"
if (unitData.isSpecial && unitData.value.function) {
// Parse the unit inside the function (e.g., "1 K" from "cel(1 K)")
const result = parseUnit(unitData.value.function.Unit);
if (result.errors.length > 0) {
throw new Error(`Failed to parse unit definition for ${unit}: ${result.errors[0]?.message || 'Unknown error'}`);
}
return result.ast || null;
}
// Regular units just have the unit expression
const result = parseUnit(unitData.value.Unit);
if (result.errors.length > 0) {
throw new Error(`Failed to parse unit definition for ${unit}: ${result.errors[0]?.message || 'Unknown error'}`);
}
return result.ast || null;
}
function normalizeUnits(units) {
const unitMap = new Map();
for (const term of units) {
unitMap.set(term.unit, (unitMap.get(term.unit) || 0) + term.exponent);
}
return Array.from(unitMap.entries())
.filter(([_, exp]) => exp !== 0)
.map(([unit, exponent]) => ({ unit, exponent }))
.sort((a, b) => a.unit.localeCompare(b.unit));
}
function calculateDimension(units) {
const dimension = {};
const dimensionMap = {
'm': 'L',
'g': 'M',
's': 'T',
'rad': 'A',
'K': 'Θ',
'C': 'Q',
'cd': 'F'
};
for (const term of units) {
const dimKey = dimensionMap[term.unit];
if (dimKey && term.exponent !== 0) {
dimension[dimKey] = term.exponent;
}
}
return dimension;
}
function extractSpecialFunction(unitData) {
if (unitData.value.function) {
return {
name: unitData.value.function.name,
value: unitData.value.function.value,
unit: unitData.value.function.Unit
};
}
return undefined;
}
// Process different AST node types
function processExpression(expr) {
switch (expr.type) {
case 'factor':
return processFactor(expr);
case 'unit':
return processUnit(expr);
case 'binary':
return processBinaryOp(expr);
case 'unary':
return processUnaryOp(expr);
case 'group':
return processGroup(expr);
default:
throw new Error(`Unknown expression type: ${expr.type}`);
}
}
function processFactor(factor) {
return {
magnitude: factor.value,
dimension: {},
units: []
};
}
function processUnit(unit) {
let magnitude = 1;
let baseUnits = [];
let specialFunction;
const unitExponent = unit.exponent || 1;
// Handle prefix - apply exponent to prefix value
if (unit.prefix) {
const prefixValue = getPrefixValue(unit.prefix);
magnitude *= Math.pow(prefixValue, unitExponent);
}
// Check if it's a base unit
if (isBaseUnit(unit.atom)) {
baseUnits.push({ unit: unit.atom, exponent: unitExponent });
}
else {
// Look up the unit
const unitData = units[unit.atom];
if (!unitData) {
throw new Error(`Unknown unit: ${unit.atom}`);
}
// Handle special units
if (unitData.isSpecial) {
specialFunction = extractSpecialFunction(unitData);
// Special units still need to be expanded to their base representation
}
// Handle dimensionless units
if (unitData.value.Unit === '1') {
magnitude *= Math.pow(parseFloat(unitData.value.value), unitExponent);
}
else {
// Expand derived unit
const definition = getUnitDefinition(unit.atom);
if (definition) {
const expanded = processExpression(definition);
// Also multiply by the unit's numeric value (e.g., hour = 60 minutes)
// Special units have value "undefined", regular units have numeric values
const unitValue = unitData.value.value === "undefined" ? 1 : parseFloat(unitData.value.value);
magnitude *= Math.pow(expanded.magnitude * unitValue, unitExponent);
// Apply the unit's exponent to all base units from the expansion
baseUnits = expanded.units.map(term => ({
...term,
exponent: term.exponent * unitExponent
}));
if (!specialFunction && expanded.specialFunction) {
specialFunction = expanded.specialFunction;
}
}
}
}
return {
magnitude,
dimension: calculateDimension(baseUnits),
units: baseUnits,
specialFunction
};
}
function processBinaryOp(op) {
const left = processExpression(op.left);
const right = processExpression(op.right);
if (op.operator === '.') {
// Multiplication
return {
magnitude: left.magnitude * right.magnitude,
dimension: Dimension.multiply(left.dimension, right.dimension),
units: [...left.units, ...right.units],
specialFunction: left.specialFunction || right.specialFunction
};
}
else if (op.operator === '/') {
// Division - negate right side exponents
const negatedRightUnits = right.units.map(term => ({
...term,
exponent: -term.exponent
}));
return {
magnitude: left.magnitude / right.magnitude,
dimension: Dimension.divide(left.dimension, right.dimension),
units: [...left.units, ...negatedRightUnits],
specialFunction: left.specialFunction || right.specialFunction
};
}
else {
throw new Error(`Unknown operator: ${op.operator}`);
}
}
function processUnaryOp(op) {
const operand = processExpression(op.operand);
if (op.operator === '/') {
// Leading division - negate all exponents
const negatedUnits = operand.units.map(term => ({
...term,
exponent: -term.exponent
}));
return {
magnitude: 1 / operand.magnitude,
dimension: Dimension.divide({}, operand.dimension),
units: negatedUnits,
specialFunction: operand.specialFunction
};
}
else {
throw new Error(`Unknown unary operator: ${op.operator}`);
}
}
function processGroup(group) {
return processExpression(group.expression);
}
// Main functions
export function toCanonicalFormFromAST(expr) {
const result = processExpression(expr);
// Normalize the units (combine like terms and sort)
result.units = normalizeUnits(result.units);
// Recalculate dimension from normalized units
result.dimension = calculateDimension(result.units);
return result;
}
export function toCanonicalForm(unitExpression) {
const result = parseUnit(unitExpression);
if (result.errors.length > 0) {
throw new Error(result.errors[0]?.message || 'Unknown parse error');
}
if (!result.ast) {
throw new Error('No AST generated');
}
return toCanonicalFormFromAST(result.ast);
}