Matthew Lindfield Seager

Matthew Lindfield Seager

Calculating Dates in JavaScript

I was struggling recently to calculate dates using JavaScript. I wanted a minimum date to be two weeks from today but at the start of the day (midnight). In a Rails app (or a Ruby app with ActiveSupport) I would simply chain calculations on to the end of DateTime.now:

minimum_date = DateTime.now.beginning_of_day + 2.weeks

I figured I could do something similar in JavaScript so I researched out how to calculate the start of today (new Date().setHours(0, 0, 0, 0)) and how to get 14 days from now (day.setDate(day.getDate() + 14)). Separately they both worked, but no matter what I tried I couldn’t combine the two:

// Non-example 1
let minimumDate = new Date().setHours(0, 0, 0, 0).getDate() + 14
> TypeError: new Date().setHours(0, 0, 0, 0).getDate is not a function.

// Non-example 2
let startOfToday = new Date().setHours(0, 0, 0, 0)
let minimumDate = startOfToday.setDate(startOfToday.getDate() + 14)
> TypeError: startOfToday.getDate is not a function.

// Non-example 3
let now = new Date()
let twoWeeksFromNow = now.setDate(now.getDate() + 14)
let minimumDate = twoWeeksFromNow.setHours(0, 0, 0, 0)
> TypeError: twoWeeksFromNow.setHours is not a function.

Eventually I learned that both setHours() and setDate() were updating their receivers but returning an integer representation of the adjusted date (milliseconds since the epoch), not the date itself1,2.

With this knowledge in hand, one way to set the minimum date would be to just use a single variable:

let minimumDate = new Date()
minimumDate.setDate(minimumDate.getDate() + 14)
minimumDate.setHours(0, 0, 0, 0)

It works and it’s quite explicit but I don’t love that the variable name is (temporarily) wrong for the first two lines. I like my code to be self documenting and easy for future me to decipher (hence why I tried to use interim variables in Examples 2 & 3 to spell out the steps I’m taking).

As part of my exploration I went back to Ruby to figure out how I would solve the same problem without ActiveSupport. The clearest way seemed to be to create two (accurately named) dates:

now = Time.now
minimum_date = Time.new(now.year, now.month, now.day + 14)

Sure enough, we can do the same thing in JavaScript:

let now = new Date();
let minimumDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 14)

Again, that works but it relies on knowledge of how the Date constructor works (that is, it’s not very explicit). Using the explicit option but extracting it into its own method makes my intent even clearer:

let minimumDate = twoWeeksFromNowAtStartOfDay()

function twoWeeksFromNowAtStartOfDay() {
  let returnValue = new Date()
  returnValue.setDate(minimumDate.getDate() + 14)
  returnValue.setHours(0, 0, 0, 0)
  return returnValue
}

1 Ruby tries to follow the Principle of Least Surprise. Mutating a Date object but returning a number is very suprising to me. The principle in JavaScript seems to be “expect the unexpected”

2 I also learned that let is a way to declare a variable with a limited scope (whereas var creates a global variable). I had assumed that let was how you declared a constant and so I was surprised I was allowed to mutate the contents of the “constants”.

I later tested a variation of example 3 with const and it turns out I can mutate a date constant (see also footnote 1 about expecting the unexpected). I don’t want to go any further into this rabbit hole right now so the many questions this raises will remain unanswered for the time being.

// Using a "constant"
const now = new Date()
> undefined
now
> Sun May 17 2020 13:32:34 GMT+1000 (AEST)
now.setDate(now.getDate() + 14)
> 1590895954088
now
> Sun May 31 2020 13:32:34 GMT+1000 (AEST)
now.setHours(0, 0, 0, 0)
> 1590847200000
Sun May 31 2020 00:00:00 GMT+1000 (AEST)