Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Inconsistent/broken behavior in parseDate #5036

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 16 additions & 68 deletions src/date_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { endOfDay } from "date-fns/endOfDay";
import { endOfMonth } from "date-fns/endOfMonth";
import { endOfWeek } from "date-fns/endOfWeek";
import { endOfYear } from "date-fns/endOfYear";
import { format, longFormatters } from "date-fns/format";
import { format } from "date-fns/format";
import { getDate } from "date-fns/getDate";
import { getDay } from "date-fns/getDay";
import { getHours } from "date-fns/getHours";
Expand Down Expand Up @@ -100,10 +100,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 @@ -122,77 +118,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];
martijnrusschen marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -240,13 +194,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 @@ -594,13 +593,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 @@ -638,28 +636,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 @@ -978,11 +978,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 @@ -998,7 +1022,7 @@ describe("date_utils", () => {
});

it("should parse date without strict parsing", () => {
const value = "01/15/20";
const value = "1/2/2020";
martijnrusschen marked this conversation as resolved.
Show resolved Hide resolved
const dateFormat = "MM/dd/yyyy";

expect(parseDate(value, dateFormat, undefined, false)).not.toBeNull();
Expand All @@ -1014,6 +1038,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 @@ -940,7 +940,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 @@ -1722,7 +1722,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 @@ -1733,11 +1733,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 @@ -1829,7 +1829,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
Loading