diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts
index 13d0d61cfa..9db719aba6 100644
--- a/packages/common/src/common.ts
+++ b/packages/common/src/common.ts
@@ -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';
diff --git a/packages/common/src/i18n/format_date.ts b/packages/common/src/i18n/format_date.ts
index cfb952c82b..981257ecb7 100644
--- a/packages/common/src/i18n/format_date.ts
+++ b/packages/common/src/i18n/format_date.ts
@@ -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());
+}
diff --git a/packages/common/src/i18n/format_number.ts b/packages/common/src/i18n/format_number.ts
index 9033486829..4f31086881 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, 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)) {
diff --git a/packages/common/src/i18n/locale_data_api.ts b/packages/common/src/i18n/locale_data_api.ts
index b88db6f2bc..eb90cedacc 100644
--- a/packages/common/src/i18n/locale_data_api.ts
+++ b/packages/common/src/i18n/locale_data_api.ts
@@ -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) {
diff --git a/packages/common/src/pipes/date_pipe.ts b/packages/common/src/pipes/date_pipe.ts
index 2e2c1ededc..da00c9ca9b 100644
--- a/packages/common/src/pipes/date_pipe.ts
+++ b/packages/common/src/pipes/date_pipe.ts
@@ -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());
-}
diff --git a/packages/common/src/pipes/deprecated/date_pipe.ts b/packages/common/src/pipes/deprecated/date_pipe.ts
index 0ff452e679..7cbd954cc0 100644
--- a/packages/common/src/pipes/deprecated/date_pipe.ts
+++ b/packages/common/src/pipes/deprecated/date_pipe.ts
@@ -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';
diff --git a/packages/common/src/pipes/number_pipe.ts b/packages/common/src/pipes/number_pipe.ts
index 9b49b6018d..824b15f4aa 100644
--- a/packages/common/src/pipes/number_pipe.ts
+++ b/packages/common/src/pipes/number_pipe.ts
@@ -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:
- * {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}
+ * Where:
+ * - `value` is a number
+ * - `digitInfo` is a `string` which has a following format:
+ * {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}
.
* - `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;
+}
diff --git a/packages/common/test/i18n/format_date_spec.ts b/packages/common/test/i18n/format_date_spec.ts
new file mode 100644
index 0000000000..a57e3929ca
--- /dev/null
+++ b/packages/common/test/i18n/format_date_spec.ts
@@ -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"/);
+ });
+ });
+});
diff --git a/packages/common/test/i18n/format_number_spec.ts b/packages/common/test/i18n/format_number_spec.ts
new file mode 100644
index 0000000000..970b68f5ef
--- /dev/null
+++ b/packages/common/test/i18n/format_number_spec.ts
@@ -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');
+ });
+ });
+ });
+});
diff --git a/packages/common/test/i18n/locale_data_api_spec.ts b/packages/common/test/i18n/locale_data_api_spec.ts
index e44ec258e6..9fee387070 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, 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);
});
});
diff --git a/packages/common/test/pipes/date_pipe_spec.ts b/packages/common/test/pipes/date_pipe_spec.ts
index 5103b4549a..5b84bb45c6 100644
--- a/packages/common/test/pipes/date_pipe_spec.ts
+++ b/packages/common/test/pipes/date_pipe_spec.ts
@@ -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'));
});
});
}
diff --git a/packages/common/test/pipes/number_pipe_spec.ts b/packages/common/test/pipes/number_pipe_spec.ts
index 9e5c40ccf5..adc9676f44 100644
--- a/packages/common/test/pipes/number_pipe_spec.ts
+++ b/packages/common/test/pipes/number_pipe_spec.ts
@@ -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); });
- });
});
}
diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts
index b40100e9df..9d07132c20 100644
--- a/tools/public_api_guard/common/common.d.ts
+++ b/tools/public_api_guard/common/common.d.ts
@@ -64,6 +64,18 @@ export declare class DeprecatedPercentPipe implements PipeTransform {
/** @stable */
export declare const DOCUMENT: InjectionToken;
+/** @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 {