11

I'm calculating aspect ratio height from x number, in this example I'm using 4:3 ratio, and a width of 800, the result (height) should be 600, but bash is returning 800, and I'm not sure why.

I've tried other languages, most seem to have issues too, php seems to be one of few that work.

PHP (returns 600)

php -r 'echo 800/(4/3);'

Python (returns 800)

python -c "print(800/(4/3))"

bc -l kinda works (returns 600.00000000000000000150)

-l is "Define the standard math library", not to sure what that means, but it seems to get me closer to my goal, but where is the extra 0's and 150 coming from?

echo '800 / (4 / 3)' | bc -l

I'm guessing it's something to do with floating point handling, or truncating the result of 3/4.

Now I could just use php, and call it a day, but seems kinda overkill for a relatively simple calculation. Any idea what's going on here.

Mike Pierce
  • 737
  • 1
  • 6
  • 23
Mint
  • 265
  • 2
  • 8
  • 13
    BTW, Python 2 reached its [End Of Life](https://www.python.org/doc/sunset-python-2/) over 2 years ago. You really should consider upgrading to Python 3... – PM 2Ring Feb 01 '22 at 11:15
  • 5
    Building on PM 2Ring's comment, only Python 2 returns 800 with that command. If you run `python3 -c "print(800/(4/3))"`, it prints `600.0`. – Sylvester Kruin Feb 01 '22 at 21:22
  • 4
    Does this answer your question? [How to do integer & float calculations, in bash or other languages/frameworks?](https://unix.stackexchange.com/questions/40786/how-to-do-integer-float-calculations-in-bash-or-other-languages-frameworks) – Kusalananda Feb 02 '22 at 07:50
  • @PM2Ring yeah I keep forgetting `python` means Python 2 on my system. I've got Python 3 installed, just not the default. – Mint Feb 02 '22 at 19:32
  • @they doesn't directly answer it, but definitely worth a comment as I feel it would be helpful to others. While in part my issue is due to the floats, it's also in part due to the use of parentheses. – Mint Feb 02 '22 at 19:34
  • 1
    The `600.00000000000000000150` is a rounding error, similar to that (1/3)*3 is 0.9999999 and not 1. – Thorbjørn Ravn Andersen Feb 02 '22 at 22:20
  • 1
    @Mint Depending on the tasks and colleagues around, there may be good reasons to adopt Python3 for anything newly written .and. to retain (some spare version) of Python2, too. You may instruct Linux to *interpret* `python` as call for `python3`, and still access the legacy with (then explicit) call `python2`. See [this answer](https://stackoverflow.com/questions/62902303/configure-python3-pointing-to-usr-bin-python3-8-in-linux-mint-after-installing/67269324#67269324) of mine on stackoverflow for an example. – Buttonwood Feb 03 '22 at 15:20
  • You wouldn't get the rounding error if you rephrased as `800 * (3 / 4)`, and you can do it all in integers as `800 * 3 / 4`. – Toby Speight Feb 10 '22 at 15:31

2 Answers2

56

Bash arithmetic is integer only. So 4/3 returns 1. And 800/1 is 800.

If you can control the inputs then you can re-factor and do the multiplication before the division

$ echo $(( 800*3/4 ))
600

Your other examples are also "integer". If, for example, you force python floating point by replace 4 with 4.0 then you get a different answer (Python 3 doesn't need this)

$ python -c "print(800/(4.0/3))"
600.0

bc -l loads the standard math library (with functions like s() for sine, l() for natural logarithm, etc), but more importantly here, sets scale to 20. scale defines how many decimals after the radix to generate in divisions, so 4/3 there will be 1.33333333333333333333 (in effect 133333333333333333333/1e+20), and that explains why you get 600.00000000000000000150.

echo 'scale=1000; 800/(4/3)' | bc

Will get you more precision (without having to load the math library), but you'll never get just 600 there as 4/3 cannot be represented in decimal.

Braiam
  • 35,380
  • 25
  • 108
  • 167
Stephen Harris
  • 42,369
  • 5
  • 94
  • 123
  • Thank you! Just curious, how come `800*3/4` works, like 3/4 is 0.75, which is still a float no? (not integer) – Mint Feb 01 '22 at 01:57
  • 9
    @Mint 800*3/4 calculates first 800*3, then divides it by 4. – peterh Feb 01 '22 at 01:59
  • 4
    Without any (...) it will do standard left-to-right evaluation; so 800*3=2400; 2400/4=600. In comparison 800*(3/4) would return 0 because the 3/4 would be 0, and 800*0 is 0. – Stephen Harris Feb 01 '22 at 02:02
  • Thanks for the explanations, it now makes sense. (I really need to re-do math, I've forgotten everything.) – Mint Feb 01 '22 at 02:05
  • 2
    If one ***really*** want to muck with floats it in bash one can use `printf` with exponents / E-notation as in if one for example want `9 / 4` one can do `printf '%f\n' "$(( 9 * 100 / 4 ))e-2"` :P, – ibuprofen Feb 01 '22 at 02:48
  • 1
    4/3 is an infinitely repeating decimal, so your result is always going to be dependent on how the language implementation handles floating point. – Marc Wilson Feb 01 '22 at 15:17
  • 3
    @Mint Don't forget that computer math is not always the same as real math. See https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html and https://stackoverflow.com/questions/588004/is-floating-point-math-broken – Barmar Feb 01 '22 at 15:46
  • Note that Python's Decimal library will do floating-point, unlimited precision math without the binary rounding errors (trying to convert from base 10 to base 2 and back is where the 150 at the end of the bc output comes from. Thirds aren't a binary fraction.) – Perkins Feb 01 '22 at 20:11
  • @Perkins No, bc does not convert to binary. It does math in plain decimal. That's why it can represent exactly the number `1/5`. Try `bc <<<"scale=1000;1/5"`. However, most other programs (that use double floats) do get somewhat imprecise with `1/5` (which is exactly `0.2` in decimal), try: `python3 -c 'print(format(1/5,".60f"))'`. –  Feb 02 '22 at 07:35
  • @StephenHarris In particular, bc use decimals, yes, but in general the issue is that `1/3` can not be exactly represented in binary (as opposed to decimal). All programs that use doubles show that limit, As a counter example: the number `1/5` is exactly `0,2` in decimal but not so in binary, Try: `python3 -c 'print(format(1/5,".60f"))'` for example. –  Feb 02 '22 at 07:40
1

Shell

In general, in shells, you need an external program to perform general math.

$ bc -l <<<'800/(4/3)'
$ bc -l <<<'scale=200;800/(4/3)'
$ awk 'BEGIN{print 800/(4/3)}'
$ awk 'BEGIN{printf "%.60f\n",800/(4/3)}'
$ python3 -c 'print(800/(4/3))'
$ python3 -c 'print(format(800/(4/3),".60f"))'

Why

It's a common result of two issues:

  • Individual operation precision.
  • Order of operations.

Individual precision

If the operations are carried out as integer, the value of (4/3) is 1.

Even in Python (well, Python 2 as the / means "Integer Division" there):

$ python2 -c 'print(4/3)'
1

But not in Python3:

$ python3 -c 'print(4/3)'
1.3333333333333333

That is why a 800/(4/3) becomes 800/1 and result in 800. (in Python2)

$ python2 -c 'print(800/(4/3))'
800

$ python3 -c 'print(800/(4/3))'
600.0

Bash (as most shells) is similar to python2 (integer):

$ bash -c 'echo (800/(4/3))'
800

Order

You can re-order the math to avoid the integer conversion problem.

$ python2 -c 'print(800*3/4)'
600

Or tell python to use floats:

$ python2 -c 'print(800/(float(4)/3))'
600.0

Limit

But don't fall into the illusion that such number is exact. It certainly may look like that:

python2 -c 'print(format(800/(4.0/3),".80f"))'
600.00000000000000000000000000000000000000000000000000000000000000000000000000000000

And a 2^-50 (or 55) value might be exactly represented in binary:

$ python2 -c 'print(format(2**-50,".80f"))'
0.00000000000000088817841970012523233890533447265625000000000000000000000000000000

$ python2 -c 'print(format(2**-55,".80f"))'
0.00000000000000002775557561562891351059079170227050781250000000000000000000000000

But as soon as you mix integers and floats (or do general numeric math) you are bound to get "out of limits" results:

$ python2 -c 'print(format(1 + 2**-50,".80f"))'
1.00000000000000088817841970012523233890533447265625000000000000000000000000000000

$ python2 -c 'print(format(1 + 2**-55,".80f"))'
1.00000000000000000000000000000000000000000000000000000000000000000000000000000000

In general: Numbers with more than 53 binary digits get truncated in double precision floats.