Skip to content

Commit 437eeb1

Browse files
Merge pull request #5036 from laug/pr-3988-rebase-v3
fix: Inconsistent/broken behavior in `parseDate`
2 parents f89ae4b + 88a9dd4 commit 437eeb1

File tree

5 files changed

+71
-100
lines changed

5 files changed

+71
-100
lines changed

src/date_utils.ts

Lines changed: 15 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import {
3636
isDate,
3737
isValid as isValidDate,
3838
isWithinInterval,
39-
longFormatters,
4039
max,
4140
min,
4241
parse,
@@ -103,10 +102,6 @@ function getLocaleScope() {
103102

104103
export const DEFAULT_YEAR_ITEM_NUMBER = 12;
105104

106-
// This RegExp catches symbols escaped by quotes, and also
107-
// sequences of symbols P, p, and the combinations like `PPPPPPPppppp`
108-
const longFormattingTokensRegExp = /P+p+|P+|p+|''|'(''|[^'])+('|$)|./g;
109-
110105
// ** Date Constructors **
111106

112107
export function newDate(value?: string | Date | number | null): Date {
@@ -125,77 +120,35 @@ export function newDate(value?: string | Date | number | null): Date {
125120
* @param dateFormat - The date format.
126121
* @param locale - The locale.
127122
* @param strictParsing - The strict parsing flag.
128-
* @param minDate - The minimum date.
123+
* @param refDate - The base date to be passed to date-fns parse() function.
129124
* @returns - The parsed date or null.
130125
*/
131126
export function parseDate(
132127
value: string,
133128
dateFormat: string | string[],
134129
locale: Locale | undefined,
135130
strictParsing: boolean,
136-
minDate?: Date,
131+
refDate: Date = newDate(),
137132
): Date | null {
138-
let parsedDate = null;
139133
const localeObject =
140134
getLocaleObject(locale) || getLocaleObject(getDefaultLocale());
141-
let strictParsingValueMatch = true;
142-
if (Array.isArray(dateFormat)) {
143-
dateFormat.forEach((df) => {
144-
const tryParseDate = parse(value, df, new Date(), {
145-
locale: localeObject,
146-
useAdditionalWeekYearTokens: true,
147-
useAdditionalDayOfYearTokens: true,
148-
});
149-
if (strictParsing) {
150-
strictParsingValueMatch =
151-
isValid(tryParseDate, minDate) &&
152-
value === formatDate(tryParseDate, df, locale);
153-
}
154-
if (isValid(tryParseDate, minDate) && strictParsingValueMatch) {
155-
parsedDate = tryParseDate;
156-
}
157-
});
158-
return parsedDate;
159-
}
160135

161-
parsedDate = parse(value, dateFormat, new Date(), {
162-
locale: localeObject,
163-
useAdditionalWeekYearTokens: true,
164-
useAdditionalDayOfYearTokens: true,
165-
});
136+
const formats = Array.isArray(dateFormat) ? dateFormat : [dateFormat];
166137

167-
if (strictParsing) {
168-
strictParsingValueMatch =
138+
for (const format of formats) {
139+
const parsedDate = parse(value, format, refDate, {
140+
locale: localeObject,
141+
useAdditionalWeekYearTokens: true,
142+
useAdditionalDayOfYearTokens: true,
143+
});
144+
if (
169145
isValid(parsedDate) &&
170-
value === formatDate(parsedDate, dateFormat, locale);
171-
} else if (!isValid(parsedDate)) {
172-
const format = (dateFormat.match(longFormattingTokensRegExp) ?? [])
173-
.map(function (substring) {
174-
const firstCharacter = substring[0];
175-
if (firstCharacter === "p" || firstCharacter === "P") {
176-
// The type in date-fns is `Record<string, LongFormatter>` so we can do our firstCharacter a bit loos but I don't think that this is a good idea
177-
const longFormatter = longFormatters[firstCharacter]!;
178-
return localeObject
179-
? longFormatter(substring, localeObject.formatLong)
180-
: firstCharacter;
181-
}
182-
return substring;
183-
})
184-
.join("");
185-
186-
if (value.length > 0) {
187-
parsedDate = parse(value, format.slice(0, value.length), new Date(), {
188-
useAdditionalWeekYearTokens: true,
189-
useAdditionalDayOfYearTokens: true,
190-
});
191-
}
192-
193-
if (!isValid(parsedDate)) {
194-
parsedDate = new Date(value);
146+
(!strictParsing || value === formatDate(parsedDate, format, locale))
147+
) {
148+
return parsedDate;
195149
}
196150
}
197-
198-
return isValid(parsedDate) && strictParsingValueMatch ? parsedDate : null;
151+
return null;
199152
}
200153

201154
// ** Date "Reflection" **
@@ -243,13 +196,7 @@ export function formatDate(
243196
`A locale object was not found for the provided string ["${locale}"].`,
244197
);
245198
}
246-
if (
247-
!localeObj &&
248-
!!getDefaultLocale() &&
249-
!!getLocaleObject(getDefaultLocale())
250-
) {
251-
localeObj = getLocaleObject(getDefaultLocale());
252-
}
199+
localeObj = localeObj || getLocaleObject(getDefaultLocale());
253200
return format(date, formatStr, {
254201
locale: localeObj,
255202
useAdditionalWeekYearTokens: true,

src/index.tsx

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import React, { Component, cloneElement } from "react";
44
import Calendar from "./calendar";
55
import CalendarIcon from "./calendar_icon";
66
import {
7-
set,
87
newDate,
98
isDate,
109
isBefore,
@@ -599,13 +598,12 @@ export default class DatePicker extends Component<
599598
lastPreSelectChange: PRESELECT_CHANGE_VIA_INPUT,
600599
});
601600

602-
const {
603-
dateFormat = DatePicker.defaultProps.dateFormat,
604-
strictParsing = DatePicker.defaultProps.strictParsing,
605-
selectsRange,
606-
startDate,
607-
endDate,
608-
} = this.props;
601+
const { selectsRange, startDate, endDate } = this.props;
602+
603+
const dateFormat =
604+
this.props.dateFormat ?? DatePicker.defaultProps.dateFormat;
605+
const strictParsing =
606+
this.props.strictParsing ?? DatePicker.defaultProps.strictParsing;
609607

610608
const value =
611609
event?.target instanceof HTMLInputElement ? event.target.value : "";
@@ -643,28 +641,14 @@ export default class DatePicker extends Component<
643641
this.props.onChange?.([startDateNew, endDateNew], event);
644642
} else {
645643
// not selectsRange
646-
let date = parseDate(
644+
const date = parseDate(
647645
value,
648646
dateFormat,
649647
this.props.locale,
650648
strictParsing,
651-
this.props.minDate,
649+
this.props.selected ?? undefined,
652650
);
653651

654-
// Use date from `selected` prop when manipulating only time for input value
655-
if (
656-
this.props.showTimeSelectOnly &&
657-
this.props.selected &&
658-
date &&
659-
!isSameDay(date, this.props.selected)
660-
) {
661-
date = set(this.props.selected, {
662-
hours: getHours(date),
663-
minutes: getMinutes(date),
664-
seconds: getSeconds(date),
665-
});
666-
}
667-
668652
// Update selection if either (1) date was successfully parsed, or (2) input field is empty
669653
if (date || !value) {
670654
this.setSelected(date, event, true);

src/test/date_utils_test.test.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -981,11 +981,35 @@ describe("date_utils", () => {
981981

982982
it("should parse date that matches one of the formats", () => {
983983
const value = "01/15/2019";
984-
const dateFormat = ["MM/dd/yyyy", "yyyy-MM-dd"];
984+
const dateFormat = ["yyyy-MM-dd", "MM/dd/yyyy"];
985985

986986
expect(parseDate(value, dateFormat, undefined, true)).not.toBeNull();
987987
});
988988

989+
it("should prefer the first matching format in array (strict)", () => {
990+
const value = "01/06/2019";
991+
const valueLax = "1/6/2019";
992+
const dateFormat = ["MM/dd/yyyy", "dd/MM/yyyy"];
993+
994+
const expected = new Date(2019, 0, 6);
995+
996+
expect(parseDate(value, dateFormat, undefined, true)).toEqual(expected);
997+
expect(parseDate(valueLax, dateFormat, undefined, true)).toBeNull();
998+
});
999+
1000+
it("should prefer the first matching format in array", () => {
1001+
const value = "01/06/2019";
1002+
const valueLax = "1/6/2019";
1003+
const dateFormat = ["MM/dd/yyyy", "dd/MM/yyyy"];
1004+
1005+
const expected = new Date(2019, 0, 6);
1006+
1007+
expect(parseDate(value, dateFormat, undefined, false)).toEqual(expected);
1008+
expect(parseDate(valueLax, dateFormat, undefined, false)).toEqual(
1009+
expected,
1010+
);
1011+
});
1012+
9891013
it("should not parse date that does not match the format", () => {
9901014
const value = "01/15/20";
9911015
const dateFormat = "MM/dd/yyyy";
@@ -1001,7 +1025,7 @@ describe("date_utils", () => {
10011025
});
10021026

10031027
it("should parse date without strict parsing", () => {
1004-
const value = "01/15/20";
1028+
const value = "1/2/2020";
10051029
const dateFormat = "MM/dd/yyyy";
10061030

10071031
expect(parseDate(value, dateFormat, undefined, false)).not.toBeNull();
@@ -1017,6 +1041,22 @@ describe("date_utils", () => {
10171041
expect(actual).toEqual(expected);
10181042
});
10191043

1044+
it("should parse date based on locale w/o strict", () => {
1045+
const valuePt = "26. fev 1995";
1046+
const valueEn = "26. feb 1995";
1047+
1048+
const locale = "pt-BR";
1049+
const dateFormat = "d. MMM yyyy";
1050+
1051+
const expected = new Date(1995, 1, 26);
1052+
1053+
expect(parseDate(valuePt, dateFormat, locale, false)).toEqual(expected);
1054+
expect(parseDate(valueEn, dateFormat, undefined, false)).toEqual(
1055+
expected,
1056+
);
1057+
expect(parseDate(valueEn, dateFormat, locale, false)).toBeNull();
1058+
});
1059+
10201060
it("should not parse date based on locale without a given locale", () => {
10211061
const value = "26/05/1995";
10221062
const dateFormat = "P";

src/test/datepicker_test.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ describe("DatePicker", () => {
995995
const input = safeQuerySelector<HTMLInputElement>(container, "input");
996996
fireEvent.change(input, {
997997
target: {
998-
value: newDate("2014-01-02"),
998+
value: "01/02/2014",
999999
},
10001000
});
10011001

@@ -1776,7 +1776,7 @@ describe("DatePicker", () => {
17761776
return render(
17771777
<DatePicker
17781778
selected={new Date("1993-07-02")}
1779-
minDate={new Date("1800/01/01")}
1779+
minDate={new Date("1800-01-01")}
17801780
open
17811781
/>,
17821782
);
@@ -1787,11 +1787,11 @@ describe("DatePicker", () => {
17871787
const input = safeQuerySelector<HTMLInputElement>(container, "input");
17881788
fireEvent.change(input, {
17891789
target: {
1790-
value: "1801/01/01",
1790+
value: "01/01/1801",
17911791
},
17921792
});
17931793

1794-
expect(container.querySelector("input")?.value).toBe("1801/01/01");
1794+
expect(container.querySelector("input")?.value).toBe("01/01/1801");
17951795
expect(
17961796
container.querySelector(".react-datepicker__current-month")?.innerHTML,
17971797
).toBe("January 1801");
@@ -1883,7 +1883,7 @@ describe("DatePicker", () => {
18831883
it("should update the selected date on manual input", () => {
18841884
const data = getOnInputKeyDownStuff();
18851885
fireEvent.change(data.dateInput, {
1886-
target: { value: "02/02/2017" },
1886+
target: { value: "2017-02-02" },
18871887
});
18881888
fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter));
18891889
data.copyM = newDate("2017-02-02");

src/test/min_time_test.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ describe("Datepicker minTime", () => {
8181
<DatePickerWithState minTime={minTime} maxTime={maxTime} />,
8282
);
8383
const input = safeQuerySelector<HTMLInputElement>(container, "input");
84-
fireEvent.change(input, { target: { value: "2023-03-10 16:00" } });
84+
fireEvent.change(input, { target: { value: "03/10/2023 16:00" } });
8585
fireEvent.focusOut(input);
8686

8787
expect(input.value).toEqual("03/10/2023 16:00");

0 commit comments

Comments
 (0)