UNPKG

@atomic-ehr/ucum

Version:

TypeScript implementation of UCUM (Unified Code for Units of Measure)

221 lines (220 loc) 7.62 kB
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); }