1 package SL::Helper::Number;
4 use Exporter qw(import);
5 use List::Util qw(max min);
6 use List::UtilsBy qw(rev_nsort_by);
10 _format_number _round_number
11 _format_total _round_total
14 our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
17 my ($amount, $places, %params) = @_;
19 my $dash = $params{dash} // '';
20 my $numberformat = $params{numberformat} // $::myconfig{numberformat};
21 my $neg = $amount < 0;
22 my $force_places = defined $places && $places >= 0;
24 $amount = _round_number($amount, abs $places) if $force_places;
25 $neg = 0 if $amount == 0; # don't show negative zero
26 $amount = sprintf "%.*f", ($force_places ? $places : 10), abs $amount; # 6 is default for %fa
28 # before the sprintf amount was a number, afterwards it's a string. because of the dynamic nature of perl
29 # this is easy to confuse, so keep in mind: before this comment no s///, m//, concat or other strong ops on
30 # $amount. after this comment no +,-,*,/,abs. it will only introduce subtle bugs.
32 $amount =~ s/0*$// unless defined $places && $places == 0; # cull trailing 0s
34 my @d = reverse $numberformat =~ /(\D)/g; # get delim chars
35 my @p = split(/\./, $amount); # split amount at decimal point
37 $p[0] =~ s/\B(?=(...)*$)/$d[1]/g if $d[1]; # add 1,000 delimiters
39 if ($places || $p[1]) {
42 . (0 x max(abs($places || 0) - length ($p[1]||''), 0)); # pad the fraction
46 ($dash =~ /-/) ? ($neg ? "($amount)" : "$amount" ) :
47 ($dash =~ /DRCR/) ? ($neg ? "$amount " . $main::locale->text('DR') : "$amount " . $main::locale->text('CR') ) :
48 ($neg ? "-$amount" : "$amount" ) ;
55 my ($amount, $places, $adjust) = @_;
57 return 0 if !defined $amount;
63 my $precision = $::instance_conf->get_precision || 0.01;
64 return _round_number( _round_number($amount / $precision, 0) * $precision, $places);
67 # We use Perl's knowledge of string representation for
68 # rounding. First, convert the floating point number to a string
69 # with a high number of places. Then split the string on the decimal
70 # sign and use integer calculation for rounding the decimal places
71 # part. If an overflow occurs then apply that overflow to the part
72 # before the decimal sign as well using integer arithmetic again.
74 my $int_amount = int(abs $amount);
75 my $str_places = max(min(10, 16 - length("$int_amount") - $places), $places);
76 my $amount_str = sprintf '%.*f', $places + $str_places, abs($amount);
78 return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$};
80 my ($pre, $post) = ($1, $2);
81 my $decimals = '1' . substr($post, 0, $places);
83 my $propagation_limit = $Config{i32size} == 4 ? 7 : 18;
84 my $add_for_rounding = substr($post, $places, 1) >= 5 ? 1 : 0;
86 if ($places > $propagation_limit) {
87 $decimals = Math::BigInt->new($decimals)->badd($add_for_rounding);
88 $pre = Math::BigInt->new($decimals)->badd(1) if substr($decimals, 0, 1) eq '2';
91 $decimals += $add_for_rounding;
92 $pre += 1 if substr($decimals, 0, 1) eq '2';
95 $amount = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0);
101 my ($amount, %params) = @_;
103 return 0 if !defined $amount || $amount eq '';
105 my $numberformat = $params{numberformat} // $::myconfig{numberformat};
107 if ( ($numberformat eq '1.000,00')
108 || ($numberformat eq '1000,00')) {
113 if ($numberformat eq "1'000.00") {
119 # Make sure no code wich is not a math expression ends up in eval().
120 return 0 unless $amount =~ /^ [\s \d \( \) \- \+ \* \/ \. ]* $/x;
122 # Prevent numbers from being parsed as octals;
123 $amount =~ s{ (?<! [\d.] ) 0+ (?= [1-9] ) }{}gx;
125 return scalar(eval($amount)) * 1 ;
128 sub _format_total { _format_number($_[0], 2, @_[1..$#_]) }
129 sub _round_total { _round_number($_[0], 2, @_[1..$#_]) }
139 SL::Helper::Number - number formating functions formerly sitting in SL::Form
143 use SL::Helper::Number qw(all);
145 my $str = _format_number($val, 2); # round to 2
146 my $str = _format_number($val, 2, %::myconfig); # also works, is implied
147 my $str = _format_number($val, 2, numberformat => '1.000,00'); # with custom numberformat
148 my $total = _format_total($val); # round to 2
149 my $total = _format_total($val, numberformat => '1.000,00');
151 my $val = _parse_number($str); # parse with the current numberformat
152 my $val = _parse_number($str, numberformat => '1.000,00'); # parse with the current numberformat
154 my $str = _round_number($val, 2);
155 my $total = _round_total($val); # rounded to 2
159 This package contains all the number parsing/formating functions that were
160 previously in SL::Form.
162 Instead of invoking them as methods on C<$::form> these are pure functions.
168 =item * C<_format_number VALUE PLACES PARAMS>
170 The old C<SL::Form::format_amount> with a different signature.
172 The value is expected to be a numeric value, but undef and empty string will be
173 vivified to 0 for convinience. Bigints are supported.
175 For the semantics of places, see L</PLACES>.
177 If C<params> contains a dash parameter, it will change the formatting of
178 positive/negative numbers. If C<-> is given for dash, negative numbers will
179 instead be formatted with prentheses. If C<DRCR> is given, the numbers will be
180 formatted absolute, but suffixed with the localized versions of C<DR> and
183 =item * _format_total
185 A curried version used for formatting ledger entries. C<myconfig> is set from the
186 current user, C<places> is set to 2. C<dash> is left empty.
188 =item * _parse_number VALUE PARAMS
190 Parses expressions into numbers. C<PARAMS> may contain C<numberformat> just
191 like with C<L/_format_amount>.
193 Also implements basic arithmetic interpretation, so that C<2 * 1400> is
196 =item * _round_number VALUE PLACES
198 Rounds a number. Due to the way Perl handles floating point we take a lot of
199 precautions that rounding ends up being close to where we want. Usually the
200 internal floats have more than enough precision to not have any floating point
201 issues, but the cumulative error can interfere with proper formatting later.
203 For places, see L</PLACES>
207 A curried version used for rounding ledger entries. C<places> is set to 2.
219 In that case a representation is chosen that looks sufficiently human. For
220 example C<1/10> equals C<.1000000000000000555> but will be displayed as the
221 localized version of 0.1.
225 The number will be rounded to the nearest integer (towards 0).
227 =item * a positive integer
229 The number will be rounded to this many places. Formatting functions will then
230 make sure to pad the output to this many places.
232 =item * a negative inteher
234 The number will not be rounded, but padded to at least this many places.
238 =head1 ERROR REPORTING
240 All of these do not thow exceptions and will simply return undef should
241 something unforeseen happen.
243 =head1 BUGS AND CAVEATS
245 Beware that the old C<amount> is now called plain C<number>. C<amount> is
246 deliberately unused in the new version for that reason.
250 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>