Format a duration ( from seconds ) using date-fns
Asked Answered
E

7

59

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?

Efface answered 13/2, 2018 at 21:43 Comment(0)
S
31

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>
Schuller answered 9/3, 2018 at 9:48 Comment(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 @Efface but not everyone's ;-)Schuller
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.Mitten
as @Mitten 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'); Linger
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. timeanddate.com/time/time-zones-interesting.htmlSoldier
M
75

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
Mayce answered 13/1, 2021 at 23:32 Comment(7)
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 incorrectFilial
This is incorrect, instead of 30:07 it will present 30:7Glennglenna
@Glennglenna If you want to zero-pad the numbers, here is a simple example from another SO answer that uses String.prototype.padStart(): String(num).padStart(2, '0')Epileptic
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.Bevatron
@Bevatron 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)Epileptic
All this... code. I better switch to a different library.Symptom
Adding to @Bevatron comment - from v3 onwards, zero values are not returned by intervalToDuration (see here). So you need to adapt accordingly if you want to show zero values, ie to show things like 01:00:01, 00:01:01, 01:01:00 ${zeroPad(hours)}:${zeroPad(minutes)}:${zeroPad(seconds)}Euphorbia
G
36

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'
Gimcrack answered 11/10, 2018 at 20:44 Comment(3)
I like this solution, it is formatted and very readableAbdominous
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))Fed
@Fed In that instance, you could use one of the direct helpers that already exists in date-fns: formatDistanceToNow / formatDistanceToNowStrictEpileptic
S
31

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>
Schuller answered 9/3, 2018 at 9:48 Comment(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 @Efface but not everyone's ;-)Schuller
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.Mitten
as @Mitten 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'); Linger
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. timeanddate.com/time/time-zones-interesting.htmlSoldier
B
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}`;
};
Bicycle answered 9/1, 2019 at 11:44 Comment(1)
Nicely written. Awesome naming.File
C
11

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');
Conditional answered 10/11, 2021 at 21:27 Comment(2)
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.Epileptic
I get RangeError: Invalid time valueJutland
G
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}`;
};
Gwenny answered 23/12, 2019 at 10:51 Comment(0)
N
0

In case you want an output like "12h 30min" I used this simple function:

import { secondsToMinutes, secondsToHours, format } from "date-fns";

export function convertSecondsToHoursMinutes(seconds: number ){
    const hours = secondsToHours(seconds);
    const minutes = secondsToMinutes(seconds) - hours * 60;

    return `${hours}h ${minutes.toString().padStart(2,"0")}min`;
    // e.g. 1h 00min or 300h 12min
}
Neb answered 31/5, 2024 at 10:20 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.