Skip to content

Commit 04430aa

Browse files
committed
feat(material/core): improve date parsing in the NativeDateAdapter
Improve date parsing by taking the supplied locale into account for date component ordering and inferring current year if it hasn't been supplied.
1 parent d141f83 commit 04430aa

File tree

2 files changed

+124
-3
lines changed

2 files changed

+124
-3
lines changed

src/material/core/datetime/native-date-adapter.spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {LOCALE_ID} from '@angular/core';
22
import {TestBed} from '@angular/core/testing';
3-
import {DEC, FEB, JAN, MAR} from '../../testing';
3+
import {DEC, FEB, JAN, JUL, JUN, MAR} from '../../testing';
44
import {DateAdapter, MAT_DATE_LOCALE, NativeDateAdapter, NativeDateModule} from './index';
55

66
describe('NativeDateAdapter', () => {
@@ -627,6 +627,24 @@ describe('NativeDateAdapter', () => {
627627
expect(result).not.toBe(initial);
628628
expect(result.getTime() - initial.getTime()).toBe(amount * 1000);
629629
});
630+
631+
it('should parse strings taking locale date formats into account', () => {
632+
adapter.setLocale('fr-BE');
633+
expect(adapter.parse('6/7/2023')).toEqual(new Date(2023, JUL, 6));
634+
expect(adapter.parse('7/6/2023')).toEqual(new Date(2023, JUN, 7));
635+
adapter.setLocale('en-US');
636+
expect(adapter.parse('7/6/2023')).toEqual(new Date(2023, JUN, 7));
637+
expect(adapter.parse('7/6/2023')).toEqual(new Date(2023, JUL, 6));
638+
});
639+
640+
it('should infer current year if not supplied when parsing', () => {
641+
expect(adapter.parse('01-01')).toEqual(new Date(new Date().getFullYear(), JAN, 1));
642+
});
643+
644+
it('should not return dates with a non zero local time component when parsing', () => {
645+
expect(adapter.parse('2023-01-01')).toEqual(new Date(2023, JAN, 1, 0, 0, 0));
646+
expect(adapter.parse('1-1-2023')).toEqual(new Date(2023, JAN, 1, 0, 0, 0));
647+
});
630648
});
631649

632650
describe('NativeDateAdapter with MAT_DATE_LOCALE override', () => {

src/material/core/datetime/native-date-adapter.ts

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const ISO_8601_REGEX =
3030
*/
3131
const TIME_REGEX = /^(\d?\d)[:.](\d?\d)(?:[:.](\d?\d))?\s*(AM|PM)?$/i;
3232

33+
const DATE_COMPONENT_SEPARATOR_REGEX = /[ \/.:,'"|\\_-]+/;
34+
3335
/** Creates an array and fills it with values. */
3436
function range<T>(length: number, valueFunction: (index: number) => T): T[] {
3537
const valuesArray = Array(length);
@@ -148,7 +150,7 @@ export class NativeDateAdapter extends DateAdapter<Date> {
148150

149151
let result = this._createDateWithOverflow(year, month, date);
150152
// Check that the date wasn't above the upper bound for the month, causing the month to overflow
151-
if (result.getMonth() != month && (typeof ngDevMode === 'undefined' || ngDevMode)) {
153+
if (result.getMonth() !== month && (typeof ngDevMode === 'undefined' || ngDevMode)) {
152154
throw Error(`Invalid date "${date}" for month with index "${month}".`);
153155
}
154156

@@ -165,7 +167,65 @@ export class NativeDateAdapter extends DateAdapter<Date> {
165167
if (typeof value == 'number') {
166168
return new Date(value);
167169
}
168-
return value ? new Date(Date.parse(value)) : null;
170+
171+
if (!value) {
172+
return null;
173+
}
174+
175+
if (typeof value !== 'string') {
176+
return new Date(value);
177+
}
178+
179+
const dateParts = (value as string)
180+
.trim()
181+
.split(DATE_COMPONENT_SEPARATOR_REGEX)
182+
.map(part => parseInt(part, 10))
183+
.filter(part => !isNaN(part));
184+
185+
if (dateParts.length < 2) {
186+
return this.invalid();
187+
}
188+
189+
const localeFormatParts = Intl.DateTimeFormat(this.locale, {
190+
year: 'numeric',
191+
month: '2-digit',
192+
day: '2-digit',
193+
}).formatToParts();
194+
195+
let year: number | null = null;
196+
let month: number | null = null;
197+
let day: number | null = null;
198+
199+
const valueHasYear = dateParts.length > 2;
200+
201+
if (!valueHasYear) {
202+
// Year is implied to be current year if only 2 date components are given.
203+
year = new Date().getFullYear();
204+
}
205+
206+
let parsedPartIndex = 0;
207+
208+
for (const part of localeFormatParts) {
209+
switch (part.type) {
210+
case 'year':
211+
if (valueHasYear) {
212+
year = dateParts[parsedPartIndex++];
213+
}
214+
break;
215+
case 'month':
216+
month = dateParts[parsedPartIndex++] - 1;
217+
break;
218+
case 'day':
219+
day = dateParts[parsedPartIndex++];
220+
break;
221+
}
222+
}
223+
224+
if (year !== null && month !== null && day !== null && this._dateComponentsAreValid(year, month, day)) {
225+
return this.createDate(year, month, day);
226+
}
227+
228+
return this._nativeParseFallback(value);
169229
}
170230

171231
format(date: Date, displayFormat: Object): string {
@@ -351,6 +411,49 @@ export class NativeDateAdapter extends DateAdapter<Date> {
351411
return dtf.format(d);
352412
}
353413

414+
private _nativeParseFallback(value: string): Date {
415+
const date = new Date(Date.parse(value));
416+
if (!this.isValid(date)) {
417+
return date;
418+
}
419+
420+
// Native parsing sometimes assumes UTC, sometimes does not.
421+
// We have to remove the difference between the two in order to get the date as a local date.
422+
423+
const compareDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
424+
const difference = date.getTime() - compareDate.getTime();
425+
if (difference === 0) {
426+
return date;
427+
}
428+
429+
return new Date(
430+
date.getUTCFullYear(),
431+
date.getUTCMonth(),
432+
date.getUTCDate(),
433+
date.getUTCHours(),
434+
date.getUTCMinutes(),
435+
date.getUTCSeconds(),
436+
date.getUTCMilliseconds(),
437+
);
438+
}
439+
440+
private _dateComponentsAreValid(year: number, month: number, day: number) {
441+
if (year < 0 || year > 9999 || month < 0 || month > 11 || day < 1 || day > 31) {
442+
return false;
443+
}
444+
445+
if (month === 1) {
446+
const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
447+
return isLeapYear ? day <= 29 : day <= 28;
448+
}
449+
450+
if (month === 3 || month === 5 || month === 8 || month === 10) {
451+
return day <= 30;
452+
}
453+
454+
return true;
455+
}
456+
354457
/**
355458
* Attempts to parse a time string into a date object. Returns null if it cannot be parsed.
356459
* @param value Time string to parse.

0 commit comments

Comments
 (0)