algalg
algalg

Reputation: 821

Bash: Display Seconds in Days, Hours, Minutes, Seconds

I have a function, inspired by hms from here, but I wish to extend it to include handling and displaying Days.

I started editing the script but quickly realized I'm out of my depth with handling the logic of hours running into days and visa versa...

Here's what I have so far:

#!/bin/bash

rendertimer(){
    # convert seconds to Days, Hours, Minutes, Seconds
    # thanks to https://www.shellscript.sh/tips/hms/
    local seconds D H M S MM D_TAG H_TAG M_TAG S_TAG
    seconds=${1:-0}
    S=$((seconds%60))
    MM=$((seconds/60)) # total number of minutes
    M=$((MM%60))
    H=$((MM/60))
    D=$((H/24))

    # set up "x day(s), x hour(s), x minute(s) and x second(s)" format
    [ "$D" -eq "1" ] && D_TAG="day" || D_TAG="days"
    [ "$H" -eq "1" ] && H_TAG="hour" || H_TAG="hours"
    [ "$M" -eq "1" ] && M_TAG="minute" || M_TAG="minutes"
    [ "$S" -eq "1" ] && S_TAG="second" || S_TAG="seconds"
    # logic for handling display
    [ "$D" -gt "0" ] && printf "%d %s " $D "${D_TAG},"
    [ "$H" -gt "0" ] && printf "%d %s " $H "${H_TAG},"
    [ "$seconds" -ge "60" ] && printf "%d %s " $M "${M_TAG} and"
    printf "%d %s\n" $S "${S_TAG}"
}

duration=${1}

howlong="$(rendertimer $duration)"
echo "That took ${howlong} to run."

Desired output

349261 seconds: "That took 4 days, 1 hour, 1 minute and 1 second to run."

127932 seconds: "That took 1 day, 11 hours, 32 minutes and 12 seconds to run."

86400 seconds: "That took 1 day to run."

Actual output

349261 seconds: "That took 4 days, 97 hours, 1 minute and 1 second to run."

127932 seconds: "That took 1 day, 35 hours, 32 minutes and 12 seconds to run."

86400 seconds: "That took 1 day, 24 hours, 0 minutes and 0 seconds to run."

Can someone help me figure this out?

Upvotes: 0

Views: 844

Answers (3)

Paul Hodges
Paul Hodges

Reputation: 15246

I know this is an old post, but a little more abstraction could make it more concise and flexible, and hopefully the examples of argument parsing, error handling, and usage instructions will come in handy for someone...and it was fun.

Suggestions for improvement welcome.

breakout () {
  local secs msg s unit all cnt bad times=() use="
    Use: breakout [ [-{a|m}] NUMBER_OF_SECONDS_n.. ]

    Breaks NUMBER_OF_SECONDS into weeks, days, hours, minutes seconds.
    Only lists seconds with zero values without -a or -m

    Dash-options apply only to the next NUMBER_OF_SECONDS 
    -m  reports time units in the 'middle', after a nonzero value
    -a  reports weeks to seconds (overrides -m if stacked)
  "
  (( $# )) || { echo "$use" >&2; return; }
  declare -A fill sxper=([week]=604800 [day]=86400 [hour]=3600 [minute]=60 [second]=1) 
  for secs in "$@"
  do case $secs in
         -[am]) fill[${secs#-}]=true;                         continue ;;
      *[^0-9]*) echo "Invalid argument '$secs'" >&2; fill=(); continue ;;
     esac
     for unit in week day hour minute second
     do all=${sxper[$unit]}
        if (( secs >= all ))
        then cnt=$((secs/all)); ((cnt>1)) && s=s || s=""; msg+="${msg:+, }$cnt $unit${s:-}"; secs=$((secs%all))
        elif [[ -n "${fill[a]:-}" ]]; then msg+="${msg:+, }0 ${unit}s";
        elif [[ -n "${fill[m]:-}" ]]; then msg+="${msg:+, 0 ${unit}s}";
        fi
     done
     echo ${msg:-0 seconds}; msg=""; fill=()
  done
}

This lets you provide multiple inputs with varying formats, and reasonably sane defaults after an error.

$: breakout

    Use: breakout [ [-{a|m}] NUMBER_OF_SECONDS_n.. ]

    Breaks NUMBER_OF_SECONDS into weeks, days, hours, minutes seconds.
    Only lists seconds with zero values without -a or -m

    Dash-options apply only to the next NUMBER_OF_SECONDS
    -m  reports time units in the 'middle', after a nonzero value
    -a  reports weeks to seconds (overrides -m if stacked)

$: breakout 86401 -a 86401 -m 86401 -a foo -x 86401 -m 86401
1 day, 1 second
0 weeks, 1 day, 0 hours, 0 minutes, 1 second
1 day, 0 hours, 0 minutes, 1 second
Invalid argument 'foo'
Invalid argument '-x'
1 day, 1 second
1 day, 0 hours, 0 minutes, 1 second

Weeks is probably not useful very often, but it was easy to add since it's a fixed number of seconds. I didn't bother with months and years because they are very seldom going to be needed (OP didn't ask for weeks), and probably aren't worth the extra work to handle the varying lengths.

Upvotes: 1

algalg
algalg

Reputation: 821

Thanks to @BeardOverflow and @NikolaySidorov, this works great:

#!/bin/bash

rendertimer(){
    # convert seconds to Days, Hours, Minutes, Seconds
    # thanks to Nikolay Sidorov and https://www.shellscript.sh/tips/hms/
    local parts seconds D H M S D_TAG H_TAG M_TAG S_TAG
    seconds=${1:-0}
    # all days
    D=$((seconds / 60 / 60 / 24))
    # all hours
    H=$((seconds / 60 / 60))
    H=$((H % 24))
    # all minutes
    M=$((seconds / 60))
    M=$((M % 60))
    # all seconds
    S=$((seconds % 60))

    # set up "x day(s), x hour(s), x minute(s) and x second(s)" language
    [ "$D" -eq "1" ] && D_TAG="day" || D_TAG="days"
    [ "$H" -eq "1" ] && H_TAG="hour" || H_TAG="hours"
    [ "$M" -eq "1" ] && M_TAG="minute" || M_TAG="minutes"
    [ "$S" -eq "1" ] && S_TAG="second" || S_TAG="seconds"

    # put parts from above that exist into an array for sentence formatting
    parts=()
    [ "$D" -gt "0" ] && parts+=("$D $D_TAG")
    [ "$H" -gt "0" ] && parts+=("$H $H_TAG")
    [ "$M" -gt "0" ] && parts+=("$M $M_TAG")
    [ "$S" -gt "0" ] && parts+=("$S $S_TAG")

    # construct the sentence
    result=""
    lengthofparts=${#parts[@]}
    for (( currentpart = 0; currentpart < lengthofparts; currentpart++ )); do
        result+="${parts[$currentpart]}"
        # if current part is not the last portion of the sentence, append a comma
        [ $currentpart -ne $((lengthofparts-1)) ] && result+=", "
    done
    echo "$result"
}

duration=$1
howlong="$(rendertimer "${duration}")"
echo "That took ${howlong} to run."

Upvotes: 1

BeardOverflow
BeardOverflow

Reputation: 1070

Replace

H=$((MM/60))
D=$((H/24))

to

H=$((MM/60%24))
D=$((MM/60/24))

So, the days are the quotient of your total minutes and the hours are the modulus of your total minutes.

Also, your second example is wrong. The desire output for 127932 is That took 1 day, 11 hours, 32 minutes and 12 seconds to run

Upvotes: 2

Related Questions