UNPKG

@atomic-ehr/ucum

Version:

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

420 lines (355 loc) 11.6 kB
import type { DimensionObject } from './dimension'; import type { CanonicalForm } from './canonical-form'; import { toCanonicalForm } from './canonical-form'; import { convert, ConversionError, IncompatibleDimensionsError } from './conversion'; import { Dimension } from './dimension'; import { units } from './units'; import { parseUnit } from './parser'; // Quantity type representing a value with a unit export interface Quantity { value: number; unit: string; _canonicalForm?: CanonicalForm; // Cached for performance } // Error types for quantity operations export class SpecialUnitArithmeticError extends Error { constructor(unit: string, operation: string) { super(`Cannot perform ${operation} on special unit: ${unit}`); this.name = 'SpecialUnitArithmeticError'; } } export class ArbitraryUnitConversionError extends Error { constructor(from: string, to: string) { super(`Cannot convert between arbitrary units: ${from} and ${to}`); this.name = 'ArbitraryUnitConversionError'; } } // Helper functions for unit classification export function isSpecialUnit(unit: string): boolean { try { const canonical = toCanonicalForm(unit); return canonical.specialFunction !== undefined; } catch { return false; } } function checkArbitraryInExpression(expr: any): boolean { if (!expr) return false; switch (expr.type) { case 'unit': // Check the atom directly const unitData = units[expr.atom]; return unitData?.property === 'arbitrary'; case 'binary': return checkArbitraryInExpression(expr.left) || checkArbitraryInExpression(expr.right); case 'unary': return checkArbitraryInExpression(expr.operand); case 'group': return checkArbitraryInExpression(expr.content); default: return false; } } export function isArbitraryUnit(unit: string): boolean { try { // First check if the unit itself is in the database const unitData = units[unit]; if (unitData?.property === 'arbitrary') { return true; } // For complex expressions, parse and check the AST const parseResult = parseUnit(unit); if (parseResult.ast) { return checkArbitraryInExpression(parseResult.ast); } return false; } catch { return false; } } export function areUnitsCompatible(unit1: string, unit2: string): boolean { try { const canonical1 = toCanonicalForm(unit1); const canonical2 = toCanonicalForm(unit2); return Dimension.equals(canonical1.dimension, canonical2.dimension); } catch { return false; } } // Factory function to create a Quantity export function quantity(value: number, unit: string): Quantity { // Validate the unit by trying to parse it and get canonical form try { // This will validate against the units database toCanonicalForm(unit); } catch (error) { throw new ConversionError(`Invalid unit: ${unit}`); } return { value, unit }; } // Get canonical form with caching function getCanonicalForm(q: Quantity): CanonicalForm { if (!q._canonicalForm) { q._canonicalForm = toCanonicalForm(q.unit); } return q._canonicalForm; } // Addition export function add(q1: Quantity, q2: Quantity): Quantity { // Check for special units if (isSpecialUnit(q1.unit) || isSpecialUnit(q2.unit)) { throw new SpecialUnitArithmeticError( isSpecialUnit(q1.unit) ? q1.unit : q2.unit, 'addition' ); } // Check for arbitrary units const isArb1 = isArbitraryUnit(q1.unit); const isArb2 = isArbitraryUnit(q2.unit); if (isArb1 || isArb2) { // Both must be the same arbitrary unit if (q1.unit !== q2.unit) { throw new ArbitraryUnitConversionError(q1.unit, q2.unit); } // Same arbitrary unit - can add return quantity(q1.value + q2.value, q1.unit); } // Check dimensional compatibility if (!areUnitsCompatible(q1.unit, q2.unit)) { const dim1 = getCanonicalForm(q1).dimension; const dim2 = getCanonicalForm(q2).dimension; throw new IncompatibleDimensionsError(q1.unit, q2.unit, dim1, dim2); } // Convert q2 to q1's unit and add const q2InQ1Units = convert(q2.value, q2.unit, q1.unit); return quantity(q1.value + q2InQ1Units, q1.unit); } // Subtraction export function subtract(q1: Quantity, q2: Quantity): Quantity { // Check for special units if (isSpecialUnit(q1.unit) || isSpecialUnit(q2.unit)) { throw new SpecialUnitArithmeticError( isSpecialUnit(q1.unit) ? q1.unit : q2.unit, 'subtraction' ); } // Check for arbitrary units const isArb1 = isArbitraryUnit(q1.unit); const isArb2 = isArbitraryUnit(q2.unit); if (isArb1 || isArb2) { // Both must be the same arbitrary unit if (q1.unit !== q2.unit) { throw new ArbitraryUnitConversionError(q1.unit, q2.unit); } // Same arbitrary unit - can subtract return quantity(q1.value - q2.value, q1.unit); } // Check dimensional compatibility if (!areUnitsCompatible(q1.unit, q2.unit)) { const dim1 = getCanonicalForm(q1).dimension; const dim2 = getCanonicalForm(q2).dimension; throw new IncompatibleDimensionsError(q1.unit, q2.unit, dim1, dim2); } // Convert q2 to q1's unit and subtract const q2InQ1Units = convert(q2.value, q2.unit, q1.unit); return quantity(q1.value - q2InQ1Units, q1.unit); } // Multiplication export function multiply(q1: Quantity, q2: Quantity | number): Quantity { // Handle scalar multiplication if (typeof q2 === 'number') { return quantity(q1.value * q2, q1.unit); } // Check for special units if (isSpecialUnit(q1.unit) || isSpecialUnit(q2.unit)) { throw new SpecialUnitArithmeticError( isSpecialUnit(q1.unit) ? q1.unit : q2.unit, 'multiplication' ); } // Check for arbitrary units - result is arbitrary if any operand is const isArb1 = isArbitraryUnit(q1.unit); const isArb2 = isArbitraryUnit(q2.unit); // Multiply values const resultValue = q1.value * q2.value; // Combine units let resultUnit: string; if (q1.unit === '1' && q2.unit === '1') { resultUnit = '1'; } else if (q1.unit === '1') { resultUnit = q2.unit; } else if (q2.unit === '1') { resultUnit = q1.unit; } else { // Create compound unit resultUnit = `${q1.unit}.${q2.unit}`; } return quantity(resultValue, resultUnit); } // Division export function divide(q1: Quantity, q2: Quantity | number): Quantity { // Handle scalar division if (typeof q2 === 'number') { if (q2 === 0) { throw new Error('Division by zero'); } return quantity(q1.value / q2, q1.unit); } // Check for division by zero if (q2.value === 0) { throw new Error('Division by zero'); } // Check for special units if (isSpecialUnit(q1.unit) || isSpecialUnit(q2.unit)) { throw new SpecialUnitArithmeticError( isSpecialUnit(q1.unit) ? q1.unit : q2.unit, 'division' ); } // Check for arbitrary units - result is arbitrary if any operand is const isArb1 = isArbitraryUnit(q1.unit); const isArb2 = isArbitraryUnit(q2.unit); // Divide values const resultValue = q1.value / q2.value; // Combine units let resultUnit: string; if (q1.unit === q2.unit) { // Units cancel out resultUnit = '1'; } else if (q2.unit === '1') { resultUnit = q1.unit; } else if (q1.unit === '1') { // Inverse of q2 unit resultUnit = `/${q2.unit}`; } else { // Create compound unit resultUnit = `${q1.unit}/${q2.unit}`; } return quantity(resultValue, resultUnit); } // Power export function pow(q: Quantity, exponent: number): Quantity { // Check for special units if (isSpecialUnit(q.unit)) { throw new SpecialUnitArithmeticError(q.unit, 'exponentiation'); } // Check for arbitrary units if (isArbitraryUnit(q.unit)) { throw new SpecialUnitArithmeticError(q.unit, 'exponentiation'); } // Handle special cases if (exponent === 0) { return quantity(1, '1'); } if (exponent === 1) { return quantity(q.value, q.unit); } // Calculate result value const resultValue = Math.pow(q.value, exponent); // Calculate result unit let resultUnit: string; if (q.unit === '1') { resultUnit = '1'; } else { resultUnit = `${q.unit}${exponent}`; } return quantity(resultValue, resultUnit); } // Comparison operations export function equals(q1: Quantity, q2: Quantity, tolerance: number = 1e-10): boolean { // Check for arbitrary units const isArb1 = isArbitraryUnit(q1.unit); const isArb2 = isArbitraryUnit(q2.unit); if (isArb1 || isArb2) { // Can only compare same arbitrary units if (q1.unit !== q2.unit) { return false; } return Math.abs(q1.value - q2.value) < tolerance; } // Check dimensional compatibility if (!areUnitsCompatible(q1.unit, q2.unit)) { return false; } // Convert and compare try { const q2InQ1Units = convert(q2.value, q2.unit, q1.unit); return Math.abs(q1.value - q2InQ1Units) < tolerance; } catch { return false; } } export function lessThan(q1: Quantity, q2: Quantity): boolean { // Check for arbitrary units const isArb1 = isArbitraryUnit(q1.unit); const isArb2 = isArbitraryUnit(q2.unit); if (isArb1 || isArb2) { // Can only compare same arbitrary units if (q1.unit !== q2.unit) { throw new ArbitraryUnitConversionError(q1.unit, q2.unit); } return q1.value < q2.value; } // Check dimensional compatibility if (!areUnitsCompatible(q1.unit, q2.unit)) { const dim1 = getCanonicalForm(q1).dimension; const dim2 = getCanonicalForm(q2).dimension; throw new IncompatibleDimensionsError(q1.unit, q2.unit, dim1, dim2); } // Convert and compare const q2InQ1Units = convert(q2.value, q2.unit, q1.unit); return q1.value < q2InQ1Units; } export function greaterThan(q1: Quantity, q2: Quantity): boolean { return lessThan(q2, q1); } export function lessThanOrEqual(q1: Quantity, q2: Quantity): boolean { return !greaterThan(q1, q2); } export function greaterThanOrEqual(q1: Quantity, q2: Quantity): boolean { return !lessThan(q1, q2); } // Utility functions export function toUnit(q: Quantity, targetUnit: string): Quantity { // Check for arbitrary units const isArb1 = isArbitraryUnit(q.unit); const isArb2 = isArbitraryUnit(targetUnit); if (isArb1 && isArb2) { // Both are arbitrary - must be the same unit if (q.unit !== targetUnit) { throw new ArbitraryUnitConversionError(q.unit, targetUnit); } // Same unit, no conversion needed return quantity(q.value, q.unit); } else if (isArb1 || isArb2) { // One is arbitrary, one is not - cannot convert throw new ArbitraryUnitConversionError(q.unit, targetUnit); } // Use existing conversion logic const convertedValue = convert(q.value, q.unit, targetUnit); return quantity(convertedValue, targetUnit); } export function getValue(q: Quantity, inUnit?: string): number { if (!inUnit || inUnit === q.unit) { return q.value; } // Check for arbitrary units const isArb1 = isArbitraryUnit(q.unit); const isArb2 = isArbitraryUnit(inUnit); if (isArb1 || isArb2) { if (q.unit !== inUnit) { throw new ArbitraryUnitConversionError(q.unit, inUnit); } return q.value; } return convert(q.value, q.unit, inUnit); } export function areCompatible(q1: Quantity, q2: Quantity): boolean { return areUnitsCompatible(q1.unit, q2.unit); } export function getDimension(q: Quantity): DimensionObject { return getCanonicalForm(q).dimension; }