Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision add0f69b

Von Moritz Bunkus vor mehr als 10 Jahren hinzugefügt

round_amount: Perls Repräsentationsalgorithmus fürs exakte Runden nutzen

Als Erläuterung paste ich schlicht den relevanten Teil des Kommentars,
der nun auch in der Funktion steht:

Trying to round with more precision first only shifts the problem to rarer
cases, which nevertheless exist.

Now we exploit the presentation rounding of Perl. Since it really tries hard
to recognize integers, we double $amount, and let Perl give us a representation.
If Perl recognizes it as a slightly too small integer, and rounds up to the
next odd integer, we follow suit and treat the fraction as .5 or greater.

Unterschiede anzeigen:

SL/Form.pm
952 952

  
953 953
  # Rounding like "Kaufmannsrunden" (see http://de.wikipedia.org/wiki/Rundung )
954 954

  
955
  # Round amounts to eight places before rounding to the requested
956
  # number of places. This gets rid of errors due to internal floating
957
  # point representation.
958
  $amount   = $self->round_amount($amount, 8) if $places < 8;
959

  
960
  # Remember the amount's sign but calculate in positive values only.
961
  my $sign  = $amount <=> 0;
962
  $amount   = abs $amount;
963

  
964
  # Shift the amount left by $places+1 decimal places and truncate it
965
  # to integer. Then to the integer equivalent of rounding to the next
966
  # multiple of 10: first add half of it (5). Then truncate it back to
967
  # the lower multiple of 10 by subtracting $amount modulo 10.
968
  my $shift = 10 ** ($places + 1);
969
  $amount   = int($amount * $shift) + 5;
970
  $amount  -= $amount % 10;
971

  
972
  # Lastly shift the amount back right by $places+1 decimal places and
973
  # restore its sign. Then we're done.
974
  $amount   = ($amount / $shift) * $sign;
955
  # If you search for rounding in Perl, you'll likely get the first version of
956
  # this algorithm:
957
  #
958
  # ($amount <=> 0) * int(abs($amount) * 10**$places) + .5) / 10**$places
959
  #
960
  # That doesn't work. It falls apart for certain values that are exactly 0.5
961
  # over the cutoff, because the internal IEEE754 representation is slightly
962
  # below the cutoff. Perl makes matters worse in that it really, really tries to
963
  # recognize exact values for presentation to you, even if they are not.
964
  #
965
  # Example: take the value 64.475 and round to 2 places.
966
  #
967
  # printf("%.20f\n", 64.475) gives you 64.47499999999999431566
968
  #
969
  # Then 64.475 * 100 + 0.5 is 6447.99999999999909050530, and
970
  # int(64.475 * 100 + 0.5) / 100 = 64.47
971
  #
972
  # Trying to round with more precision first only shifts the problem to rarer
973
  # cases, which nevertheless exist.
974
  #
975
  # Now we exploit the presentation rounding of Perl. Since it really tries hard
976
  # to recognize integers, we double $amount, and let Perl give us a representation.
977
  # If Perl recognizes it as a slightly too small integer, and rounds up to the
978
  # next odd integer, we follow suit and treat the fraction as .5 or greater.
979

  
980
  my $sign               = $amount <=> 0;
981
  $amount                = abs $amount;
982

  
983
  my $shift              = 10 ** ($places);
984
  my $shifted_and_double = $amount * $shift * 2;
985
  my $rounding_bias      = sprintf('%f', $shifted_and_double) % 2;
986
  $amount                = int($amount * $shift) + $rounding_bias;
987
  $amount                = $amount / $shift * $sign;
975 988

  
976 989
  return $amount;
977 990
}
t/form/round_amount.t
23 23
is($::form->round_amount(33.675, 1), '33.7',  '33.675 @ 1');
24 24
is($::form->round_amount(33.675, 0), '34',    '33.675 @ 0');
25 25

  
26
is($::form->round_amount(64.475, 2), '64.48', '64.475 @ 2');
27
is($::form->round_amount(64.475, 1), '64.5',  '64.475 @ 1');
28
is($::form->round_amount(64.475, 0), '64',    '64.475 @ 0');
29

  
30
is($::form->round_amount(64.475499, 5), '64.4755', '64.475499 @ 5');
31
is($::form->round_amount(64.475499, 4), '64.4755', '64.475499 @ 4');
32
is($::form->round_amount(64.475499, 3), '64.475',  '64.475499 @ 3');
33
is($::form->round_amount(64.475499, 2), '64.48',   '64.475499 @ 2');
34
is($::form->round_amount(64.475499, 1), '64.5',    '64.475499 @ 1');
35
is($::form->round_amount(64.475499, 0), '64',      '64.475499 @ 0');
36

  
37
is($::form->round_amount(64.475999, 5), '64.476', '64.475999 @ 5');
38
is($::form->round_amount(64.475999, 4), '64.476', '64.475999 @ 4');
39
is($::form->round_amount(64.475999, 3), '64.476', '64.475999 @ 3');
40
is($::form->round_amount(64.475999, 2), '64.48',  '64.475999 @ 2');
41
is($::form->round_amount(64.475999, 1), '64.5',   '64.475999 @ 1');
42
is($::form->round_amount(64.475999, 0), '64',     '64.475999 @ 0');
43

  
26 44
is($::form->round_amount(44.9 * 0.75, 2), '33.68', '44.9 * 0.75 @ 2');
27 45
is($::form->round_amount(44.9 * 0.75, 1), '33.7',  '44.9 * 0.75 @ 1');
28 46
is($::form->round_amount(44.9 * 0.75, 0), '34',    '44.9 * 0.75 @ 0');

Auch abrufbar als: Unified diff