Floating Point Math in Bash

 in

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.

______________________

Mitch Frazier is an Associate Editor for Linux Journal.

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Code License?

JadeStar's picture

Mitch, Thanks for the awesome floating point functions!

May I reuse this code within other scripts and projects? Which license does it carry (if any)? Thanks, JadeStar.

GPLv2

Mitch Frazier's picture

Yes, you may reuse it. License == GPLv2.

Mitch Frazier is an Associate Editor for Linux Journal.

A concise method, maybe?

Glen M.'s picture

This is actually my first bash script and I'm not an experienced programmer, so feel free to criticize. Interestingly, since I'm not used to programming in bash, I found a use for floating point math in my first bash script, because I didn't know it wasn't natively supported. Nevertheless, I like my solution, so far, but since I'm not my own best critic when it comes to coding, please feel free to give me some pointers. Anyway, below is the essence of the code. I'm using my floats as a counter variable, so I end up using bc both in a condition and in a calculation. (oh and thanks Mitch for giving me the essentials, so that I could strike out on my own.)

#!/bin/bash

$decrement=.1
$upperLimit=15
$lowerLimit=10
#.........omitted code.........

while (( `echo "$upperLimit >= $lowerLimit" | bc` ))
do
echo "$upperLimit" #...........altered code.........
upperLimit=`echo "$upperLimit - $decrement" | bc`
done

exit 0

Presumably not your final version

Mitch Frazier's picture

I assume this is an early version since
the following statements are not even syntactically valid:

$decrement=.1
$upperLimit=15
$lowerLimit=10

(You don't use the $ when assigning variables).

But beyond that it looks like you might be getting there.

Mitch Frazier is an Associate Editor for Linux Journal.

I used this techique too!

DAR's picture

I actually used this exact technique (of piping a floating point comparison into bc) pretty recently (and before reading about it here). I wanted a cron script that would monitor the CPU temp on my home server and notify me if it went too high. So I wrote a script that grabbed the output from "sensors" (from the lm-sensors package), "grep"ped for the line containing CPU temp, "cut" away all but the raw temperature value, and then piped a simple floating point comparison string into bc to compare the CPU temp vs. the threshold temp. If bc returns a 1 (true) result, I have the script spit out a message, which cron then emails to root (which is aliased to my email address). Nice little unix-esque solution of assembling a bunch of small, simple things together so as to achieve a more complex piece of behavior.

script didt run

Sanjay VS's picture

The script simply didnt run.

ssanjayv@localhost:~/bin/my_scripts> sh float.sh
: command not found
: command not found
: command not found
: command not found
: command not found
'loat.sh: line 14: syntax error near unexpected token `
'loat.sh: line 14: `function float_eval()

any idea whats causing the problem ??

Bash??

Mitch Frazier's picture

Are you sure your shell is bash? Try running it via "sh -x float.sh".

Mitch Frazier is an Associate Editor for Linux Journal.

Can do. Should do?

Paddy3118's picture

If you have to do the odd bit of floating point then you might look at writing the whole script in some scripting language that supports floating point natively. It just seems error prone and fragile to pass floats as strings between bash and bc.
AWK, TCL, Python, Perl, Lua, Pike, Ruby, ... These might all be better solutions, and if your running on a striped-down, ultra small, unix platform, then it might be best to include something small but with more features like Pike or Lua rather than bc.

- Paddy.

sleep

dm's picture

One place I'd want them is for "sleep".

Occasionally it can be useful to sleep for a fraction of a second
(for instance when you are automatically terminating spawned processes
if they run for more than pre-specified amount of time...
see timeout.sh script in Bash examples.)

Floating points are supported by Korn shell (Bash == Korn + readline)
For instance:
echo $((1.2 + 2.9))
4.1

Why reinvent the ol' good Korn?
:-)

Also ZSH

Zealot S. Hell's picture

Zsh does floating point also. I use zsh over korn since it has better line editor, cycle-tab completion (cmd.exe style), and many other nice features. But yeah, why anybody uses bash is beyond me.

Webinar
One Click, Universal Protection: Implementing Centralized Security Policies on Linux Systems

As Linux continues to play an ever increasing role in corporate data centers and institutions, ensuring the integrity and protection of these systems must be a priority. With 60% of the world's websites and an increasing share of organization's mission-critical workloads running on Linux, failing to stop malware and other advanced threats on Linux can increasingly impact an organization's reputation and bottom line.

Learn More

Sponsored by Bit9

Webinar
Linux Backup and Recovery Webinar

Most companies incorporate backup procedures for critical data, which can be restored quickly if a loss occurs. However, fewer companies are prepared for catastrophic system failures, in which they lose all data, the entire operating system, applications, settings, patches and more, reducing their system(s) to “bare metal.” After all, before data can be restored to a system, there must be a system to restore it to.

In this one hour webinar, learn how to enhance your existing backup strategies for better disaster recovery preparedness using Storix System Backup Administrator (SBAdmin), a highly flexible bare-metal recovery solution for UNIX and Linux systems.

Learn More

Sponsored by Storix