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:

committed by
Victor Berchet

parent
094666da17
commit
4180912538
@ -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());
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user