diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts index 1041b7bf3e..13d0d61cfa 100644 --- a/packages/common/src/common.ts +++ b/packages/common/src/common.ts @@ -14,7 +14,7 @@ export * from './location/index'; export {NgLocaleLocalization, NgLocalization} from './i18n/localization'; export {registerLocaleData} from './i18n/locale_data'; -export {Plural, NumberFormatStyle, FormStyle, Time, TranslationWidth, FormatWidth, NumberSymbol, WeekDay, getCurrencySymbol, getLocaleDayPeriods, getLocaleDayNames, getLocaleMonthNames, getLocaleId, getLocaleEraNames, getLocaleWeekEndRange, getLocaleFirstDayOfWeek, getLocaleDateFormat, getLocaleDateTimeFormat, getLocaleExtraDayPeriodRules, getLocaleExtraDayPeriods, getLocalePluralCase, getLocaleTimeFormat, getLocaleNumberSymbol, getLocaleNumberFormat, getLocaleCurrencyName, getLocaleCurrencySymbol} from './i18n/locale_data_api'; +export {Plural, NumberFormatStyle, FormStyle, Time, TranslationWidth, FormatWidth, NumberSymbol, WeekDay, getNbOfCurrencyDigits, getCurrencySymbol, getLocaleDayPeriods, getLocaleDayNames, getLocaleMonthNames, getLocaleId, getLocaleEraNames, getLocaleWeekEndRange, getLocaleFirstDayOfWeek, getLocaleDateFormat, getLocaleDateTimeFormat, getLocaleExtraDayPeriodRules, getLocaleExtraDayPeriods, getLocalePluralCase, getLocaleTimeFormat, getLocaleNumberSymbol, getLocaleNumberFormat, getLocaleCurrencyName, getLocaleCurrencySymbol} from './i18n/locale_data_api'; export {parseCookieValue as ɵparseCookieValue} from './cookie'; export {CommonModule, DeprecatedI18NPipesModule} from './common_module'; export {NgClass, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index'; diff --git a/packages/common/src/i18n/currencies.ts b/packages/common/src/i18n/currencies.ts index f041820a89..4bac89d23e 100644 --- a/packages/common/src/i18n/currencies.ts +++ b/packages/common/src/i18n/currencies.ts @@ -12,106 +12,141 @@ export type CurrenciesSymbols = [string] | [string | undefined, string]; /** @internal */ -export const CURRENCIES_EN: {[code: string]: CurrenciesSymbols} = { - 'AOA': [, 'Kz'], - 'ARS': [, '$'], - 'AUD': ['A$', '$'], - 'BAM': [, 'KM'], - 'BBD': [, '$'], - 'BDT': [, '৳'], - 'BMD': [, '$'], - 'BND': [, '$'], - 'BOB': [, 'Bs'], - 'BRL': ['R$'], - 'BSD': [, '$'], - 'BWP': [, 'P'], - 'BYN': [, 'р.'], - 'BZD': [, '$'], - 'CAD': ['CA$', '$'], - 'CLP': [, '$'], - 'CNY': ['CN¥', '¥'], - 'COP': [, '$'], - 'CRC': [, '₡'], - 'CUC': [, '$'], - 'CUP': [, '$'], - 'CZK': [, 'Kč'], - 'DKK': [, 'kr'], - 'DOP': [, '$'], - 'EGP': [, 'E£'], - 'ESP': [, '₧'], - 'EUR': ['€'], - 'FJD': [, '$'], - 'FKP': [, '£'], - 'GBP': ['£'], - 'GEL': [, '₾'], - 'GIP': [, '£'], - 'GNF': [, 'FG'], - 'GTQ': [, 'Q'], - 'GYD': [, '$'], - 'HKD': ['HK$', '$'], - 'HNL': [, 'L'], - 'HRK': [, 'kn'], - 'HUF': [, 'Ft'], - 'IDR': [, 'Rp'], - 'ILS': ['₪'], - 'INR': ['₹'], - 'ISK': [, 'kr'], - 'JMD': [, '$'], - 'JPY': ['¥'], - 'KHR': [, '៛'], - 'KMF': [, 'CF'], - 'KPW': [, '₩'], - 'KRW': ['₩'], - 'KYD': [, '$'], - 'KZT': [, '₸'], - 'LAK': [, '₭'], - 'LBP': [, 'L£'], - 'LKR': [, 'Rs'], - 'LRD': [, '$'], - 'LTL': [, 'Lt'], - 'LVL': [, 'Ls'], - 'MGA': [, 'Ar'], - 'MMK': [, 'K'], - 'MNT': [, '₮'], - 'MUR': [, 'Rs'], - 'MXN': ['MX$', '$'], - 'MYR': [, 'RM'], - 'NAD': [, '$'], - 'NGN': [, '₦'], - 'NIO': [, 'C$'], - 'NOK': [, 'kr'], - 'NPR': [, 'Rs'], - 'NZD': ['NZ$', '$'], - 'PHP': [, '₱'], - 'PKR': [, 'Rs'], - 'PLN': [, 'zł'], - 'PYG': [, '₲'], - 'RON': [, 'lei'], - 'RUB': [, '₽'], - 'RUR': [, 'р.'], - 'RWF': [, 'RF'], - 'SBD': [, '$'], - 'SEK': [, 'kr'], - 'SGD': [, '$'], - 'SHP': [, '£'], - 'SRD': [, '$'], - 'SSP': [, '£'], - 'STD': [, 'Db'], - 'SYP': [, '£'], - 'THB': [, '฿'], - 'TOP': [, 'T$'], - 'TRY': [, '₺'], - 'TTD': [, '$'], - 'TWD': ['NT$', '$'], - 'UAH': [, '₴'], - 'USD': ['$'], - 'UYU': [, '$'], - 'VEF': [, 'Bs'], - 'VND': ['₫'], - 'XAF': ['FCFA'], - 'XCD': ['EC$', '$'], - 'XOF': ['CFA'], - 'XPF': ['CFPF'], - 'ZAR': [, 'R'], - 'ZMW': [, 'ZK'] -}; +export const CURRENCIES_EN: + {[code: string]: CurrenciesSymbols | [string | undefined, string | undefined, number]} = { + 'ADP': [, , 0], + 'AFN': [, , 0], + 'ALL': [, , 0], + 'AMD': [, , 0], + 'AOA': [, 'Kz'], + 'ARS': [, '$'], + 'AUD': ['A$', '$'], + 'BAM': [, 'KM'], + 'BBD': [, '$'], + 'BDT': [, '৳'], + 'BHD': [, , 3], + 'BIF': [, , 0], + 'BMD': [, '$'], + 'BND': [, '$'], + 'BOB': [, 'Bs'], + 'BRL': ['R$'], + 'BSD': [, '$'], + 'BWP': [, 'P'], + 'BYN': [, 'р.', 2], + 'BYR': [, , 0], + 'BZD': [, '$'], + 'CAD': ['CA$', '$', 2], + 'CHF': [, , 2], + 'CLF': [, , 4], + 'CLP': [, '$', 0], + 'CNY': ['CN¥', '¥'], + 'COP': [, '$', 0], + 'CRC': [, '₡', 2], + 'CUC': [, '$'], + 'CUP': [, '$'], + 'CZK': [, 'Kč', 2], + 'DJF': [, , 0], + 'DKK': [, 'kr', 2], + 'DOP': [, '$'], + 'EGP': [, 'E£'], + 'ESP': [, '₧', 0], + 'EUR': ['€'], + 'FJD': [, '$'], + 'FKP': [, '£'], + 'GBP': ['£'], + 'GEL': [, '₾'], + 'GIP': [, '£'], + 'GNF': [, 'FG', 0], + 'GTQ': [, 'Q'], + 'GYD': [, '$', 0], + 'HKD': ['HK$', '$'], + 'HNL': [, 'L'], + 'HRK': [, 'kn'], + 'HUF': [, 'Ft', 2], + 'IDR': [, 'Rp', 0], + 'ILS': ['₪'], + 'INR': ['₹'], + 'IQD': [, , 0], + 'IRR': [, , 0], + 'ISK': [, 'kr', 0], + 'ITL': [, , 0], + 'JMD': [, '$'], + 'JOD': [, , 3], + 'JPY': ['¥', , 0], + 'KHR': [, '៛'], + 'KMF': [, 'CF', 0], + 'KPW': [, '₩', 0], + 'KRW': ['₩', , 0], + 'KWD': [, , 3], + 'KYD': [, '$'], + 'KZT': [, '₸'], + 'LAK': [, '₭', 0], + 'LBP': [, 'L£', 0], + 'LKR': [, 'Rs'], + 'LRD': [, '$'], + 'LTL': [, 'Lt'], + 'LUF': [, , 0], + 'LVL': [, 'Ls'], + 'LYD': [, , 3], + 'MGA': [, 'Ar', 0], + 'MGF': [, , 0], + 'MMK': [, 'K', 0], + 'MNT': [, '₮', 0], + 'MRO': [, , 0], + 'MUR': [, 'Rs', 0], + 'MXN': ['MX$', '$'], + 'MYR': [, 'RM'], + 'NAD': [, '$'], + 'NGN': [, '₦'], + 'NIO': [, 'C$'], + 'NOK': [, 'kr', 2], + 'NPR': [, 'Rs'], + 'NZD': ['NZ$', '$'], + 'OMR': [, , 3], + 'PHP': [, '₱'], + 'PKR': [, 'Rs', 0], + 'PLN': [, 'zł'], + 'PYG': [, '₲', 0], + 'RON': [, 'lei'], + 'RSD': [, , 0], + 'RUB': [, '₽'], + 'RUR': [, 'р.'], + 'RWF': [, 'RF', 0], + 'SBD': [, '$'], + 'SEK': [, 'kr', 2], + 'SGD': [, '$'], + 'SHP': [, '£'], + 'SLL': [, , 0], + 'SOS': [, , 0], + 'SRD': [, '$'], + 'SSP': [, '£'], + 'STD': [, 'Db', 0], + 'SYP': [, '£', 0], + 'THB': [, '฿'], + 'TMM': [, , 0], + 'TND': [, , 3], + 'TOP': [, 'T$'], + 'TRL': [, , 0], + 'TRY': [, '₺'], + 'TTD': [, '$'], + 'TWD': ['NT$', '$', 2], + 'TZS': [, , 0], + 'UAH': [, '₴'], + 'UGX': [, , 0], + 'USD': ['$'], + 'UYI': [, , 0], + 'UYU': [, '$'], + 'UZS': [, , 0], + 'VEF': [, 'Bs'], + 'VND': ['₫', , 0], + 'VUV': [, , 0], + 'XAF': ['FCFA', , 0], + 'XCD': ['EC$', '$'], + 'XOF': ['CFA', , 0], + 'XPF': ['CFPF', , 0], + 'YER': [, , 0], + 'ZAR': [, 'R'], + 'ZMK': [, , 0], + 'ZMW': [, 'ZK'], + 'ZWD': [, , 0] + }; diff --git a/packages/common/src/i18n/format_number.ts b/packages/common/src/i18n/format_number.ts index 4e0f7c9114..9033486829 100644 --- a/packages/common/src/i18n/format_number.ts +++ b/packages/common/src/i18n/format_number.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {NumberFormatStyle, NumberSymbol, getLocaleNumberFormat, getLocaleNumberSymbol} from './locale_data_api'; +import {NumberFormatStyle, NumberSymbol, getLocaleNumberFormat, getLocaleNumberSymbol, getNbOfCurrencyDigits} from './locale_data_api'; export const NUMBER_FORMAT_REGEXP = /^(\d+)?\.((\d+)(-(\d+))?)?$/; const MAX_DIGITS = 22; @@ -36,21 +36,18 @@ function strToNumber(value: number | string): number { * Transforms a number to a locale string based on a style and a format */ function formatNumber( - value: number | string, locale: string, style: NumberFormatStyle, groupSymbol: NumberSymbol, - decimalSymbol: NumberSymbol, digitsInfo?: string): string { - const format = getLocaleNumberFormat(locale, style); - const num = strToNumber(value); - - const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign)); + value: number | string, pattern: ParsedNumberFormat, locale: string, groupSymbol: NumberSymbol, + decimalSymbol: NumberSymbol, digitsInfo?: string, isPercent = false): string { let formattedText = ''; let isZero = false; + const num = strToNumber(value); if (!isFinite(num)) { formattedText = getLocaleNumberSymbol(locale, NumberSymbol.Infinity); } else { let parsedNumber = parseNumber(num); - if (style === NumberFormatStyle.Percent) { + if (isPercent) { parsedNumber = toPercent(parsedNumber); } @@ -142,13 +139,20 @@ function formatNumber( /** * Formats a currency to a locale string + * + * @internal */ export function formatCurrency( value: number | string, locale: string, currency: string, currencyCode?: string, digitsInfo?: string): string { + const format = getLocaleNumberFormat(locale, NumberFormatStyle.Currency); + const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign)); + + pattern.minFrac = getNbOfCurrencyDigits(currencyCode !); + pattern.maxFrac = pattern.minFrac; + const res = formatNumber( - value, locale, NumberFormatStyle.Currency, NumberSymbol.CurrencyGroup, - NumberSymbol.CurrencyDecimal, digitsInfo); + value, pattern, locale, NumberSymbol.CurrencyGroup, NumberSymbol.CurrencyDecimal, digitsInfo); return res .replace(CURRENCY_CHAR, currency) // if we have 2 time the currency character, the second one is ignored @@ -157,22 +161,27 @@ export function formatCurrency( /** * Formats a percentage to a locale string + * + * @internal */ export function formatPercent(value: number | string, locale: string, digitsInfo?: string): string { + const format = getLocaleNumberFormat(locale, NumberFormatStyle.Percent); + const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign)); const res = formatNumber( - value, locale, NumberFormatStyle.Percent, NumberSymbol.Group, NumberSymbol.Decimal, - digitsInfo); + value, pattern, locale, NumberSymbol.Group, NumberSymbol.Decimal, digitsInfo, true); return res.replace( new RegExp(PERCENT_CHAR, 'g'), getLocaleNumberSymbol(locale, NumberSymbol.PercentSign)); } /** * Formats a number to a locale string + * + * @internal */ export function formatDecimal(value: number | string, locale: string, digitsInfo?: string): string { - return formatNumber( - value, locale, NumberFormatStyle.Decimal, NumberSymbol.Group, NumberSymbol.Decimal, - digitsInfo); + const format = getLocaleNumberFormat(locale, NumberFormatStyle.Decimal); + const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign)); + return formatNumber(value, pattern, locale, NumberSymbol.Group, NumberSymbol.Decimal, digitsInfo); } interface ParsedNumberFormat { diff --git a/packages/common/src/i18n/locale_data.ts b/packages/common/src/i18n/locale_data.ts index af36a3caa2..1d061dc6d8 100644 --- a/packages/common/src/i18n/locale_data.ts +++ b/packages/common/src/i18n/locale_data.ts @@ -71,4 +71,4 @@ export const enum ExtraLocaleDataIndex { /** * Index of each value in currency data (used to describe CURRENCIES_EN in currencies.ts) */ -export const enum CurrencyIndex {Symbol = 0, SymbolNarrow} +export const enum CurrencyIndex {Symbol = 0, SymbolNarrow, NbOfDigits} diff --git a/packages/common/src/i18n/locale_data_api.ts b/packages/common/src/i18n/locale_data_api.ts index 7b065109db..b88db6f2bc 100644 --- a/packages/common/src/i18n/locale_data_api.ts +++ b/packages/common/src/i18n/locale_data_api.ts @@ -550,3 +550,21 @@ export function getCurrencySymbol(code: string, format: 'wide' | 'narrow', local return currency[CurrencyIndex.Symbol] || code; } + +// Most currencies have cents, that's why the default is 2 +const DEFAULT_NB_OF_CURRENCY_DIGITS = 2; + +/** + * Returns the number of decimal digits for the given currency. + * Its value depends upon the presence of cents in that particular currency. + * + * @experimental i18n support is experimental. + */ +export function getNbOfCurrencyDigits(code: string): number { + let digits; + const currency = CURRENCIES_EN[code]; + if (currency) { + digits = currency[CurrencyIndex.NbOfDigits]; + } + return typeof digits === 'number' ? digits : DEFAULT_NB_OF_CURRENCY_DIGITS; +} diff --git a/packages/common/test/i18n/locale_data_api_spec.ts b/packages/common/test/i18n/locale_data_api_spec.ts index e7bc84ff06..e44ec258e6 100644 --- a/packages/common/test/i18n/locale_data_api_spec.ts +++ b/packages/common/test/i18n/locale_data_api_spec.ts @@ -13,7 +13,7 @@ import localeZh from '@angular/common/locales/zh'; import localeFrCA from '@angular/common/locales/fr-CA'; import localeEnAU from '@angular/common/locales/en-AU'; import {registerLocaleData} from '../../src/i18n/locale_data'; -import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth} from '../../src/i18n/locale_data_api'; +import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth, getNbOfCurrencyDigits} from '../../src/i18n/locale_data_api'; { describe('locale data api', () => { @@ -74,6 +74,15 @@ import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth} fro }); }); + describe('getNbOfCurrencyDigits', () => { + it('should return the correct value', () => { + expect(getNbOfCurrencyDigits('USD')).toEqual(2); + expect(getNbOfCurrencyDigits('IDR')).toEqual(0); + expect(getNbOfCurrencyDigits('BHD')).toEqual(3); + expect(getNbOfCurrencyDigits('unexisting_ISO_code')).toEqual(2); + }); + }); + describe('getLastDefinedValue', () => { it('should find the last defined date format when format not defined', () => { expect(getLocaleDateFormat('zh', FormatWidth.Long)).toEqual('y年M月d日'); }); diff --git a/packages/common/test/pipes/number_pipe_spec.ts b/packages/common/test/pipes/number_pipe_spec.ts index a2a79f3dd8..9e5c40ccf5 100644 --- a/packages/common/test/pipes/number_pipe_spec.ts +++ b/packages/common/test/pipes/number_pipe_spec.ts @@ -136,6 +136,16 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin expect(pipe.transform(5.1234, 'USD', 'Custom name')).toEqual('Custom name5.12'); }); + it('should round to the default number of digits if no digitsInfo', () => { + // IDR has a default number of digits of 0 + expect(pipe.transform(5.1234, 'IDR')).toEqual('IDR5'); + expect(pipe.transform(5.1234, 'IDR', 'symbol', '.2')).toEqual('IDR5.12'); + expect(pipe.transform(5.1234, 'IDR', 'Custom name')).toEqual('Custom name5'); + // BHD has a default number of digits of 3 + expect(pipe.transform(5.1234, 'BHD')).toEqual('BHD5.123'); + expect(pipe.transform(5.1234, 'BHD', 'symbol', '.1-2')).toEqual('BHD5.12'); + }); + it('should not support other objects', () => { expect(() => pipe.transform({})) .toThrowError( diff --git a/tools/gulp-tasks/cldr/extract.js b/tools/gulp-tasks/cldr/extract.js index e255a470b7..9660c232fa 100644 --- a/tools/gulp-tasks/cldr/extract.js +++ b/tools/gulp-tasks/cldr/extract.js @@ -158,6 +158,7 @@ export default ${stringify(dayPeriodsSupplemental).replace(/undefined/g, '')}; */ function generateBaseCurrencies(localeData, addDigits) { const currenciesData = localeData.main('numbers/currencies'); + const fractions = new cldrJs('en').get(`supplemental/currencyData/fractions`); const currencies = {}; Object.keys(currenciesData).forEach(key => { let symbolsArray = []; @@ -173,6 +174,16 @@ function generateBaseCurrencies(localeData, addDigits) { symbolsArray = [, symbolNarrow]; } } + if (addDigits && fractions[key] && fractions[key]['_digits']) { + const digits = parseInt(fractions[key]['_digits'], 10); + if (symbolsArray.length === 2) { + symbolsArray.push(digits); + } else if (symbolsArray.length === 1) { + symbolsArray = [...symbolsArray, , digits]; + } else { + symbolsArray = [, , digits]; + } + } if (symbolsArray.length > 0) { currencies[key] = symbolsArray; } @@ -219,7 +230,7 @@ function generateCurrenciesFile() { export type CurrenciesSymbols = [string] | [string | undefined, string]; /** @internal */ -export const CURRENCIES_EN: {[code: string]: CurrenciesSymbols} = ${stringify(baseCurrencies, true)}; +export const CURRENCIES_EN: {[code: string]: CurrenciesSymbols | [string | undefined, string | undefined, number]} = ${stringify(baseCurrencies, true)}; `; } diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index 523109388e..d4fe448659 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -132,6 +132,9 @@ export declare function getLocaleTimeFormat(locale: string, width: FormatWidth): /** @experimental */ export declare function getLocaleWeekEndRange(locale: string): [WeekDay, WeekDay]; +/** @experimental */ +export declare function getNbOfCurrencyDigits(code: string): number; + /** @stable */ export declare class HashLocationStrategy extends LocationStrategy { constructor(_platformLocation: PlatformLocation, _baseHref?: string);