PageSourceSearch

https://www.banquemondiale.org/ext/scripts/number-formatter.js

js banquemondiale.org collected 2026-09-24 08:48:55 UTC 8,350 bytes, 271 lines download raw bytes

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.