Skip to content

Commit bf85f04

Browse files
abarghoudalxhub
authored andcommitted
fix(common): date is not correctly formatted when year is between 0 and 99 (#40448)
use setFullYear method when parsing date to avoid javascript date factory behaviour Fixes #40377 PR Close #40448
1 parent fffefd1 commit bf85f04

File tree

3 files changed

+73
-5
lines changed

3 files changed

+73
-5
lines changed

packages/common/src/i18n/format_date.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,38 @@ export function formatDate(
101101
return text;
102102
}
103103

104+
/**
105+
* Create a new Date object with the given date value, and the time set to midnight.
106+
*
107+
* We cannot use `new Date(year, month, date)` because it maps years between 0 and 99 to 1900-1999.
108+
* See: https://github.com/angular/angular/issues/40377
109+
*
110+
* Note that this function returns a Date object whose time is midnight in the current locale's
111+
* timezone. In the future we might want to change this to be midnight in UTC, but this would be a
112+
* considerable breaking change.
113+
*/
114+
function createDate(year: number, month: number, date: number): Date {
115+
// The `newDate` is set to midnight (UTC) on January 1st 1970.
116+
// - In PST this will be December 31st 1969 at 4pm.
117+
// - In GMT this will be January 1st 1970 at 1am.
118+
// Note that they even have different years, dates and months!
119+
const newDate = new Date(0);
120+
121+
// `setFullYear()` allows years like 0001 to be set correctly. This function does not
122+
// change the internal time of the date.
123+
// Consider calling `setFullYear(2019, 8, 20)` (September 20, 2019).
124+
// - In PST this will now be September 20, 2019 at 4pm
125+
// - In GMT this will now be September 20, 2019 at 1am
126+
127+
newDate.setFullYear(year, month, date);
128+
// We want the final date to be at local midnight, so we reset the time.
129+
// - In PST this will now be September 20, 2019 at 12am
130+
// - In GMT this will now be September 20, 2019 at 12am
131+
newDate.setHours(0, 0, 0);
132+
133+
return newDate;
134+
}
135+
104136
function getNamedFormat(locale: string, format: string): string {
105137
const localeId = getLocaleId(locale);
106138
NAMED_FORMATS[localeId] = NAMED_FORMATS[localeId] || {};
@@ -362,13 +394,13 @@ function timeZoneGetter(width: ZoneWidth): DateFormatter {
362394
const JANUARY = 0;
363395
const THURSDAY = 4;
364396
function getFirstThursdayOfYear(year: number) {
365-
const firstDayOfYear = (new Date(year, JANUARY, 1)).getDay();
366-
return new Date(
397+
const firstDayOfYear = createDate(year, JANUARY, 1).getDay();
398+
return createDate(
367399
year, 0, 1 + ((firstDayOfYear <= THURSDAY) ? THURSDAY : THURSDAY + 7) - firstDayOfYear);
368400
}
369401

370402
function getThursdayThisWeek(datetime: Date) {
371-
return new Date(
403+
return createDate(
372404
datetime.getFullYear(), datetime.getMonth(),
373405
datetime.getDate() + (THURSDAY - datetime.getDay()));
374406
}
@@ -720,7 +752,7 @@ export function toDate(value: string|number|Date): Date {
720752
is applied.
721753
Note: ISO months are 0 for January, 1 for February, ... */
722754
const [y, m = 1, d = 1] = value.split('-').map((val: string) => +val);
723-
return new Date(y, m - 1, d);
755+
return createDate(y, m - 1, d);
724756
}
725757

726758
const parsedNb = parseFloat(value);

packages/common/src/pipes/date_pipe.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
114114
* | | O, OO & OOO | Short localized GMT format | GMT-8 |
115115
* | | OOOO | Long localized GMT format | GMT-08:00 |
116116
*
117-
* Note that timezone correction is not applied to an ISO string that has no time component, such as "2016-09-19"
118117
*
119118
* ### Format examples
120119
*

packages/common/test/i18n/format_date_spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,32 @@ describe('Format date', () => {
347347
.toEqual('10:14 AM');
348348
});
349349

350+
// The following test is disabled because backwards compatibility requires that date-only ISO
351+
// strings are parsed with the local timezone.
352+
353+
// it('should create UTC date objects when an ISO string is passed with no time components',
354+
// () => {
355+
// expect(formatDate('2019-09-20', `MMM d, y, h:mm:ss a ZZZZZ`, ɵDEFAULT_LOCALE_ID))
356+
// .toEqual('Sep 20, 2019, 12:00:00 AM Z');
357+
// });
358+
359+
// This test is to ensure backward compatibility for parsing date-only ISO strings.
360+
it('should create local timezone date objects when an ISO string is passed with no time components',
361+
() => {
362+
// Dates created with individual components are evaluated against the local timezone. See
363+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#Individual_date_and_time_component_values
364+
const localDate = new Date(2019, 8, 20, 0, 0, 0, 0);
365+
expect(formatDate('2019-09-20', `MMM d, y, h:mm:ss a ZZZZZ`, ɵDEFAULT_LOCALE_ID))
366+
.toEqual(formatDate(localDate, `MMM d, y, h:mm:ss a ZZZZZ`, ɵDEFAULT_LOCALE_ID));
367+
});
368+
369+
it('should create local timezone date objects when an ISO string is passed with time components',
370+
() => {
371+
const localDate = new Date(2019, 8, 20, 0, 0, 0, 0);
372+
expect(formatDate('2019-09-20T00:00:00', `MMM d, y, h:mm:ss a ZZZZZ`, ɵDEFAULT_LOCALE_ID))
373+
.toEqual(formatDate(localDate, `MMM d, y, h:mm:ss a ZZZZZ`, ɵDEFAULT_LOCALE_ID));
374+
});
375+
350376
it('should remove bidi control characters',
351377
() => expect(formatDate(date, 'MM/dd/yyyy', ɵDEFAULT_LOCALE_ID)!.length).toEqual(10));
352378

@@ -389,6 +415,17 @@ describe('Format date', () => {
389415
expect(formatDate('2013-12-29', 'YYYY', 'en')).toEqual('2014');
390416
expect(formatDate('2010-01-02', 'YYYY', 'en')).toEqual('2009');
391417
expect(formatDate('2010-01-04', 'YYYY', 'en')).toEqual('2010');
418+
expect(formatDate('0049-01-01', 'YYYY', 'en')).toEqual('0048');
419+
expect(formatDate('0049-01-04', 'YYYY', 'en')).toEqual('0049');
392420
});
421+
422+
// https://github.com/angular/angular/issues/40377
423+
it('should format date with year between 0 and 99 correctly', () => {
424+
expect(formatDate('0098-01-11', 'YYYY', ɵDEFAULT_LOCALE_ID)).toEqual('0098');
425+
expect(formatDate('0099-01-11', 'YYYY', ɵDEFAULT_LOCALE_ID)).toEqual('0099');
426+
expect(formatDate('0100-01-11', 'YYYY', ɵDEFAULT_LOCALE_ID)).toEqual('0100');
427+
expect(formatDate('0001-01-11', 'YYYY', ɵDEFAULT_LOCALE_ID)).toEqual('0001');
428+
expect(formatDate('0000-01-11', 'YYYY', ɵDEFAULT_LOCALE_ID)).toEqual('0000');
429+
});
393430
});
394431
});

0 commit comments

Comments
 (0)