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

@ -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;
}