From 692dd5f5a8c2fade1e00a58220634f64c0a74bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1r=20=C3=96rlygsson?= Date: Thu, 6 Apr 2023 13:40:25 +0000 Subject: [PATCH 1/4] tests: Add failing locale test: bad value is parsed using default locale Parsing `valueEn` as a Portugese date should fail, but accidentally gets parsed using the default/en-US locale, because parseDate internally retries and accidentally omits[^1] the original locale option that time around. [^1]: https://github.com/Hacker0x01/react-datepicker/blob/5c1d6d923931535f105f3dddbb6f3e10fd8dd25c/src/date_utils.js#L121 --- test/date_utils_test.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/date_utils_test.js b/test/date_utils_test.js index 323a230c5d..55bc9b4731 100644 --- a/test/date_utils_test.js +++ b/test/date_utils_test.js @@ -932,6 +932,29 @@ describe("date_utils", function () { assert(isEqual(actual, 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); + + assert( + isEqual(parseDate(valuePt, dateFormat, locale, false), expected), + "valuePT with pt-BR" + ); + assert( + isEqual(parseDate(valueEn, dateFormat, null, false), expected), + "valueEn with default (en-US)" + ); + expect( + parseDate(valueEn, dateFormat, locale, false), + "valueEn with (pt-BR)" + ).to.be.null; + }); + it("should not parse date based on locale without a given locale", () => { const value = "26/05/1995"; const dateFormat = "P"; From bb7177fd1a47721a65095b1b33e607d84ce14328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1r=20=C3=96rlygsson?= Date: Thu, 19 Jan 2023 15:54:23 +0100 Subject: [PATCH 2/4] fix: Remove weird, redundant code, fix bug illustrated in #45ce1fa3 --- src/date_utils.js | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/date_utils.js b/src/date_utils.js index 58c7038350..16e1827fdf 100644 --- a/src/date_utils.js +++ b/src/date_utils.js @@ -61,10 +61,6 @@ import longFormatters from "date-fns/esm/_lib/format/longFormatters"; 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` -var longFormattingTokensRegExp = /P+p+|P+|p+|''|'(''|[^'])+('|$)|./g; - // ** Date Constructors ** export function newDate(value) { @@ -105,27 +101,7 @@ export function parseDate(value, dateFormat, locale, strictParsing, minDate) { isValid(parsedDate) && value === formatDate(parsedDate, dateFormat, locale); } else if (!isValid(parsedDate)) { - dateFormat = dateFormat - .match(longFormattingTokensRegExp) - .map(function (substring) { - var firstCharacter = substring[0]; - if (firstCharacter === "p" || firstCharacter === "P") { - var longFormatter = longFormatters[firstCharacter]; - return localeObject - ? longFormatter(substring, localeObject.formatLong) - : firstCharacter; - } - return substring; - }) - .join(""); - - if (value.length > 0) { - parsedDate = parse(value, dateFormat.slice(0, value.length), new Date()); - } - - if (!isValid(parsedDate)) { - parsedDate = new Date(value); - } + parsedDate = new Date(value); } return isValid(parsedDate) && strictParsingValueMatch ? parsedDate : null; From ffdf5283df6f2a72501e4442f738d3630025a860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1r=20=C3=96rlygsson?= Date: Mon, 23 Jan 2023 09:09:53 +0000 Subject: [PATCH 3/4] tests: Fix side-effecty+inconsistent formatting of date value inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …making sure input values are applied in a more consistent manner, and with formatting that matches the currently active dateFormat (usually the default format). This only affects clarity/readability. All tests still pass. --- test/date_utils_test.js | 2 +- test/datepicker_test.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/date_utils_test.js b/test/date_utils_test.js index 55bc9b4731..7cab12d177 100644 --- a/test/date_utils_test.js +++ b/test/date_utils_test.js @@ -916,7 +916,7 @@ describe("date_utils", function () { }); 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, null, false)).to.not.be.null; diff --git a/test/datepicker_test.js b/test/datepicker_test.js index 2141f63841..cb2c2aa138 100644 --- a/test/datepicker_test.js +++ b/test/datepicker_test.js @@ -507,9 +507,9 @@ describe("DatePicker", () => { /> ); - var input = ReactDOM.findDOMNode(datePicker.input); - input.value = utils.newDate("2014-01-02"); - TestUtils.Simulate.change(input); + TestUtils.Simulate.change(datePicker.input, { + target: { value: "01/02/2014" }, + }); expect(utils.getHours(date)).to.equal(10); expect(utils.getMinutes(date)).to.equal(11); @@ -880,7 +880,7 @@ describe("DatePicker", () => { datePicker = TestUtils.renderIntoDocument( ); @@ -889,11 +889,11 @@ describe("DatePicker", () => { it("should auto update calendar when the updated date text is after props.minDate", () => { TestUtils.Simulate.change(datePicker.input, { target: { - value: "1801/01/01", + value: "01/01/1801", }, }); - expect(datePicker.input.value).to.equal("1801/01/01"); + expect(datePicker.input.value).to.equal("01/01/1801"); expect( datePicker.calendar.componentNode.querySelector( ".react-datepicker__current-month" @@ -965,7 +965,7 @@ describe("DatePicker", () => { it("should update the selected date on manual input", () => { var data = getOnInputKeyDownStuff(); TestUtils.Simulate.change(data.nodeInput, { - target: { value: "02/02/2017" }, + target: { value: "2017-02-02" }, }); TestUtils.Simulate.keyDown(data.nodeInput, getKey("Enter")); data.copyM = utils.newDate("2017-02-02"); From 59658cdcca72689cd3fa7311ae95d185627f6328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1r=20=C3=96rlygsson?= Date: Mon, 23 Jan 2023 09:16:21 +0000 Subject: [PATCH 4/4] fix: `parseDate` should prefer the first matching format in array Fix involves vastly simplifying the internal code-paths of `parseDate`, to prevent further and repeated divergence of behavior when parsing `dateFormat` as `Array` vs. as `string` NOTE: Removing the (redundant) `minDate` parameter has no effect on the tests, as minDate/maxDate boundry checks are enforced elsewhere in the component's value-updating lifecycle. NOTE 2: Adding instead `refDate` (using `props.selected`) to fully utilize the features of `date-fns/parse`. NOTE 3: The old behavior of re-parsing borked values using `new Date()` was somewhat dubious as it gave different results depending on the Browser/OS running the code. See more here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#parameters --- src/date_utils.js | 55 +++++++++++++---------------------------- src/index.jsx | 1 + test/date_utils_test.js | 36 ++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 39 deletions(-) diff --git a/src/date_utils.js b/src/date_utils.js index 16e1827fdf..e4d8270b78 100644 --- a/src/date_utils.js +++ b/src/date_utils.js @@ -72,39 +72,24 @@ export function newDate(value) { return isValid(d) ? d : null; } -export function parseDate(value, dateFormat, locale, strictParsing, minDate) { - let parsedDate = null; - let localeObject = +export function parseDate(value, dateFormat, locale, strictParsing, refDate) { + const localeObject = getLocaleObject(locale) || getLocaleObject(getDefaultLocale()); - let strictParsingValueMatch = true; - if (Array.isArray(dateFormat)) { - dateFormat.forEach((df) => { - let tryParseDate = parse(value, df, new Date(), { - locale: localeObject, - }); - 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 }); + const formats = Array.isArray(dateFormat) ? dateFormat : [dateFormat]; + refDate = refDate || newDate(); - if (strictParsing) { - strictParsingValueMatch = - isValid(parsedDate) && - value === formatDate(parsedDate, dateFormat, locale); - } else if (!isValid(parsedDate)) { - parsedDate = new Date(value); + for (let i = 0, len = formats.length; i < len; i++) { + const format = formats[i]; + const parsedDate = parse(value, format, refDate, { locale: localeObject }); + if ( + isValid(parsedDate /* , minDate */) && + (!strictParsing || value === formatDate(parsedDate, format, locale)) + ) { + return parsedDate; + } } - - return isValid(parsedDate) && strictParsingValueMatch ? parsedDate : null; + return null; } // ** Date "Reflection" ** @@ -122,21 +107,15 @@ export function formatDate(date, formatStr, locale) { if (locale === "en") { return format(date, formatStr, { awareOfUnicodeTokens: true }); } - let localeObj = getLocaleObject(locale); + const localeObj = + getLocaleObject(locale) || getLocaleObject(getDefaultLocale()) || null; if (locale && !localeObj) { console.warn( `A locale object was not found for the provided string ["${locale}"].` ); } - if ( - !localeObj && - !!getDefaultLocale() && - !!getLocaleObject(getDefaultLocale()) - ) { - localeObj = getLocaleObject(getDefaultLocale()); - } return format(date, formatStr, { - locale: localeObj ? localeObj : null, + locale: localeObj, awareOfUnicodeTokens: true, }); } diff --git a/src/index.jsx b/src/index.jsx index be0fc3051d..01f3805bd9 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -496,6 +496,7 @@ export default class DatePicker extends React.Component { this.props.dateFormat, this.props.locale, this.props.strictParsing, + this.props.selected, this.props.minDate ); // Use date from `selected` prop when manipulating only time for input value diff --git a/test/date_utils_test.js b/test/date_utils_test.js index 7cab12d177..8c6f36a7ed 100644 --- a/test/date_utils_test.js +++ b/test/date_utils_test.js @@ -896,11 +896,45 @@ describe("date_utils", function () { 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, null, true)).to.not.be.null; }); + 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); + + assert( + isEqual(parseDate(value, dateFormat, null, true), expected), + "Value with exact format" + ); + expect( + parseDate(valueLax, dateFormat, null, true), + "Value with lax format" + ).to.be.null; + }); + + 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); + + assert( + isEqual(parseDate(value, dateFormat, null, false), expected), + "Value with exact format" + ); + assert( + isEqual(parseDate(valueLax, dateFormat, null, false), expected), + "Value with lax format" + ); + }); + it("should not parse date that does not match the format", () => { const value = "01/15/20"; const dateFormat = "MM/dd/yyyy";