1
\$\begingroup\$

This is a simple script to convert a given unit of time to other units of time. I wrote this mainly to practice writing scripts that incorporate positional parameters. It actually turned out better than I expected. It tests if the number of parameters is correct. It tests if the content of the parameter is valid. Any invalid options triggers a usage help function before exiting.

The script can convert seconds, minutes, hours, days, weeks, months and years. But it attempts to only display conversions that are most applicable or relevant to the information fed to it. For instance converting 600 minutes returns values for the corresponding seconds, hours and days but it does not return any values for weeks, months or years. Those values are filtered out using regular expressions. It supports using numbers with decimals in the input string. Up to 6 decimal places are supported. Any more than 6 and it defaults back to 2.

I'd like to know if there is anything I overlooked. Or maybe other possible arguments I could implement. Converting milliseconds for instance Or perhaps I could implement a setting that doesn't filter results or limit the number of decimals places. Anything I could do better. Redundancies I could remove. Honestly I'd appreciate the harshest most scathing, unforgiving review anyone has to offer. Rip into me please. :)

#!/usr/local/bin/bash

usage() {
    echo "You must specify a unit of time; e.g. 60 --minutes"
    echo "Examples:"
    printf '%b\n' "\t$0 -m 60"
    printf '%b\n' "\t$0 39.5 --hours"
    exit 1
}

declare ss=
declare mm=
declare hh=
declare dd=
declare ww=
declare nn=
declare yy=

main() {

    [[ $# -eq 2 ]] || usage

    while [[ "$1" ]]; do
        case "$1" in

            -s|-second|-seconds|--second|--seconds)
                ss=1            
            ;;

            -m|-minute|-minutes|--minute|--minutes)
                mm=1
            ;;

            -h|-hour|-hours|--hour|--hours)
                hh=1
            ;;

            -d|-day|-days|--day|--days)
                dd=1
            ;;

            -w|-week|-weeks|--week|--weeks)
                ww=1
            ;;

            -t|-month|-months|--month|--months)
                nn=1
            ;;

            -y|-year|-years|--year|--years)
                yy=1
            ;;

            *)      
                [[ "$1" =~ ^[0-9]+\.{0,1}[0-9]*$ ]] || usage
                int="$1"

                ## determine number of decimal places 
                if [[ $int =~ \. ]] ; then 
                    n="${int#*.}"; 
                    n="${#n}";
                    [[ $n -lt 2 ]] && n=2
                    [[ $n -gt 6 ]] && n=6
                else
                    n=2
                fi


            ;;  
        esac
        shift
    done

    ## values of each unit in seconds
    minute=60
    hour=3600
    day=86400
    week=604800
    month=2629746
    year=31536000

    [[ $ss ]] && s="$int"
    [[ $mm ]] && s=$(bc -ql <<< "scale=$n; ($int*$minute)");
    [[ $hh ]] && s=$(bc -ql <<< "scale=$n; ($int*$hour)");
    [[ $dd ]] && s=$(bc -ql <<< "scale=$n; ($int*$day)");
    [[ $ww ]] && s=$(bc -ql <<< "scale=$n; ($int*$week)");
    [[ $nn ]] && s=$(bc -ql <<< "scale=$n; ($int*$month)");
    [[ $yy ]] && s=$(bc -ql <<< "scale=$n; ($int*$year)");

    results() {

        LC_ALL=en_US.UTF-8

        secs=$(printf "%'.${n}f" "$s")
        mins=$(printf "%'.${n}f\n" $(bc -ql <<< "scale=$n; ($s/$minute)"));
        hours=$(printf "%'.${n}f\n" $(bc -ql <<< "scale=$n; ($s/$hour)")); 
        days=$(printf "%'.${n}f\n" $(bc -ql <<< "scale=$n; ($s/$day)"));
        weeks=$(printf "%'.${n}f\n" $(bc -ql <<< "scale=$n; ($s/$week)"));
        months=$(printf "%'.${n}f\n" $(bc -ql <<< "scale=$n; ($s/$month)"));
        years=$(printf "%'.${n}f\n" $(bc -ql <<< "scale=$n; ($s/$year)"));

        echo "Seconds:  $secs"
        echo "Minutes:  $mins"
        echo "Hours:    $hours"
        echo "Days:     $days"
        echo "Weeks:    $weeks"
        echo "Months:   $months"
        echo "Years:    $years"

    }

    ## regular expression to remove values that equal 0
    regex='^[A-Z]\w+:\s+0*\.*0$'

    while read; do

        ## strip trailing 0's 
        if [[ $REPLY =~ [0-9]*\.0+[^1-9]* ]]; then 
            REPLY=${REPLY%.*}
        fi

        echo $REPLY 
    done < <(results) | grep -Ev $regex | column -t

} 

main "$@"
\$\endgroup\$
  • 2
    \$\begingroup\$ it's nonsensical to say that 28 days is 0.91 months. Sometimes 28 days is exactly 1 month; sometimes 29 days is exactly 1 month. \$\endgroup\$ – glenn jackman Sep 21 '17 at 21:07
  • \$\begingroup\$ It's logical from the standpoint of 1 month representing a fraction of time rather than the current month of September. How would you suggest I go about it? \$\endgroup\$ – I0_ol Sep 22 '17 at 0:25
  • \$\begingroup\$ Since there is not a fixed number of seconds per month, I would just not convert to months. Weeks perhaps. Or use the average number of days per month, leap days included: (365*400 + 100 - 4 + 1) / 400 / 12 = 30.4368 -> implies 365.2425 days per year, or 31556952 seconds per year \$\endgroup\$ – glenn jackman Sep 22 '17 at 1:42
  • \$\begingroup\$ Calendrical calculations are always tricky, even if you assume there's always 60 seconds in a minute, or 24 hours in a day. \$\endgroup\$ – Toby Speight Sep 22 '17 at 9:53
1
\$\begingroup\$

Honestly I'd appreciate the harshest most scathing, unforgiving review anyone has to offer. Rip into me please. :)

Oh yes. Evil laughter.

Or perhaps I could implement a setting that doesn't filter results or limit the number of decimals places.

I think it would be better to always display all values. No magic. Consistent, predictable output. Incidentally, this will simplify the implementation a lot, as you will be able to eliminate all the ss, mm, and similar flag variables.

Regexes

This regex to match a number could be improved:

^[0-9]+\.{0,1}[0-9]*$ 
  • \.{0,1} can be written shorter as \.?
  • Although it doesn't break the script, this will match a number ending with a dot. It would be more accurately written as ^[0-9]+(\.[0-9]+)?$
  • It also accepts numbers starting with zeros. So to make it match numbers properly would take some more work.

Pointless semicolons

The semicolons at the end of lines are pointless:

n="${int#*.}"; 
n="${#n}";

The same goes for other similar lines in the script.

Also, at this point, since the content of int and n are already verified, the double-quotes are not necessary.

Conditionals

When n is less than 2, the second line will be evaluated unnecessarily:

[[ $n -lt 2 ]] && n=2
[[ $n -gt 6 ]] && n=6

It would be better to write this with proper conditionals:

if [[ $n -lt 2 ]]; then
    n=2
elif [[ $n -gt 6 ]]; then
    n=6
fi

Quoting

You should double-quote command line arguments that may contain special characters, for example instead of grep -Ev $regex you should write grep -Ev "$regex".

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.