Working out the precise difference between two dates in Day.js isn’t the only problem in the library. I found that adding a difference to one date to get another accurately is also an issue.

New bug?

After I’d figured out a workaround for the difference bug in Day.js, I found another related one.

When you add a duration object to a date in Day.js, using the add method, it also doesn’t work accurately or as expected. As you’ll see this is for the same reason as explained last time – the duration object is completely decoupled from any calendar context. So when adding an amount of time to a date to get a new date, the duration just calculates these to milliseconds using monthly and yearly averages.

On smaller timescales this mostly works but becomes a problem with months (because calendar months vary in length) and years (because years vary too).

So I decided to come up with a workaround for this one too – as you’ll see this was fairly easy because it’s kind of the same problem I faced before but in reverse.

But thinking and writing about this also got me reflecting on the readability of the code I wrote last time.

Refactored code

After a break from the helper function I wrote and described in my last post, I came back and realised it might not be the most readable. The fact that I spent a fair bit my blog post explaining it is a good indicator of a problem.

So I refactored the function slightly to make it more readable.

Last time, I’d written in a style that is possibly a little unforgiving.

function diffDates (startDate, endDate, includeWeeks) {
  const units = ["y", "M"]
    .concat((includeWeeks ? ["w"] : [])
    .concat(["d", "h", "m", "s"]))
  const whittleDate = (givenDate, givenUnits, duration) => {
      let diff = givenDate.diff(startDate, givenUnits[0])
      let diffRemainder = givenDate.subtract(diff, givenUnits[0])
      let newduration = duration.add(diff, givenUnits[0])
      return (givenUnits.length > 1)
        ? whittleDate(diffRemainder, givenUnits.slice(1), newduration)
        : newduration
    }
  return whittleDate(endDate, units, dayjs.duration())
}

I didn’t want to come up with a completely different approach – I think I still prefer recursion to, for example, a while or a for loop. But I did want to clear things up a bit.

For example, the way I’d created a list of units of time was a little obtuse before…

const units = ["y", "M"]
  .concat((includeWeeks ? ["w"] : [])
  .concat(["d", "h", "m", "s"]))

So I renamed the variable for greater clarity, but I also settled on a simpler ternary for determining the list of units the function needs to work through.

const unitsOfTime = includeWeeks
  ? ["y", "M", "w", "d", "h", "m", "s"]
  : ["y", "M", "d", "h", "m", "s"]

I also flipped around the arguments in the internal whittleDate function. So now the signature requires a given end date, then the duration found so far, and then the list of units we’re working from.

const whittleDate = (givenDate, givenDuration, unitList) => { /**/ }

Then I decided to create an extra variable for readability. Technically, this isn’t needed at all. The return line I’d written for the end of my original code did everything all at once.

return (givenUnits.length > 1)
   ? whittleDate(diffRemainder, givenUnits.slice(1), newduration)
   : newduration

But for readability’s sake, I created the variable remainingUnits to make it clearer what’s going on here – that we’re disregarding the unit of time we’ve being using thus far and working with the remainder of the list in the recursive function call.

let remainingUnits = unitList.slice(1)
return remainingUnits.length
  ? whittleDate(givenDateMinusDelta, updatedDuration, remainingUnits)
  : updatedDuration

You might also notice newduration has been renamed updatedDuration, for clarity of purpose and consistency of formatting. And other variables have been renamed too.

I won’t go through every change. Instead, you can see the refactored version in its entirety.

function diffDates (startDate, endDate, includeWeeks) {
  const unitsOfTime = includeWeeks
    ? ["y", "M", "w", "d", "h", "m", "s"]
    : ["y", "M", "d", "h", "m", "s"]
  const whittleDate = (givenDate, givenDuration, unitList) => {
      let unitInFocus = unitList[0]
      let deltaStartDateGivenDate = givenDate.diff(startDate, unitInFocus)
      let givenDateMinusDelta = givenDate.subtract(deltaStartDateGivenDate, unitInFocus)
      let updatedDuration = givenDuration.add(deltaStartDateGivenDate, unitInFocus)
      let remainingUnits = unitList.slice(1)
      return remainingUnits.length
        ? whittleDate(givenDateMinusDelta, updatedDuration, remainingUnits)
        : updatedDuration
    }
  const emptyDurationObject = dayjs.duration()
  return whittleDate(endDate, emptyDurationObject, unitsOfTime)
}

So having done that, here’s how I dealt with the new bug I’d found.

Adding to dates: a new helper function

Adding an amount of time to a date to get a new date is basically the reverse of what we did last time.

Rather than taking two dates and calculating the difference between them by working through a list of units of time and building up a duration object of all the differences, unit-by-unit, we’re instead building on top of a date with a given duration object, unit-by-unit, until we get another date.

In fact, it’s a little simpler.

We don’t need to worry about whether or not to both with “weeks” as a unit of time. Our new helper function is receiving a duration object as an argument instead of returning one.

So the signature is simpler…

function addDatesDiff (startDate, diffDuration) { \**\ }

And the first line is simpler too.

const units = ["y", "M", "w", "d", "h", "m", "s"]

If there are not weeks to deal with simply won’t find them in our duration object.

Then instead of whittleDate as our internal, recursively called function – which was named that way to say we were going to whittle down our end date by first taking off the difference in years, then the difference in months, and so on – this time we’re going to augment a date. That is, we’re going to add to it, first by years, then by months and so on.

Otherwise the function signature looks very similar.

const augmentDate = (givenDate, givenDuration, unitList) => { \**\ }

Within this we’re still grabbing the first unit on the list as the one to focus on.

let unitInFocus = unitList[0]

Then we’re asking for the given duration with that unit. The first time around this will be the duration that first gets provided to the function.

let durationAsUnitInFocus = givenDuration.as(unitInFocus)

Let’s add that to our given date. The first time around this will be the start date, but this date will be an augmented date each time we call the function.

let augmentedDate = givenDate.add(durationAsUnitInFocus, unitInFocus)

The next time we call the augmentDate function though, we won’t want to add the same amount again, so let’s created a new duration that has this amount subtracted so we’re just let with the remainder.

let remainingDurationToAdd = givenDuration.subtract(durationAsUnitInFocus, unitInFocus)

Finally, let’s do away with the unit we’ve just been using by creating a new unit list without it. Then let’s call augmentDate again, if there are any units anything left in the list to work through, with our augmented date and the duration object with whatever’s left to go.

let remainingUnits = unitList.slice(1)
    return remainingUnits.length
      ? augmentDate(augmentedDate, remainingDurationToAdd, remainingUnits)
      : augmentedDate

And that’s it.

The internal augmentDate function is called with the initial arguments at first but then calls itself until it’s worked through all the units.

It might be easier now to see that whole function is very similar to the function for calculating differences between dates.

function addDatesDiff (startDate, diffDuration) {
  const unitsOfTime = ["y", "M", "w", "d", "h", "m", "s"]
  const augmentDate = (givenDate, givenDuration, unitList) => {
    let unitInFocus = unitList[0]
    let durationAsUnitInFocus = givenDuration.as(unitInFocus)
    let augmentedDate = givenDate.add(durationAsUnitInFocus, unitInFocus)
    let remainingDurationToAdd = givenDuration.subtract(durationAsUnitInFocus, unitInFocus)
    let remainingUnits = unitList.slice(1)
    return remainingUnits.length
      ? augmentDate(augmentedDate, remainingDurationToAdd, remainingUnits)
      : augmentedDate
  }
  return augmentDate(startDate, diffDuration, unitsOfTime)
}

Seeing it in action

You can see the helper function in the same Day.js diff and duration workaround notebook on Observable that I used last time.

But I also hope to put this to more practical use soon and share an example of it working in something useful.

Follow-up thoughts

In putting this together, I reflected a little on the way I write code.

Readability

I tend to write code as tightly as I can but this can make it a little obtuse to read – both for others and perhaps for myself returning to it later.

I’d be interested to see on returning to these functions after another bit of time has passed whether I find them just as easy to understand or if the fact that they’re now longer (both in terms of number of lines and length of variable names) makes them more intimidating somehow.

Recursion

Recursion might also be an issue – I’m fond of it as a way of repeating actions. There’s something that seems elegant about it. But as I wrote the explanation of what my functions do I realise it isn’t the easiest to read or explain linearly.

You have to read recursive functions as a whole to understand what they’re doing – which is what I think gives recursive patterns their elegance, but means they also might not be readily comprehendible.

What next?

It would be great to not only put these helper functions to use, but also perhaps fix Day.js properly so that it works.

You can already find the issue about durations and date diffs on the Day.js repo.

Perhaps if I can work out how best to contribute these functions to the Day.js codebase I’ll raise a pull request to see if they can be added so that helper functions like mine aren’t needed in future.