diff --git a/modules/@angular/common/src/pipes/date_pipe.ts b/modules/@angular/common/src/pipes/date_pipe.ts index 5315441e46..ff85cba0b3 100644 --- a/modules/@angular/common/src/pipes/date_pipe.ts +++ b/modules/@angular/common/src/pipes/date_pipe.ts @@ -54,6 +54,9 @@ import {InvalidPipeArgumentError} from './invalid_pipe_argument_error'; * * Timezone of the formatted text will be the local system timezone of the end-user's machine. * + * When the expression is a ISO string without time (e.g. 2016-09-19) the time zone offset is not + * applied and the formatted text will have the same day, month and year of the expression. + * * WARNINGS: * - this pipe is marked as pure hence it will not be re-evaluated when the input is mutated. * Instead users should treat the date as an immutable object and change the reference when the @@ -95,17 +98,30 @@ export class DatePipe implements PipeTransform { constructor(@Inject(LOCALE_ID) private _locale: string) {} transform(value: any, pattern: string = 'mediumDate'): string { + let date: Date; + if (isBlank(value)) return null; if (typeof value === 'string') { value = value.trim(); } - let date: Date; if (isDate(value)) { date = value; } else if (NumberWrapper.isNumeric(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) => parseInt(val, 10)); + date = new Date(y, m - 1, d); } else { date = new Date(value); } diff --git a/modules/@angular/common/test/pipes/date_pipe_spec.ts b/modules/@angular/common/test/pipes/date_pipe_spec.ts index 0bd7f7a5d9..afda87cd82 100644 --- a/modules/@angular/common/test/pipes/date_pipe_spec.ts +++ b/modules/@angular/common/test/pipes/date_pipe_spec.ts @@ -13,8 +13,14 @@ import {browserDetection} from '@angular/platform-browser/testing/browser_util'; export function main() { describe('DatePipe', () => { let date: Date; + const isoStringWithoutTime = '2015-01-01'; let pipe: DatePipe; + // 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, @@ -47,84 +53,139 @@ export function main() { it('should return null for empty string', () => expect(pipe.transform('')).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(); }); }); describe('transform', () => { it('should format each component correctly', () => { - expect(pipe.transform(date, 'y')).toEqual('2015'); - expect(pipe.transform(date, 'yy')).toEqual('15'); - expect(pipe.transform(date, 'M')).toEqual('6'); - expect(pipe.transform(date, 'MM')).toEqual('06'); - expect(pipe.transform(date, 'MMM')).toEqual('Jun'); - expect(pipe.transform(date, 'MMMM')).toEqual('June'); - expect(pipe.transform(date, 'd')).toEqual('15'); - expect(pipe.transform(date, 'EEE')).toEqual('Mon'); - expect(pipe.transform(date, 'EEEE')).toEqual('Monday'); + let dateFixtures: any = { + 'y': '2015', + 'yy': '15', + 'M': '6', + 'MM': '06', + 'MMM': 'Jun', + 'MMMM': 'June', + 'd': '15', + 'dd': '15', + 'EEE': 'Mon', + 'EEEE': 'Monday' + }; + + let 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) { - expect(pipe.transform(date, 'h')).toEqual('9'); - expect(pipe.transform(date, 'hh')).toEqual('09'); - expect(pipe.transform(date, 'j')).toEqual('9 AM'); + 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) { - expect(pipe.transform(date, 'HH')).toEqual('09'); + dateFixtures['HH'] = '09'; + isoStringWithoutTimeFixtures['HH'] = '00'; } - - expect(pipe.transform(date, 'E')).toEqual('M'); - expect(pipe.transform(date, 'L')).toEqual('J'); - expect(pipe.transform(date, 'm')).toEqual('3'); - expect(pipe.transform(date, 's')).toEqual('1'); - expect(pipe.transform(date, 'mm')).toEqual('03'); - expect(pipe.transform(date, 'ss')).toEqual('01'); + 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]); + }); + + Object.keys(isoStringWithoutTimeFixtures).forEach((pattern: string) => { + expectDateFormatAs(isoStringWithoutTime, pattern, isoStringWithoutTimeFixtures[pattern]); + }); + expect(pipe.transform(date, 'Z')).toBeDefined(); }); it('should format common multi component patterns', () => { - expect(pipe.transform(date, 'EEE, M/d/y')).toEqual('Mon, 6/15/2015'); - expect(pipe.transform(date, 'EEE, M/d')).toEqual('Mon, 6/15'); - expect(pipe.transform(date, 'MMM d')).toEqual('Jun 15'); - expect(pipe.transform(date, 'dd/MM/yyyy')).toEqual('15/06/2015'); - expect(pipe.transform(date, 'MM/dd/yyyy')).toEqual('06/15/2015'); - expect(pipe.transform(date, 'yMEEEd')).toEqual('20156Mon15'); - expect(pipe.transform(date, 'MEEEd')).toEqual('6Mon15'); - expect(pipe.transform(date, 'MMMd')).toEqual('Jun15'); - expect(pipe.transform(date, 'yMMMMEEEEd')).toEqual('Monday, June 15, 2015'); + let 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) { - expect(pipe.transform(date, 'ms')).toEqual('31'); + dateFixtures['ms'] = '31'; } + if (!browserDetection.isOldChrome) { - expect(pipe.transform(date, 'jm')).toEqual('9:03 AM'); + dateFixtures['jm'] = '9:03 AM'; } + + Object.keys(dateFixtures).forEach((pattern: string) => { + expectDateFormatAs(date, pattern, dateFixtures[pattern]); + }); + }); it('should format with pattern aliases', () => { + let 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) { - expect(pipe.transform(date, 'medium')).toEqual('Jun 15, 2015 9:03:01 AM'); - expect(pipe.transform(date, 'short')).toEqual('6/15/2015 9:03 AM'); + dateFixtures['medium'] = 'Jun 15, 2015 9:03:01 AM'; + dateFixtures['short'] = '6/15/2015 9:03 AM'; } else { - expect(pipe.transform(date, 'medium')).toEqual('Jun 15, 2015, 9:03:01 AM'); - expect(pipe.transform(date, 'short')).toEqual('6/15/2015, 9:03 AM'); + dateFixtures['medium'] = 'Jun 15, 2015, 9:03:01 AM'; + dateFixtures['short'] = '6/15/2015, 9:03 AM'; } } - expect(pipe.transform(date, 'MM/dd/yyyy')).toEqual('06/15/2015'); - expect(pipe.transform(date, 'fullDate')).toEqual('Monday, June 15, 2015'); - expect(pipe.transform(date, 'longDate')).toEqual('June 15, 2015'); - expect(pipe.transform(date, 'mediumDate')).toEqual('Jun 15, 2015'); - expect(pipe.transform(date, 'shortDate')).toEqual('6/15/2015'); + if (!browserDetection.isOldChrome) { - expect(pipe.transform(date, 'mediumTime')).toEqual('9:03:01 AM'); - expect(pipe.transform(date, 'shortTime')).toEqual('9:03 AM'); + dateFixtures['mediumTime'] = '9:03:01 AM'; + dateFixtures['shortTime'] = '9:03 AM'; } + + Object.keys(dateFixtures).forEach((pattern: string) => { + expectDateFormatAs(date, pattern, dateFixtures[pattern]); + }); + }); it('should remove bidi control characters',