1 package SL::Helper::Number;
4 use Exporter qw(import);
5 use List::Util qw(max min);
9 _format_number _round_number
10 _format_total _round_total
13 our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
16 my ($amount, $places, %params) = @_;
18 my $dash = $params{dash} // '';
19 my $numberformat = $params{numberformat} // $::myconfig{numberformat};
20 my $neg = $amount < 0;
21 my $force_places = defined $places && $places >= 0;
23 $amount = _round_number($amount, abs $places) if $force_places;
24 $neg = 0 if $amount == 0; # don't show negative zero
25 $amount = sprintf "%.*f", ($force_places ? $places : 10), abs $amount; # 6 is default for %fa
27 # before the sprintf amount was a number, afterwards it's a string. because of the dynamic nature of perl
28 # this is easy to confuse, so keep in mind: before this comment no s///, m//, concat or other strong ops on
29 # $amount. after this comment no +,-,*,/,abs. it will only introduce subtle bugs.
31 $amount =~ s/0*$// unless defined $places && $places == 0; # cull trailing 0s
33 my @d = reverse $numberformat =~ /(\D)/g; # get delim chars
34 my @p = split(/\./, $amount); # split amount at decimal point
36 $p[0] =~ s/\B(?=(...)*$)/$d[1]/g if $d[1]; # add 1,000 delimiters
38 if ($places || $p[1]) {
41 . (0 x max(abs($places || 0) - length ($p[1]||''), 0)); # pad the fraction
45 ($dash =~ /-/) ? ($neg ? "($amount)" : "$amount" ) :
46 ($dash =~ /DRCR/) ? ($neg ? "$amount " . $main::locale->text('DR') : "$amount " . $main::locale->text('CR') ) :
47 ($neg ? "-$amount" : "$amount" ) ;
54 my ($amount, $places, $adjust) = @_;
56 return 0 if !defined $amount;
62 my $precision = $::instance_conf->get_precision || 0.01;
63 return _round_number( _round_number($amount / $precision, 0) * $precision, $places);
66 # We use Perl's knowledge of string representation for
67 # rounding. First, convert the floating point number to a string
68 # with a high number of places. Then split the string on the decimal
69 # sign and use integer calculation for rounding the decimal places
70 # part. If an overflow occurs then apply that overflow to the part
71 # before the decimal sign as well using integer arithmetic again.
73 my $int_amount = int(abs $amount);
74 my $str_places = max(min(10, 16 - length("$int_amount") - $places), $places);
75 my $amount_str = sprintf '%.*f', $places + $str_places, abs($amount);
77 return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$};
79 my ($pre, $post) = ($1, $2);
80 my $decimals = '1' . substr($post, 0, $places);
82 my $propagation_limit = $Config{i32size} == 4 ? 7 : 18;
83 my $add_for_rounding = substr($post, $places, 1) >= 5 ? 1 : 0;
85 if ($places > $propagation_limit) {
86 $decimals = Math::BigInt->new($decimals)->badd($add_for_rounding);
87 $pre = Math::BigInt->new($decimals)->badd(1) if substr($decimals, 0, 1) eq '2';
90 $decimals += $add_for_rounding;
91 $pre += 1 if substr($decimals, 0, 1) eq '2';
94 $amount = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0);
100 my ($amount, %params) = @_;
102 return 0 if !defined $amount || $amount eq '';
104 my $numberformat = $params{numberformat} // $::myconfig{numberformat};
106 if ( ($numberformat eq '1.000,00')
107 || ($numberformat eq '1000,00')) {
112 if ($numberformat eq "1'000.00") {
118 # Make sure no code wich is not a math expression ends up in eval().
119 return 0 unless $amount =~ /^ [\s \d \( \) \- \+ \* \/ \. ]* $/x;
121 # Prevent numbers from being parsed as octals;
122 $amount =~ s{ (?<! [\d.] ) 0+ (?= [1-9] ) }{}gx;
124 return scalar(eval($amount)) * 1 ;
127 sub _format_total { _format_number($_[0], 2, @_[1..$#_]) }
128 sub _round_total { _round_number($_[0], 2, @_[1..$#_]) }
138 SL::Helper::Number - number formating functions formerly sitting in SL::Form
142 use SL::Helper::Number qw(all);
144 my $str = _format_number($val, 2); # round to 2
145 my $str = _format_number($val, 2, %::myconfig); # also works, is implied
146 my $str = _format_number($val, 2, numberformat => '1.000,00'); # with custom numberformat
147 my $total = _format_total($val); # round to 2
148 my $total = _format_total($val, numberformat => '1.000,00');
150 my $val = _parse_number($str); # parse with the current numberformat
151 my $val = _parse_number($str, numberformat => '1.000,00'); # parse with the current numberformat
153 my $str = _round_number($val, 2);
154 my $total = _round_total($val); # rounded to 2
158 This package contains all the number parsing/formating functions that were
159 previously in SL::Form.
161 Instead of invoking them as methods on C<$::form> these are pure functions.
167 =item * C<_format_number VALUE PLACES PARAMS>
169 The old C<SL::Form::format_amount> with a different signature.
171 The value is expected to be a numeric value, but undef and empty string will be
172 vivified to 0 for convinience. Bigints are supported.
174 For the semantics of places, see L</PLACES>.
176 If C<params> contains a dash parameter, it will change the formatting of
177 positive/negative numbers. If C<-> is given for dash, negative numbers will
178 instead be formatted with prentheses. If C<DRCR> is given, the numbers will be
179 formatted absolute, but suffixed with the localized versions of C<DR> and
182 =item * _format_total
184 A curried version used for formatting ledger entries. C<myconfig> is set from the
185 current user, C<places> is set to 2. C<dash> is left empty.
187 =item * _parse_number VALUE PARAMS
189 Parses expressions into numbers. C<PARAMS> may contain C<numberformat> just
190 like with C<L/_format_amount>.
192 Also implements basic arithmetic interpretation, so that C<2 * 1400> is
195 =item * _round_number VALUE PLACES
197 Rounds a number. Due to the way Perl handles floating point we take a lot of
198 precautions that rounding ends up being close to where we want. Usually the
199 internal floats have more than enough precision to not have any floating point
200 issues, but the cumulative error can interfere with proper formatting later.
202 For places, see L</PLACES>
206 A curried version used for rounding ledger entries. C<places> is set to 2.
218 In that case a representation is chosen that looks sufficiently human. For
219 example C<1/10> equals C<.1000000000000000555> but will be displayed as the
220 localized version of 0.1.
224 The number will be rounded to the nearest integer (towards 0).
226 =item * a positive integer
228 The number will be rounded to this many places. Formatting functions will then
229 make sure to pad the output to this many places.
231 =item * a negative inteher
233 The number will not be rounded, but padded to at least this many places.
237 =head1 ERROR REPORTING
239 All of these do not thow exceptions and will simply return undef should
240 something unforeseen happen.
242 =head1 BUGS AND CAVEATS
244 Beware that the old C<amount> is now called plain C<number>. C<amount> is
245 deliberately unused in the new version for that reason.
249 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>