49

Given int value 1807, format to 30:07 using date-fns?

Yes, I know this can be achieved with vanilla js, but is this possible using date-fns?

Simon
  • 2,484
  • 6
  • 35
  • 54

6 Answers6

58

According to the example code in date-fns/date-fns#229 (comment), we can now use intervalToDuration to convert seconds (passed as an Interval) to a Duration, which can then simplify formatting as desired by the OP:

import {  intervalToDuration } from 'date-fns'

const seconds = 10000

intervalToDuration({ start: 0, end: seconds * 1000 })
// { hours: 2, minutes: 46, seconds: 40 }

So for the OP's needs:

import {  intervalToDuration } from 'date-fns'

const seconds = 1807
const duration = intervalToDuration({ start: 0, end: seconds * 1000 })
// { minutes: 30, seconds: 7 }

const formatted = `${duration.minutes}:${duration.seconds}`
// 30:7

Edit (2022-08-04): It was pointed out that the above simplistic code won't 0-pad the numbers, so you will end up with 30:7 rather than 30:07. This padding can be achieved by using String.prototype.padStart() as follows:

import {  intervalToDuration } from 'date-fns'

const seconds = 1807
const duration = intervalToDuration({ start: 0, end: seconds * 1000 })
// { minutes: 30, seconds: 7 }

const zeroPad = (num) => String(num).padStart(2, '0')

const formatted = `${zeroPad(duration.minutes)}:${zeroPad(duration.seconds)}`
// 30:07

It was also pointed out that if the Interval goes above 60 minutes it will start incrementing the hours within the Duration, which the above code wouldn't display. So here is another slightly more complex example that handles this as well as the zeroPad case:

import {  intervalToDuration } from 'date-fns'

const seconds = 1807
const duration = intervalToDuration({ start: 0, end: seconds * 1000 })
// { minutes: 30, seconds: 7 }

const zeroPad = (num) => String(num).padStart(2, '0')

const formatted = [
  duration.hours,
  duration.minutes,
  duration.seconds,
]
.filter(Boolean)
.map(zeroPad)
.join(':')
// 30:07

There is also an issue on GitHub asking how to use a custom format with formatDuration, which suggest that currently the only way to do so is by providing a custom Locale. GitHub user @marselmustafin provided an example using this workaround. Following this same pattern, we could implement the OP's desired functionality roughly as follows:

import { intervalToDuration, formatDuration } from "date-fns";

const seconds = 1807;
const duration = intervalToDuration({ start: 0, end: seconds * 1000 });
// { minutes: 30, seconds: 7 }

const zeroPad = (num) => String(num).padStart(2, "0");

const formatted = formatDuration(duration, {
  format: ["minutes", "seconds"],
  // format: ["hours", "minutes", "seconds"],
  zero: true,
  delimiter: ":",
  locale: {
    formatDistance: (_token, count) => zeroPad(count)
  }
});
// 30:07
Glenn 'devalias' Grant
  • 1,928
  • 1
  • 21
  • 33
  • 6
    Do note that if you go above 60 mn it'll start incrementing the hours and the minutes count will go back to 0 so if you only display the minutes and seconds as you did in `formatted` it'll be incorrect – maxime1992 Apr 02 '21 at 09:15
  • 1
    This is incorrect, instead of 30:07 it will present 30:7 – Boat Aug 02 '22 at 07:47
  • @Boat If you want to zero-pad the numbers, here is a simple example from [another SO answer](https://stackoverflow.com/questions/2998784/how-to-output-numbers-with-leading-zeros-in-javascript/2998874#2998874) that uses [`String.prototype.padStart()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart): `String(num).padStart(2, '0')` – Glenn 'devalias' Grant Aug 04 '22 at 03:40
  • 1
    Another small caveat with this otherwise nice approach: if you provide i with 0 seconds it will return the empty string, however, I would expect 0 seconds to be formatted as '00:00'. edit: it goes deeper than this. Any part of the hours/minutes/seconds that are 0 will be omitted from the formatted string. – nover Sep 21 '22 at 06:54
  • @nover Is that when using the example with the `.filter(Boolean)`? That filters out any results that are falsy (eg. the `0`'s), so if you remove that line they will be included, and would result in a formatted result of `00:30:07` using `1807` seconds as above. You're probably better off using the 'custom format' example below it though, as my original solution is starting to feel more and more hacky as edge cases are found. With the 'custom format' example, the `zero: true` boolean controls whether the `0`'s are included (eg. `1800` seconds will give `30:00` or `30` depending on this flag) – Glenn 'devalias' Grant Sep 23 '22 at 00:16
32

Here's the simple implementation:

import { formatDistance } from 'date-fns'

const duration = s => formatDistance(0, s * 1000, { includeSeconds: true })

duration(50) // 'less than a minute'
duration(1000) // '17 minutes'

This is basically the same as:

import moment from 'moment'    

const duration = s => moment.duration(s, 'seconds').humanize()

duration(50) // 'a minute'
duration(1000) // '17 minutes'
Glenn 'devalias' Grant
  • 1,928
  • 1
  • 21
  • 33
Max Yokha
  • 331
  • 3
  • 6
  • I like this solution, it is formatted and very readable – Joseph Norman Jul 19 '21 at 15:32
  • 1
    I expect lots of people have a date that needs to be compared to now and show the duration in between as above: duration(differenceInSeconds(new Date(), yourDate)) – Marc Sep 22 '21 at 11:36
  • 1
    @Marc In that instance, you could use one of the direct helpers that already exists in `date-fns`: [`formatDistanceToNow`](https://date-fns.org/docs/formatDistanceToNow) / [`formatDistanceToNowStrict`](https://date-fns.org/docs/formatDistanceToNowStrict) – Glenn 'devalias' Grant Sep 23 '22 at 00:30
29

You can do this using date-fns by simple modifying an intermediate helper date. Using new Date( 0 ) you'll get a date set to January 1, 1970, 00:00:00 UTC. You can then use addSeconds from date-fns to add the relevant seconds (actually you could use the native date setTime( 1000 * seconds ) for this). Formatting the minutes and seconds of this will give you your desired result.

var input = document.getElementById('seconds');
var output = document.getElementById('out');

output.innerText = formattedTime(input.value);
input.addEventListener('input', function() {
  output.innerText = formattedTime(input.value);
});

function formattedTime(seconds) {
  var helperDate = dateFns.addSeconds(new Date(0), seconds);
  return dateFns.format(helperDate, 'mm:ss');
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.26.0/date_fns.min.js"></script>

<input type="number" id="seconds" value="1807">
<pre id="out"></pre>
alex3683
  • 1,460
  • 14
  • 25
  • 4
    It works, but perhaps not the way you expected. I just assumed he liked to display the minute / second part of a larger amount of time only, since he only asked for that. If the amount is larger, you'd have to extract the hours or days and decide how to display that. So it fits the usecase of @Simon but not everyone's ;-) – alex3683 Feb 13 '19 at 07:10
  • 6
    i think there is something about `var helperDate = dateFns.addSeconds(new Date(0), seconds)` that adds your timezone offset to the resulting date, ie if you format the result with `hh:mm:ss` - `180` seconds will produce `10:03:00` and `240` seconds will produce `10:04:00` if you are in a `+10` timezone offset. – user1063287 Jul 06 '19 at 05:08
  • 6
    as @user1063287 noted, your time setting will be zoned. So if you want to display `HH:mm:ss` you will always see `01:00:00` for `Europe/Berlin` with `new Date(0)`. I solved it by using `date-fns-tz` to set a fixed timezone where I know the offset of (e.g. `Europe/Berlin == +1 Hour to GMT`), then I subtract that value with the `addHour` function of `date-fns` ```javascript const dateHelper: Date = addSeconds(new Date(0), duration); const utcDate = zonedTimeToUtc(dateHelper, TIME_ZONE); this.durationDisplay = format(addHours(utcDate, -1), 'HH:mm:ss'); ``` – HasBert Mar 14 '20 at 02:44
  • FYI, depending on where your users are, in some time zones even the minutes are offset, resulting in e.g. `30:05` for a duration of 5 seconds. https://www.timeanddate.com/time/time-zones-interesting.html – Alex Wally Oct 16 '22 at 20:01
11

This function will convert seconds to duration format hh:mm:ss, its analogue duration in moment.js

import { addHours, getMinutes, getHours, getSeconds } from 'date-fns';

export const convertToDuration = (secondsAmount: number) => {
    const normalizeTime = (time: string): string =>
    time.length === 1 ? `0${time}` : time;

    const SECONDS_TO_MILLISECONDS_COEFF = 1000;
    const MINUTES_IN_HOUR = 60;

    const milliseconds = secondsAmount * SECONDS_TO_MILLISECONDS_COEFF;

    const date = new Date(milliseconds);
    const timezoneDiff = date.getTimezoneOffset() / MINUTES_IN_HOUR;
    const dateWithoutTimezoneDiff = addHours(date, timezoneDiff);

    const hours = normalizeTime(String(getHours(dateWithoutTimezoneDiff)));
    const minutes = normalizeTime(String(getMinutes(dateWithoutTimezoneDiff)));
    const seconds = normalizeTime(String(getSeconds(dateWithoutTimezoneDiff)));

    const hoursOutput = hours !== '00' ? `${hours}:` : '';

    return `${hoursOutput}${minutes}:${seconds}`;
};
Glenn 'devalias' Grant
  • 1,928
  • 1
  • 21
  • 33
Borisov Semen
  • 448
  • 5
  • 8
9

Just use format from date-fns like that:

format((seconds * 1000), 'mm:ss')

if there is a need to remove timezone hours offset as well, getTimezoneOffset (in minutes) can be used:

let dt = new Date((seconds * 1000));
dt = addMinutes(dt, dt.getTimezoneOffset());
return format(dt, 'mm:ss');
Glenn 'devalias' Grant
  • 1,928
  • 1
  • 21
  • 33
EladTal
  • 2,167
  • 1
  • 18
  • 10
  • 1
    While this works for the exact example of `1807` seconds to `mm:ss`, if you wanted to be able to handle hours as well, it doesn't cope as well. Eg. Changing the format string to `hh:mm:ss` will result in an output of `12:30:07`, even though the hours should actually be `0` in this case. – Glenn 'devalias' Grant Sep 23 '22 at 00:43
  • I get RangeError: Invalid time value – Or Nakash Oct 02 '22 at 07:06
3

There doesn't seem to be a direct equivalent of moment.duration in date-fns...

This function I wrote might help someone. Date-fns.differenceInSeconds is used to get the total difference in seconds. There are equivalent methods for milliseconds, minutes, etc. Then I use vanilla js math to format that

/**
* calculates the duration between two dates.
* @param {date object} start The start date.
* @param {date object} finish The finish date.
* @return {string} a string of the duration with format 'hh:mm'
*/
export const formatDuration = (start, finish) => {
    const diffTime = differenceInSeconds(finish, start);
    if (!diffTime) return '00:00'; // divide by 0 protection
    const minutes = Math.abs(Math.floor(diffTime / 60) % 60).toString();
    const hours = Math.abs(Math.floor(diffTime / 60 / 60)).toString();
    return `${hours.length < 2 ? 0 + hours : hours}:${minutes.length < 2 ? 0 + minutes : minutes}`;
};
Glenn 'devalias' Grant
  • 1,928
  • 1
  • 21
  • 33
Tim Day
  • 105
  • 1
  • 5