change Round function to use sprintf and combat machine rounding error#1422
change Round function to use sprintf and combat machine rounding error#1422Alex-Jordan wants to merge 1 commit into
Conversation
|
This approach is dependent on the magnitude of the number being rounded. For example, your approach will round Peter Staab and I worked on this idea for his SignificantFigures context, and came up with the following (adjusted to your setting): sub Round {
my ($x, $n) = @_;
my $e = (split(/E/, sprintf("%E", $x)))[1] + 0; # exponent for $x
my $s = ($x < 0 ? -1 : 1); # the sign of $x
my $N = $e + $n; # number of digits to retain
my $m = main::max($N, 14 - $N); # position to use to adjust for repeated 9s
$x += $s * 10**($e - $m); # adjust for repeated 9s
return sprintf("%.${N}E", $x) + 0 unless $N == -1; # round the adjusted value
# For zero original digits, we add a digit just above the first one
# in $x, round that, then remove the added digit, getting 0 if $x
# didn't round up, or 1 in the proper place if it did. This means
# that 0.005 rounds to .01, for example, when 2 digits are requested.
my $d = $s * 10**($e + 1);
return sprintf("%.0E", $x + $d) - $d;
}This uses the |
|
Oh, how silly of me not to recognize the dependency on order of magnitude. Is it best for this, for the "14" to be hard coded? |
Yes. This is based on the fact that floating-point real numbers are stored with 16 to 17 decimal digits of precision, so the 14 is two or three digits from the end, which should pick up a run of 9s (sometimes you end up with .4999999999945 or something like that, so need to be a little in from the end). |
|
The fact that the unit test is failing for a test specifically testing the |
|
Yes, I didn't miss the unit test failing. I just haven't come back to it yet. |
My fault. It was a rushed translation of code that took the number of significant digits to one that takes a number of decimal places. I think it is the correct one: sub Round {
my ($x, $n) = @_;
my $e = (split(/E/, sprintf("%E", $x)))[1] + 0; # exponent for $x
my $s = ($x < 0 ? -1 : 1); # the sign of $x
my $N = $e + $n; # number of digits to retain
$x += $s * 10**($e - 15); # adjust for repeated 9s
return sprintf("%.${N}E", $x) + 0 unless $N == -1; # round the adjusted value
# For zero original digits, we add a digit just above the first one
# in $x, round that, then remove the added digit, getting 0 if $x
# didn't round up, or 1 in the proper place if it did. This means
# that 0.005 rounds to .01, for example, when 2 digits are requested.
my $d = $s * 10**($e + 1);
return sprintf("%.0E", $x + $d) - $d;
}Sorry about that! |
|
Thanks @dpvc, I just updated it. The tests pass now. This is still just for discussion. Although I support a change to Round like this, I understand the argument to leave things be here. I just find it hard to imagine a PG problem that truly intended to work with numbers like |
|
I don't think you could even reliably deal with |
|
Yes, so the question here is: Given that we are writing PG exercises, is it reasonable/acceptable to officially assume that a number like This most often comes up for me in problems where the answer is Currency, to the cent. So something like |
|
This would be a good candidate for a unit test to make sure that a lot of the cases work as expected. |
This may be controversial, and I will close it in a heartbeat if other developers don't like it. But it's to address #403.
The philosophy is that if we have a number like
134.49999999999997it was probably supposed to be134.5and should round up to135. So we add an extremely small amount (numZeroLevelTolDefault) before doing the rounding.Of course the risk is that the number really was supposed to be
134.49999999999997, and really should round down. It is hard for me to come up with a scenario where you'd really be working with a number like this and need to round it.To test, this problem gives different results before and after this commit: