Is it possible to easily format seconds as a human-readable time in bash?
I don't want to format it as a date, but as the number of days/hours/minutes, etc...
Is it possible to easily format seconds as a human-readable time in bash?
I don't want to format it as a date, but as the number of days/hours/minutes, etc...
You can use something like this:
function displaytime {
local T=$1
local D=$((T/60/60/24))
local H=$((T/60/60%24))
local M=$((T/60%60))
local S=$((T%60))
(( $D > 0 )) && printf '%d days ' $D
(( $H > 0 )) && printf '%d hours ' $H
(( $M > 0 )) && printf '%d minutes ' $M
(( $D > 0 || $H > 0 || $M > 0 )) && printf 'and '
printf '%d seconds\n' $S
}
Examples:
$ displaytime 11617
3 hours 13 minutes and 37 seconds
$ displaytime 42
42 seconds
$ displaytime 666
11 minutes and 6 seconds
Easiest and cleanest way is this one liner (here assuming GNU date
):
If the number of seconds is, say:
seconds=123456789 # as in one of the answers above
date -ud "@$seconds" +"$(( $seconds/3600/24 )) days %H hours %M minutes %S seconds"
--> output: 1428 days 21 hours 33 minutes 09 seconds
+%T
format for Hours:Minutes:Seconds for this date +%T "1970-01-01 + ${seconds} seconds"
to get 21:33:09
Commented
Nov 9, 2019 at 4:58
-u
to simply stay in UTC since it is only a duration: date -u +"%T" -d "@${seconds}"
.
Commented
Oct 11, 2023 at 5:09
Credit goes to Stéphane Gimenez but if someone would like to display seconds only if a period is less than a minute here is my modified version that I use (also with fixed pluralization):
converts()
{
local t=$1
local d=$((t/60/60/24))
local h=$((t/60/60%24))
local m=$((t/60%60))
local s=$((t%60))
if [[ $d > 0 ]]; then
[[ $d = 1 ]] && echo -n "$d day " || echo -n "$d days "
fi
if [[ $h > 0 ]]; then
[[ $h = 1 ]] && echo -n "$h hour " || echo -n "$h hours "
fi
if [[ $m > 0 ]]; then
[[ $m = 1 ]] && echo -n "$m minute " || echo -n "$m minutes "
fi
if [[ $d = 0 && $h = 0 && $m = 0 ]]; then
[[ $s = 1 ]] && echo -n "$s second" || echo -n "$s seconds"
fi
echo
}
An alternative example in POSIX:
converts(){
t=$1
d=$((t/60/60/24))
h=$((t/60/60%24))
m=$((t/60%60))
s=$((t%60))
if [ $d -gt 0 ]; then
[ $d = 1 ] && printf "%d day " $d || printf "%d days " $d
fi
if [ $h -gt 0 ]; then
[ $h = 1 ] && printf "%d hour " $h || printf "%d hours " $h
fi
if [ $m -gt 0 ]; then
[ $m = 1 ] && printf "%d minute " $m || printf "%d minutes " $m
fi
if [ $d = 0 ] && [ $h = 0 ] && [ $m = 0 ]; then
[ $s = 1 ] && printf "%d second" $s || printf "%d seconds" $s
fi
printf '\n'
}
convert $s
to get pretty output.
Commented
Oct 19, 2016 at 19:25
I'd do it like this:
$ seconds=123456789; echo $((seconds/86400))" days "$(date -d "1970-01-01 + $seconds seconds" "+%H hours %M minutes %S seconds")
1428 days 21 hours 33 minutes 09 seconds
$
Here's the one liner above, broken down so that it's easier to understand:
$ seconds=123456789
$ echo $((seconds/86400))" days"\
$(date -d "1970-01-01 + $seconds seconds" "+%H hours %M minutes %S seconds")
In the above I'm echoing out the output of another command that's run inside the $( ... )
sub-command. That sub-command is doing this, calculating the number of days (seconds/86400), then using the date
command in another sub-command $(date -d ... )
, to generate the hours, minutes, and seconds for a given number of seconds.
echo
? and factoring in the timezone also makes no sense! || much easier would be date -u +"$(( ${seconds} / 86400 )) days %H hours %M minutes %S seconds" -d "@${seconds}"
.
Commented
Oct 11, 2023 at 8:10
I'm building on atti's answer which I liked as an idea.
You can do this with the bash builtin printf
which will take the seconds since the epoch as an argument. No need to fork to run date
.
You have to set the timezone to UTC for printf
because it formats the time in your local timezone and you will get the wrong answer if you are not in UTC time.
$ seconds=123456789
$ TZ=UTC printf "%d days %(%H hours %M minutes %S seconds)T\n" $((seconds/86400)) $seconds
1428 days 21 hours 33 minutes 09 seconds
In my local time (which is currently NZDT - +1300) the answer is wrong if I do not set the timezone
$ seconds=123456789
$ printf "%d days %(%H hours %M minutes %S seconds)T\n" $((seconds/86400)) $seconds
1428 days 09 hours 33 minutes 09 seconds
With and without setting the timezone
$ seconds=$(( 3600 * 25))
$ printf "%d days %(%H hours %M minutes %S seconds)T\n" $((seconds/86400)) $seconds
1 days 13 hours 00 minutes 00 seconds
$ TZ=UTC printf "%d days %(%H hours %M minutes %S seconds)T\n" $((seconds/86400)) $seconds
1 days 01 hours 00 minutes 00 seconds
printf
introduced this %(datefmt)T
notation beginning with bash-4.2-alpha.
I modified the displaytime function above... as follows:
seconds2time ()
{
T=$1
D=$((T/60/60/24))
H=$((T/60/60%24))
M=$((T/60%60))
S=$((T%60))
if [[ ${D} != 0 ]]
then
printf '%d days %02d:%02d:%02d' $D $H $M $S
else
printf '%02d:%02d:%02d' $H $M $S
fi
}
because I always want to see HH:MM:SS, even if they are zeros.
In addition to the other answers, to get an output in [[[[d and ]hh:]mm:]ss | ss 's']
format it could be done like this:
function format_seconds() {
(($1 >= 86400)) && printf '%d days and ' $(($1 / 86400)) # days
(($1 >= 3600)) && printf '%02d:' $(($1 / 3600 % 24)) # hours
(($1 >= 60)) && printf '%02d:' $(($1 / 60 % 60)) # minutes
printf '%02d%s\n' $(($1 % 60)) "$( (($1 < 60 )) && echo ' s.' || echo '')"
}
For example, if we execute:
format_seconds 1000000
format_seconds 86450
format_seconds 9000
format_seconds 2500
format_seconds 60
format_seconds 34
We will get the following output:
11 days and 13:46:40
1 days and 00:00:50
02:30:00
41:40
01:00
34 s.
Although the question was asked more than 8 years ago i hope it helps to someone :)
Building on Stéphane Gimenez's answer, but an alternative to dimir's:
Since printf is already in play, may as well use %s and pass is the s (or not) to pluralize when needed:
displaytime() {
local T=$1
local D=$((T/60/60/24))
local H=$((T/60/60%24))
local M=$((T/60%60))
local S=$((T%60))
(( $D > 0 )) && printf '%d day%s ' $D $( (( $D > 1 )) && echo s)
(( $H > 0 )) && printf '%d hour%s ' $H $( (( $H > 1 )) && echo s)
(( $M > 0 )) && printf '%d minute%s ' $M $( (( $M > 1 )) && echo s)
(( $D > 0 || $H > 0 || $M > 0 )) && printf 'and '
printf '%d second%s\n' $S $( (( $S != 1 )) && echo s)
}
I slightly improved @Jacques' answer into this:
FormatSeconds () {
seconds="$1"
date -ud @${seconds} +"$(( seconds/3600/24 ))d %-Hh %-Mm %-Ss" \
| sed -E 's/\b0(s|m|h|d) ?//g; s/ +$//; s/^$/0s/'
}
which will just use "d/h/m/s", strip off leading zeroes and omit zero parts. So:
> FormatSeconds 123456789
1428d 21h 33m 9s
> FormatSeconds $((1 * 24 * 60 * 60 + 60 + 1))
1d 1m 1s
sed
, but your implementation is way too complicated! || date
can prevent leading 0 by itself with %-
and this makes the sed part even more easier than it could be already: date -u +"$(( ${seconds} / 86400 ))d %-Hh %-Mmin %-Ss" -d "@${seconds}" | sed -E -e 's/\<0(d|h|min|s)\> ?//g ; s/^$/0s/'
Commented
Oct 11, 2023 at 9:12
seconds
in your date
-command misses at least a preceded $
! even better would be ${seconds}
.
Commented
Oct 11, 2023 at 9:16
seconds
does not need a $
because that's how arithmetic evaluation works, see e.g. bash -c 'ONE=1 ; echo $((ONE + 1))'
.
Commented
Oct 15, 2023 at 22:03
seconds=$((60*60+60))
.
Commented
Oct 15, 2023 at 22:21
.../86400
instead of .../3600/24
and use ${1}
straight instead of ${seconds}
.
Commented
Oct 16, 2023 at 7:36
Using GNU 'units' will allow for a one-liner. Usually installed as /usr/bin/units
.
The usage information is all contained in the man page.
units '46825 seconds' 'hour;minute;second'
13 hour + 25 second
units '1524691 sec' 'wk;day;hr;min;sec'
2 wk + 3 day + 15 hr + 31 min + 31 sec
units '123hr+27332sec' 'weeks;days;hours;minutes;seconds'
5 days + 10 hours + 35 minutes + 32 seconds
Only non-zero values are displayed.
The units can be written with or without the final 's'.
The available units are displayed when using the --conformable
option:
units --conformable sec
will output a lengthy list of TIME related units such as: second, minute, hour, day, week, month, year, decade and more.
Here one
secs=378444
echo $(($secs/86400))d $(($(($secs - $secs/86400*86400))/3600))h:$(($(($secs - $secs/86400*86400))%3600/60))m:$(($(($secs - $secs/86400*86400))%60))s
Output:
4d 9h:7m:24s
date --date '@1005454800'
gives you Sun Nov 11 00:00:00 EST 2001
, which is 1005454800 seconds after the Unix epoch. You can format that with the date +FORMAT option.