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>