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.

Mint
  • 265
  • 2
  • 8

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,991
  • 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 8003/4 calculates first 8003, then divides it by 4. – peterh Feb 01 '22 at 01:59
  • 4
    Without any (...) it will do standard left-to-right evaluation; so 8003=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.