Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 26a5973f

Von Moritz Bunkus vor mehr als 10 Jahren hinzugefügt

  • ID 26a5973f82e070ba072f22ab9cf71fad0c5b92c3
  • Vorgänger 0152cc2e
  • Nachfolger a75bd966

round_amount: Fix für falsches Runden bestimmter Werte

Gewisse Werte wie z.B. 33,675 wurden bei 2 Stellen falsch gerundet,
nämlich auf 33,67 anstelle von 33,68. Bei anderen Werten hingegen
funktionierte es (beispielsweise 149,175 @ 2 → 149,18).

Grund war, dass durch das Addieren von 0.5 wieder Fließkommaberechnung
und damit die Ungenauigkeit der Präsentation der IEEE-Fließkommazahlen
ins Spiel kommt. Das anschließende int() schneidet dann die
Fließkommazahl falsch ab, ungefähr so:

- Initial: 33,675
- Linksshift um 2 Dezimalstellen, also * 100: 3367,5
- Dann + 0.5 und truncate, hier passierts: +0.5 =
3367,499999999999999999999958 (auch wenn Perl das in der Ausgabe als
3368 darstellen würde) oder so, davon int() ergibt nun mal 3367 vor
anschließendem Rechtsshift um 2 Dezimalstellen

Lösung ist, bis auf das Links-/Rechtsshiften um die Dezimalstellen gar
keine Fließkommaberechnung zu verwenden. Eine Variante ist, eine Stelle
mehr zu shiften als man an Genauigkeit will, dann 5 zu addieren und
anschließend auf das nächst kleinere Vielfache von 10 zu
reduzieren (durch simples Abziehen vom Modulo 10).

Um die Logik leicht einfacher zu halten, wird das Vorzeichen anfangs
ermittelt und ab dann nur noch mit dem Absolutwert der Zahl
gerechnet. Das ursprüngliche Vorzeichen wird erst nach dem erneuten
Rechtsshift, also ganz am Schluss der Berechnung, wieder hergestellt.

Unterschiede anzeigen:

SL/Form.pm
948 948
}
949 949

  
950 950
sub round_amount {
951
  $main::lxdebug->enter_sub(2);
952

  
953 951
  my ($self, $amount, $places) = @_;
954
  my $round_amount;
955 952

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

  
958 955
  # Round amounts to eight places before rounding to the requested
959 956
  # number of places. This gets rid of errors due to internal floating
960 957
  # point representation.
961
  $amount       = $self->round_amount($amount, 8) if $places < 8;
962
  $amount       = $amount * (10**($places));
963
  $round_amount = int($amount + .5 * ($amount <=> 0)) / (10**($places));
958
  $amount   = $self->round_amount($amount, 8) if $places < 8;
964 959

  
965
  $main::lxdebug->leave_sub(2);
960
  # Remember the amount's sign but calculate in positive values only.
961
  my $sign  = $amount <=> 0;
962
  $amount   = abs $amount;
966 963

  
967
  return $round_amount;
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;
968 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;
975

  
976
  return $amount;
969 977
}
970 978

  
971 979
sub parse_template {
t/form/round_amount.t
1
use strict;
2
use Test::More;
3

  
4
use lib 't';
5
use Support::TestSetup;
6

  
7
Support::TestSetup::login();
8

  
9
my $config = {};
10

  
11
$config->{numberformat} = '1.000,00';
12

  
13
# Positive values
14
is($::form->round_amount(1.05, 2), '1.05', '1.05 @ 2');
15
is($::form->round_amount(1.05, 1), '1.1',  '1.05 @ 1');
16
is($::form->round_amount(1.05, 0), '1',    '1.05 @ 0');
17

  
18
is($::form->round_amount(1.045, 2), '1.05', '1.045 @ 2');
19
is($::form->round_amount(1.045, 1), '1',    '1.045 @ 1');
20
is($::form->round_amount(1.045, 0), '1',    '1.045 @ 0');
21

  
22
is($::form->round_amount(33.675, 2), '33.68', '33.675 @ 2');
23
is($::form->round_amount(33.675, 1), '33.7',  '33.675 @ 1');
24
is($::form->round_amount(33.675, 0), '34',    '33.675 @ 0');
25

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

  
30
is($::form->round_amount(149.175, 2), '149.18', '149.175 @ 2');
31
is($::form->round_amount(149.175, 1), '149.2',  '149.175 @ 1');
32
is($::form->round_amount(149.175, 0), '149',    '149.175 @ 0');
33

  
34
is($::form->round_amount(198.90 * 0.75, 2), '149.18', '198.90 * 0.75 @ 2');
35
is($::form->round_amount(198.90 * 0.75, 1), '149.2',  '198.90 * 0.75 @ 1');
36
is($::form->round_amount(198.90 * 0.75, 0), '149',    '198.90 * 0.75 @ 0');
37

  
38
# Negative values
39
is($::form->round_amount(-1.05, 2), '-1.05', '-1.05 @ 2');
40
is($::form->round_amount(-1.05, 1), '-1.1',  '-1.05 @ 1');
41
is($::form->round_amount(-1.05, 0), '-1',    '-1.05 @ 0');
42

  
43
is($::form->round_amount(-1.045, 2), '-1.05', '-1.045 @ 2');
44
is($::form->round_amount(-1.045, 1), '-1',    '-1.045 @ 1');
45
is($::form->round_amount(-1.045, 0), '-1',    '-1.045 @ 0');
46

  
47
is($::form->round_amount(-33.675, 2), '-33.68', '33.675 @ 2');
48
is($::form->round_amount(-33.675, 1), '-33.7',  '33.675 @ 1');
49
is($::form->round_amount(-33.675, 0), '-34',    '33.675 @ 0');
50

  
51
is($::form->round_amount(-44.9 * 0.75, 2), '-33.68', '-44.9 * 0.75 @ 2');
52
is($::form->round_amount(-44.9 * 0.75, 1), '-33.7',  '-44.9 * 0.75 @ 1');
53
is($::form->round_amount(-44.9 * 0.75, 0), '-34',    '-44.9 * 0.75 @ 0');
54

  
55
is($::form->round_amount(-149.175, 2), '-149.18', '-149.175 @ 2');
56
is($::form->round_amount(-149.175, 1), '-149.2',  '-149.175 @ 1');
57
is($::form->round_amount(-149.175, 0), '-149',    '-149.175 @ 0');
58

  
59
is($::form->round_amount(-198.90 * 0.75, 2), '-149.18', '-198.90 * 0.75 @ 2');
60
is($::form->round_amount(-198.90 * 0.75, 1), '-149.2',  '-198.90 * 0.75 @ 1');
61
is($::form->round_amount(-198.90 * 0.75, 0), '-149',    '-198.90 * 0.75 @ 0');
62

  
63
done_testing;
64

  
65
1;

Auch abrufbar als: Unified diff