Skip to content
Closed
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
77 changes: 16 additions & 61 deletions src/date_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -76,59 +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 });

if (strictParsing) {
strictParsingValueMatch =
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());
}
const formats = Array.isArray(dateFormat) ? dateFormat : [dateFormat];
refDate = refDate || newDate();

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 });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I have not tried this myself (I'm not experienced enough to know how), I believe this would fix a bug of mine that I discovered last week. I'm using UTCDate objects with the datepicker (set as selected, minDate, and maxDate), but dates entered manually in the input box are not getting parsed as UTCDates, and each iteration of manual date input results in an additional timezone offset to the datetime. I think this is because the original call to parse() passed in a new Date() for the reference date to date-fns's parse() instead of the component's minDate or some other UTCDate object.

My temporary workaround is to use startOfDay(new UTCDate(newDate)).toISOString() in the onChange function.

if (
isValid(parsedDate /* , minDate */) &&
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to keep /* , minDate */ here?

🔹 Dead Code (Nice to have)

Image of Dallas Dallas

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

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

// ** Date "Reflection" **
Expand All @@ -146,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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding || getLocaleObject(getDefaultLocale()) here will prevent the warning below from being shown when the locale string is invalid, as localeObj will not be nullish even when the locale string is invalid.

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,
});
}
Expand Down
1 change: 1 addition & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

@laug laug Aug 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseDate only takes 5 parameters but is being passed 6 parameters here. This PR renames the 5th from minDate to refDate, but minDate is probably still needed to validate the parsed date correctly.

Edit: based on the commit message, minDate does not need to be passed to parseDate, so it should be removed here.

);
// Use date from `selected` prop when manipulating only time for input value
Expand Down
61 changes: 59 additions & 2 deletions test/date_utils_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -916,7 +950,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;
Expand All @@ -932,6 +966,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";
Expand Down
14 changes: 7 additions & 7 deletions test/datepicker_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -880,7 +880,7 @@ describe("DatePicker", () => {
datePicker = TestUtils.renderIntoDocument(
<DatePicker
selected={new Date("1993-07-02")}
minDate={new Date("1800/01/01")}
minDate={new Date("1800-01-01")}
open
/>
);
Expand All @@ -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"
Expand Down Expand Up @@ -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");
Expand Down