12

I'm using DateFNS and I need to generate a countdown with it. distanceInWordsToNow only outputs in about 3 years but I need the exact time like 3 Years, 11 Months, 20 Days, 3 Hours, 2 Minutes. How to archive that with DateFNS?

Here is a CodePen example: https://codepen.io/anon/pen/qGajJB

SCRIPT

todaysDateMin: dateFns.distanceInWordsToNow(new Date(2022, 6, 2, 0, 0, 15), {addSuffix: true})
Akrion
  • 18,117
  • 1
  • 34
  • 54
Tom
  • 5,588
  • 20
  • 77
  • 129
  • 1
    it seems to me like you could use differenceInMinutes then achieve the rest yourself by dividing by minutes in a year, etcetera. – Jeremy Kahan May 15 '19 at 12:13
  • There's an [open issue](https://github.com/date-fns/date-fns/issues/229) in their tracker. This feature is not yet implemented, but you can work it out yourself as Jeremy described. This library has functions like `differenceInYears`, `differenceInMonths`, `differenceInDays` etc. All you need to do is to build the final string by using them. In the future, they're planning to introduce an additional parameter to `distanceInWords` which would allow to specify the output format. – Tomasz Kasperczyk May 15 '19 at 12:25
  • @Tom, Would you be ok if the solution was achieved using plain JS? And no library? – RichS May 16 '19 at 04:49

3 Answers3

23

The formatDuration() function in date-fns v2 does exactly what you want it to.

import { formatDuration, intervalToDuration } from 'date-fns'
let duration = intervalToDuration({
    start: new Date(2022, 6, 2, 0, 0, 15), 
    end: new Date(),
})

formatDuration(duration, {
    delimiter: ', '
})
// 2 years, 15 days, 23 hours, 49 minutes, 35 seconds

You can even filter it.

// take the first three nonzero units
const units = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds']
const nonzero = Object.entries(duration).filter(([_, value]) => value || 0 > 0).map(([unit, _]) => unit)

formatDuration(duration, {
    format: units.filter(i => new Set(nonzero).has(i)).slice(0, 3),
    delimiter: ', '
})
// 2 years, 15 days, 23 hours
I'll Eat My Hat
  • 1,150
  • 11
  • 20
  • 1
    For both modules I get this error: `Module '"date-fns"' has no exported member 'formatDuration'.ts(2305)`. In `package.json` we have `"date-fns": "^2.0.1"`, so it should be available already. Do you know what is the issue? – katericata Sep 21 '21 at 08:03
  • According to the documentation, the `formatDuration` function is added in v2.14.0, so update the library. https://date-fns.org/v2.14.0/docs/formatDuration – I'll Eat My Hat Sep 22 '21 at 03:54
  • This solved the problem, thanks. – katericata Sep 23 '21 at 08:34
  • 1
    Just wondering if the `|| 0 > 0` check is needed. It gave me an eslint warning so I took it out and everything seems to still work great. Nice answer by the way. – cham Jul 14 '22 at 01:00
  • you're probably right. I'm pretty sure implicit coercion will cause `filter` to automatically remove falsy values anyways. I am too used to working in typed languages. – I'll Eat My Hat Jul 21 '22 at 05:22
  • But how do you count down? That is, how do you subtract 1 second from a Duration? – Eliezer Berlin Dec 29 '22 at 10:55
  • *literally* counting down is probably ill advised. Computers are not so exact that they can perform operations at the exact second, causing your counter to drift. Where time matters, it's best to rely on the system clock and run the above code once in a while using events or something. – I'll Eat My Hat Jun 02 '23 at 16:56
  • But to specifically answer your question, date-fns doesn't provide any math helper functions for `Duration` objects, though they wouldn't be hard to write. It's just an object with some fields. – I'll Eat My Hat Jun 02 '23 at 17:02
8

You can try something like this:

let humanizeFutureToNow = fDate => {
  let result = [], now = new Date()
  let parts = ['year', 'month', 'day', 'hour', 'minute']

  parts.forEach((p, i) => {
    let uP = p.charAt(0).toUpperCase() + p.slice(1)
    let t = dateFns[`differenceIn${uP}s`](fDate, now);
    if (t) {
      result.push(`${i===parts.length-1 ? 'and ' : ''}${t} ${uP}${t===1 ? '' : 's'}`);
      if (i < parts.length)
        fDate = dateFns[`sub${uP}s`](fDate, t);
    }
  })
  return result.join(' ');
}

console.log(humanizeFutureToNow(new Date('2022-11-11')))
<script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.min.js"></script>

The idea is to run through all the time periods you want (and have supported date-fns functions) and generate an array with all the strings. We do this by running the supported differenceIn<PARTGOESHERE> first to get the time distance and then subtract it using the sub<PARTGOESHERE> function. After that just join them to compose the final string.

From here you can customize and export the parts as a parameter, as well as the uppercasing of the first letters in the output etc etc.

Here is also a lodash version:

let humanizeFutureToNow = fDate => {
  let result = [], now = new Date()
  let parts = ['year', 'month', 'day', 'hour', 'minute']

  _.each(parts, (p, i) => {
    let scPart = _.startCase(p)
    let t = _.invoke(dateFns, `differenceIn${scPart}s`, fDate, now);
    if (t) {
      result.push(`${i===parts.length-1 ? 'and ' : ''}${t} ${scPart}${t===1 ? '' : 's'}`);
      if (i < parts.length)
        fDate = _.invoke(dateFns, `sub${scPart}s`, fDate, t);
    }
  })
  return result.join(' ');
}

console.log(humanizeFutureToNow(new Date('2022-11-11')))
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.min.js"></script>
Akrion
  • 18,117
  • 1
  • 34
  • 54
  • Thanks for your example and lodash suggestion. How would you do this in lodash? – Tom May 16 '19 at 11:43
  • I cleaned up the plain js version and added a lodash version. They are pretty close just maybe the lodash one is somewhat more readable. There probably could be more enhancements to that version if you really focus on it. – Akrion May 16 '19 at 18:56
  • 1
    I like the way you were able to build the loop rather than repeat yourself (also how this could easily be extended to include seconds and ms) – Jeremy Kahan May 17 '19 at 01:51
4

Use the following dateFns functions to get the components, then concatenate them together with the strings.

let x be the smaller year, y be the larger save differenceInYears called on x and y which gives the whole number of years in a variable pass x and that as a parameter to addYears, assign to x

call differenceInMonths on x and y, save call addMonths on x and that saved number of months

do the same with differenceInDays and addDays, differenceInHours and addHours Now x is less than 60 minutes away from y, so call differenceInMinutes and you're done (assuming your countDown is down to the minute).

Here is your example as run on runkit.com to illustrate the method.

var dateFns = require("date-fns");
var x = new Date();
var y = new Date(2022, 2, 6, 0, 0, 15);
var temp;
temp = dateFns.differenceInYears(y, x);
var result = temp + " years ";
x = dateFns.addYears(x, temp);
temp = dateFns.differenceInMonths(y, x);
result = result + temp + " months ";
x = dateFns.addMonths(x, temp);
temp = dateFns.differenceInDays(y, x);
result = result + temp + " days ";
x = dateFns.addDays(x, temp);
temp = dateFns.differenceInHours(y, x);
result = result + temp + " hours ";
x = dateFns.addHours(x, temp);
temp = dateFns.differenceInMinutes(y, x);
result = result + temp + " minutes ";
x = dateFns.addMinutes(x, temp);
temp = dateFns.differenceInSeconds(y, x);
result = result + temp + " seconds";
console.log(result);
Jeremy Kahan
  • 3,796
  • 1
  • 10
  • 23