feat(common): drop use of the Intl API to improve browser support (#18284)

BREAKING CHANGE: Because of multiple bugs and browser inconsistencies, we have dropped the intl api in favor of data exported from the Unicode Common Locale Data Repository (CLDR).
Unfortunately we had to change the i18n pipes (date, number, currency, percent) and there are some breaking changes.

1. I18n pipes
* Breaking change:
  - By default Angular now only contains locale data for the language `en-US`, if you set the value of `LOCALE_ID` to another locale, you will have to import new locale data for this language because we don't use the intl API anymore.

* Features:
  - you don't need to use the intl polyfill for Angular anymore.
  - all i18n pipes now have an additional last parameter `locale` which allows you to use a specific locale instead of the one defined in the token `LOCALE_ID` (whose value is `en-US` by default).
  - the new locale data extracted from CLDR are now available to developers as well and can be used through an API (which should be especially useful for library authors).
  - you can still use the old pipes for now, but their names have been changed and they are no longer included in the `CommonModule`. To use them, you will have to import the `DeprecatedI18NPipesModule` after the `CommonModule` (the order is important):

  ```ts
  import { NgModule } from '@angular/core';
  import { CommonModule, DeprecatedI18NPipesModule } from '@angular/common';

  @NgModule({
    imports: [
      CommonModule,
      // import deprecated module after
      DeprecatedI18NPipesModule
    ]
  })
  export class AppModule { }
  ```

  Dont forget that you will still need to import the intl API polyfill if you want to use those deprecated pipes.

2. Date pipe
* Breaking changes:
  - the predefined formats (`short`, `shortTime`, `shortDate`, `medium`, ...) now use the patterns given by CLDR (like it was in AngularJS) instead of the ones from the intl API. You might notice some changes, e.g. `shortDate` will be `8/15/17` instead of `8/15/2017` for `en-US`.
  - the narrow version of eras is now `GGGGG` instead of `G`, the format `G` is now similar to `GG` and `GGG`.
  - the narrow version of months is now `MMMMM` instead of `L`, the format `L` is now the short standalone version of months.
  - the narrow version of the week day is now `EEEEE` instead of `E`, the format `E` is now similar to `EE` and `EEE`.
  - the timezone `z` will now fallback to `O` and output `GMT+1` instead of the complete zone name (e.g. `Pacific Standard Time`), this is because the quantity of data required to have all the zone names in all of the existing locales is too big.
  - the timezone `Z` will now output the ISO8601 basic format, e.g. `+0100`, you should now use `ZZZZ` to get `GMT+01:00`.

  | Field type | Format        | Example value         | v4 | v5            |
  |------------|---------------|-----------------------|----|---------------|
  | Eras       | Narrow        | A for AD              | G  | GGGGG         |
  | Months     | Narrow        | S for September       | L  | MMMMM         |
  | Week day   | Narrow        | M for Monday          | E  | EEEEE         |
  | Timezone   | Long location | Pacific Standard Time | z  | Not available |
  | Timezone   | Long GMT      | GMT+01:00             | Z  | ZZZZ          |

* Features
  - new predefined formats `long`, `full`, `longTime`, `fullTime`.
  - the format `yyy` is now supported, e.g. the year `52` will be `052` and the year `2017` will be `2017`.
  - standalone months are now supported with the formats `L` to `LLLLL`.
  - week of the year is now supported with the formats `w` and `ww`, e.g. weeks `5` and `05`.
  - week of the month is now supported with the format `W`, e.g. week `3`.
  - fractional seconds are now supported with the format `S` to `SSS`.
  - day periods for AM/PM now supports additional formats `aa`, `aaa`, `aaaa` and `aaaaa`. The formats `a` to `aaa` are similar, while `aaaa` is the wide version if available (e.g. `ante meridiem` for `am`), or equivalent to `a` otherwise, and `aaaaa` is the narrow version (e.g. `a` for `am`).
  - extra day periods are now supported with the formats `b` to `bbbbb` (and `B` to `BBBBB` for the standalone equivalents), e.g. `morning`, `noon`, `afternoon`, ....
  - the short non-localized timezones are now available with the format `O` to `OOOO`. The formats `O` to `OOO` will output `GMT+1` while the format `OOOO` will be `GMT+01:00`.
  - the ISO8601 basic time zones are now available with the formats `Z` to `ZZZZZ`. The formats `Z` to `ZZZ` will output `+0100`, while the format `ZZZZ` will be `GMT+01:00` and `ZZZZZ` will be `+01:00`.

* Bug fixes
  - the date pipe will now work exactly the same across all browsers, which will fix a lot of bugs for safari and IE.
  - eras can now be used on their own without the date, e.g. the format `GG` will be `AD` instead of `8 15, 2017 AD`.

3. Currency pipe
* Breaking change:
  - the default value for `symbolDisplay` is now `symbol` instead of `code`. This means that by default you will see `$4.99` for `en-US` instead of `USD4.99` previously.

* Deprecation:
  - the second parameter of the currency pipe (`symbolDisplay`) is no longer a boolean, it now takes the values `code`, `symbol` or `symbol-narrow`. A boolean value is still valid for now, but it is deprecated and it will print a warning message in the console.

* Features:
  - you can now choose between `code`, `symbol` or `symbol-narrow` which gives you access to more options for some currencies (e.g. the canadian dollar with the code `CAD` has the symbol `CA$` and the symbol-narrow `$`).

4. Percent pipe
* Breaking change
  - if you don't specify the number of digits to round to, the local format will be used (and it usually rounds numbers to 0 digits, instead of not rounding previously), e.g. `{{ 3.141592 | percent }}` will output `314%` for the locale `en-US` instead of `314.1592%` previously.

Fixes #10809, #9524, #7008, #9324, #7590, #6724, #3429, #17576, #17478, #17319, #17200, #16838, #16624, #16625, #16591, #14131, #12632, #11376, #11187

PR Close #18284
This commit is contained in:
Olivier Combe
2017-08-22 20:30:59 +02:00
committed by Miško Hevery
parent a73389bc71
commit 079d884b6c
35 changed files with 3087 additions and 738 deletions

View File

@ -6,10 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {DatePipe} from '@angular/common';
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 {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import localeEn from '../../i18n_data/locale_en';
import localeEnExtra from '../../i18n_data/extra/locale_en';
import localeDe from '../../i18n_data/locale_de';
import localeHu from '../../i18n_data/locale_hu';
import localeSr from '../../i18n_data/locale_sr';
import localeTh from '../../i18n_data/locale_th';
export function main() {
describe('DatePipe', () => {
@ -22,16 +27,16 @@ export function main() {
expect(pipe.transform(date, pattern)).toEqual(output);
}
// TODO: reactivate the disabled expectations once emulators are fixed in SauceLabs
// In some old versions of Chrome in Android emulators, time formatting returns dates in the
// timezone of the VM host,
// instead of the device timezone. Same symptoms as
// https://bugs.chromium.org/p/chromium/issues/detail?id=406382
// This happens locally and in SauceLabs, so some checks are disabled to avoid failures.
// Tracking issue: https://github.com/angular/angular/issues/11187
beforeAll(() => {
registerLocaleData(localeEn, localeEnExtra);
registerLocaleData(localeDe);
registerLocaleData(localeHu);
registerLocaleData(localeSr);
registerLocaleData(localeTh);
});
beforeEach(() => {
date = new Date(2015, 5, 15, 9, 3, 1);
date = new Date(2015, 5, 15, 9, 3, 1, 550);
pipe = new DatePipe('en-US');
});
@ -67,71 +72,149 @@ export function main() {
describe('transform', () => {
it('should format each component correctly', () => {
const dateFixtures: any = {
'y': '2015',
'yy': '15',
'M': '6',
'MM': '06',
'MMM': 'Jun',
'MMMM': 'June',
'd': '15',
'dd': '15',
'EEE': 'Mon',
'EEEE': 'Monday'
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 = {
'y': '2015',
'yy': '15',
'M': '1',
'MM': '01',
'MMM': 'Jan',
'MMMM': 'January',
'd': '1',
'dd': '01',
'EEE': 'Thu',
'EEEE': 'Thursday'
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',
};
if (!browserDetection.isOldChrome) {
dateFixtures['h'] = '9';
dateFixtures['hh'] = '09';
dateFixtures['j'] = '9 AM';
isoStringWithoutTimeFixtures['h'] = '12';
isoStringWithoutTimeFixtures['hh'] = '12';
isoStringWithoutTimeFixtures['j'] = '12 AM';
}
// IE and Edge can't format a date to minutes and seconds without hours
if (!browserDetection.isEdge && !browserDetection.isIE ||
!browserDetection.supportsNativeIntlApi) {
if (!browserDetection.isOldChrome) {
dateFixtures['HH'] = '09';
isoStringWithoutTimeFixtures['HH'] = '00';
}
dateFixtures['E'] = 'M';
dateFixtures['L'] = 'J';
dateFixtures['m'] = '3';
dateFixtures['s'] = '1';
dateFixtures['mm'] = '03';
dateFixtures['ss'] = '01';
isoStringWithoutTimeFixtures['m'] = '0';
isoStringWithoutTimeFixtures['s'] = '0';
isoStringWithoutTimeFixtures['mm'] = '00';
isoStringWithoutTimeFixtures['ss'] = '00';
}
Object.keys(dateFixtures).forEach((pattern: string) => {
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
});
if (!browserDetection.isOldChrome) {
Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => {
expectDateFormatAs(
isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]);
});
}
Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => {
expectDateFormatAs(isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]);
});
});
expect(pipe.transform(date, 'Z')).toBeDefined();
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', () => {
@ -144,19 +227,13 @@ export function main() {
'yMEEEd': '20156Mon15',
'MEEEd': '6Mon15',
'MMMd': 'Jun15',
'yMMMMEEEEd': 'Monday, June 15, 2015'
'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'
};
// IE and Edge can't format a date to minutes and seconds without hours
if (!browserDetection.isEdge && !browserDetection.isIE ||
!browserDetection.supportsNativeIntlApi) {
dateFixtures['ms'] = '31';
}
if (!browserDetection.isOldChrome) {
dateFixtures['jm'] = '9:03 AM';
}
Object.keys(dateFixtures).forEach((pattern: string) => {
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
});
@ -166,33 +243,23 @@ export function main() {
it('should format with pattern aliases', () => {
const dateFixtures: any = {
'MM/dd/yyyy': '06/15/2015',
'fullDate': 'Monday, June 15, 2015',
'longDate': 'June 15, 2015',
'mediumDate': 'Jun 15, 2015',
'shortDate': '6/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}/,
};
if (!browserDetection.isOldChrome) {
// IE and Edge do not add a coma after the year in these 2 cases
if ((browserDetection.isEdge || browserDetection.isIE) &&
browserDetection.supportsNativeIntlApi) {
dateFixtures['medium'] = 'Jun 15, 2015 9:03:01 AM';
dateFixtures['short'] = '6/15/2015 9:03 AM';
} else {
dateFixtures['medium'] = 'Jun 15, 2015, 9:03:01 AM';
dateFixtures['short'] = '6/15/2015, 9:03 AM';
}
}
if (!browserDetection.isOldChrome) {
dateFixtures['mediumTime'] = '9:03:01 AM';
dateFixtures['shortTime'] = '9:03 AM';
}
Object.keys(dateFixtures).forEach((pattern: string) => {
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
expect(pipe.transform(date, pattern)).toMatch(dateFixtures[pattern]);
});
});
it('should format invalid in IE ISO date',
@ -201,8 +268,39 @@ export function main() {
it('should format invalid in Safari ISO date',
() => expect(pipe.transform('2017-01-20T19: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 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('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"/);
});
});
});
}

View File

@ -0,0 +1,208 @@
/**
* @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 {DeprecatedDatePipe} from '@angular/common';
import {PipeResolver} from '@angular/compiler/src/pipe_resolver';
import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
export function main() {
describe('DeprecatedDatePipe', () => {
let date: Date;
const isoStringWithoutTime = '2015-01-01';
let pipe: DeprecatedDatePipe;
// Check the transformation of a date into a pattern
function expectDateFormatAs(date: Date | string, pattern: any, output: string): void {
expect(pipe.transform(date, pattern)).toEqual(output);
}
// TODO: reactivate the disabled expectations once emulators are fixed in SauceLabs
// In some old versions of Chrome in Android emulators, time formatting returns dates in the
// timezone of the VM host,
// instead of the device timezone. Same symptoms as
// https://bugs.chromium.org/p/chromium/issues/detail?id=406382
// This happens locally and in SauceLabs, so some checks are disabled to avoid failures.
// Tracking issue: https://github.com/angular/angular/issues/11187
beforeEach(() => {
date = new Date(2015, 5, 15, 9, 3, 1);
pipe = new DeprecatedDatePipe('en-US');
});
it('should be marked as pure', () => {
expect(new PipeResolver(new JitReflector()).resolve(DeprecatedDatePipe) !.pure).toEqual(true);
});
describe('supports', () => {
it('should support date', () => { expect(() => pipe.transform(date)).not.toThrow(); });
it('should support int', () => { expect(() => pipe.transform(123456789)).not.toThrow(); });
it('should support numeric strings',
() => { expect(() => pipe.transform('123456789')).not.toThrow(); });
it('should support decimal strings',
() => { expect(() => pipe.transform('123456789.11')).not.toThrow(); });
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 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/));
});
describe('transform', () => {
it('should format each component correctly', () => {
const dateFixtures: any = {
'y': '2015',
'yy': '15',
'M': '6',
'MM': '06',
'MMM': 'Jun',
'MMMM': 'June',
'd': '15',
'dd': '15',
'EEE': 'Mon',
'EEEE': 'Monday'
};
const isoStringWithoutTimeFixtures: any = {
'y': '2015',
'yy': '15',
'M': '1',
'MM': '01',
'MMM': 'Jan',
'MMMM': 'January',
'd': '1',
'dd': '01',
'EEE': 'Thu',
'EEEE': 'Thursday'
};
if (!browserDetection.isOldChrome) {
dateFixtures['h'] = '9';
dateFixtures['hh'] = '09';
dateFixtures['j'] = '9 AM';
isoStringWithoutTimeFixtures['h'] = '12';
isoStringWithoutTimeFixtures['hh'] = '12';
isoStringWithoutTimeFixtures['j'] = '12 AM';
}
// IE and Edge can't format a date to minutes and seconds without hours
if (!browserDetection.isEdge && !browserDetection.isIE ||
!browserDetection.supportsNativeIntlApi) {
if (!browserDetection.isOldChrome) {
dateFixtures['HH'] = '09';
isoStringWithoutTimeFixtures['HH'] = '00';
}
dateFixtures['E'] = 'M';
dateFixtures['L'] = 'J';
dateFixtures['m'] = '3';
dateFixtures['s'] = '1';
dateFixtures['mm'] = '03';
dateFixtures['ss'] = '01';
isoStringWithoutTimeFixtures['m'] = '0';
isoStringWithoutTimeFixtures['s'] = '0';
isoStringWithoutTimeFixtures['mm'] = '00';
isoStringWithoutTimeFixtures['ss'] = '00';
}
Object.keys(dateFixtures).forEach((pattern: string) => {
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
});
if (!browserDetection.isOldChrome) {
Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => {
expectDateFormatAs(
isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]);
});
}
expect(pipe.transform(date, 'Z')).toBeDefined();
});
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',
'yMMMMEEEEd': 'Monday, June 15, 2015'
};
// IE and Edge can't format a date to minutes and seconds without hours
if (!browserDetection.isEdge && !browserDetection.isIE ||
!browserDetection.supportsNativeIntlApi) {
dateFixtures['ms'] = '31';
}
if (!browserDetection.isOldChrome) {
dateFixtures['jm'] = '9:03 AM';
}
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',
'fullDate': 'Monday, June 15, 2015',
'longDate': 'June 15, 2015',
'mediumDate': 'Jun 15, 2015',
'shortDate': '6/15/2015'
};
if (!browserDetection.isOldChrome) {
// IE and Edge do not add a coma after the year in these 2 cases
if ((browserDetection.isEdge || browserDetection.isIE) &&
browserDetection.supportsNativeIntlApi) {
dateFixtures['medium'] = 'Jun 15, 2015 9:03:01 AM';
dateFixtures['short'] = '6/15/2015 9:03 AM';
} else {
dateFixtures['medium'] = 'Jun 15, 2015, 9:03:01 AM';
dateFixtures['short'] = '6/15/2015, 9:03 AM';
}
}
if (!browserDetection.isOldChrome) {
dateFixtures['mediumTime'] = '9:03:01 AM';
dateFixtures['shortTime'] = '9:03 AM';
}
Object.keys(dateFixtures).forEach((pattern: string) => {
expectDateFormatAs(date, pattern, dateFixtures[pattern]);
});
});
it('should format invalid in IE ISO date',
() => expect(pipe.transform('2017-01-11T09:25:14.014-0500')).toEqual('Jan 11, 2017'));
it('should format invalid in Safari ISO date',
() => expect(pipe.transform('2017-01-20T19:00:00+0000')).toEqual('Jan 20, 2017'));
it('should remove bidi control characters',
() => expect(pipe.transform(date, 'MM/dd/yyyy') !.length).toEqual(10));
});
});
}

View File

@ -0,0 +1,109 @@
/**
* @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 {DeprecatedCurrencyPipe, DeprecatedDecimalPipe, DeprecatedPercentPipe} from '@angular/common';
import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testing_internal';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
export function main() {
function isNumeric(value: any): boolean { return !isNaN(value - parseFloat(value)); }
// Between the symbol and the number, Edge adds a no breaking space and IE11 adds a standard space
function normalize(s: string): string { return s.replace(/\u00A0| /g, ''); }
describe('Number pipes', () => {
describe('DeprecatedDecimalPipe', () => {
let pipe: DeprecatedDecimalPipe;
beforeEach(() => { pipe = new DeprecatedDecimalPipe('en-US'); });
describe('transform', () => {
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');
});
it('should support strings', () => {
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');
});
it('should not support other objects', () => {
expect(() => pipe.transform(new Object())).toThrowError();
expect(() => pipe.transform('123abc')).toThrowError();
});
});
});
describe('DeprecatedPercentPipe', () => {
let pipe: DeprecatedPercentPipe;
beforeEach(() => { pipe = new DeprecatedPercentPipe('en-US'); });
describe('transform', () => {
it('should return correct value for numbers', () => {
expect(normalize(pipe.transform(1.23) !)).toEqual('123%');
expect(normalize(pipe.transform(1.2, '.2') !)).toEqual('120.00%');
});
it('should not support other objects',
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
});
});
describe('DeprecatedCurrencyPipe', () => {
let pipe: DeprecatedCurrencyPipe;
beforeEach(() => { pipe = new DeprecatedCurrencyPipe('en-US'); });
describe('transform', () => {
it('should return correct value for numbers', () => {
// In old Chrome, default formatiing for USD is different
if (browserDetection.isOldChrome) {
expect(normalize(pipe.transform(123) !)).toEqual('USD123');
} else {
expect(normalize(pipe.transform(123) !)).toEqual('USD123.00');
}
expect(normalize(pipe.transform(12, 'EUR', false, '.1') !)).toEqual('EUR12.0');
expect(normalize(pipe.transform(5.1234, 'USD', false, '.0-3') !)).toEqual('USD5.123');
});
it('should not support other objects',
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
});
});
describe('isNumeric', () => {
it('should return true when passing correct numeric string',
() => { expect(isNumeric('2')).toBe(true); });
it('should return true when passing correct double string',
() => { expect(isNumeric('1.123')).toBe(true); });
it('should return true when passing correct negative string',
() => { expect(isNumeric('-2')).toBe(true); });
it('should return true when passing correct scientific notation string',
() => { expect(isNumeric('1e5')).toBe(true); });
it('should return false when passing incorrect numeric',
() => { expect(isNumeric('a')).toBe(false); });
it('should return false when passing parseable but non numeric',
() => { expect(isNumeric('2a')).toBe(false); });
});
});
}

View File

@ -6,19 +6,25 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CurrencyPipe, DecimalPipe, PercentPipe} from '@angular/common';
import {isNumeric} from '@angular/common/src/pipes/number_pipe';
import localeEn from '../../i18n_data/locale_en';
import localeEsUS from '../../i18n_data/locale_es-US';
import {registerLocaleData, CurrencyPipe, DecimalPipe, PercentPipe} from '@angular/common';
import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testing_internal';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
export function main() {
describe('Number pipes', () => {
beforeAll(() => {
registerLocaleData(localeEn);
registerLocaleData(localeEsUS);
});
function isNumeric(value: any): boolean { return !isNaN(value - parseFloat(value)); }
describe('DecimalPipe', () => {
let pipe: DecimalPipe;
beforeEach(() => { pipe = new DecimalPipe('en-US'); });
describe('transform', () => {
let pipe: DecimalPipe;
beforeEach(() => { pipe = new DecimalPipe('en-US'); });
it('should return correct value for numbers', () => {
expect(pipe.transform(12345)).toEqual('12,345');
expect(pipe.transform(123, '.2')).toEqual('123.00');
@ -26,6 +32,8 @@ export function main() {
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', () => {
@ -38,9 +46,20 @@ export function main() {
});
it('should not support other objects', () => {
expect(() => pipe.transform(new Object())).toThrowError();
expect(() => pipe.transform({})).toThrowError();
expect(() => pipe.transform('123abc')).toThrowError();
});
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', () => {
it('should return the correct format for es-US in IE11', () => {
const pipe = new DecimalPipe('es-US');
expect(pipe.transform('9999999.99', '1.2-2')).toEqual('9,999,999.99');
});
});
});
@ -51,12 +70,12 @@ export function main() {
describe('transform', () => {
it('should return correct value for numbers', () => {
expect(normalize(pipe.transform(1.23) !)).toEqual('123%');
expect(normalize(pipe.transform(1.2, '.2') !)).toEqual('120.00%');
expect(pipe.transform(1.23)).toEqual('123%');
expect(pipe.transform(1.2, '.2')).toEqual('120.00%');
});
it('should not support other objects',
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
() => { expect(() => pipe.transform({})).toThrowError(); });
});
});
@ -67,18 +86,24 @@ export function main() {
describe('transform', () => {
it('should return correct value for numbers', () => {
// In old Chrome, default formatiing for USD is different
if (browserDetection.isOldChrome) {
expect(normalize(pipe.transform(123) !)).toEqual('USD123');
} else {
expect(normalize(pipe.transform(123) !)).toEqual('USD123.00');
}
expect(normalize(pipe.transform(12, 'EUR', false, '.1') !)).toEqual('EUR12.0');
expect(normalize(pipe.transform(5.1234, 'USD', false, '.0-3') !)).toEqual('USD5.123');
expect(pipe.transform(123)).toEqual('$123.00');
expect(pipe.transform(12, 'EUR', 'code', '.1')).toEqual('EUR12.0');
expect(pipe.transform(5.1234, 'USD', 'code', '.0-3')).toEqual('USD5.123');
expect(pipe.transform(5.1234, 'USD', 'code')).toEqual('USD5.12');
expect(pipe.transform(5.1234, 'USD', 'symbol')).toEqual('$5.12');
expect(pipe.transform(5.1234, 'CAD', 'symbol')).toEqual('CA$5.12');
expect(pipe.transform(5.1234, 'CAD', 'symbol-narrow')).toEqual('$5.12');
});
it('should not support other objects',
() => { expect(() => pipe.transform(new Object())).toThrowError(); });
() => { expect(() => pipe.transform({})).toThrowError(); });
it('should warn if you are using the v4 signature', () => {
const warnSpy = spyOn(console, 'warn');
pipe.transform(123, 'USD', true);
expect(warnSpy).toHaveBeenCalledWith(
`Warning: the currency pipe has been changed in Angular v5. The symbolDisplay option (third parameter) is now a string instead of a boolean. The accepted values are "code", "symbol" or "symbol-narrow".`);
});
});
});
@ -103,8 +128,3 @@ export function main() {
});
});
}
// Between the symbol and the number, Edge adds a no breaking space and IE11 adds a standard space
function normalize(s: string): string {
return s.replace(/\u00A0| /g, '');
}