3

I hate decimal numbers. For 1.005 I don't get the result I expect with the following code.

#!/usr/bin/perl -w

use strict;
use POSIX qw(floor);

my $num = (1.005 * 100) + 0.5;
print $num . "\n";          # 101
print floor($num) . "\n";   # 100
print int($num) . "\n";     # 100

For 2.005 and 3.005 it works fine.

With this ugly "hack" I get the expected result.

#!/usr/bin/perl -w

use strict;
use POSIX qw(floor);

my $num = (1.005 * 100) + 0.5;
$num = "$num";
print $num . "\n";          # 101
print floor($num) . "\n";   # 101
print int($num) . "\n";     # 101

What is the correct way to do this?

Orbling
  • 20,413
  • 3
  • 53
  • 64
Johan Soderberg
  • 2,650
  • 1
  • 15
  • 12
  • 4
    What Every Computer Scientist Should Know About Floating-Point Arithmetic: http://docs.sun.com/source/806-3568/ncg_goldberg.html. You might try using pairs of integers as rational numbers instead of this. – nlucaroni Dec 06 '10 at 21:24
  • 2
    You are actually working with floating-point numbers, not decimal numbers as that term is usually defined in architecture theory. – cdhowie Dec 06 '10 at 21:24
  • Yeah, I know decimal numbers aren't exact. I call them decimal numbers even though the correct term may be floating-point numbers. English isn't my main language and I don't write in English very often. Bare with me. ;) – Johan Soderberg Dec 06 '10 at 21:58

2 Answers2

3

floor() is not for rounding, it goes down to the nearest integer.

See this old post: How do you round a floating point number in Perl?

Community
  • 1
  • 1
Orbling
  • 20,413
  • 3
  • 53
  • 64
  • The point was to go down to the nearest integer. 1.005 * 100 = 100.5. 100.5 + 0.5 = 101. I expected floor(101) to return 101 and not 100. I know decimal numbers aren't exact, and a common trick is to add 0.5 and then truncate. – Johan Soderberg Dec 06 '10 at 21:46
  • How did you get your comment on multiple lines? Enter sends the comment for me. ;) Anyway, round seems to work so I'll use that. Thanks! – Johan Soderberg Dec 06 '10 at 21:47
  • @Johan: Regrettably floats are highly perilous with certain numbers. `100.999999` may be as near as it can get to `101`, but `floor(100.999999)` is technically `100`. :-/ – Orbling Dec 06 '10 at 21:51
  • Yeah, I know decimal numbers aren't exact. Hmmz, after more thinking I'm not sure I can rely on round either. What if 1.005 * 100 (on some other number) is 100.4999999999, then round will fail too. Is there any way to get exact numbers in Perl? Like there is in Python. – Johan Soderberg Dec 06 '10 at 21:56
  • Then your interpretation of what you are doing and expect is wrong. `(100 * 1.005) = 100.4999999999999`; and adding `0.5` becomes `100.9999999999999857`. So, `floor` is doing exactly what you want. You may want to add `0.500005`, but you should read that article and follow any recommendations they make. – nlucaroni Dec 06 '10 at 21:59
  • 1
    BigFloat or BigRat (for rational numbers, as I mentioned previously). – nlucaroni Dec 06 '10 at 22:01
  • You are right on the rounding score, very awkward with fixed precision numbers. Have a look at the `Math::BigFloat` module: http://perldoc.perl.org/Math/BigFloat.html – Orbling Dec 06 '10 at 22:02
  • @nlucaroni: Yeah, that's probably what's happening. The article is way too long to read for this problem. I usually don't work with numbers that needs to be this exact. It's just an extra check I added that failed because the calculated value didn't match the field. For now I use $num = "$num", if the check fails again I might have to re-think the solution. Unless someone can give me a short version that works. ;) – Johan Soderberg Dec 06 '10 at 22:04
  • my $x = Math::BigFloat->new(1.005); $x = $x * 100; $x->ffround(0); print $x->bstr() . "\n"; Displays 100. Seems like round in BigFloat also use the same method mentioned in "How do you round a floating point number in Perl?". my $y = Math::BigFloat->new(1.005); $y->bmul(100); $y->ffround(0); print $y->bstr() . "\n"; Also prints 100. Am I missing something? – Johan Soderberg Dec 06 '10 at 22:23
  • Try creating the `100` as a `BigFloat` too: `my $century = Math::BigFloat->new('100');` Perhaps also put quotes around the initial number, `my $y = Math::BigFloat('1.005');` – Orbling Dec 06 '10 at 22:27
  • print "-----\n"; my $y = Math::BigFloat->new('1.005'); my $z = Math::BigFloat->new(100); $y->bmul($z); $y->ffround(0); print $y->bstr() . "\n"; It prints "100". I think the problem there is the same as in the other question. It rounds to the nearest even integer (for 0.5). Seems like I should be able to set rounding method using round_method (for BigFloat), but I can't find a list of valid $round_mode. Tried "nearest" as $round_mode and it seemed to work. Now I just need to verify that it's correct. – Johan Soderberg Dec 06 '10 at 22:39
  • my $x = Math::BigFloat->new(1.005); $x = $x * 100; $x->ffround(0, "nearest"); print $x->bstr() . "\n"; This displays 101, but can I trust this code? ;) – Johan Soderberg Dec 06 '10 at 22:40
  • LOL, well as the whole idea with the arbitrary precision libraries is they don't lose accuracy, one would hope so! ;-) – Orbling Dec 06 '10 at 22:42
  • Hehe. :) Do you know if "nearest" is a valid rounding mode that rounds to the nearest integer? That's the only part I'm worried about now since I just guessed the name of that method. Haven't read about it anywhere. – Johan Soderberg Dec 06 '10 at 22:45
  • 3
    You guys **REALLY** need to understand the IEEE “round towards even” floating-point rule, and why that’s what gets used. `perl -e 'printf "%.0f ", $_+.5 for -10 .. +10` produces the sequence `-10 -8 -8 -6 -6 -4 -4 -2 -2 -0 0 2 2 4 4 6 6 8 8 10 10`. HTH! – tchrist Dec 06 '10 at 23:15
  • @tchrist: That's what the +0.5 and floor was supposed to compensate. Then it has nothing to do with rounding. If the rounding method "nearest" works that should be good (for BigFloat). The default rounding method rounds towards even so that can't be used. – Johan Soderberg Dec 06 '10 at 23:48
  • 2
    @Johan: Round toward even is mathematically sound. Always rounding 5’s up is unsound and biased. Then you have five digits going up and four going down. That’s just wrong. – tchrist Dec 06 '10 at 23:50
  • @tchrist: I do understand IEEE float spec, have done so for the last 15 or so years ta. I do not however agree it is more mathematically sound. When rounding, the object is to get to the nearest integer, which means minimizing the error. Round towards even introduces a ±1 (open) error, round to nearest is +0.5 (closed) error. Yes there is slight bias towards the higher number, but the overall error is less. – Orbling Dec 06 '10 at 23:59
  • 2
    @Orbling: Are you telling me that 7.5 is closer to 8 than it is to 7? – tchrist Dec 07 '10 at 00:06
  • 1
    @tchrist. No, obviously not, because contrary to your believe I am not a complete moron. However, I would rather `7.5` did not round to the same value as `8.5` - that does not give good distribution. – Orbling Dec 07 '10 at 00:09
  • 1
    When calculating amounts etcetera every solution I've come in contact with rounds 5 up. That's how it's done and that's what I need to do. What is theoretically correct isn't very interesting when implementing a solution that needs to function in the real world. What everyone else does is what I must do too. It's probably defined somewhere that 5 should be rounded up. When I learned how to round things in school I learned that 5 is rounded up. Don't know anyone who has learned that 5 should sometimes be rounded up and sometimes down. – Johan Soderberg Dec 07 '10 at 00:31
  • @Johan: My thoughts exactly, in the real world - people round to nearest decimal place with 5 going up. I think I learnt that when I was about 6/7 years old. Almost all software works on that principle, certainly financial calculations are required to. The interesting mathematical properties of round to even (which I am not blind too) are all very well, but not what is desired or expected. – Orbling Dec 07 '10 at 00:36
  • @Orbling: I don’t know where your insecurity is coming from, but I certainly made no such assumption. On the other hand, I *do* have a problem with the misleading `"nearest"` designation. It’s bogus. Try `perl -MMath::BigFloat -le '$x = new Math::BigFloat "1.005"; $x *= 100; print STDOUT ffround $x 0 => "farthest"'` and `perl -MMath::BigFloat -le '$x = new Math::BigFloat "1.005"; $x *= 100; print STDOUT ffround $x 0 => "nearest"'` and `perl -MMath::BigFloat -le '$x = new Math::BigFloat "1.005"; $x *= 100; print STDOUT ffround $x 0 => "wrongest"'`. Any problem there? – tchrist Dec 07 '10 at 00:41
  • @Johan, @Orbling: "real world" is an offensive put-down term. Got anything else to add? – tchrist Dec 07 '10 at 00:42
  • @Johan: Where I went to school, they certainly taught that five was biased and that it led to problems to pretend that it is somehow closer to the integer after it than it is to the one before it. Clearly it is not. But education standards differ. – tchrist Dec 07 '10 at 00:43
  • @tchrist: It was not intended to be offensive - just separating the world of exact mathematics from the day-to-day standard expectation and usage. As for the insecurity, you asked me a question equivalent to if I believed half was nearer to one than zero. You'll be asking me if I believe in God next. ;-) – Orbling Dec 07 '10 at 00:47
  • @tchrist: Your point on offensive terms: "But education standards differ." As it goes, I was taught that, a fair bit later, but also taught what is the standard practice and what is not. – Orbling Dec 07 '10 at 00:50
  • @tchrist: when you go shopping and something cost 30.5 does the store round it down to 30 or up to 31? Rounding up is what is used. – Johan Soderberg Dec 07 '10 at 08:11
  • 1
    @tchrist: you're right about you're example with farthest/nearest/wrongest. I need to find a list of valid rounding methods for BigFloat. – Johan Soderberg Dec 07 '10 at 08:13
  • @Johan: 'common' is what you want, see: http://search.cpan.org/~tels/Math-BigInt/lib/Math/BigInt.pm#Rounding_mode_R – Orbling Dec 07 '10 at 14:17
  • @Johan: Valid round modes are 'even', 'odd', '+inf', '‑inf', 'zero', 'trunc', and 'common'. I’ve never been charged less than a cent to my knowledge. I don’t know what they do with taxes and rounding. Now I’m curious. – tchrist Dec 07 '10 at 14:28
  • @tchrist: It is a curious issue, depends on bank/treasury policy, sometimes balanced out, sometimes accumulated. Did you ever see Superman 3? :-) – Orbling Dec 07 '10 at 14:40
  • @Orbling, @tchrist: Thanks for your help! "common" is what I will use. :) @tchrist: Every invoice I have seen rounds 5 up (tax/amount). – Johan Soderberg Dec 07 '10 at 14:52
2

The “correct” way to round is whatever way you choose to define as correct for whatever purpose you have in mind. People have thought a lot about it, and some strategies are more appropriate for some application areas than they are in others.

Rounding toward zero by using int() is seldom what people want. Usually they want something that is numerically unbiased.

Fair splits the fives; hence the shorthand term, “round towards even”. The nearer integer isn’t defined: there is no nearer integer when the least siginficant digit is 5. There are 9 things that when rounded give a different answer: 1,2,3,4,5,6,7,8,9.

To be fair, you must have half of those go one way and half go the other. But there are nine numbers, so you have four go one way and four go the other, but now you have a fairness problem. The only way therefore to avoid bias is for the 5 cases to alternate up and down. That’s why it works this way.

% perl -e 'printf "%.0f\n", $_+.5 for -10 .. +10'

produces the sequence

-10 -8 -8 -6 -6 -4 -4 -2 -2 -0 0 2 2 4 4 6 6 8 8 10 10

While this round-towards-zero approach:

% perl -e 'print int($_+.5)," " for -10 .. +10; print "\n"'

makes this with a hump in the middle:

 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 0 1 2 3 4 5 6 7 8 9 10 

Looking again at round-towards-even:

% perl -le 'printf "%.1f ", $_+.05 for -10 .. +10'

-9.9 -8.9 -8.0 -7.0 -6.0 -5.0 -4.0 -3.0 -1.9 -0.9 0.1 1.1 2.0 3.0 4.0 5.0 6.0 7.0 8.1 9.1 10.1 

That makes more sense if you do this:

% perl -le 'printf "%.2f ", $_+.05 for -10 .. +10'

-9.95 -8.95 -7.95 -6.95 -5.95 -4.95 -3.95 -2.95 -1.95 -0.95 0.05 1.05 2.05 3.05 4.05 5.05 6.05 7.05 8.05 9.05 10.05 

and then consider what towards even means.

tchrist
  • 78,834
  • 30
  • 123
  • 180