Skip to content

Commit

Permalink
Merge pull request #5036 from laug/pr-3988-rebase-v3
Browse files Browse the repository at this point in the history
fix: Inconsistent/broken behavior in `parseDate`
  • Loading branch information
martijnrusschen authored Jan 31, 2025
2 parents f89ae4b + 88a9dd4 commit 437eeb1
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 100 deletions.
83 changes: 15 additions & 68 deletions src/date_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {
isDate,
isValid as isValidDate,
isWithinInterval,
longFormatters,
max,
min,
parse,
Expand Down Expand Up @@ -103,10 +102,6 @@ function getLocaleScope() {

export const DEFAULT_YEAR_ITEM_NUMBER = 12;

// This RegExp catches symbols escaped by quotes, and also
// sequences of symbols P, p, and the combinations like `PPPPPPPppppp`
const longFormattingTokensRegExp = /P+p+|P+|p+|''|'(''|[^'])+('|$)|./g;

// ** Date Constructors **

export function newDate(value?: string | Date | number | null): Date {
Expand All @@ -125,77 +120,35 @@ export function newDate(value?: string | Date | number | null): Date {
* @param dateFormat - The date format.
* @param locale - The locale.
* @param strictParsing - The strict parsing flag.
* @param minDate - The minimum date.
* @param refDate - The base date to be passed to date-fns parse() function.
* @returns - The parsed date or null.
*/
export function parseDate(
value: string,
dateFormat: string | string[],
locale: Locale | undefined,
strictParsing: boolean,
minDate?: Date,
refDate: Date = newDate(),
): Date | null {
let parsedDate = null;
const localeObject =
getLocaleObject(locale) || getLocaleObject(getDefaultLocale());
let strictParsingValueMatch = true;
if (Array.isArray(dateFormat)) {
dateFormat.forEach((df) => {
const tryParseDate = parse(value, df, new Date(), {
locale: localeObject,
useAdditionalWeekYearTokens: true,
useAdditionalDayOfYearTokens: true,
});
if (strictParsing) {
strictParsingValueMatch =
isValid(tryParseDate, minDate) &&
value === formatDate(tryParseDate, df, locale);
}
if (isValid(tryParseDate, minDate) && strictParsingValueMatch) {
parsedDate = tryParseDate;
}
});
return parsedDate;
}

parsedDate = parse(value, dateFormat, new Date(), {
locale: localeObject,
useAdditionalWeekYearTokens: true,
useAdditionalDayOfYearTokens: true,
});
const formats = Array.isArray(dateFormat) ? dateFormat : [dateFormat];

if (strictParsing) {
strictParsingValueMatch =
for (const format of formats) {
const parsedDate = parse(value, format, refDate, {
locale: localeObject,
useAdditionalWeekYearTokens: true,
useAdditionalDayOfYearTokens: true,
});
if (
isValid(parsedDate) &&
value === formatDate(parsedDate, dateFormat, locale);
} else if (!isValid(parsedDate)) {
const format = (dateFormat.match(longFormattingTokensRegExp) ?? [])
.map(function (substring) {
const firstCharacter = substring[0];
if (firstCharacter === "p" || firstCharacter === "P") {
// 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
const longFormatter = longFormatters[firstCharacter]!;
return localeObject
? longFormatter(substring, localeObject.formatLong)
: firstCharacter;
}
return substring;
})
.join("");

if (value.length > 0) {
parsedDate = parse(value, format.slice(0, value.length), new Date(), {
useAdditionalWeekYearTokens: true,
useAdditionalDayOfYearTokens: true,
});
}

if (!isValid(parsedDate)) {
parsedDate = new Date(value);
(!strictParsing || value === formatDate(parsedDate, format, locale))
) {
return parsedDate;
}
}

return isValid(parsedDate) && strictParsingValueMatch ? parsedDate : null;
return null;
}

// ** Date "Reflection" **
Expand Down Expand Up @@ -243,13 +196,7 @@ export function formatDate(
`A locale object was not found for the provided string ["${locale}"].`,
);
}
if (
!localeObj &&
!!getDefaultLocale() &&
!!getLocaleObject(getDefaultLocale())
) {
localeObj = getLocaleObject(getDefaultLocale());
}
localeObj = localeObj || getLocaleObject(getDefaultLocale());
return format(date, formatStr, {
locale: localeObj,
useAdditionalWeekYearTokens: true,
Expand Down
32 changes: 8 additions & 24 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import React, { Component, cloneElement } from "react";
import Calendar from "./calendar";
import CalendarIcon from "./calendar_icon";
import {
set,
newDate,
isDate,
isBefore,
Expand Down Expand Up @@ -599,13 +598,12 @@ export default class DatePicker extends Component<
lastPreSelectChange: PRESELECT_CHANGE_VIA_INPUT,
});

const {
dateFormat = DatePicker.defaultProps.dateFormat,
strictParsing = DatePicker.defaultProps.strictParsing,
selectsRange,
startDate,
endDate,
} = this.props;
const { selectsRange, startDate, endDate } = this.props;

const dateFormat =
this.props.dateFormat ?? DatePicker.defaultProps.dateFormat;
const strictParsing =
this.props.strictParsing ?? DatePicker.defaultProps.strictParsing;

const value =
event?.target instanceof HTMLInputElement ? event.target.value : "";
Expand Down Expand Up @@ -643,28 +641,14 @@ export default class DatePicker extends Component<
this.props.onChange?.([startDateNew, endDateNew], event);
} else {
// not selectsRange
let date = parseDate(
const date = parseDate(
value,
dateFormat,
this.props.locale,
strictParsing,
this.props.minDate,
this.props.selected ?? undefined,
);

// Use date from `selected` prop when manipulating only time for input value
if (
this.props.showTimeSelectOnly &&
this.props.selected &&
date &&
!isSameDay(date, this.props.selected)
) {
date = set(this.props.selected, {
hours: getHours(date),
minutes: getMinutes(date),
seconds: getSeconds(date),
});
}

// Update selection if either (1) date was successfully parsed, or (2) input field is empty
if (date || !value) {
this.setSelected(date, event, true);
Expand Down
44 changes: 42 additions & 2 deletions src/test/date_utils_test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,11 +981,35 @@ describe("date_utils", () => {

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

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

it("should prefer the first matching format in array (strict)", () => {
const value = "01/06/2019";
const valueLax = "1/6/2019";
const dateFormat = ["MM/dd/yyyy", "dd/MM/yyyy"];

const expected = new Date(2019, 0, 6);

expect(parseDate(value, dateFormat, undefined, true)).toEqual(expected);
expect(parseDate(valueLax, dateFormat, undefined, true)).toBeNull();
});

it("should prefer the first matching format in array", () => {
const value = "01/06/2019";
const valueLax = "1/6/2019";
const dateFormat = ["MM/dd/yyyy", "dd/MM/yyyy"];

const expected = new Date(2019, 0, 6);

expect(parseDate(value, dateFormat, undefined, false)).toEqual(expected);
expect(parseDate(valueLax, dateFormat, undefined, false)).toEqual(
expected,
);
});

it("should not parse date that does not match the format", () => {
const value = "01/15/20";
const dateFormat = "MM/dd/yyyy";
Expand All @@ -1001,7 +1025,7 @@ describe("date_utils", () => {
});

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

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

it("should parse date based on locale w/o strict", () => {
const valuePt = "26. fev 1995";
const valueEn = "26. feb 1995";

const locale = "pt-BR";
const dateFormat = "d. MMM yyyy";

const expected = new Date(1995, 1, 26);

expect(parseDate(valuePt, dateFormat, locale, false)).toEqual(expected);
expect(parseDate(valueEn, dateFormat, undefined, false)).toEqual(
expected,
);
expect(parseDate(valueEn, dateFormat, locale, false)).toBeNull();
});

it("should not parse date based on locale without a given locale", () => {
const value = "26/05/1995";
const dateFormat = "P";
Expand Down
10 changes: 5 additions & 5 deletions src/test/datepicker_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@ describe("DatePicker", () => {
const input = safeQuerySelector<HTMLInputElement>(container, "input");
fireEvent.change(input, {
target: {
value: newDate("2014-01-02"),
value: "01/02/2014",
},
});

Expand Down Expand Up @@ -1776,7 +1776,7 @@ describe("DatePicker", () => {
return render(
<DatePicker
selected={new Date("1993-07-02")}
minDate={new Date("1800/01/01")}
minDate={new Date("1800-01-01")}
open
/>,
);
Expand All @@ -1787,11 +1787,11 @@ describe("DatePicker", () => {
const input = safeQuerySelector<HTMLInputElement>(container, "input");
fireEvent.change(input, {
target: {
value: "1801/01/01",
value: "01/01/1801",
},
});

expect(container.querySelector("input")?.value).toBe("1801/01/01");
expect(container.querySelector("input")?.value).toBe("01/01/1801");
expect(
container.querySelector(".react-datepicker__current-month")?.innerHTML,
).toBe("January 1801");
Expand Down Expand Up @@ -1883,7 +1883,7 @@ describe("DatePicker", () => {
it("should update the selected date on manual input", () => {
const data = getOnInputKeyDownStuff();
fireEvent.change(data.dateInput, {
target: { value: "02/02/2017" },
target: { value: "2017-02-02" },
});
fireEvent.keyDown(data.dateInput, getKey(KeyType.Enter));
data.copyM = newDate("2017-02-02");
Expand Down
2 changes: 1 addition & 1 deletion src/test/min_time_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe("Datepicker minTime", () => {
<DatePickerWithState minTime={minTime} maxTime={maxTime} />,
);
const input = safeQuerySelector<HTMLInputElement>(container, "input");
fireEvent.change(input, { target: { value: "2023-03-10 16:00" } });
fireEvent.change(input, { target: { value: "03/10/2023 16:00" } });
fireEvent.focusOut(input);

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

0 comments on commit 437eeb1

Please sign in to comment.