2

In bash, how can I modify a float so that the part coming before the dot is having at least two digits?

I want to make numbers in Column A be displayed as in Column B:

A (Current)   B (Desired)
-----         ------
8.456         08.456
4.19          04.19
3.5           03.5

I did a lot of searches, but most of what I found talked about how to display numbers with N decimals (e.g. 17.7647 to 17.76) which was of no help.

Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
nino
  • 554
  • 2
  • 7
  • 20
  • bash does not have floating point arithmetic, only integer arithmetic. You could revert for the formatting to external tools (such as Perl or Ruby), or code your own formatting routine. – user1934428 Apr 29 '21 at 07:46

3 Answers3

5

Here is a utility function in bash to achieve this using printf:

fpad() {
   local n="${1?needs an argument}"
   [[ $n == [0-9].* ]] && echo "0$n" || echo "$n"
}

Use it as:

fpad "8.456"
08.456

fpad "4.19"
04.19

fpad "3.5"
03.5

fpad "13.25"
13.25

fpad "1325"
1325

Using Glen's suggestion with read:

fpad() {
   local n="${1?needs an argument}"
   local num frac
   IFS=. read num frac <<< "$n"
   printf '%02d.%d\n' "$num" "$frac"
}
anubhava
  • 761,203
  • 64
  • 569
  • 643
  • 2
    Great answer. One edge case: `fpad 1234` outputs `1234.1234` – glenn jackman Apr 28 '21 at 19:29
  • 1
    So, manual utilities seem to be the only way. Thanks. That works perfectly fine. I just have to add a line to check if the argument is really a float - just in case. – nino Apr 28 '21 at 19:30
  • 3
    I'd suggest `fpad() { local n="${1?needs an argument}"; local num frac; IFS=. read num frac <<<"$n"; printf '%02d.%d\n' "$num" "$frac"; }` – glenn jackman Apr 28 '21 at 19:31
  • 1
    @anubhava Sure it did. Appreciate your help. – nino Apr 28 '21 at 19:43
2

You can expand a bit and take the great answer by @anubhava a bit further by allowing the width on both the real-part and fractional-part to be set and fully validating the input and handling any errors in your function. The approach though is the same, using the printf format string is key.

For example your function can take the floating-point number as the first argument, the width of the real-part as the second, and optionally take the width of the fractional-part as the third, e.g.

#!/bin/bash

formatfloat () {
    [ -z "$1" -o -z "$2" ] && { ## validate at least 2 arguments given
        printf "error: formatfloat() insufficient arguments.\n" >&2
        printf "usage: formatfloat() float width_real [width_fract]\n" >&2
        return 1;
    }
    [[ $1 =~ ^-*[[:digit:]]*[.][[:digit:]]* ]] || { ## validate float
        printf "error: formatfloat() not a valid floating point number.\n" >&2
        return 1
    }
    [ "$2" -eq "$2" > /dev/null ] || {  ## validate width is an integer
        printf "error: formatfloat() width_real not an integer.\n" >&2
        return 1
    }
    local realpart="${1%.*}"    ## parameter expansion separates real/fraction parts
    local fractpart="${1#*.}"
    local width_real="$2"
    
    if [ -n "$3" ]; then                    ## if fractional part width given
        [ "$3" -eq "$3" > /dev/null ] || {  ## validate it is an integer
            printf "error: formatfloat() width_fract not an integer.\n" >&2
            return 1
        }
        local width_fract="$3"  ## output setting both widths
        local fppad="$((width_fract - ${#fractpart}))"  ## needed fract part rt-pad
        [ "$fppad" -le 0 ] && { ## no padding needed
            printf "%0*d.%d\n" $width_real $realpart $fractpart
            return 0
        }
        for ((i = 0; i < fppad; i++)); do   ## add padding to fract part
            fractpart="${fractpart}0"
        done
        printf "%0*d.%*d\n" $width_real $realpart $width_fract $fractpart
    else                        ## output setting real-part width
        printf "%0*d.%d\n" $width_real $realpart $fractpart
    fi
    
    return 0
}

A short example with your data could be:

printf "A (Current)   B (Desired)\n-----         ------\n"

for n in 8.456 4.19 3.5; do
    printf "%-14s%s\n" "$n" "$(formatfloat "$n" 2)"
done

Output

$ bash formatfloat.sh
A (Current)   B (Desired)
-----         ------
8.456         08.456
4.19          04.19
3.5           03.5

Setting Width of Both


for n in 8.456 4.19 3.5; do
    printf "%-14s%s\n" "$n" "$(formatfloat "$n" 2 3)"
done

Output

$ bash formatfloat.sh
A (Current)   B (Desired)
-----         ------
8.456         08.456
4.19          04.190
3.5           03.500
David C. Rankin
  • 81,885
  • 6
  • 58
  • 85
  • This is a much better one considering different conditions. Just made little teaks and am using it now. – nino Apr 29 '21 at 03:48
  • 1
    I liked it too. I was writing in when @anubhava answered, and he is usually right on the money. It just took a little longer to finish as I was thinking about letting your set the width of both. Good to learn from all the answers you get. Good luck with your scripting. – David C. Rankin Apr 29 '21 at 03:54
  • I modified the `no padding needed` section so as to also use `$3` as the limit for the fractpart: `[ "$fppad" -lt 0 ] && { fractpart=${fractpart::$width_fract} ;}` – nino Apr 29 '21 at 05:46
  • That works, interesting way to to print whatever the `fractpart` is. Using the string-index *parameter expansion* with zero `offset` and `length` greater than length of `fractpart` will just print the number of chars in `fractpart` `:)` – David C. Rankin Apr 29 '21 at 05:56
  • So, this way, the `$2` and `$3` passed to the function will return a float whose **realpart** and **fracpart** are $2 and $3 digits in length respectively (for the realpart it will add a leading zero if needed, and for the fracpart it will either add a trailing zero or cut it to the desired length). Perfect! – nino Apr 29 '21 at 06:23
  • Bash is pretty capable and cool as far as all of the string manipulations it lets you do. While it isn't the fastest around, as long as you only have a few hundred up to a thousand conversion or so, it is plenty fast enough. You get to 10,000 or more "things" you need to manipulate and then you start looking at `awk` or just write something in C, etc.. I feel for those folks in the 80's that has nothing but `/bin/sh` to work with... – David C. Rankin Apr 29 '21 at 06:29
0

how can I modify a float so that the part coming before the dot is having at least two digits.

There is no utility for that - write your own code for doing that. You would:

  • count the number of digits before comma
  • if the number of digits is less then 2
    • if the number of digits is one
      • add one leading zero
    • if zero
      • add two leading zeros

I could also see a sed with two expressions that each one matches with a regex the number of digits before a comma.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • That seems doable - separating, manipulating and joining back. But ... is it the only way? – nino Apr 28 '21 at 19:10
  • @nino : Actually, the requirement is a bit unusual. Mostly, when tabulating floats, we want to either line up the decimal point of the numbers, or have all the numbers the same width, or something like this. In your case, if I imagine to have the numbers `5.1`, `14.24` and `105.35789` printed one below the other, I don't see any advantage of having exactly the first of those being preceded by a zero. So there is no surprise that no standard utility offers this formating. – user1934428 Apr 29 '21 at 07:57
  • @user1934428 I agree, but the reason I want to do that is consistency, but for aesthetics or better readability the floats can be lined up based on their dots as you mentioned above. – nino Apr 29 '21 at 11:18