Skip to content

Commit b9ab291

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 fbc7bdd commit b9ab291

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 {waitForAsync, inject, 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', () => {
@@ -464,6 +464,24 @@ describe('NativeDateAdapter', () => {
464464
it('should not throw when attempting to format a date with a year greater than 9999', () => {
465465
expect(() => adapter.format(new Date(10000, 1, 1), {})).not.toThrow();
466466
});
467+
468+
it('should parse strings taking locale date formats into account', () => {
469+
adapter.setLocale('fr-BE');
470+
expect(adapter.parse('6/7/2023')).toEqual(new Date(2023, JUL, 6));
471+
expect(adapter.parse('7/6/2023')).toEqual(new Date(2023, JUN, 7));
472+
adapter.setLocale('en-US');
473+
expect(adapter.parse('7/6/2023')).toEqual(new Date(2023, JUN, 7));
474+
expect(adapter.parse('7/6/2023')).toEqual(new Date(2023, JUL, 6));
475+
});
476+
477+
it('should infer current year if not supplied when parsing', () => {
478+
expect(adapter.parse('01-01')).toEqual(new Date(new Date().getFullYear(), JAN, 1));
479+
});
480+
481+
it('should not return dates with a non zero local time component when parsing', () => {
482+
expect(adapter.parse('2023-01-01')).toEqual(new Date(2023, JAN, 1, 0, 0, 0));
483+
expect(adapter.parse('1-1-2023')).toEqual(new Date(2023, JAN, 1, 0, 0, 0));
484+
});
467485
});
468486

469487
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
@@ -17,6 +17,8 @@ import {DateAdapter, MAT_DATE_LOCALE} from './date-adapter';
1717
const ISO_8601_REGEX =
1818
/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|(?:(?:\+|-)\d{2}:\d{2}))?)?$/;
1919

20+
const DATE_COMPONENT_SEPARATOR_REGEX = /[ \/.:,'"|\\_-]+/;
21+
2022
/** Creates an array and fills it with values. */
2123
function range<T>(length: number, valueFunction: (index: number) => T): T[] {
2224
const valuesArray = Array(length);
@@ -118,7 +120,7 @@ export class NativeDateAdapter extends DateAdapter<Date> {
118120

119121
let result = this._createDateWithOverflow(year, month, date);
120122
// Check that the date wasn't above the upper bound for the month, causing the month to overflow
121-
if (result.getMonth() != month && (typeof ngDevMode === 'undefined' || ngDevMode)) {
123+
if (result.getMonth() !== month && (typeof ngDevMode === 'undefined' || ngDevMode)) {
122124
throw Error(`Invalid date "${date}" for month with index "${month}".`);
123125
}
124126

@@ -135,7 +137,65 @@ export class NativeDateAdapter extends DateAdapter<Date> {
135137
if (typeof value == 'number') {
136138
return new Date(value);
137139
}
138-
return value ? new Date(Date.parse(value)) : null;
140+
141+
if (!value) {
142+
return null;
143+
}
144+
145+
if (typeof value !== 'string') {
146+
return new Date(value);
147+
}
148+
149+
const dateParts = (value as string)
150+
.trim()
151+
.split(DATE_COMPONENT_SEPARATOR_REGEX)
152+
.map(part => parseInt(part, 10))
153+
.filter(part => !isNaN(part));
154+
155+
if (dateParts.length < 2) {
156+
return this.invalid();
157+
}
158+
159+
const localeFormatParts = Intl.DateTimeFormat(this.locale, {
160+
year: 'numeric',
161+
month: '2-digit',
162+
day: '2-digit',
163+
}).formatToParts();
164+
165+
let year: number | null = null;
166+
let month: number | null = null;
167+
let day: number | null = null;
168+
169+
const valueHasYear = dateParts.length > 2;
170+
171+
if (!valueHasYear) {
172+
// Year is implied to be current year if only 2 date components are given.
173+
year = new Date().getFullYear();
174+
}
175+
176+
let parsedPartIndex = 0;
177+
178+
for (const part of localeFormatParts) {
179+
switch (part.type) {
180+
case 'year':
181+
if (valueHasYear) {
182+
year = dateParts[parsedPartIndex++];
183+
}
184+
break;
185+
case 'month':
186+
month = dateParts[parsedPartIndex++] - 1;
187+
break;
188+
case 'day':
189+
day = dateParts[parsedPartIndex++];
190+
break;
191+
}
192+
}
193+
194+
if (year !== null && month !== null && day !== null && this._dateComponentsAreValid(year, month, day)) {
195+
return this.createDate(year, month, day);
196+
}
197+
198+
return this._nativeParseFallback(value);
139199
}
140200

141201
format(date: Date, displayFormat: Object): string {
@@ -257,4 +317,47 @@ export class NativeDateAdapter extends DateAdapter<Date> {
257317
d.setUTCHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
258318
return dtf.format(d);
259319
}
320+
321+
private _nativeParseFallback(value: string): Date {
322+
const date = new Date(Date.parse(value));
323+
if (!this.isValid(date)) {
324+
return date;
325+
}
326+
327+
// Native parsing sometimes assumes UTC, sometimes does not.
328+
// We have to remove the difference between the two in order to get the date as a local date.
329+
330+
const compareDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
331+
const difference = date.getTime() - compareDate.getTime();
332+
if (difference === 0) {
333+
return date;
334+
}
335+
336+
return new Date(
337+
date.getUTCFullYear(),
338+
date.getUTCMonth(),
339+
date.getUTCDate(),
340+
date.getUTCHours(),
341+
date.getUTCMinutes(),
342+
date.getUTCSeconds(),
343+
date.getUTCMilliseconds(),
344+
);
345+
}
346+
347+
private _dateComponentsAreValid(year: number, month: number, day: number) {
348+
if (year < 0 || year > 9999 || month < 0 || month > 11 || day < 1 || day > 31) {
349+
return false;
350+
}
351+
352+
if (month === 1) {
353+
const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
354+
return isLeapYear ? day <= 29 : day <= 28;
355+
}
356+
357+
if (month === 3 || month === 5 || month === 8 || month === 10) {
358+
return day <= 30;
359+
}
360+
361+
return true;
362+
}
260363
}

0 commit comments

Comments
 (0)