Floating Point Math in Bash

When you think about it, it's surprising how many programming tasks don't require the use of floating point numbers. If you're an embedded systems programmer, you'd probably get fired for using "double" in a C program. If you write PHP or JavaScript, quick, do they even support floating point? One language that doesn't support it is Bash, but let's not let that stop us...

The obvious candidate for adding floating point capabilities to bash is bc. bc is, to quote the man page:

An arbitrary precision calculator language
As an aside, take note of the last word in that quote: "language". That's right bc is actually a programming language, it contains if statements and while loops among others. I say as an aside because it's largely irrelevant to what we want to do today, not completely irrelevant but largely.

To use bc in our bash scripts we'll package it up into a couple of functions:

    float_eval EXPRESSION
and
    float_cond CONDITIONAL-EXPRESSION
Both functions expect a single floating point expression, float_eval writes the result of the expression evaluation to standard out, float_cond assumes the expression is a conditional expression and sets the return/status code to zero if the expression is true and one if it's false.

Usage is quite simple:

  float_eval '12.0 / 3.0'
  if float_cond '10.0 > 9.0'; then
    echo 'As expected, 10.0 is greater than 9.0'
  fi
  a=12.0
  b=3.0
  c=$(float_eval "$a / $b")

The code for the functions follows:

#!/bin/bash
#
# Floating point number functions.

#####################################################################
# Default scale used by float functions.

float_scale=2


#####################################################################
# Evaluate a floating point number expression.

function float_eval()
{
    local stat=0
    local result=0.0
    if [[ $# -gt 0 ]]; then
        result=$(echo "scale=$float_scale; $*" | bc -q 2>/dev/null)
        stat=$?
        if [[ $stat -eq 0  &&  -z "$result" ]]; then stat=1; fi
    fi
    echo $result
    return $stat
}


#####################################################################
# Evaluate a floating point number conditional expression.

function float_cond()
{
    local cond=0
    if [[ $# -gt 0 ]]; then
        cond=$(echo "$*" | bc -q 2>/dev/null)
        if [[ -z "$cond" ]]; then cond=0; fi
        if [[ "$cond" != 0  &&  "$cond" != 1 ]]; then cond=0; fi
    fi
    local stat=$((cond == 0))
    return $stat
}


# Test code if invoked directly.
if [[ $(basename $0 .sh) == 'float' ]]; then
    # Use command line arguments if there are any.
    if [[ $# -gt 0 ]]; then
        echo $(float_eval $*)
    else
        # Turn off pathname expansion so * doesn't get expanded
        set -f
        e="12.5 / 3.2"
        echo $e is $(float_eval "$e")
        e="100.4 / 4.2 + 3.2 * 6.5"
        echo $e is $(float_eval "$e")
        if float_cond '10.0 > 9.3'; then
            echo "10.0 is greater than 9.3"
        fi
        if float_cond '10.0 < 9.3'; then
            echo "Oops"
        else
            echo "10.0 is not less than 9.3"
        fi
        a=12.0
        b=3.0
        c=$(float_eval "$a / $b")
        echo "$a / $b" is $c
        set +f
    fi
fi

# vim: tabstop=4: shiftwidth=4: noexpandtab:
# kate: tab-width 4; indent-width 4; replace-tabs false;

The work of the functions is done by feeding the arguments to bc:

   result=$(echo "scale=$float_scale; $*" | bc -q 2>/dev/null)
By default bc outputs its result with no digits to the right of the decimal point and without a decimal point. To change this you have to change one of bc's builtin variables: scale. This is where the "language" features of bc are relevant, in bc as in C statements are separated by semi-colons. We set bc's scale variable by preceding the expression that we pass to bc with scale=$float_scale;. This sets the scale in bc to the value of the bash global variable float_scale, which is by default set to two (near the top of the script).

The main gotcha here has to do with the fact that "*", "<", and ">" have other meanings in bash. You can eliminate the problem of "<" and ">" by quoting your expressions, but this only works with "*" if you use single quotes and that would mean you couldn't include bash variables in the expression. The other option is to bracket your code with "set -f" and "set +f" to turn off pathname/wildcard expansion.

If you save the script as float.sh and run it directly it will execute the test code at the bottom:

  $ sh float.sh
  12.5 / 3.2 is 3.90
  100.4 / 4.2 + 3.2 * 6.5 is 44.70
  10.0 is greater than 9.3
  10.0 is not less than 9.3
  12.0 / 3.0 is 4.00

The one unaswered question you may have is: "and why would I want to do this?" Next time around I'll show you one place you can put this to real world use.

Load Disqus comments