1/** 2 * Scalable Number Formatter 3 * Provides locale-aware number formatting for all supported languages 4 * 5 * This is an ADDITIVE feature - it does not replace existing formatting logic. 6 * Usage is opt-in via feature flag to ensure backward compatibility. 7 * 8 * @module NumberFormatter 9 */ 10 11import { getLocaleConfig } from './locale-config.js'; 12import { getLanguage } from './utils.js'; 13 14/** 15 * NumberFormatter class 16 * Handles all number formatting with locale awareness 17 */ 18class NumberFormatter { 19 constructor(langCode = null) { 20 this.langCode = langCode || getLanguage(); 21 this.config = getLocaleConfig(this.langCode); 22 } 23 24 /** 25 * Format a basic number with locale-specific separators 26 * @param {number} value - The number to format 27 * @param {Object} options - Formatting options 28 * @param {number} options.decimals - Number of decimal places 29 * @param {boolean} options.useGrouping - Use thousands separator (default: true) 30 * @returns {string} Formatted number 31 */ 32 formatNumber(value, options = {}) { 33 try { 34 const numericValue = Number(value); 35 if (isNaN(numericValue)) { 36 return String(value); 37 } 38 39 const { 40 decimals = null, 41 useGrouping = true, 42 } = options; 43 44 // Use Intl.NumberFormat if configured and available 45 if (this.config.useIntlNumberFormat && typeof Intl !== 'undefined') { 46 const formatOptions = { 47 useGrouping, 48 }; 49 50 if (decimals !== null) { 51 formatOptions.minimumFractionDigits = decimals; 52 formatOptions.maximumFractionDigits = decimals; 53 } else { 54 formatOptions.minimumFractionDigits = Number.isInteger(numericValue) ? 0 : 1; 55 formatOptions.maximumFractionDigits = Number.isInteger(numericValue) ? 0 : 2; 56 } 57 58 return new Intl.NumberFormat(this.config.locale, formatOptions).format(numericValue); 59 } 60 61 // Fallback to manual formatting 62 return this._manualFormatNumber(numericValue, decimals, useGrouping); 63 } catch (error) { 64 console.warn('NumberFormatter.formatNumber error:', error); 65 return String(value); 66 } 67 } 68 69 /** 70 * Format a percentage with locale-specific rules 71 * @param {number} value - The percentage value (e.g., 6.3 for 6.3%) 72 * @param {Object} options - Formatting options 73 * @param {number} options.decimals - Number of decimal places 74 * @returns {string} Formatted percentage 75 */ 76 formatPercentage(value, options = {}) { 77 try { 78 const numericValue = Number(value); 79 if (isNaN(numericValue)) { 80 return String(value); 81 } 82 83 const { decimals = 1 } = options; 84 const formattedNumber = this.formatNumber(numericValue, { decimals, useGrouping: false }); 85 86 // Apply locale-specific percentage format 87 return this.config.percentageFormat.replace('{value}', formattedNumber); 88 } catch (error) { 89 console.warn('NumberFormatter.formatPercentage error:', error); 90 return String(value); 91 } 92 } 93 94 /** 95 * Format currency with locale-specific rules 96 * @param {number} value - The currency value 97 * @param {string} currency - Currency code (e.g., 'USD', 'EUR') 98 * @param {Object} options - Formatting options 99 * @param {number} options.decimals - Number of decimal places 100 * @param {boolean} options.compact - Use compact notation (e.g., $1.2M) 101 * @returns {string} Formatted currency 102 */ 103 formatCurrency(value, currency = 'USD', options = {}) { 104 try { 105 const numericValue = Number(value); 106 if (isNaN(numericValue)) { 107 return String(value); 108 } 109 110 const { decimals = 2, compact = false } = options; 111 112 let formattedNumber; 113 if (compact) { 114 formattedNumber = this.formatCompact(numericValue, { decimals }); 115 } else { 116 formattedNumber = this.formatNumber(numericValue, { decimals }); 117 } 118 119 // Apply locale-specific currency format 120 let result = this.config.currencyFormat 121 .replace('{value}', formattedNumber) 122 .replace('{currency}', currency); 123 124 return result; 125 } catch (error) { 126 console.warn('NumberFormatter.formatCurrency error:', error); 127 return String(value); 128 } 129 } 130 131 /** 132 * Format number with compact notation (K, M, B, T) 133 * @param {number} value - The number to format 134 * @param {Object} options - Formatting options 135 * @param {number} options.decimals - Number of decimal places 136 * @returns {string} Formatted compact number 137 */ 138 formatCompact(value, options = {}) { 139 try { 140 const numericValue = Number(value); 141 if (isNaN(numericValue)) { 142 return String(value); 143 } 144 145 const { decimals = 1 } = options; 146 const absValue = Math.abs(numericValue); 147 148 let result; 149 const notation = this.config.compactNotation; 150 151 if (absValue >= 1_000_000_000_000) { 152 result = this.formatNumber(numericValue / 1_000_000_000_000, { decimals, useGrouping: false }) + notation.trillion; 153 } else if (absValue >= 1_000_000_000) { 154 result = this.formatNumber(numericValue / 1_000_000_000, { decimals, useGrouping: false }) + notation.billion; 155 } else if (absValue >= 1_000_000) { 156 result = this.formatNumber(numericValue / 1_000_000, { decimals, useGrouping: false }) + notation.million; 157 } else if (absValue >= 1_000) { 158 result = this.formatNumber(numericValue / 1_000, { decimals, useGrouping: false }) + notation.thousand; 159 } else { 160 result = this.formatNumber(numericValue, { decimals }); 161 } 162 163 return result; 164 } catch (error) { 165 console.warn('NumberFormatter.formatCompact error:', error); 166 return String(value); 167 } 168 } 169 170 /** 171 * Manual number formatting (fallback when Intl is not available) 172 * @private 173 */ 174 _manualFormatNumber(value, decimals, useGrouping) { 175 const decimalPlaces = decimals !== null ? decimals : (Number.isInteger(value) ? 0 : 2); 176 let [intPart, decPart] = value.toFixed(decimalPlaces).split('.'); 177 178 // Add thousands separator 179 if (useGrouping && this.config.thousandsSeparator) { 180 intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, this.config.thousandsSeparator); 181 } 182 183 // Combine with decimal separator 184 if (decPart && decimalPlaces > 0) { 185 return intPart + this.config.decimalSeparator + decPart; 186 } 187 188 return intPart; 189 } 190 191 /** 192 * Get the current locale configuration 193 * @returns {Object} Locale configuration 194 */ 195 getConfig() { 196 return this.config; 197 } 198 199 /** 200 * Change the locale 201 * @param {string} langCode - New language code 202 */ 203 setLocale(langCode) { 204 this.langCode = langCode;
205 this.config = getLocaleConfig(langCode); 206 } 207} 208 209// Export singleton instance for convenience 210let _defaultFormatter = null; 211 212/** 213 * Get the default formatter instance (singleton) 214 * @returns {NumberFormatter} 215 */ 216export function getFormatter() { 217 if (!_defaultFormatter) { 218 _defaultFormatter = new NumberFormatter(); 219 } 220 return _defaultFormatter; 221} 222 223/** 224 * Reset the default formatter (useful for testing or language changes) 225 */ 226export function resetFormatter() { 227 _defaultFormatter = null; 228} 229 230/** 231 * Convenience function: Format a number 232 * @param {number} value - Number to format 233 * @param {Object} options - Formatting options 234 * @returns {string} 235 */ 236export function formatNumber(value, options = {}) { 237 return getFormatter().formatNumber(value, options); 238} 239 240/** 241 * Convenience function: Format a percentage 242 * @param {number} value - Percentage value 243 * @param {Object} options - Formatting options 244 * @returns {string} 245 */ 246export function formatPercentage(value, options = {}) { 247 return getFormatter().formatPercentage(value, options); 248} 249 250/** 251 * Convenience function: Format currency 252 * @param {number} value - Currency value 253 * @param {string} currency - Currency code 254 * @param {Object} options - Formatting options 255 * @returns {string} 256 */ 257export function formatCurrency(value, currency = 'USD', options = {}) { 258 return getFormatter().formatCurrency(value, currency, options); 259} 260 261/** 262 * Convenience function: Format compact number 263 * @param {number} value - Number to format 264 * @param {Object} options - Formatting options 265 * @returns {string} 266 */ 267export function formatCompact(value, options = {}) { 268 return getFormatter().formatCompact(value, options); 269} 270 271export default NumberFormatter;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.