Skip to main content

The selected date must be within the last 10 years

By Gerardo Rodriguez

Published on October 10th, 2023

Topics
An HTML input of type "date" with a pretend min value of "Ten years ago" and a pretend max value of "today" playing off of the article title.

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!

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.

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.

A “purchase date” input where the selected date 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.

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:

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:

The browser restricts future dates from selection if the max attribute value is set to today’s date.

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 falseCode 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.

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 Date.prototype.toISOString to get a date string in the ISO 8601-based format
    • e.g., '2023-03-21T04:15:47.000Z'
  • Split the date string to get the proper YYYY-MM-DD format
    • e.g., '2023-03-21'
// 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.

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.

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 :

/**
 * 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.

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.

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

  1. 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

Comments

Petar said:

I wonder, what would happen to the ‘ten years ago’ logic if today is February the 29th in a leap year…