Minimum and Maximum Constraints for Date and Time Pickers

Giancarlo Radaelli
JavaScript in Plain English
5 min readMar 18, 2021

--

A simple exercise in functional React/JavaScript that exemplify a common use-case with date pickers.

In the following I will refer to the widely used react-datepicker component, although the considerations I will make can be applied to any date picker that provides similar capabilities.

The Problem

React-datepicker component provides two properties to constraint the selection of dates: minDate and maxDate.

When the minDate property is set, previous days cannot be selected and browsing to previous months is inhibited. The same happens in the opposite direction with maxDate.

If you enable the time picker section and specify a minDate that contains a minimum time, the expected behavior would be to have the same type of constraint in the time picker as well: the selection of times before the minDate time should not be enabled.

Well, this is not the case: times before the minimum time can still be selected.

To constraint the selection of times, the component provides two additional properties: minTime and maxTime.
Their operation is subject to the following rules:

  • Both must be set to take effect. For example, to disable the selection of times before 08:30AM, minTime must be set at 08:30 AM and maxTime must be set at 11:59PM
  • When specified, the constraints are applied regardless of the selected day. Therefore, to apply them correctly, the two properties must be related to the current selected day.
Static constraint of time does not work because it is applied to all dates

There are several scenarios where the above rules are useful. For example, when setting the day and time of a meeting, the selected time cannot exceed the working hours.

The component provides other possibilities to enable/disable time selection. They allow to implement more complex use cases, but below I will consider only the simple scenario where we want to constrain the selection of times, only when the selected date is equal to minDate or maxDate.

Day equality

We need to compare two dates (with their time) to check if they refer to the same day.

Date equality can be checked very quickly by retrieving the number of milliseconds elapsed since the reference date (January 1, 1970 00:00:00):

date1.getTime() === date2.getTime()

For the equality of days the algorithm gets a little more complicated and involves the equality test between the three components of dates:

date1.getFullYear() === date2.getFullYear()
&& date1.getMonth() === date2.getMonth()
&& date1.getDate() === date2.getDate()

However, there is a more elegant way to do this that requires only a single comparison.

It involves calculating the number of days elapsed since the reference date: just divide the number of milliseconds by the number of milliseconds of a day (86400000) and take the floor (not trunc as I will explain later).

There is a small complication: getTime returns the milliseconds in the UTC time zone. To get the number of days elapsed in the date’s time zone we need to add the time zone offset (in milliseconds) to the UTC milliseconds.

The function getTimezoneOffset returns the offset in minutes for the inverse transformation (i.e. from the date time zone to UTC): the offset is negative for time zones east of Greenwich.
So we actually have to subtract the offset multiplied by 60000.

const daysFromRefDate = (date) => 
date &&
Math.floor(
(date.getTime() - date.getTimezoneOffset() * 60000) / 86400000
)

Can this relatively complicated function be more efficient than the previous chain of three comparisons? Well, it is, because JavaScript stores dates as milliseconds and each of the previous get functions must perform a similar calculation.
The function, operating on a single date, can be conveniently memoized using the useMemo hook.

To check if two dates are related to the same day it is sufficient to compare the elapsed days:

daysFromRefDate(date1) === daysFromRefDate(date2)

Days since the moon landing

Why does the daysFromRefDate function use floor instead of trunc? Well, because floor always returns the closest left integer: floor(-0.01) == -1 while trunc(-0.01) == 0.
This is important when we handle dates prior to the reference date (1/1/1970): for them the getTime function returns negative values.
This way the function always gives the correct result.

The days elapsed between two dates are always the difference of the days elapsed since the reference date.

moonLanding = new Date('July 20, 69 00:20:18 GMT+00:00')
daysSinceMoonLanding = daysFromRefDate(new Date()) -
daysFromRefDate(moonLanding)

Compute the constraints

In our use case there are three different settings for the minTime and maxTime pair (in these settings only the time part od the date is meaningful):

  • Day of selected date equal to the day of minDate: minTime must be set to the time of minDate while maxTime must be equal to 11:59 PM
minTime = minDate
maxTime = new Date().setHours(23, 59, 0, 0)
  • Day of selected date equal to the day of maxDate: maxTime must be set to the time of maxDate while minTime must be equal to 12:00 AM
minTime = new Date().setHours(0, 0, 0, 0)
maxTime = maxDate
  • Day of the selected date between the day of minDate and maxDate: minTime and max Time shall be both set to null.

Compose the date & time picker wrapper

We have all the bricks to build a date picker component that is able to correctly constraint the time of min and max dates.

Alternative implementation with filterTime

The React-datepicker component also provides another way to constrain the time selection: it is possible to pass a time filter function to the component.

The filter function is called every time the component is rendered, so it is less efficient than the previous solution where selDays is calculated only when selDate changes.

The function itself is interesting because it’s an example of a custom hook: a function that uses React’s hooks internally.

--

--