Permalink
Browse files

feat(common): export functions to format numbers, percents, currencie…

…s & 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
  • Loading branch information...
ocombe authored and vicb committed Feb 23, 2018
1 parent 094666d commit 418091253887d45f38022fbf7db6dedad2857ac9
@@ -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';
@@ -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());
}
@@ -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
*
* Formats a number as percentage.
*
* @internal
* 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
*
* 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.
*
* @internal
* @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)) {
@@ -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) {
Oops, something went wrong.

0 comments on commit 4180912

Please sign in to comment.