UNPKG

semantic-ds-toolkit

Version:

Performance-first semantic layer for modern data stacks - Stable Column Anchors & intelligent inference

282 lines 11 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UnitConverter = void 0; const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const yaml_1 = __importDefault(require("yaml")); const fx_cache_1 = require("./fx-cache"); class UnitConverter { fxCache; config; unitDefinitions; aliases; unitMappingsPath; constructor(config = {}) { this.config = { cacheTTL: 3600000, offlineMode: fx_cache_1.OfflineMode.CACHE_FIRST, ...config }; this.unitMappingsPath = this.resolveMappingsPath(this.config.unitMappingsPath); this.fxCache = new fx_cache_1.FXCache(this.config.cacheTTL || 3600000, { fallbackRates: this.config.fallbackRates, defaultMode: this.config.offlineMode, }); this.unitDefinitions = new Map(); this.aliases = new Map(); this.loadUnitsFromYAML(); } resolveMappingsPath(customPath) { const candidatePaths = []; if (customPath) { candidatePaths.push(path_1.default.resolve(customPath)); } const moduleDir = typeof __dirname !== 'undefined' ? __dirname : process.cwd(); candidatePaths.push(path_1.default.resolve(moduleDir, '../data/unit-mappings.yml')); candidatePaths.push(path_1.default.resolve(process.cwd(), 'semantics/mappings/unit-mappings.yml')); for (const candidate of candidatePaths) { if ((0, fs_1.existsSync)(candidate)) { return candidate; } } throw new Error('Unable to locate unit-mappings.yml. Provide unitMappingsPath in configuration.'); } loadUnitsFromYAML() { try { const fileContents = (0, fs_1.readFileSync)(this.unitMappingsPath, 'utf8'); const parsed = yaml_1.default.parse(fileContents); const unitMap = new Map(); const aliasMap = new Map(); const unitsSection = parsed?.units ?? {}; Object.entries(unitsSection).forEach(([categoryKey, unitEntries]) => { const typedCategory = this.normalizeCategory(categoryKey); if (!typedCategory) { return; } const baseSymbol = this.findBaseSymbol(unitEntries); Object.entries(unitEntries).forEach(([unitKey, unitConfig]) => { const canonicalKey = (unitConfig.iso_code || unitKey).toString(); const symbol = (unitConfig.symbol || canonicalKey).toString(); const name = unitConfig.name || canonicalKey; const baseUnitSymbol = unitConfig.base_unit ? canonicalKey : baseSymbol; const definition = { symbol, name, category: typedCategory, baseUnit: baseUnitSymbol, }; if (unitConfig.conversion_type === 'function' && typedCategory === 'temperature') { definition.conversionFunction = this.convertTemperature.bind(this); } if (typeof unitConfig.factor === 'number') { definition.conversionFactor = unitConfig.factor; } else if (unitConfig.base_unit) { definition.conversionFactor = 1; } unitMap.set(canonicalKey, definition); const symbolAliasKey = symbol.toLowerCase(); if (symbolAliasKey !== canonicalKey.toLowerCase()) { aliasMap.set(symbolAliasKey, canonicalKey); } aliasMap.set(canonicalKey.toLowerCase(), canonicalKey); }); }); const aliasSection = parsed?.aliases ?? {}; Object.entries(aliasSection).forEach(([aliasKey, canonical]) => { aliasMap.set(aliasKey.toLowerCase(), canonical); }); this.unitDefinitions = unitMap; this.aliases = aliasMap; } catch (error) { throw new Error(`Failed to load unit definitions: ${error}`); } } normalizeCategory(category) { const normalized = category.toLowerCase(); switch (normalized) { case 'currency': case 'temperature': case 'distance': case 'time': case 'mass': case 'volume': case 'area': return normalized; default: return null; } } findBaseSymbol(entries) { for (const [unitKey, config] of Object.entries(entries)) { if (config.base_unit) { return config.iso_code || unitKey; } } return undefined; } refreshUnitDefinitions() { this.loadUnitsFromYAML(); } normalizeUnit(unit) { if (!unit) { return unit; } const trimmed = unit.trim(); if (this.unitDefinitions.has(trimmed)) { return trimmed; } const upper = trimmed.toUpperCase(); if (this.unitDefinitions.has(upper)) { return upper; } const lower = trimmed.toLowerCase(); const aliasTarget = this.aliases.get(lower); if (aliasTarget && this.unitDefinitions.has(aliasTarget)) { return aliasTarget; } return trimmed; } convertTemperature(value, fromUnit, toUnit) { // Convert to Celsius first let celsius; switch (fromUnit) { case 'C': celsius = value; break; case 'F': celsius = (value - 32) * 5 / 9; break; case 'K': celsius = value - 273.15; break; default: throw new Error(`Unsupported temperature unit: ${fromUnit}`); } // Convert from Celsius to target unit switch (toUnit) { case 'C': return celsius; case 'F': return (celsius * 9 / 5) + 32; case 'K': return celsius + 273.15; default: throw new Error(`Unsupported temperature unit: ${toUnit}`); } } async convert(value, fromUnit, toUnit) { const startTime = Date.now(); const normalizedFrom = this.normalizeUnit(fromUnit); const normalizedTo = this.normalizeUnit(toUnit); if (normalizedFrom === normalizedTo) { return { value, fromUnit: normalizedFrom, toUnit: normalizedTo, timestamp: new Date() }; } const fromDef = this.unitDefinitions.get(normalizedFrom); const toDef = this.unitDefinitions.get(normalizedTo); if (!fromDef || !toDef) { throw new Error(`Unsupported unit conversion: ${fromUnit} to ${toUnit}`); } if (fromDef.category !== toDef.category) { throw new Error(`Cannot convert between different unit categories: ${fromDef.category} to ${toDef.category}`); } let convertedValue; let rate; let metadata; switch (fromDef.category) { case 'currency': { const fxResult = await this.fxCache.getExchangeRate(normalizedFrom, normalizedTo, { mode: this.config.offlineMode, }); convertedValue = value * fxResult.rate; rate = fxResult.rate; metadata = { rateSource: fxResult.source, rateAgeMs: fxResult.ageMs, confidence: fxResult.confidence, audit: { timestamp: fxResult.timestamp, conversion: `${normalizedFrom}${normalizedTo}`, rate: fxResult.rate, source: fxResult.source, stalenessMs: fxResult.ageMs, } }; break; } case 'temperature': if (fromDef.conversionFunction) { convertedValue = fromDef.conversionFunction(value, normalizedFrom, normalizedTo); } else { convertedValue = this.convertTemperature(value, normalizedFrom, normalizedTo); } metadata = { confidence: 1 }; break; case 'distance': case 'time': case 'mass': case 'volume': case 'area': { // Convert to base unit first, then to target unit const baseValue = value * (fromDef.conversionFactor || 1); convertedValue = baseValue / (toDef.conversionFactor || 1); metadata = { confidence: 1 }; break; } default: throw new Error(`Unsupported unit category: ${fromDef.category}`); } const endTime = Date.now(); const elapsed = endTime - startTime; if (elapsed > 50) { console.warn(`Unit conversion took ${elapsed}ms, exceeding 50ms target`); } return { value: convertedValue, fromUnit: normalizedFrom, toUnit: normalizedTo, rate, timestamp: new Date(), metadata }; } async convertBatch(conversions) { const results = await Promise.all(conversions.map(conv => this.convert(conv.value, conv.fromUnit, conv.toUnit))); return results; } getSupportedUnits(category) { if (category) { return Array.from(this.unitDefinitions.values()) .filter(def => def.category === category) .map(def => def.symbol); } return Array.from(this.unitDefinitions.keys()); } getUnitInfo(unit) { const normalized = this.normalizeUnit(unit); return this.unitDefinitions.get(normalized); } addCustomUnit(definition) { this.unitDefinitions.set(definition.symbol, definition); } async prefetchCommonRates(pairs = this.config.commonCurrencyPairs) { const defaults = pairs ?? [ { from: 'USD', to: 'EUR' }, { from: 'EUR', to: 'GBP' }, { from: 'USD', to: 'JPY' } ]; await this.fxCache.preloadRates(defaults, { mode: fx_cache_1.OfflineMode.NETWORK_FIRST }); } } exports.UnitConverter = UnitConverter; //# sourceMappingURL=unit-convert.js.map