The selected date must be within the last 10 years
At 4/19/2024
What comes to mind if I give you the following form date validation requirement?
The user must select a date, which is a required field.
Easy enough. I can use a type="date"
input and ensure it has a required
attribute.
The selected date must be within the last ten years.
Deep breath…I’ll tell you what comes to mind for me…dates are hard!
Programming dates and timezones…argh!
It was a weeknight, 9:30 p.m. local time (new parent life), and as I shifted gears onto my next feature task, I noticed something…the date validation code working earlier in the day was suddenly broken.
What happened? I hadn’t changed anything. Was I dreaming? Was it sleep deprivation? I could’ve sworn the code was working perfectly fine just hours ago!
I was so confused.
It turns out my logic was off by one day, allowing me to choose tomorrow’s date when I wasn’t supposed to. But I only found out because I had shifted my schedule later than normal. During regular daytime working hours, the code worked great! But past 5 p.m. local time, nope!
Long story short, dates and time zones are hard. One of the most comprehensive resources I found was a transcript of a talk by Zach Holman, which begins by saying, “Programming time, dates, timezones, recurring events, leap seconds… everything is pretty terrible.” The best advice on dealing with time zones comes from Tom Scott, who said, “You should never, ever deal with time zones if you can help it.”
I think you get my point.
Date range validation
In the demo for my recently published Progressively Enhanced Form Validation series, I included a “purchase date” input field with a validation constraint where the selected date value must be within the last ten years.
In this article, I want to look at lessons learned related to this validation constraint. Past me would’ve gone down a rabbit hole on how to write custom date validation logic, eventually finding myself confused about time zones and then blowing past my half-day estimate with only a partially-working solution to offer.
So, past me, this is for you.
Lesson 1: Use built-in validation features
To validate that a date value is within the last ten years, past me started on the right track by adding type="date"
, required
, and max
attributes. I was missing the min
attribute and wasn’t planning to use browser validation APIs.
Present me now knows that everything I need to ensure the selected date is within a specified range is built-in to the web platform:
- The
max
attribute value should be today’s date - The
min
attribute value should be ten years ago from today - Use the Constraint Validation API’s
checkValidity
method to validate that the date meets themin
/max
requirements
For example, assuming today is September 5, 2023, the input
would look something like this:
<!-- Use HTML min/max validation constraint attributes -->
<input
id="purchase-date"
name="purchaseDate"
type="date"
min="2013-09-05"
max="2023-09-05"
required
>
Code language: HTML, XML (xml)
A bonus: By adding min
/max
values, the browser’s built-in calendar won’t allow users to select a date before or after the min
/max
constraints. For example, in Chrome, all dates after today’s date are lighter in color and restricted:
To validate that the “purchase date” selection is within the specified range, I can use the Constraint Validation API:
// Use the Constraint Validation API to validate the input
const purchaseDate = document.querySelector('#purchase-date');
purchaseDate.checkValidity(); // true or false
Code language: JavaScript (javascript)
Past me’s mind is blown. 🤯
Lesson for past me: Don’t complicate things more than needed; use the browser’s built-in validation features.
Lesson 2: Be careful about accidentally switching between local time and UTC
Since we don’t want the user to select a future value for the purchase date, the max
attribute value must be today’s date and appropriately formatted (YYYY-MM-DD
).
My initial attempt to generate the max
value was as follows:
- Use
new Date()
to get today’s date - Use
to get a date string in the ISO 8601-based formatDate.prototype.toISOString
- e.g.,
'2023-03-21T04:15:47.000Z'
- e.g.,
- Split the date string to get the proper
YYYY-MM-DD
format- e.g.,
'2023-03-21'
- e.g.,
// Assuming today is September 5, 2023…
const today = new Date();
const todayFormatted = today.toISOString().split('T')[0];
console.log(todayFormatted); // "2023-09-05"
Code language: JavaScript (javascript)
The format looks great. Good to go, right? Not quite.
There’s a gotcha: The date string returned by Date.prototype.toISOString
is always UTC (Coordinated Universal Time).
You will get unexpected results using a UTC date in a non-UTC timezone. My short story above is a prime example; I accidentally discovered a bug in my code where I could select tomorrow’s date if it were after 5 p.m. my local time. Oops.
Let’s take a closer look to understand why
Continuing with the assumption that today is September 5, 2023, 5:30 p.m. my local time, new Date()
returns my local timezone date string, as expected:
const today = new Date();
console.log(today);
// Tue Sep 05 2023 17:30:17 GMT-0700 (Pacific Daylight Time)
Code language: JavaScript (javascript)
If I call the Date.prototype.toISOString
method on today
, tomorrow’s date is returned…this wasn’t what I was expecting:
const todayAsISOString = today.toISOString();
console.log(todayAsISOString);
// "2023-09-06T00:30:17.479Z"
Code language: JavaScript (javascript)
Remember, Date.prototype.toISOString
returns a UTC date string. My local timezone is seven hours behind UTC. Therefore, if you take my 5:30 p.m. local time and add seven hours to it, you get the next day, 12:30 a.m., in UTC.
So, now what?
We could get tricky and offset the UTC date string by the difference between the user’s local timezone and UTC using Date.prototype.getTimezoneOffset
. I took this path initially. It works, but we can do something more straightforward.
Instead of jumping back and forth between the user’s local timezone and UTC with timezone offset trickery, let’s stay in the user’s timezoneFootnote 1 :
- Use
Date.prototype.getFullYear
to get the local 4-digit year - Use
Date.prototype.getMonth
to get the local month value- Returns an integer between 0 and 11
- Use
Date.prototype.getDate
to get the day of the month- Returns an integer between 1 and 31
- Use
String.prototype.padStart
to ensure the month and day are always two-digit values - Combine them using a template string literal to create the expected
YYYY-MM-DD
format
/**
* Generates a date string from a Date object in the format: YYYY-MM-DD
* @param {Date} date The date object to format
* @returns {string} A date string formatted as follow: YYYY-MM-DD
*/
export const getISOFormattedDate = (date) => {
// Get 4-digit year.
const year = date.getFullYear();
// Use padding to ensure 2 digits.
// Note: January is 0, February is 1, and so on.
const month = (date.getMonth() + 1).toString().padStart(2, '0');
// Use padding to ensure 2 digits.
const day = date.getDate().toString().padStart(2, '0');
// Return the date formatted as YYYY-MM-DD.
return `${year}-${month}-${day}`;
};
Code language: JavaScript (javascript)
Assuming you are using a framework like Astro (or Svelte, Vue, or something similar), the max
attribute for the “purchase date” input could then be updated as follows:
<input
id="purchase-date"
name="purchaseDate"
type="date"
min="2013-09-05"
max={getISOFormattedDate(new Date())}
required
>
Code language: HTML, XML (xml)
If using vanilla JavaScript, you could do something like the following:
const purchaseDate = document.querySelector('#purchase-date');
purchaseDate.setAttribute('max', getISOFormattedDate(new Date()));
Code language: JavaScript (javascript)
Lesson for past me: Pay attention to the Date
API method return values (are they UTC?) and test after midnight UTC to ensure the date logic isn’t off by one day.
Lesson 3: How to calculate years into the past
Let’s finish adding the last bit of logic for the “purchase date” field, which generates the min
attribute value. We need to generate a date value ten years earlier than today for the min
attribute. There isn’t a built-in way to get a specific amount of years ago from today using JavaScript.
My online research kept on showing me something similar to the following:
const tenYearsAgoToday = new Date();
tenYearsAgoToday.setFullYear(tenYearsAgoToday.getFullYear() - 10);
console.log(tenYearsAgoToday);
// Thu Sep 05 2013 14:17:38 GMT-0700 (Pacific Daylight Time)
Code language: JavaScript (javascript)
Pretty neat! I hadn’t seen the setFullYear
/getFullYear
pattern before.
Date.prototype.getFullYear
and Date.prototype.setFullYear
are local time-specific methods. Since the user’s local timezone is all we need to worry about, there’s no need to reach for the UTC-variant methods.
I ended up wrapping this logic in a utility function as well (also available in the demo source):
/**
* Returns a Date object representing the number of years ago from today.
* @param {number} years - The number of years ago from today.
* @returns {Date} - A Date object.
*/
const yearsAgoFromToday = (years) => {
const date = new Date();
date.setFullYear(date.getFullYear() - years);
return date;
};
Code language: JavaScript (javascript)
Using this utility function (combined with the getISOFormattedDate
function from above for formatting), the “purchase date” input could be updated as follows:
<input
id="purchase-date"
name="purchaseDate"
type="date"
min={getISOFormattedDate(yearsAgoFromToday(10))}
max={getISOFormattedDate(new Date())}
required
>
Code language: HTML, XML (xml)
Similar to before, if using vanilla JavaScript, you could do the following:
const purchaseDate = document.querySelector('#purchaseDate');
purchaseDate.setAttribute(
'min',
getISOFormattedDate(yearsAgoFromToday(10))
);
Code language: JavaScript (javascript)
Lesson for past me: Calculating the date as a past value doesn’t have to be complex. Stick with local time methods and keep it simple.
Wrapping up
When validating a date input field that has a date range requirement:
- Use browser built-in validation features (
min
/max
and the Constraint Validation API) - Consider time zones when writing date logic. Do you need to account for them?
- Pay attention to the return values from the
Date
methods; some are UTC, and some are not - Test against midnight UTC to ensure the date logic isn’t off by one day
While programming for dates and time zones is still difficult and confusing, adding form validation for a date range doesn’t have to be scary.
Past me, I hope this article makes you feel better, now go to sleep.
Footnotes
- A big thank you to Tyler for this suggestion. I was getting hung up and missing the more straightforward solution. Return to the text before footnote 1