4

I read topics about how to get bc to print the first zero, but that is not exactly what I want. I want more...

I want a function that returns floating point numbers with eight decimal digits. I am open to any solutions, using awk or whatever to be fair. An example will illustrate what I mean:

hypothenuse () {
        local a=${1}
        local b=${2}
        echo "This is a ${a} and b ${b}"
        local x=`echo "scale=8; $a^2" | bc -l`
        local y=`echo "scale=8; $b^2" | bc -l`
        echo "This is x ${x} and y ${y}"
#       local sum=`awk -v k=$x -v k=$y 'BEGIN {print (k + l)}'`
#       echo "This is sum ${sum}"
        local c=`echo "scale=8; sqrt($a^2 + $b^2)" | bc -l`
        echo "This is c ${c}"
}

Sometime, a and b are 0.00000000, and I need to keep all these 0s when c is returned. Currently, when this happens, this code give back the following output:

This is a 0.00000000 and b 0.00000000
This is x 0 and y 0
This is c 0

And I would like it to print

This is a 0.00000000 and b 0.00000000
This is x 0.00000000 and y 0.00000000
This is c 0.00000000

Help will be much appreciated!

10

In bc, the solution is to divide by 1:

$ bc -l <<<"scale=8; x=25*20; x"
500

$ bc -l <<<"scale=8; x=25*20; x/1"
500.00000000

So, your script could be like this:

hypothenuse () {
        local a b c x y
        a=${1}; b=${2}
        echo "This is a ${a} and b ${b}"
        x=$(echo "scale=8; $a^2/1" | bc -l)
        y=$(echo "scale=8; $b^2/1" | bc -l)
        echo "This is x ${x} and y ${y}"
#       local sum=`awk -v k=$x -v k=$y 'BEGIN {print (k + l)}'`
#       echo "This is sum ${sum}"
        c=$(echo "scale=8; sqrt($a^2 + $b^2)/1" | bc -l)
        echo "This is c ${c}"
}

I strongly suggest that you use $(…) instead of `…`.

But even that fails with values of 0.

The best solution is to let the scale of bc be 20 (from bc -l), make all the math required in one call to bc and then format the output as required with printf. Yes printf could format floats.

Assuming bash

hypothenuse () {  local a b c x y
                  a=${1:-0} b=${2:-0}
                  read -d '' x y c < <(
                  bc -l <<<"a=$a; b=$b; x=a^2; y=b^2; c=sqrt(x+y); x;y;c"
                  )

                  printf 'This is a %14.8f and b %14.8f\n' "$a" "$b"
                  printf 'This is x %14.8f and y %14.8f\n' "$x" "$y"
                  printf 'This is c %14.8f             \n' "$c"
               }
| improve this answer | |
  • 1
    Seems better than my solution, since it doesn't require an additional command call. Wish bc didn't require a division by 1 though. – Amessihel Jan 7 at 19:21
  • Thank you very much, both for the fast answer, and the precision of it! The only thing is, what does -0 do in a=${1:-0} ? – Clement S Jan 8 at 9:59
  • 1
    @ClementSoulie, -0 stands for "affect 0 if $1 is empty" – Amessihel Jan 8 at 10:25
  • Awesome, Thanks !! – Clement S Jan 8 at 11:16
6

You can externalize formatting this way, using printf:

printf "%0.8f" ${x}

Example:

x=3
printf "%0.8f\n" ${x}
3.00000000

Note: printf output depends on your locale settings.

| improve this answer | |
3

Just use awk:

$ cat tst.sh
#!/bin/env bash

hypothenuse() {
    awk -v a="$1" -v b="$2" 'BEGIN {
        printf "This is a %0.8f and b %0.8f\n", a, b
        c = sqrt(a^2 + b^2)
        printf "This is c %0.8f\n", c
    }'
}

hypothenuse "$@"

$ ./tst.sh 0 0
This is a 0.00000000 and b 0.00000000
This is c 0.00000000

$ ./tst.sh 17.12 23.567
This is a 17.12000000 and b 23.56700000
This is c 29.12898709
| improve this answer | |
1

Your question is a little ambiguous.  You say “… I need to keep all these 0s …”.  Others have told you how to get output with eight decimal digits.  But, do you want to determine dynamically how many decimal digits the input values a and b already have (so you can output c with the same precision)?

You can do this internally in bash:

a_digits=0
if [[ $a =~ \.([0-9]*)$ ]]
then
        a_digits="${#BASH_REMATCH[1]}"
fi
b_digits=0
if [[ $b =~ \.([0-9]*)$ ]]
then
        b_digits="${#BASH_REMATCH[1]}"
fi

=~ compares a string to a regular expression.  The regular expression \.([0-9]*)$ matches a . followed by a sequence of digits at the end of the string, with the sequence of digits in a group.  BASH_REMATCH is an array that contains the match for the entire regex and the capture groups (if any).  So, if a is 3.1416, BASH_REMATCH[1] is 1416.  Then a_digits gets the length of that, which is 4.

If you don’t have bash, you can do the same thing with the expr command:

a_digits=$( expr length '(' "$a" : '.*\.\([0-9]*\)$' ')' )
b_digits=$( expr length '(' "$b" : '.*\.\([0-9]*\)$' ')' )

which is the exact same logic.

You can then get the larger (maximum) value:

if [ "$a_digits" -gt "$b_digits" ]
then
        digits="$a_digits"
else
        digits="$b_digits"
fi

or the reverse, to get the smaller (minimum) value.

You can then use this dynamically determined precision in the other solutions:

printf "%0.${digits}f\n" "$c"

or

printf '%0.*f\n' "$digits" "$c"

or

…scale=$digits; …
| improve this answer | |

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.