Revision ed531c37
Von Moritz Bunkus vor mehr als 10 Jahren hinzugefügt
SL/Form.pm | ||
---|---|---|
41 | 41 |
use Data::Dumper; |
42 | 42 |
|
43 | 43 |
use Carp; |
44 |
use Config; |
|
44 | 45 |
use CGI; |
45 | 46 |
use Cwd; |
46 | 47 |
use Encode; |
47 | 48 |
use File::Copy; |
48 | 49 |
use IO::File; |
50 |
use Math::BigInt; |
|
49 | 51 |
use SL::Auth; |
50 | 52 |
use SL::Auth::DB; |
51 | 53 |
use SL::Auth::LDAP; |
... | ... | |
950 | 952 |
sub round_amount { |
951 | 953 |
my ($self, $amount, $places) = @_; |
952 | 954 |
|
953 |
# Rounding like "Kaufmannsrunden" (see http://de.wikipedia.org/wiki/Rundung ) |
|
954 |
|
|
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; |
|
955 |
# We use Perl's knowledge of string representation for |
|
956 |
# rounding. First, convert the floating point number to a string |
|
957 |
# with a high number of places. Then split the string on the decimal |
|
958 |
# sign and use integer calculation for rounding the decimal places |
|
959 |
# part. If an overflow occurs then apply that overflow to the part |
|
960 |
# before the decimal sign as well using integer arithmetic again. |
|
961 |
|
|
962 |
my $amount_str = sprintf '%.*f', $places + 10, abs($amount); |
|
963 |
|
|
964 |
return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$}; |
|
965 |
|
|
966 |
my ($pre, $post) = ($1, $2); |
|
967 |
my $decimals = '1' . substr($post, 0, $places); |
|
968 |
|
|
969 |
my $propagation_limit = $Config{i32size} == 4 ? 7 : 18; |
|
970 |
my $add_for_rounding = substr($post, $places, 1) >= 5 ? 1 : 0; |
|
971 |
|
|
972 |
if ($places > $propagation_limit) { |
|
973 |
$decimals = Math::BigInt->new($decimals)->badd($add_for_rounding); |
|
974 |
$pre = Math::BigInt->new($decimals)->badd(1) if substr($decimals, 0, 1) eq '2'; |
|
975 |
|
|
976 |
} else { |
|
977 |
$decimals += $add_for_rounding; |
|
978 |
$pre += 1 if substr($decimals, 0, 1) eq '2'; |
|
979 |
} |
|
980 |
|
|
981 |
$amount = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0); |
|
988 | 982 |
|
989 | 983 |
return $amount; |
990 | 984 |
} |
Auch abrufbar als: Unified diff
Form::round_amount: Perls Wissen über Stringifizierung nutzen
Perl weiß am besten, wann eine nicht ganz exakte Fließkommazahl
eigentlich eine für Menschen sinnvoll lesbare Fließkommazahl ist (also
dass mit 143.19999999999998863132 eigentlich 143.2 gemeint ist, wenn ich
143.2 übergebe). Also nutzen wir diese Tatsache, machen aus der
Fließkommazahl einen String und teilen diesen dann am
Dezimaltrennzeichen auf.
Danach kann mit Integerarithmetik weiter gerechnet werden. Auf die
Nachkommastellen wird entsprechend addiert, sofern die relevante Stelle
zweiten Addition auf den Vorkommaanteil addiert.
Erst zum Schluss werden diese beiden Integerzahlen mit Hilfe eines
Strings zu einer Fließkommazahl zusammengesetzt.
Dabei muss beachtet werden, dass auf 32bit-Architekturen Perls
automatische Integer-Umwandlung von Strings bei Stringlängen von 9
bereits auf die wissenschaftliche Schreibweise wechselt. Das wird
verhindert, indem das Math::BigInt-Modul in dem Moment für die
Berechnung verwendet wird, aber aus Performancegründen nur dann, wenn's
wirklich nötig ist.