diff --git a/src/date_utils.ts b/src/date_utils.ts index f74ddc829..204884692 100644 --- a/src/date_utils.ts +++ b/src/date_utils.ts @@ -36,7 +36,6 @@ import { isDate, isValid as isValidDate, isWithinInterval, - longFormatters, max, min, parse, @@ -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 { @@ -125,7 +120,7 @@ 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( @@ -133,69 +128,27 @@ export function parseDate( 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` 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" ** @@ -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, diff --git a/src/index.tsx b/src/index.tsx index 8ea9ce071..abb69c46a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,6 @@ import React, { Component, cloneElement } from "react"; import Calendar from "./calendar"; import CalendarIcon from "./calendar_icon"; import { - set, newDate, isDate, isBefore, @@ -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 : ""; @@ -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); diff --git a/src/test/date_utils_test.test.ts b/src/test/date_utils_test.test.ts index 19d504e74..b0dc9e71c 100644 --- a/src/test/date_utils_test.test.ts +++ b/src/test/date_utils_test.test.ts @@ -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"; @@ -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(); @@ -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"; diff --git a/src/test/datepicker_test.test.tsx b/src/test/datepicker_test.test.tsx index 7b8e54a0b..8ec2a2938 100644 --- a/src/test/datepicker_test.test.tsx +++ b/src/test/datepicker_test.test.tsx @@ -995,7 +995,7 @@ describe("DatePicker", () => { const input = safeQuerySelector(container, "input"); fireEvent.change(input, { target: { - value: newDate("2014-01-02"), + value: "01/02/2014", }, }); @@ -1776,7 +1776,7 @@ describe("DatePicker", () => { return render( , ); @@ -1787,11 +1787,11 @@ describe("DatePicker", () => { const input = safeQuerySelector(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"); @@ -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"); diff --git a/src/test/min_time_test.test.tsx b/src/test/min_time_test.test.tsx index 0360a1022..2eaf280c7 100644 --- a/src/test/min_time_test.test.tsx +++ b/src/test/min_time_test.test.tsx @@ -81,7 +81,7 @@ describe("Datepicker minTime", () => { , ); const input = safeQuerySelector(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");