feat(common): export functions to format numbers, percents, currencies & dates (#22423)

The utility functions `formatNumber`, `formatPercent`, `formatCurrency`, and `formatDate` used by the number, percent, currency and date pipes are now available for developers who want to use them outside of templates.

Fixes #20536

PR Close #22423
This commit is contained in:
Olivier Combe 2018-02-23 22:28:07 +01:00 committed by Victor Berchet
parent 094666da17
commit 4180912538
13 changed files with 702 additions and 483 deletions

View File

@ -12,9 +12,11 @@
* Entry point for all public APIs of the common package.
*/
export * from './location/index';
export {formatDate} from './i18n/format_date';
export {formatCurrency, formatNumber, formatPercent} from './i18n/format_number';
export {NgLocaleLocalization, NgLocalization} from './i18n/localization';
export {registerLocaleData} from './i18n/locale_data';
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 {Plural, NumberFormatStyle, FormStyle, Time, TranslationWidth, FormatWidth, NumberSymbol, WeekDay, getNumberOfCurrencyDigits, 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';

View File

@ -8,6 +8,9 @@
import {FormStyle, FormatWidth, NumberSymbol, Time, TranslationWidth, getLocaleDateFormat, getLocaleDateTimeFormat, getLocaleDayNames, getLocaleDayPeriods, getLocaleEraNames, getLocaleExtraDayPeriodRules, getLocaleExtraDayPeriods, getLocaleId, getLocaleMonthNames, getLocaleNumberSymbol, getLocaleTimeFormat} from './locale_data_api';
export const ISO8601_DATE_REGEX =
/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;
// 1 2 3 4 5 6 7 8 9 10 11
const NAMED_FORMATS: {[localeId: string]: {[format: string]: string}} = {};
const DATE_FORMATS_SPLIT =
/((?:[^GyMLwWdEabBhHmsSzZO']+)|(?:'(?:[^']|'')*')|(?:G{1,5}|y{1,4}|M{1,5}|L{1,5}|w{1,2}|W{1}|d{1,2}|E{1,6}|a{1,5}|b{1,5}|B{1,5}|h{1,2}|H{1,2}|m{1,2}|s{1,2}|S{1,3}|z{1,4}|Z{1,5}|O{1,4}))([\s\S]*)/;
@ -38,11 +41,27 @@ enum TranslationType {
}
/**
* Transforms a date to a locale string based on a pattern and a timezone
* @ngModule CommonModule
* @whatItDoes Formats a date according to locale rules.
* @description
*
* @internal
* Where:
* - `value` is a Date, a number (milliseconds since UTC epoch) or an ISO string
* (https://www.w3.org/TR/NOTE-datetime).
* - `format` indicates which date/time components to include. See {@link DatePipe} for more
* details.
* - `locale` is a `string` defining the locale to use.
* - `timezone` to be used for formatting. It understands UTC/GMT and the continental US time zone
* abbreviations, but for general use, use a time zone offset (e.g. `'+0430'`).
* If not specified, host system settings are used.
*
* See {@link DatePipe} for more details.
*
* @stable
*/
export function formatDate(date: Date, format: string, locale: string, timezone?: string): string {
export function formatDate(
value: string | number | Date, format: string, locale: string, timezone?: string): string {
let date = toDate(value);
const namedFormat = getNamedFormat(locale, format);
format = namedFormat || format;
@ -165,8 +184,10 @@ function padNumber(
neg = minusSign;
}
}
let strNum = '' + num;
while (strNum.length < digits) strNum = '0' + strNum;
let strNum = String(num);
while (strNum.length < digits) {
strNum = '0' + strNum;
}
if (trim) {
strNum = strNum.substr(strNum.length - digits);
}
@ -607,3 +628,90 @@ function convertTimezoneToLocal(date: Date, timezone: string, reverse: boolean):
const timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
return addDateMinutes(date, reverseValue * (timezoneOffset - dateTimezoneOffset));
}
/**
* Converts a value to date.
*
* Supported input formats:
* - `Date`
* - number: timestamp
* - string: numeric (e.g. "1234"), ISO and date strings in a format supported by
* [Date.parse()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse).
* Note: ISO strings without time return a date without timeoffset.
*
* Throws if unable to convert to a date.
*/
export function toDate(value: string | number | Date): Date {
if (isDate(value)) {
return value;
}
if (typeof value === 'number' && !isNaN(value)) {
return new Date(value);
}
if (typeof value === 'string') {
value = value.trim();
const parsedNb = parseFloat(value);
// any string that only contains numbers, like "1234" but not like "1234hello"
if (!isNaN(value as any - parsedNb)) {
return new Date(parsedNb);
}
if (/^(\d{4}-\d{1,2}-\d{1,2})$/.test(value)) {
/* For ISO Strings without time the day, month and year must be extracted from the ISO String
before Date creation to avoid time offset and errors in the new Date.
If we only replace '-' with ',' in the ISO String ("2015,01,01"), and try to create a new
date, some browsers (e.g. IE 9) will throw an invalid Date error.
If we leave the '-' ("2015-01-01") and try to create a new Date("2015-01-01") the timeoffset
is applied.
Note: ISO months are 0 for January, 1 for February, ... */
const [y, m, d] = value.split('-').map((val: string) => +val);
return new Date(y, m - 1, d);
}
let match: RegExpMatchArray|null;
if (match = value.match(ISO8601_DATE_REGEX)) {
return isoStringToDate(match);
}
}
const date = new Date(value as any);
if (!isDate(date)) {
throw new Error(`Unable to convert "${value}" into a date`);
}
return date;
}
/**
* Converts a date in ISO8601 to a Date.
* Used instead of `Date.parse` because of browser discrepancies.
*/
export function isoStringToDate(match: RegExpMatchArray): Date {
const date = new Date(0);
let tzHour = 0;
let tzMin = 0;
// match[8] means that the string contains "Z" (UTC) or a timezone like "+01:00" or "+0100"
const dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear;
const timeSetter = match[8] ? date.setUTCHours : date.setHours;
// if there is a timezone defined like "+01:00" or "+0100"
if (match[9]) {
tzHour = Number(match[9] + match[10]);
tzMin = Number(match[9] + match[11]);
}
dateSetter.call(date, Number(match[1]), Number(match[2]) - 1, Number(match[3]));
const h = Number(match[4] || 0) - tzHour;
const m = Number(match[5] || 0) - tzMin;
const s = Number(match[6] || 0);
const ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
timeSetter.call(date, h, m, s, ms);
return date;
}
export function isDate(value: any): value is Date {
return value instanceof Date && !isNaN(value.valueOf());
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {NumberFormatStyle, NumberSymbol, getLocaleNumberFormat, getLocaleNumberSymbol, getNbOfCurrencyDigits} from './locale_data_api';
import {NumberFormatStyle, NumberSymbol, getLocaleNumberFormat, getLocaleNumberSymbol, getNumberOfCurrencyDigits} from './locale_data_api';
export const NUMBER_FORMAT_REGEXP = /^(\d+)?\.((\d+)(-(\d+))?)?$/;
const MAX_DIGITS = 22;
@ -18,34 +18,19 @@ const DIGIT_CHAR = '#';
const CURRENCY_CHAR = '¤';
const PERCENT_CHAR = '%';
/**
* Transforms a string into a number (if needed)
*/
function strToNumber(value: number | string): number {
// Convert strings to numbers
if (typeof value === 'string' && !isNaN(+value - parseFloat(value))) {
return +value;
}
if (typeof value !== 'number') {
throw new Error(`${value} is not a number`);
}
return value;
}
/**
* Transforms a number to a locale string based on a style and a format
*/
function formatNumber(
value: number | string, pattern: ParsedNumberFormat, locale: string, groupSymbol: NumberSymbol,
function formatNumberToLocaleString(
value: number, 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)) {
if (!isFinite(value)) {
formattedText = getLocaleNumberSymbol(locale, NumberSymbol.Infinity);
} else {
let parsedNumber = parseNumber(num);
let parsedNumber = parseNumber(value);
if (isPercent) {
parsedNumber = toPercent(parsedNumber);
@ -128,7 +113,7 @@ function formatNumber(
}
}
if (num < 0 && !isZero) {
if (value < 0 && !isZero) {
formattedText = pattern.negPre + formattedText + pattern.negSuf;
} else {
formattedText = pattern.posPre + formattedText + pattern.posSuf;
@ -138,20 +123,32 @@ function formatNumber(
}
/**
* Formats a currency to a locale string
* @ngModule CommonModule
* @whatItDoes Formats a number as currency using locale rules.
* @description
*
* @internal
* Use `currency` to format a number as currency.
*
* Where:
* - `value` is a number.
* - `locale` is a `string` defining the locale to use.
* - `currency` is the string that represents the currency, it can be its symbol or its name.
* - `currencyCode` is the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code, such
* as `USD` for the US dollar and `EUR` for the euro.
* - `digitInfo` See {@link DecimalPipe} for more details.
*
* @stable
*/
export function formatCurrency(
value: number | string, locale: string, currency: string, currencyCode?: string,
value: number, 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.minFrac = getNumberOfCurrencyDigits(currencyCode !);
pattern.maxFrac = pattern.minFrac;
const res = formatNumber(
const res = formatNumberToLocaleString(
value, pattern, locale, NumberSymbol.CurrencyGroup, NumberSymbol.CurrencyDecimal, digitsInfo);
return res
.replace(CURRENCY_CHAR, currency)
@ -160,28 +157,48 @@ export function formatCurrency(
}
/**
* Formats a percentage to a locale string
* @ngModule CommonModule
* @whatItDoes Formats a number as a percentage according to locale rules.
* @description
*
* @internal
* Formats a number as percentage.
*
* Where:
* - `value` is a number.
* - `locale` is a `string` defining the locale to use.
* - `digitInfo` See {@link DecimalPipe} for more details.
*
* @stable
*/
export function formatPercent(value: number | string, locale: string, digitsInfo?: string): string {
export function formatPercent(value: number, locale: string, digitsInfo?: string): string {
const format = getLocaleNumberFormat(locale, NumberFormatStyle.Percent);
const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign));
const res = formatNumber(
const res = formatNumberToLocaleString(
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
* @ngModule CommonModule
* @whatItDoes Formats a number according to locale rules.
* @description
*
* @internal
* Formats a number as text. Group sizing and separator and other locale-specific
* configurations are based on the locale.
*
* Where:
* - `value` is a number.
* - `locale` is a `string` defining the locale to use.
* - `digitInfo` See {@link DecimalPipe} for more details.
*
* @stable
*/
export function formatDecimal(value: number | string, locale: string, digitsInfo?: string): string {
export function formatNumber(value: number, locale: string, digitsInfo?: string): string {
const format = getLocaleNumberFormat(locale, NumberFormatStyle.Decimal);
const pattern = parseNumberFormat(format, getLocaleNumberSymbol(locale, NumberSymbol.MinusSign));
return formatNumber(value, pattern, locale, NumberSymbol.Group, NumberSymbol.Decimal, digitsInfo);
return formatNumberToLocaleString(
value, pattern, locale, NumberSymbol.Group, NumberSymbol.Decimal, digitsInfo);
}
interface ParsedNumberFormat {
@ -335,7 +352,7 @@ function parseNumber(num: number): ParsedNumber {
digits = [];
// Convert string to array of digits without leading/trailing zeros.
for (j = 0; i <= zeros; i++, j++) {
digits[j] = +numStr.charAt(i);
digits[j] = Number(numStr.charAt(i));
}
}
@ -424,7 +441,6 @@ function roundNumber(parsedNumber: ParsedNumber, minFrac: number, maxFrac: numbe
}
}
/** @internal */
export function parseIntAutoRadix(text: string): number {
const result: number = parseInt(text);
if (isNaN(result)) {

View File

@ -560,7 +560,7 @@ const DEFAULT_NB_OF_CURRENCY_DIGITS = 2;
*
* @experimental i18n support is experimental.
*/
export function getNbOfCurrencyDigits(code: string): number {
export function getNumberOfCurrencyDigits(code: string): number {
let digits;
const currency = CURRENCIES_EN[code];
if (currency) {

View File

@ -10,41 +10,36 @@ import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core';
import {formatDate} from '../i18n/format_date';
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
export const ISO8601_DATE_REGEX =
/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;
// 1 2 3 4 5 6 7 8 9 10 11
// clang-format off
/**
* @ngModule CommonModule
* @whatItDoes Formats a date according to locale rules.
* @whatItDoes Uses the function {@link formatDate} to format a date according to locale rules.
* @howToUse `date_expression | date[:format[:timezone[:locale]]]`
* @description
*
* Where:
* - `expression` is a date object or a number (milliseconds since UTC epoch) or an ISO string
* - `value` is a date object or a number (milliseconds since UTC epoch) or an ISO string
* (https://www.w3.org/TR/NOTE-datetime).
* - `format` indicates which date/time components to include. The format can be predefined as
* shown below (all examples are given for `en-US`) or custom as shown in the table.
* - `'short'`: equivalent to `'M/d/yy, h:mm a'` (e.g. `6/15/15, 9:03 AM`)
* - `'medium'`: equivalent to `'MMM d, y, h:mm:ss a'` (e.g. `Jun 15, 2015, 9:03:01 AM`)
* - `'long'`: equivalent to `'MMMM d, y, h:mm:ss a z'` (e.g. `June 15, 2015 at 9:03:01 AM GMT+1`)
* - `'short'`: equivalent to `'M/d/yy, h:mm a'` (e.g. `6/15/15, 9:03 AM`).
* - `'medium'`: equivalent to `'MMM d, y, h:mm:ss a'` (e.g. `Jun 15, 2015, 9:03:01 AM`).
* - `'long'`: equivalent to `'MMMM d, y, h:mm:ss a z'` (e.g. `June 15, 2015 at 9:03:01 AM GMT+1`).
* - `'full'`: equivalent to `'EEEE, MMMM d, y, h:mm:ss a zzzz'` (e.g. `Monday, June 15, 2015 at
* 9:03:01 AM GMT+01:00`)
* - `'shortDate'`: equivalent to `'M/d/yy'` (e.g. `6/15/15`)
* - `'mediumDate'`: equivalent to `'MMM d, y'` (e.g. `Jun 15, 2015`)
* - `'longDate'`: equivalent to `'MMMM d, y'` (e.g. `June 15, 2015`)
* - `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` (e.g. `Monday, June 15, 2015`)
* - `'shortTime'`: equivalent to `'h:mm a'` (e.g. `9:03 AM`)
* - `'mediumTime'`: equivalent to `'h:mm:ss a'` (e.g. `9:03:01 AM`)
* - `'longTime'`: equivalent to `'h:mm:ss a z'` (e.g. `9:03:01 AM GMT+1`)
* - `'fullTime'`: equivalent to `'h:mm:ss a zzzz'` (e.g. `9:03:01 AM GMT+01:00`)
* - `timezone` to be used for formatting. It understands UTC/GMT and the continental US time zone
* abbreviations, but for general use, use a time zone offset, for example,
* `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
* 9:03:01 AM GMT+01:00`).
* - `'shortDate'`: equivalent to `'M/d/yy'` (e.g. `6/15/15`).
* - `'mediumDate'`: equivalent to `'MMM d, y'` (e.g. `Jun 15, 2015`).
* - `'longDate'`: equivalent to `'MMMM d, y'` (e.g. `June 15, 2015`).
* - `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` (e.g. `Monday, June 15, 2015`).
* - `'shortTime'`: equivalent to `'h:mm a'` (e.g. `9:03 AM`).
* - `'mediumTime'`: equivalent to `'h:mm:ss a'` (e.g. `9:03:01 AM`).
* - `'longTime'`: equivalent to `'h:mm:ss a z'` (e.g. `9:03:01 AM GMT+1`).
* - `'fullTime'`: equivalent to `'h:mm:ss a zzzz'` (e.g. `9:03:01 AM GMT+01:00`).
* - `timezone` to be used for formatting. It understands UTC/GMT and the continental US time zone
* abbreviations, but for general use, use a time zone offset (e.g. `'+0430'`).
* If not specified, the local system timezone of the end-user's browser will be used.
* - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default)
* - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default).
*
*
* | Field Type | Format | Description | Example Value |
@ -137,66 +132,10 @@ export class DatePipe implements PipeTransform {
transform(value: any, format = 'mediumDate', timezone?: string, locale?: string): string|null {
if (value == null || value === '' || value !== value) return null;
if (typeof value === 'string') {
value = value.trim();
try {
return formatDate(value, format, locale || this.locale, timezone);
} catch (error) {
throw invalidPipeArgumentError(DatePipe, error.message);
}
let date: Date;
let match: RegExpMatchArray|null;
if (isDate(value)) {
date = value;
} else if (!isNaN(value - parseFloat(value))) {
date = new Date(parseFloat(value));
} else if (typeof value === 'string' && /^(\d{4}-\d{1,2}-\d{1,2})$/.test(value)) {
/**
* For ISO Strings without time the day, month and year must be extracted from the ISO String
* before Date creation to avoid time offset and errors in the new Date.
* If we only replace '-' with ',' in the ISO String ("2015,01,01"), and try to create a new
* date, some browsers (e.g. IE 9) will throw an invalid Date error
* If we leave the '-' ("2015-01-01") and try to create a new Date("2015-01-01") the timeoffset
* is applied
* Note: ISO months are 0 for January, 1 for February, ...
*/
const [y, m, d] = value.split('-').map((val: string) => +val);
date = new Date(y, m - 1, d);
} else if ((typeof value === 'string') && (match = value.match(ISO8601_DATE_REGEX))) {
date = isoStringToDate(match);
} else {
date = new Date(value);
}
if (!isDate(date)) {
throw invalidPipeArgumentError(DatePipe, value);
}
return formatDate(date, format, locale || this.locale, timezone);
}
}
/** @internal */
export function isoStringToDate(match: RegExpMatchArray): Date {
const date = new Date(0);
let tzHour = 0;
let tzMin = 0;
// match[8] means that the string contains "Z" (UTC) or a timezone like "+01:00" or "+0100"
const dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear;
const timeSetter = match[8] ? date.setUTCHours : date.setHours;
// if there is a timezone defined like "+01:00" or "+0100"
if (match[9]) {
tzHour = +(match[9] + match[10]);
tzMin = +(match[9] + match[11]);
}
dateSetter.call(date, +(match[1]), +(match[2]) - 1, +(match[3]));
const h = +(match[4] || '0') - tzHour;
const m = +(match[5] || '0') - tzMin;
const s = +(match[6] || '0');
const ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
timeSetter.call(date, h, m, s, ms);
return date;
}
function isDate(value: any): value is Date {
return value instanceof Date && !isNaN(value.valueOf());
}

View File

@ -7,7 +7,7 @@
*/
import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core';
import {ISO8601_DATE_REGEX, isoStringToDate} from '../date_pipe';
import {ISO8601_DATE_REGEX, isoStringToDate} from '../../i18n/format_date';
import {invalidPipeArgumentError} from '../invalid_pipe_argument_error';
import {DateFormatter} from './intl';

View File

@ -7,29 +7,28 @@
*/
import {Inject, LOCALE_ID, Pipe, PipeTransform} from '@angular/core';
import {formatCurrency, formatDecimal, formatPercent} from '../i18n/format_number';
import {formatCurrency, formatNumber, formatPercent} from '../i18n/format_number';
import {getCurrencySymbol} from '../i18n/locale_data_api';
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
/**
* @ngModule CommonModule
* @whatItDoes Formats a number according to locale rules.
* @whatItDoes Uses the function {@link formatNumber} to format a number according to locale rules.
* @howToUse `number_expression | number[:digitInfo[:locale]]`
* @description
*
* Formats a number as text. Group sizing and separator and other locale-specific
* configurations are based on the active locale.
* configurations are based on the locale.
*
* where `expression` is a number:
* - `digitInfo` is a `string` which has a following format: <br>
* <code>{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}</code>
* Where:
* - `value` is a number
* - `digitInfo` is a `string` which has a following format: <br>
* <code>{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}</code>.
* - `minIntegerDigits` is the minimum number of integer digits to use. Defaults to `1`.
* - `minFractionDigits` is the minimum number of digits after fraction. Defaults to `0`.
* - `maxFractionDigits` is the maximum number of digits after fraction. Defaults to `3`.
* - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default)
*
* For more information on the acceptable range for each of these numbers and other
* details see your native internationalization library.
* - `minFractionDigits` is the minimum number of digits after the decimal point. Defaults to `0`.
* - `maxFractionDigits` is the maximum number of digits after the decimal point. Defaults to `3`.
* - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default).
*
* ### Example
*
@ -47,7 +46,8 @@ export class DecimalPipe implements PipeTransform {
locale = locale || this._locale;
try {
return formatDecimal(value, locale, digitsInfo);
const num = strToNumber(value);
return formatNumber(num, locale, digitsInfo);
} catch (error) {
throw invalidPipeArgumentError(DecimalPipe, error.message);
}
@ -56,16 +56,18 @@ export class DecimalPipe implements PipeTransform {
/**
* @ngModule CommonModule
* @whatItDoes Formats a number as a percentage according to locale rules.
* @whatItDoes Uses the function {@link formatPercent} to format a number as a percentage according
* to locale rules.
* @howToUse `number_expression | percent[:digitInfo[:locale]]`
*
* @description
*
* Formats a number as percentage.
*
* - `digitInfo` See {@link DecimalPipe} for a detailed description.
* - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default)
* Where:
* - `value` is a number.
* - `digitInfo` See {@link DecimalPipe} for more details.
* - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default).
*
* ### Example
*
@ -83,7 +85,8 @@ export class PercentPipe implements PipeTransform {
locale = locale || this._locale;
try {
return formatPercent(value, locale, digitsInfo);
const num = strToNumber(value);
return formatPercent(num, locale, digitsInfo);
} catch (error) {
throw invalidPipeArgumentError(PercentPipe, error.message);
}
@ -92,25 +95,28 @@ export class PercentPipe implements PipeTransform {
/**
* @ngModule CommonModule
* @whatItDoes Formats a number as currency using locale rules.
* @whatItDoes Uses the functions {@link getCurrencySymbol} and {@link formatCurrency} to format a
* number as currency using locale rules.
* @howToUse `number_expression | currency[:currencyCode[:display[:digitInfo[:locale]]]]`
* @description
*
* Use `currency` to format a number as currency.
*
* Where:
* - `value` is a number.
* - `currencyCode` is the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code, such
* as `USD` for the US dollar and `EUR` for the euro.
* - `display` indicates whether to show the currency symbol, the code or a custom value
* - `display` indicates whether to show the currency symbol, the code or a custom value:
* - `code`: use code (e.g. `USD`).
* - `symbol`(default): use symbol (e.g. `$`).
* - `symbol-narrow`: some countries have two symbols for their currency, one regular and one
* narrow (e.g. the canadian dollar CAD has the symbol `CA$` and the symbol-narrow `$`).
* - `string`: use this value instead of a code or a symbol
* - boolean (deprecated from v5): `true` for symbol and false for `code`
* - `string`: use this value instead of a code or a symbol.
* - boolean (deprecated from v5): `true` for symbol and false for `code`.
* If there is no narrow symbol for the chosen currency, the regular symbol will be used.
* - `digitInfo` See {@link DecimalPipe} for a detailed description.
* - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default)
* - `digitInfo` See {@link DecimalPipe} for more details.
* - `locale` is a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
* default).
*
* ### Example
*
@ -148,7 +154,8 @@ export class CurrencyPipe implements PipeTransform {
}
try {
return formatCurrency(value, locale, currency, currencyCode, digitsInfo);
const num = strToNumber(value);
return formatCurrency(num, locale, currency, currencyCode, digitsInfo);
} catch (error) {
throw invalidPipeArgumentError(CurrencyPipe, error.message);
}
@ -158,3 +165,17 @@ export class CurrencyPipe implements PipeTransform {
function isEmpty(value: any): boolean {
return value == null || value === '' || value !== value;
}
/**
* Transforms a string into a number (if needed)
*/
function strToNumber(value: number | string): number {
// Convert strings to numbers
if (typeof value === 'string' && !isNaN(Number(value) - parseFloat(value))) {
return Number(value);
}
if (typeof value !== 'number') {
throw new Error(`${value} is not a number`);
}
return value;
}

View File

@ -0,0 +1,315 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {registerLocaleData} from '@angular/common';
import localeAr from '@angular/common/locales/ar';
import localeDe from '@angular/common/locales/de';
import localeEn from '@angular/common/locales/en';
import localeEnExtra from '@angular/common/locales/extra/en';
import localeHu from '@angular/common/locales/hu';
import localeSr from '@angular/common/locales/sr';
import localeTh from '@angular/common/locales/th';
import {isDate, toDate, formatDate} from '@angular/common/src/i18n/format_date';
describe('Format date', () => {
describe('toDate', () => {
it('should support date', () => { expect(isDate(toDate(new Date()))).toBeTruthy(); });
it('should support int', () => { expect(isDate(toDate(123456789))).toBeTruthy(); });
it('should support numeric strings',
() => { expect(isDate(toDate('123456789'))).toBeTruthy(); });
it('should support decimal strings',
() => { expect(isDate(toDate('123456789.11'))).toBeTruthy(); });
it('should support ISO string',
() => { expect(isDate(toDate('2015-06-15T21:43:11Z'))).toBeTruthy(); });
it('should throw for empty string', () => { expect(() => toDate('')).toThrow(); });
it('should throw for alpha numeric strings',
() => { expect(() => toDate('123456789 hello')).toThrow(); });
it('should throw for NaN', () => { expect(() => toDate(Number.NaN)).toThrow(); });
it('should support ISO string without time',
() => { expect(isDate(toDate('2015-01-01'))).toBeTruthy(); });
it('should throw for objects', () => { expect(() => toDate({} as any)).toThrow(); });
});
describe('formatDate', () => {
const isoStringWithoutTime = '2015-01-01';
const defaultLocale = 'en-US';
const defaultFormat = 'mediumDate';
let date: Date;
// Check the transformation of a date into a pattern
function expectDateFormatAs(date: Date | string, pattern: any, output: string): void {
expect(formatDate(date, pattern, defaultLocale)).toEqual(output);
}
beforeAll(() => {
registerLocaleData(localeEn, localeEnExtra);
registerLocaleData(localeDe);
registerLocaleData(localeHu);
registerLocaleData(localeSr);
registerLocaleData(localeTh);
registerLocaleData(localeAr);
});
beforeEach(() => { date = new Date(2015, 5, 15, 9, 3, 1, 550); });
it('should format each component correctly', () => {
const dateFixtures: any = {
G: 'AD',
GG: 'AD',
GGG: 'AD',
GGGG: 'Anno Domini',
GGGGG: 'A',
y: '2015',
yy: '15',
yyy: '2015',
yyyy: '2015',
M: '6',
MM: '06',
MMM: 'Jun',
MMMM: 'June',
MMMMM: 'J',
L: '6',
LL: '06',
LLL: 'Jun',
LLLL: 'June',
LLLLL: 'J',
w: '25',
ww: '25',
W: '3',
d: '15',
dd: '15',
E: 'Mon',
EE: 'Mon',
EEE: 'Mon',
EEEE: 'Monday',
EEEEEE: 'Mo',
h: '9',
hh: '09',
H: '9',
HH: '09',
m: '3',
mm: '03',
s: '1',
ss: '01',
S: '6',
SS: '55',
SSS: '550',
a: 'AM',
aa: 'AM',
aaa: 'AM',
aaaa: 'AM',
aaaaa: 'a',
b: 'morning',
bb: 'morning',
bbb: 'morning',
bbbb: 'morning',
bbbbb: 'morning',
B: 'in the morning',
BB: 'in the morning',
BBB: 'in the morning',
BBBB: 'in the morning',
BBBBB: 'in the morning',
};
const isoStringWithoutTimeFixtures: any = {
G: 'AD',
GG: 'AD',
GGG: 'AD',
GGGG: 'Anno Domini',
GGGGG: 'A',
y: '2015',
yy: '15',
yyy: '2015',
yyyy: '2015',
M: '1',
MM: '01',
MMM: 'Jan',
MMMM: 'January',
MMMMM: 'J',
L: '1',
LL: '01',
LLL: 'Jan',
LLLL: 'January',
LLLLL: 'J',
w: '1',
ww: '01',
W: '1',
d: '1',
dd: '01',
E: 'Thu',
EE: 'Thu',
EEE: 'Thu',
EEEE: 'Thursday',
EEEEE: 'T',
EEEEEE: 'Th',
h: '12',
hh: '12',
H: '0',
HH: '00',
m: '0',
mm: '00',
s: '0',
ss: '00',
S: '0',
SS: '00',
SSS: '000',
a: 'AM',
aa: 'AM',
aaa: 'AM',
aaaa: 'AM',
aaaaa: 'a',
b: 'midnight',
bb: 'midnight',
bbb: 'midnight',
bbbb: 'midnight',
bbbbb: 'midnight',
B: 'midnight',
BB: 'midnight',
BBB: 'midnight',
BBBB: 'midnight',
BBBBB: 'mi',
};
Object.keys(dateFixtures).forEach((pattern: string) => {
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
});
Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => {
expectDateFormatAs(isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]);
});
});
it('should format with timezones', () => {
const dateFixtures: any = {
z: /GMT(\+|-)\d/,
zz: /GMT(\+|-)\d/,
zzz: /GMT(\+|-)\d/,
zzzz: /GMT(\+|-)\d{2}\:30/,
Z: /(\+|-)\d{2}30/,
ZZ: /(\+|-)\d{2}30/,
ZZZ: /(\+|-)\d{2}30/,
ZZZZ: /GMT(\+|-)\d{2}\:30/,
ZZZZZ: /(\+|-)\d{2}\:30/,
O: /GMT(\+|-)\d/,
OOOO: /GMT(\+|-)\d{2}\:30/,
};
Object.keys(dateFixtures).forEach((pattern: string) => {
expect(formatDate(date, pattern, defaultLocale, '+0430')).toMatch(dateFixtures[pattern]);
});
});
it('should format common multi component patterns', () => {
const dateFixtures: any = {
'EEE, M/d/y': 'Mon, 6/15/2015',
'EEE, M/d': 'Mon, 6/15',
'MMM d': 'Jun 15',
'dd/MM/yyyy': '15/06/2015',
'MM/dd/yyyy': '06/15/2015',
'yMEEEd': '20156Mon15',
'MEEEd': '6Mon15',
'MMMd': 'Jun15',
'EEEE, MMMM d, y': 'Monday, June 15, 2015',
'H:mm a': '9:03 AM',
'ms': '31',
'MM/dd/yy hh:mm': '06/15/15 09:03',
'MM/dd/y': '06/15/2015'
};
Object.keys(dateFixtures).forEach((pattern: string) => {
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
});
});
it('should format with pattern aliases', () => {
const dateFixtures: any = {
'MM/dd/yyyy': '06/15/2015',
shortDate: '6/15/15',
mediumDate: 'Jun 15, 2015',
longDate: 'June 15, 2015',
fullDate: 'Monday, June 15, 2015',
short: '6/15/15, 9:03 AM',
medium: 'Jun 15, 2015, 9:03:01 AM',
long: /June 15, 2015 at 9:03:01 AM GMT(\+|-)\d/,
full: /Monday, June 15, 2015 at 9:03:01 AM GMT(\+|-)\d{2}:\d{2}/,
shortTime: '9:03 AM',
mediumTime: '9:03:01 AM',
longTime: /9:03:01 AM GMT(\+|-)\d/,
fullTime: /9:03:01 AM GMT(\+|-)\d{2}:\d{2}/,
};
Object.keys(dateFixtures).forEach((pattern: string) => {
expect(formatDate(date, pattern, defaultLocale)).toMatch(dateFixtures[pattern]);
});
});
it('should format invalid in IE ISO date',
() => expect(formatDate('2017-01-11T12:00:00.014-0500', defaultFormat, defaultLocale))
.toEqual('Jan 11, 2017'));
it('should format invalid in Safari ISO date',
() => expect(formatDate('2017-01-20T12:00:00+0000', defaultFormat, defaultLocale))
.toEqual('Jan 20, 2017'));
// test for the following bugs:
// https://github.com/angular/angular/issues/9524
// https://github.com/angular/angular/issues/9524
it('should format correctly with iso strings that contain time',
() => expect(formatDate('2017-05-07T22:14:39', 'dd-MM-yyyy HH:mm', defaultLocale))
.toMatch(/07-05-2017 \d{2}:\d{2}/));
// test for issue https://github.com/angular/angular/issues/21491
it('should not assume UTC for iso strings in Safari if the timezone is not defined', () => {
// this test only works if the timezone is not in UTC
// which is the case for BrowserStack when we test Safari
if (new Date().getTimezoneOffset() !== 0) {
expect(formatDate('2018-01-11T13:00:00', 'HH', defaultLocale))
.not.toEqual(formatDate('2018-01-11T13:00:00Z', 'HH', defaultLocale));
}
});
// test for the following bugs:
// https://github.com/angular/angular/issues/16624
// https://github.com/angular/angular/issues/17478
it('should show the correct time when the timezone is fixed', () => {
expect(formatDate('2017-06-13T10:14:39+0000', 'shortTime', defaultLocale, '+0000'))
.toEqual('10:14 AM');
expect(formatDate('2017-06-13T10:14:39+0000', 'h:mm a', defaultLocale, '+0000'))
.toEqual('10:14 AM');
});
it('should remove bidi control characters',
() => expect(formatDate(date, 'MM/dd/yyyy', defaultLocale) !.length).toEqual(10));
it(`should format the date correctly in various locales`, () => {
expect(formatDate(date, 'short', 'de')).toEqual('15.06.15, 09:03');
expect(formatDate(date, 'short', 'ar')).toEqual('15/6/2015 9:03 ص');
expect(formatDate(date, 'dd-MM-yy', 'th')).toEqual('15-06-15');
expect(formatDate(date, 'a', 'hu')).toEqual('de.');
expect(formatDate(date, 'a', 'sr')).toEqual('пре подне');
// TODO(ocombe): activate this test when we support local numbers
// expect(formatDate(date, 'hh', 'mr')).toEqual('०९');
});
it('should throw if we use getExtraDayPeriods without loading extra locale data', () => {
expect(() => formatDate(date, 'b', 'de'))
.toThrowError(/Missing extra locale data for the locale "de"/);
});
});
});

View File

@ -0,0 +1,118 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import localeEn from '@angular/common/locales/en';
import localeEsUS from '@angular/common/locales/es-US';
import localeFr from '@angular/common/locales/fr';
import localeAr from '@angular/common/locales/ar';
import {formatCurrency, formatNumber, formatPercent, registerLocaleData} from '@angular/common';
import {describe, expect, it} from '@angular/core/testing/src/testing_internal';
describe('Format number', () => {
const defaultLocale = 'en-US';
beforeAll(() => {
registerLocaleData(localeEn);
registerLocaleData(localeEsUS);
registerLocaleData(localeFr);
registerLocaleData(localeAr);
});
describe('Number', () => {
describe('transform', () => {
it('should return correct value for numbers', () => {
expect(formatNumber(12345, defaultLocale)).toEqual('12,345');
expect(formatNumber(123, defaultLocale, '.2')).toEqual('123.00');
expect(formatNumber(1, defaultLocale, '3.')).toEqual('001');
expect(formatNumber(1.1, defaultLocale, '3.4-5')).toEqual('001.1000');
expect(formatNumber(1.123456, defaultLocale, '3.4-5')).toEqual('001.12346');
expect(formatNumber(1.1234, defaultLocale)).toEqual('1.123');
expect(formatNumber(1.123456, defaultLocale, '.2')).toEqual('1.123');
expect(formatNumber(1.123456, defaultLocale, '.4')).toEqual('1.1235');
});
it('should throw if minFractionDigits is explicitly higher than maxFractionDigits', () => {
expect(() => formatNumber(1.1, defaultLocale, '3.4-2'))
.toThrowError(/is higher than the maximum/);
});
});
describe('transform with custom locales', () => {
it('should return the correct format for es-US',
() => { expect(formatNumber(9999999.99, 'es-US', '1.2-2')).toEqual('9,999,999.99'); });
});
});
describe('Percent', () => {
describe('transform', () => {
it('should return correct value for numbers', () => {
expect(formatPercent(1.23, defaultLocale)).toEqual('123%');
expect(formatPercent(1.2, defaultLocale, '.2')).toEqual('120.00%');
expect(formatPercent(1.2, defaultLocale, '4.2')).toEqual('0,120.00%');
expect(formatPercent(1.2, 'fr', '4.2')).toEqual('0 120,00 %');
expect(formatPercent(1.2, 'ar', '4.2')).toEqual('0,120.00%');
// see issue #20136
expect(formatPercent(0.12345674, defaultLocale, '0.0-10')).toEqual('12.345674%');
expect(formatPercent(0, defaultLocale, '0.0-10')).toEqual('0%');
expect(formatPercent(0.00, defaultLocale, '0.0-10')).toEqual('0%');
expect(formatPercent(1, defaultLocale, '0.0-10')).toEqual('100%');
expect(formatPercent(0.1, defaultLocale, '0.0-10')).toEqual('10%');
expect(formatPercent(0.12, defaultLocale, '0.0-10')).toEqual('12%');
expect(formatPercent(0.123, defaultLocale, '0.0-10')).toEqual('12.3%');
expect(formatPercent(12.3456, defaultLocale, '0.0-10')).toEqual('1,234.56%');
expect(formatPercent(12.345600, defaultLocale, '0.0-10')).toEqual('1,234.56%');
expect(formatPercent(12.345699999, defaultLocale, '0.0-6')).toEqual('1,234.57%');
expect(formatPercent(12.345699999, defaultLocale, '0.4-6')).toEqual('1,234.5700%');
expect(formatPercent(100, defaultLocale, '0.4-6')).toEqual('10,000.0000%');
expect(formatPercent(100, defaultLocale, '0.0-10')).toEqual('10,000%');
expect(formatPercent(1.5e2, defaultLocale)).toEqual('15,000%');
expect(formatPercent(1e100, defaultLocale)).toEqual('1E+102%');
});
});
});
describe('Currency', () => {
const defaultCurrencyCode = 'USD';
describe('transform', () => {
it('should return correct value for numbers', () => {
expect(formatCurrency(123, defaultLocale, '$')).toEqual('$123.00');
expect(formatCurrency(12, defaultLocale, 'EUR', 'EUR', '.1')).toEqual('EUR12.0');
expect(
formatCurrency(5.1234, defaultLocale, defaultCurrencyCode, defaultCurrencyCode, '.0-3'))
.toEqual('USD5.123');
expect(formatCurrency(5.1234, defaultLocale, defaultCurrencyCode)).toEqual('USD5.12');
expect(formatCurrency(5.1234, defaultLocale, '$')).toEqual('$5.12');
expect(formatCurrency(5.1234, defaultLocale, 'CA$')).toEqual('CA$5.12');
expect(formatCurrency(5.1234, defaultLocale, '$')).toEqual('$5.12');
expect(formatCurrency(5.1234, defaultLocale, '$', defaultCurrencyCode, '5.2-2'))
.toEqual('$00,005.12');
expect(formatCurrency(5.1234, 'fr', '$', defaultCurrencyCode, '5.2-2'))
.toEqual('00 005,12 $');
expect(formatCurrency(5, 'fr', '$US', defaultCurrencyCode)).toEqual('5,00 $US');
});
it('should support any currency code name', () => {
// currency code is unknown, default formatting options will be used
expect(formatCurrency(5.1234, defaultLocale, 'unexisting_ISO_code'))
.toEqual('unexisting_ISO_code5.12');
// currency code is USD, the pipe will format based on USD but will display "Custom name"
expect(formatCurrency(5.1234, defaultLocale, '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(formatCurrency(5.1234, defaultLocale, 'IDR', 'IDR')).toEqual('IDR5');
expect(formatCurrency(5.1234, defaultLocale, 'IDR', 'IDR', '.2')).toEqual('IDR5.12');
expect(formatCurrency(5.1234, defaultLocale, 'Custom name', 'IDR')).toEqual('Custom name5');
// BHD has a default number of digits of 3
expect(formatCurrency(5.1234, defaultLocale, 'BHD', 'BHD')).toEqual('BHD5.123');
expect(formatCurrency(5.1234, defaultLocale, 'BHD', 'BHD', '.1-2')).toEqual('BHD5.12');
});
});
});
});

View File

@ -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, getNbOfCurrencyDigits} from '../../src/i18n/locale_data_api';
import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth, getNumberOfCurrencyDigits} from '../../src/i18n/locale_data_api';
{
describe('locale data api', () => {
@ -76,10 +76,10 @@ import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth, get
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);
expect(getNumberOfCurrencyDigits('USD')).toEqual(2);
expect(getNumberOfCurrencyDigits('IDR')).toEqual(0);
expect(getNumberOfCurrencyDigits('BHD')).toEqual(3);
expect(getNumberOfCurrencyDigits('unexisting_ISO_code')).toEqual(2);
});
});

View File

@ -7,15 +7,10 @@
*/
import {DatePipe, registerLocaleData} from '@angular/common';
import {PipeResolver} from '@angular/compiler/src/pipe_resolver';
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
import localeEn from '@angular/common/locales/en';
import localeEnExtra from '@angular/common/locales/extra/en';
import localeDe from '@angular/common/locales/de';
import localeHu from '@angular/common/locales/hu';
import localeSr from '@angular/common/locales/sr';
import localeTh from '@angular/common/locales/th';
import localeAr from '@angular/common/locales/ar';
import {PipeResolver} from '@angular/compiler/src/pipe_resolver';
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
{
let date: Date;
@ -28,14 +23,7 @@ import localeAr from '@angular/common/locales/ar';
expect(pipe.transform(date, pattern)).toEqual(output);
}
beforeAll(() => {
registerLocaleData(localeEn, localeEnExtra);
registerLocaleData(localeDe);
registerLocaleData(localeHu);
registerLocaleData(localeSr);
registerLocaleData(localeTh);
registerLocaleData(localeAr);
});
beforeAll(() => { registerLocaleData(localeEn, localeEnExtra); });
beforeEach(() => {
date = new Date(2015, 5, 15, 9, 3, 1, 550);
@ -60,260 +48,21 @@ import localeAr from '@angular/common/locales/ar';
it('should support ISO string',
() => expect(() => pipe.transform('2015-06-15T21:43:11Z')).not.toThrow());
it('should return null for empty string', () => expect(pipe.transform('')).toEqual(null));
it('should return null for empty string',
() => { expect(pipe.transform('')).toEqual(null); });
it('should return null for NaN', () => expect(pipe.transform(Number.NaN)).toEqual(null));
it('should return null for NaN', () => { expect(pipe.transform(Number.NaN)).toEqual(null); });
it('should support ISO string without time',
() => { expect(() => pipe.transform(isoStringWithoutTime)).not.toThrow(); });
it('should not support other objects',
() => expect(() => pipe.transform({})).toThrowError(/InvalidPipeArgument/));
() => { expect(() => pipe.transform({})).toThrowError(/InvalidPipeArgument/); });
});
describe('transform', () => {
it('should format each component correctly', () => {
const dateFixtures: any = {
G: 'AD',
GG: 'AD',
GGG: 'AD',
GGGG: 'Anno Domini',
GGGGG: 'A',
y: '2015',
yy: '15',
yyy: '2015',
yyyy: '2015',
M: '6',
MM: '06',
MMM: 'Jun',
MMMM: 'June',
MMMMM: 'J',
L: '6',
LL: '06',
LLL: 'Jun',
LLLL: 'June',
LLLLL: 'J',
w: '25',
ww: '25',
W: '3',
d: '15',
dd: '15',
E: 'Mon',
EE: 'Mon',
EEE: 'Mon',
EEEE: 'Monday',
EEEEEE: 'Mo',
h: '9',
hh: '09',
H: '9',
HH: '09',
m: '3',
mm: '03',
s: '1',
ss: '01',
S: '6',
SS: '55',
SSS: '550',
a: 'AM',
aa: 'AM',
aaa: 'AM',
aaaa: 'AM',
aaaaa: 'a',
b: 'morning',
bb: 'morning',
bbb: 'morning',
bbbb: 'morning',
bbbbb: 'morning',
B: 'in the morning',
BB: 'in the morning',
BBB: 'in the morning',
BBBB: 'in the morning',
BBBBB: 'in the morning',
};
const isoStringWithoutTimeFixtures: any = {
G: 'AD',
GG: 'AD',
GGG: 'AD',
GGGG: 'Anno Domini',
GGGGG: 'A',
y: '2015',
yy: '15',
yyy: '2015',
yyyy: '2015',
M: '1',
MM: '01',
MMM: 'Jan',
MMMM: 'January',
MMMMM: 'J',
L: '1',
LL: '01',
LLL: 'Jan',
LLLL: 'January',
LLLLL: 'J',
w: '1',
ww: '01',
W: '1',
d: '1',
dd: '01',
E: 'Thu',
EE: 'Thu',
EEE: 'Thu',
EEEE: 'Thursday',
EEEEE: 'T',
EEEEEE: 'Th',
h: '12',
hh: '12',
H: '0',
HH: '00',
m: '0',
mm: '00',
s: '0',
ss: '00',
S: '0',
SS: '00',
SSS: '000',
a: 'AM',
aa: 'AM',
aaa: 'AM',
aaaa: 'AM',
aaaaa: 'a',
b: 'midnight',
bb: 'midnight',
bbb: 'midnight',
bbbb: 'midnight',
bbbbb: 'midnight',
B: 'midnight',
BB: 'midnight',
BBB: 'midnight',
BBBB: 'midnight',
BBBBB: 'mi',
};
Object.keys(dateFixtures).forEach((pattern: string) => {
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
});
Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => {
expectDateFormatAs(isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]);
});
});
it('should format with timezones', () => {
const dateFixtures: any = {
z: /GMT(\+|-)\d/,
zz: /GMT(\+|-)\d/,
zzz: /GMT(\+|-)\d/,
zzzz: /GMT(\+|-)\d{2}\:30/,
Z: /(\+|-)\d{2}30/,
ZZ: /(\+|-)\d{2}30/,
ZZZ: /(\+|-)\d{2}30/,
ZZZZ: /GMT(\+|-)\d{2}\:30/,
ZZZZZ: /(\+|-)\d{2}\:30/,
O: /GMT(\+|-)\d/,
OOOO: /GMT(\+|-)\d{2}\:30/,
};
Object.keys(dateFixtures).forEach((pattern: string) => {
expect(pipe.transform(date, pattern, '+0430')).toMatch(dateFixtures[pattern]);
});
});
it('should format common multi component patterns', () => {
const dateFixtures: any = {
'EEE, M/d/y': 'Mon, 6/15/2015',
'EEE, M/d': 'Mon, 6/15',
'MMM d': 'Jun 15',
'dd/MM/yyyy': '15/06/2015',
'MM/dd/yyyy': '06/15/2015',
'yMEEEd': '20156Mon15',
'MEEEd': '6Mon15',
'MMMd': 'Jun15',
'EEEE, MMMM d, y': 'Monday, June 15, 2015',
'H:mm a': '9:03 AM',
'ms': '31',
'MM/dd/yy hh:mm': '06/15/15 09:03',
'MM/dd/y': '06/15/2015'
};
Object.keys(dateFixtures).forEach((pattern: string) => {
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
});
});
it('should format with pattern aliases', () => {
const dateFixtures: any = {
'MM/dd/yyyy': '06/15/2015',
shortDate: '6/15/15',
mediumDate: 'Jun 15, 2015',
longDate: 'June 15, 2015',
fullDate: 'Monday, June 15, 2015',
short: '6/15/15, 9:03 AM',
medium: 'Jun 15, 2015, 9:03:01 AM',
long: /June 15, 2015 at 9:03:01 AM GMT(\+|-)\d/,
full: /Monday, June 15, 2015 at 9:03:01 AM GMT(\+|-)\d{2}:\d{2}/,
shortTime: '9:03 AM',
mediumTime: '9:03:01 AM',
longTime: /9:03:01 AM GMT(\+|-)\d/,
fullTime: /9:03:01 AM GMT(\+|-)\d{2}:\d{2}/,
};
Object.keys(dateFixtures).forEach((pattern: string) => {
expect(pipe.transform(date, pattern)).toMatch(dateFixtures[pattern]);
});
});
it('should format invalid in IE ISO date',
() => expect(pipe.transform('2017-01-11T12:00:00.014-0500')).toEqual('Jan 11, 2017'));
it('should format invalid in Safari ISO date',
() => expect(pipe.transform('2017-01-20T12:00:00+0000')).toEqual('Jan 20, 2017'));
// test for the following bugs:
// https://github.com/angular/angular/issues/9524
// https://github.com/angular/angular/issues/9524
it('should format correctly with iso strings that contain time',
() => expect(pipe.transform('2017-05-07T22:14:39', 'dd-MM-yyyy HH:mm'))
.toMatch(/07-05-2017 \d{2}:\d{2}/));
// test for issue https://github.com/angular/angular/issues/21491
it('should not assume UTC for iso strings in Safari if the timezone is not defined', () => {
// this test only works if the timezone is not in UTC
// which is the case for BrowserStack when we test Safari
if (new Date().getTimezoneOffset() !== 0) {
expect(pipe.transform('2018-01-11T13:00:00', 'HH'))
.not.toEqual(pipe.transform('2018-01-11T13:00:00Z', 'HH'));
}
});
// test for the following bugs:
// https://github.com/angular/angular/issues/16624
// https://github.com/angular/angular/issues/17478
it('should show the correct time when the timezone is fixed', () => {
expect(pipe.transform('2017-06-13T10:14:39+0000', 'shortTime', '+0000'))
.toEqual('10:14 AM');
expect(pipe.transform('2017-06-13T10:14:39+0000', 'h:mm a', '+0000')).toEqual('10:14 AM');
});
it('should remove bidi control characters',
() => expect(pipe.transform(date, 'MM/dd/yyyy') !.length).toEqual(10));
it(`should format the date correctly in various locales`, () => {
expect(new DatePipe('de').transform(date, 'short')).toEqual('15.06.15, 09:03');
expect(new DatePipe('ar').transform(date, 'short')).toEqual('15/6/2015 9:03 ص');
expect(new DatePipe('th').transform(date, 'dd-MM-yy')).toEqual('15-06-15');
expect(new DatePipe('hu').transform(date, 'a')).toEqual('de.');
expect(new DatePipe('sr').transform(date, 'a')).toEqual('пре подне');
// TODO(ocombe): activate this test when we support local numbers
// expect(new DatePipe('mr', [localeMr]).transform(date, 'hh')).toEqual('०९');
});
it('should throw if we use getExtraDayPeriods without loading extra locale data', () => {
expect(() => new DatePipe('de').transform(date, 'b'))
.toThrowError(/Missing extra locale data for the locale "de"/);
});
it('should use "mediumDate" as the default format',
() => expect(pipe.transform('2017-01-11T10:14:39+0000')).toEqual('Jan 11, 2017'));
});
});
}

View File

@ -10,7 +10,7 @@ import localeEn from '@angular/common/locales/en';
import localeEsUS from '@angular/common/locales/es-US';
import localeFr from '@angular/common/locales/fr';
import localeAr from '@angular/common/locales/ar';
import {registerLocaleData, CurrencyPipe, DecimalPipe, PercentPipe} from '@angular/common';
import {registerLocaleData, CurrencyPipe, DecimalPipe, PercentPipe, formatNumber} from '@angular/common';
import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testing_internal';
{
@ -22,8 +22,6 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin
registerLocaleData(localeAr);
});
function isNumeric(value: any): boolean { return !isNaN(value - parseFloat(value)); }
describe('DecimalPipe', () => {
describe('transform', () => {
let pipe: DecimalPipe;
@ -31,13 +29,7 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin
it('should return correct value for numbers', () => {
expect(pipe.transform(12345)).toEqual('12,345');
expect(pipe.transform(123, '.2')).toEqual('123.00');
expect(pipe.transform(1, '3.')).toEqual('001');
expect(pipe.transform(1.1, '3.4-5')).toEqual('001.1000');
expect(pipe.transform(1.123456, '3.4-5')).toEqual('001.12346');
expect(pipe.transform(1.1234)).toEqual('1.123');
expect(pipe.transform(1.123456, '.2')).toEqual('1.123');
expect(pipe.transform(1.123456, '.4')).toEqual('1.1235');
});
it('should support strings', () => {
@ -56,10 +48,6 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin
expect(() => pipe.transform('123abc'))
.toThrowError(`InvalidPipeArgument: '123abc is not a number' for pipe 'DecimalPipe'`);
});
it('should throw if minFractionDigits is explicitly higher than maxFractionDigits', () => {
expect(() => pipe.transform('1.1', '3.4-2')).toThrowError(/is higher than the maximum/);
});
});
describe('transform with custom locales', () => {
@ -78,26 +66,7 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin
describe('transform', () => {
it('should return correct value for numbers', () => {
expect(pipe.transform(1.23)).toEqual('123%');
expect(pipe.transform(1.2, '.2')).toEqual('120.00%');
expect(pipe.transform(1.2, '4.2')).toEqual('0,120.00%');
expect(pipe.transform(1.2, '4.2', 'fr')).toEqual('0 120,00 %');
expect(pipe.transform(1.2, '4.2', 'ar')).toEqual('0,120.00%');
// see issue #20136
expect(pipe.transform(0.12345674, '0.0-10')).toEqual('12.345674%');
expect(pipe.transform(0, '0.0-10')).toEqual('0%');
expect(pipe.transform(0.00, '0.0-10')).toEqual('0%');
expect(pipe.transform(1, '0.0-10')).toEqual('100%');
expect(pipe.transform(0.1, '0.0-10')).toEqual('10%');
expect(pipe.transform(0.12, '0.0-10')).toEqual('12%');
expect(pipe.transform(0.123, '0.0-10')).toEqual('12.3%');
expect(pipe.transform(12.3456, '0.0-10')).toEqual('1,234.56%');
expect(pipe.transform(12.345600, '0.0-10')).toEqual('1,234.56%');
expect(pipe.transform(12.345699999, '0.0-6')).toEqual('1,234.57%');
expect(pipe.transform(12.345699999, '0.4-6')).toEqual('1,234.5700%');
expect(pipe.transform(100, '0.4-6')).toEqual('10,000.0000%');
expect(pipe.transform(100, '0.0-10')).toEqual('10,000%');
expect(pipe.transform(1.5e2)).toEqual('15,000%');
expect(pipe.transform(1e100)).toEqual('1E+102%');
});
it('should not support other objects', () => {
@ -136,16 +105,6 @@ 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(
@ -160,25 +119,5 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin
});
});
});
describe('isNumeric', () => {
it('should return true when passing correct numeric string',
() => { expect(isNumeric('2')).toBe(true); });
it('should return true when passing correct double string',
() => { expect(isNumeric('1.123')).toBe(true); });
it('should return true when passing correct negative string',
() => { expect(isNumeric('-2')).toBe(true); });
it('should return true when passing correct scientific notation string',
() => { expect(isNumeric('1e5')).toBe(true); });
it('should return false when passing incorrect numeric',
() => { expect(isNumeric('a')).toBe(false); });
it('should return false when passing parseable but non numeric',
() => { expect(isNumeric('2a')).toBe(false); });
});
});
}

View File

@ -64,6 +64,18 @@ export declare class DeprecatedPercentPipe implements PipeTransform {
/** @stable */
export declare const DOCUMENT: InjectionToken<Document>;
/** @stable */
export declare function formatCurrency(value: number, locale: string, currency: string, currencyCode?: string, digitsInfo?: string): string;
/** @stable */
export declare function formatDate(value: string | number | Date, format: string, locale: string, timezone?: string): string;
/** @stable */
export declare function formatNumber(value: number, locale: string, digitsInfo?: string): string;
/** @stable */
export declare function formatPercent(value: number, locale: string, digitsInfo?: string): string;
/** @experimental */
export declare enum FormatWidth {
Short = 0,
@ -133,7 +145,7 @@ export declare function getLocaleTimeFormat(locale: string, width: FormatWidth):
export declare function getLocaleWeekEndRange(locale: string): [WeekDay, WeekDay];
/** @experimental */
export declare function getNbOfCurrencyDigits(code: string): number;
export declare function getNumberOfCurrencyDigits(code: string): number;
/** @stable */
export declare class HashLocationStrategy extends LocationStrategy {