Skip to content

Commit 44154e7

Browse files
ocombemhevery
authored andcommitted
fix(common): round currencies based on decimal digits in CurrencyPipe (#21783)
By default, we now round currencies based on the number of decimal digits available for that currency instead of using the rouding defined in the number formats. More info about that can be found in http://www.unicode.org/cldr/charts/latest/supplemental/detailed_territory_currency_information.html#format_info Fixes #10189 PR Close #21783
1 parent 0b2f7d1 commit 44154e7

File tree

9 files changed

+217
-122
lines changed

9 files changed

+217
-122
lines changed

packages/common/src/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
export * from './location/index';
1515
export {NgLocaleLocalization, NgLocalization} from './i18n/localization';
1616
export {registerLocaleData} from './i18n/locale_data';
17-
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';
17+
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';
1818
export {parseCookieValue as ɵparseCookieValue} from './cookie';
1919
export {CommonModule, DeprecatedI18NPipesModule} from './common_module';
2020
export {NgClass, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index';

packages/common/src/i18n/currencies.ts

Lines changed: 138 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -12,106 +12,141 @@
1212
export type CurrenciesSymbols = [string] | [string | undefined, string];
1313

1414
/** @internal */
15-
export const CURRENCIES_EN: {[code: string]: CurrenciesSymbols} = {
16-
'AOA': [, 'Kz'],
17-
'ARS': [, '$'],
18-
'AUD': ['A$', '$'],
19-
'BAM': [, 'KM'],
20-
'BBD': [, '$'],
21-
'BDT': [, '৳'],
22-
'BMD': [, '$'],
23-
'BND': [, '$'],
24-
'BOB': [, 'Bs'],
25-
'BRL': ['R$'],
26-
'BSD': [, '$'],
27-
'BWP': [, 'P'],
28-
'BYN': [, 'р.'],
29-
'BZD': [, '$'],
30-
'CAD': ['CA$', '$'],
31-
'CLP': [, '$'],
32-
'CNY': ['CN¥', '¥'],
33-
'COP': [, '$'],
34-
'CRC': [, '₡'],
35-
'CUC': [, '$'],
36-
'CUP': [, '$'],
37-
'CZK': [, 'Kč'],
38-
'DKK': [, 'kr'],
39-
'DOP': [, '$'],
40-
'EGP': [, 'E£'],
41-
'ESP': [, '₧'],
42-
'EUR': ['€'],
43-
'FJD': [, '$'],
44-
'FKP': [, '£'],
45-
'GBP': ['£'],
46-
'GEL': [, '₾'],
47-
'GIP': [, '£'],
48-
'GNF': [, 'FG'],
49-
'GTQ': [, 'Q'],
50-
'GYD': [, '$'],
51-
'HKD': ['HK$', '$'],
52-
'HNL': [, 'L'],
53-
'HRK': [, 'kn'],
54-
'HUF': [, 'Ft'],
55-
'IDR': [, 'Rp'],
56-
'ILS': ['₪'],
57-
'INR': ['₹'],
58-
'ISK': [, 'kr'],
59-
'JMD': [, '$'],
60-
'JPY': ['¥'],
61-
'KHR': [, '៛'],
62-
'KMF': [, 'CF'],
63-
'KPW': [, '₩'],
64-
'KRW': ['₩'],
65-
'KYD': [, '$'],
66-
'KZT': [, '₸'],
67-
'LAK': [, '₭'],
68-
'LBP': [, 'L£'],
69-
'LKR': [, 'Rs'],
70-
'LRD': [, '$'],
71-
'LTL': [, 'Lt'],
72-
'LVL': [, 'Ls'],
73-
'MGA': [, 'Ar'],
74-
'MMK': [, 'K'],
75-
'MNT': [, '₮'],
76-
'MUR': [, 'Rs'],
77-
'MXN': ['MX$', '$'],
78-
'MYR': [, 'RM'],
79-
'NAD': [, '$'],
80-
'NGN': [, '₦'],
81-
'NIO': [, 'C$'],
82-
'NOK': [, 'kr'],
83-
'NPR': [, 'Rs'],
84-
'NZD': ['NZ$', '$'],
85-
'PHP': [, '₱'],
86-
'PKR': [, 'Rs'],
87-
'PLN': [, 'zł'],
88-
'PYG': [, '₲'],
89-
'RON': [, 'lei'],
90-
'RUB': [, '₽'],
91-
'RUR': [, 'р.'],
92-
'RWF': [, 'RF'],
93-
'SBD': [, '$'],
94-
'SEK': [, 'kr'],
95-
'SGD': [, '$'],
96-
'SHP': [, '£'],
97-
'SRD': [, '$'],
98-
'SSP': [, '£'],
99-
'STD': [, 'Db'],
100-
'SYP': [, '£'],
101-
'THB': [, '฿'],
102-
'TOP': [, 'T$'],
103-
'TRY': [, '₺'],
104-
'TTD': [, '$'],
105-
'TWD': ['NT$', '$'],
106-
'UAH': [, '₴'],
107-
'USD': ['$'],
108-
'UYU': [, '$'],
109-
'VEF': [, 'Bs'],
110-
'VND': ['₫'],
111-
'XAF': ['FCFA'],
112-
'XCD': ['EC$', '$'],
113-
'XOF': ['CFA'],
114-
'XPF': ['CFPF'],
115-
'ZAR': [, 'R'],
116-
'ZMW': [, 'ZK']
117-
};
15+
export const CURRENCIES_EN:
16+
{[code: string]: CurrenciesSymbols | [string | undefined, string | undefined, number]} = {
17+
'ADP': [, , 0],
18+
'AFN': [, , 0],
19+
'ALL': [, , 0],
20+
'AMD': [, , 0],
21+
'AOA': [, 'Kz'],
22+
'ARS': [, '$'],
23+
'AUD': ['A$', '$'],
24+
'BAM': [, 'KM'],
25+
'BBD': [, '$'],
26+
'BDT': [, '৳'],
27+
'BHD': [, , 3],
28+
'BIF': [, , 0],
29+
'BMD': [, '$'],
30+
'BND': [, '$'],
31+
'BOB': [, 'Bs'],
32+
'BRL': ['R$'],
33+
'BSD': [, '$'],
34+
'BWP': [, 'P'],
35+
'BYN': [, 'р.', 2],
36+
'BYR': [, , 0],
37+
'BZD': [, '$'],
38+
'CAD': ['CA$', '$', 2],
39+
'CHF': [, , 2],
40+
'CLF': [, , 4],
41+
'CLP': [, '$', 0],
42+
'CNY': ['CN¥', '¥'],
43+
'COP': [, '$', 0],
44+
'CRC': [, '₡', 2],
45+
'CUC': [, '$'],
46+
'CUP': [, '$'],
47+
'CZK': [, 'Kč', 2],
48+
'DJF': [, , 0],
49+
'DKK': [, 'kr', 2],
50+
'DOP': [, '$'],
51+
'EGP': [, 'E£'],
52+
'ESP': [, '₧', 0],
53+
'EUR': ['€'],
54+
'FJD': [, '$'],
55+
'FKP': [, '£'],
56+
'GBP': ['£'],
57+
'GEL': [, '₾'],
58+
'GIP': [, '£'],
59+
'GNF': [, 'FG', 0],
60+
'GTQ': [, 'Q'],
61+
'GYD': [, '$', 0],
62+
'HKD': ['HK$', '$'],
63+
'HNL': [, 'L'],
64+
'HRK': [, 'kn'],
65+
'HUF': [, 'Ft', 2],
66+
'IDR': [, 'Rp', 0],
67+
'ILS': ['₪'],
68+
'INR': ['₹'],
69+
'IQD': [, , 0],
70+
'IRR': [, , 0],
71+
'ISK': [, 'kr', 0],
72+
'ITL': [, , 0],
73+
'JMD': [, '$'],
74+
'JOD': [, , 3],
75+
'JPY': ['¥', , 0],
76+
'KHR': [, '៛'],
77+
'KMF': [, 'CF', 0],
78+
'KPW': [, '₩', 0],
79+
'KRW': ['₩', , 0],
80+
'KWD': [, , 3],
81+
'KYD': [, '$'],
82+
'KZT': [, '₸'],
83+
'LAK': [, '₭', 0],
84+
'LBP': [, 'L£', 0],
85+
'LKR': [, 'Rs'],
86+
'LRD': [, '$'],
87+
'LTL': [, 'Lt'],
88+
'LUF': [, , 0],
89+
'LVL': [, 'Ls'],
90+
'LYD': [, , 3],
91+
'MGA': [, 'Ar', 0],
92+
'MGF': [, , 0],
93+
'MMK': [, 'K', 0],
94+
'MNT': [, '₮', 0],
95+
'MRO': [, , 0],
96+
'MUR': [, 'Rs', 0],
97+
'MXN': ['MX$', '$'],
98+
'MYR': [, 'RM'],
99+
'NAD': [, '$'],
100+
'NGN': [, '₦'],
101+
'NIO': [, 'C$'],
102+
'NOK': [, 'kr', 2],
103+
'NPR': [, 'Rs'],
104+
'NZD': ['NZ$', '$'],
105+
'OMR': [, , 3],
106+
'PHP': [, '₱'],
107+
'PKR': [, 'Rs', 0],
108+
'PLN': [, 'zł'],
109+
'PYG': [, '₲', 0],
110+
'RON': [, 'lei'],
111+
'RSD': [, , 0],
112+
'RUB': [, '₽'],
113+
'RUR': [, 'р.'],
114+
'RWF': [, 'RF', 0],
115+
'SBD': [, '$'],
116+
'SEK': [, 'kr', 2],
117+
'SGD': [, '$'],
118+
'SHP': [, '£'],
119+
'SLL': [, , 0],
120+
'SOS': [, , 0],
121+
'SRD': [, '$'],
122+
'SSP': [, '£'],
123+
'STD': [, 'Db', 0],
124+
'SYP': [, '£', 0],
125+
'THB': [, '฿'],
126+
'TMM': [, , 0],
127+
'TND': [, , 3],
128+
'TOP': [, 'T$'],
129+
'TRL': [, , 0],
130+
'TRY': [, '₺'],
131+
'TTD': [, '$'],
132+
'TWD': ['NT$', '$', 2],
133+
'TZS': [, , 0],
134+
'UAH': [, '₴'],
135+
'UGX': [, , 0],
136+
'USD': ['$'],
137+
'UYI': [, , 0],
138+
'UYU': [, '$'],
139+
'UZS': [, , 0],
140+
'VEF': [, 'Bs'],
141+
'VND': ['₫', , 0],
142+
'VUV': [, , 0],
143+
'XAF': ['FCFA', , 0],
144+
'XCD': ['EC$', '$'],
145+
'XOF': ['CFA', , 0],
146+
'XPF': ['CFPF', , 0],
147+
'YER': [, , 0],
148+
'ZAR': [, 'R'],
149+
'ZMK': [, , 0],
150+
'ZMW': [, 'ZK'],
151+
'ZWD': [, , 0]
152+
};

packages/common/src/i18n/format_number.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {NumberFormatStyle, NumberSymbol, getLocaleNumberFormat, getLocaleNumberSymbol} from './locale_data_api';
9+
import {NumberFormatStyle, NumberSymbol, getLocaleNumberFormat, getLocaleNumberSymbol, getNbOfCurrencyDigits} from './locale_data_api';
1010

1111
export const NUMBER_FORMAT_REGEXP = /^(\d+)?\.((\d+)(-(\d+))?)?$/;
1212
const MAX_DIGITS = 22;
@@ -36,21 +36,18 @@ function strToNumber(value: number | string): number {
3636
* Transforms a number to a locale string based on a style and a format
3737
*/
3838
function formatNumber(
39-
value: number | string, locale: string, style: NumberFormatStyle, groupSymbol: NumberSymbol,
40-
decimalSymbol: NumberSymbol, digitsInfo?: string): string {
41-
const format = getLocaleNumberFormat(locale, style);
42-
const num = strToNumber(value);
43-
44-
const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign));
39+
value: number | string, pattern: ParsedNumberFormat, locale: string, groupSymbol: NumberSymbol,
40+
decimalSymbol: NumberSymbol, digitsInfo?: string, isPercent = false): string {
4541
let formattedText = '';
4642
let isZero = false;
43+
const num = strToNumber(value);
4744

4845
if (!isFinite(num)) {
4946
formattedText = getLocaleNumberSymbol(locale, NumberSymbol.Infinity);
5047
} else {
5148
let parsedNumber = parseNumber(num);
5249

53-
if (style === NumberFormatStyle.Percent) {
50+
if (isPercent) {
5451
parsedNumber = toPercent(parsedNumber);
5552
}
5653

@@ -142,13 +139,20 @@ function formatNumber(
142139

143140
/**
144141
* Formats a currency to a locale string
142+
*
143+
* @internal
145144
*/
146145
export function formatCurrency(
147146
value: number | string, locale: string, currency: string, currencyCode?: string,
148147
digitsInfo?: string): string {
148+
const format = getLocaleNumberFormat(locale, NumberFormatStyle.Currency);
149+
const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign));
150+
151+
pattern.minFrac = getNbOfCurrencyDigits(currencyCode !);
152+
pattern.maxFrac = pattern.minFrac;
153+
149154
const res = formatNumber(
150-
value, locale, NumberFormatStyle.Currency, NumberSymbol.CurrencyGroup,
151-
NumberSymbol.CurrencyDecimal, digitsInfo);
155+
value, pattern, locale, NumberSymbol.CurrencyGroup, NumberSymbol.CurrencyDecimal, digitsInfo);
152156
return res
153157
.replace(CURRENCY_CHAR, currency)
154158
// if we have 2 time the currency character, the second one is ignored
@@ -157,22 +161,27 @@ export function formatCurrency(
157161

158162
/**
159163
* Formats a percentage to a locale string
164+
*
165+
* @internal
160166
*/
161167
export function formatPercent(value: number | string, locale: string, digitsInfo?: string): string {
168+
const format = getLocaleNumberFormat(locale, NumberFormatStyle.Percent);
169+
const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign));
162170
const res = formatNumber(
163-
value, locale, NumberFormatStyle.Percent, NumberSymbol.Group, NumberSymbol.Decimal,
164-
digitsInfo);
171+
value, pattern, locale, NumberSymbol.Group, NumberSymbol.Decimal, digitsInfo, true);
165172
return res.replace(
166173
new RegExp(PERCENT_CHAR, 'g'), getLocaleNumberSymbol(locale, NumberSymbol.PercentSign));
167174
}
168175

169176
/**
170177
* Formats a number to a locale string
178+
*
179+
* @internal
171180
*/
172181
export function formatDecimal(value: number | string, locale: string, digitsInfo?: string): string {
173-
return formatNumber(
174-
value, locale, NumberFormatStyle.Decimal, NumberSymbol.Group, NumberSymbol.Decimal,
175-
digitsInfo);
182+
const format = getLocaleNumberFormat(locale, NumberFormatStyle.Decimal);
183+
const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign));
184+
return formatNumber(value, pattern, locale, NumberSymbol.Group, NumberSymbol.Decimal, digitsInfo);
176185
}
177186

178187
interface ParsedNumberFormat {

packages/common/src/i18n/locale_data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ export const enum ExtraLocaleDataIndex {
7171
/**
7272
* Index of each value in currency data (used to describe CURRENCIES_EN in currencies.ts)
7373
*/
74-
export const enum CurrencyIndex {Symbol = 0, SymbolNarrow}
74+
export const enum CurrencyIndex {Symbol = 0, SymbolNarrow, NbOfDigits}

packages/common/src/i18n/locale_data_api.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,3 +550,21 @@ export function getCurrencySymbol(code: string, format: 'wide' | 'narrow', local
550550

551551
return currency[CurrencyIndex.Symbol] || code;
552552
}
553+
554+
// Most currencies have cents, that's why the default is 2
555+
const DEFAULT_NB_OF_CURRENCY_DIGITS = 2;
556+
557+
/**
558+
* Returns the number of decimal digits for the given currency.
559+
* Its value depends upon the presence of cents in that particular currency.
560+
*
561+
* @experimental i18n support is experimental.
562+
*/
563+
export function getNbOfCurrencyDigits(code: string): number {
564+
let digits;
565+
const currency = CURRENCIES_EN[code];
566+
if (currency) {
567+
digits = currency[CurrencyIndex.NbOfDigits];
568+
}
569+
return typeof digits === 'number' ? digits : DEFAULT_NB_OF_CURRENCY_DIGITS;
570+
}

packages/common/test/i18n/locale_data_api_spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import localeZh from '@angular/common/locales/zh';
1313
import localeFrCA from '@angular/common/locales/fr-CA';
1414
import localeEnAU from '@angular/common/locales/en-AU';
1515
import {registerLocaleData} from '../../src/i18n/locale_data';
16-
import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth} from '../../src/i18n/locale_data_api';
16+
import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth, getNbOfCurrencyDigits} from '../../src/i18n/locale_data_api';
1717

1818
{
1919
describe('locale data api', () => {
@@ -74,6 +74,15 @@ import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth} fro
7474
});
7575
});
7676

77+
describe('getNbOfCurrencyDigits', () => {
78+
it('should return the correct value', () => {
79+
expect(getNbOfCurrencyDigits('USD')).toEqual(2);
80+
expect(getNbOfCurrencyDigits('IDR')).toEqual(0);
81+
expect(getNbOfCurrencyDigits('BHD')).toEqual(3);
82+
expect(getNbOfCurrencyDigits('unexisting_ISO_code')).toEqual(2);
83+
});
84+
});
85+
7786
describe('getLastDefinedValue', () => {
7887
it('should find the last defined date format when format not defined',
7988
() => { expect(getLocaleDateFormat('zh', FormatWidth.Long)).toEqual('y年M月d日'); });

0 commit comments

Comments
 (0)