Revision a967d2a4
Von Sven Schöling vor mehr als 4 Jahren hinzugefügt
SL/Form.pm | ||
---|---|---|
42 | 42 |
use Data::Dumper; |
43 | 43 |
|
44 | 44 |
use Carp; |
45 |
use Config; |
|
46 | 45 |
use CGI; |
47 | 46 |
use Cwd; |
48 | 47 |
use Encode; |
... | ... | |
88 | 87 |
use List::MoreUtils qw(all any apply); |
89 | 88 |
use SL::DB::Tax; |
90 | 89 |
use SL::Helper::File qw(:all); |
90 |
use SL::Helper::Number; |
|
91 | 91 |
use SL::Helper::CreatePDF qw(merge_pdfs); |
92 | 92 |
|
93 | 93 |
use strict; |
... | ... | |
699 | 699 |
return @columns; |
700 | 700 |
} |
701 | 701 |
# |
702 |
sub format_amount { |
|
703 |
$main::lxdebug->enter_sub(2); |
|
704 |
|
|
705 |
my ($self, $myconfig, $amount, $places, $dash) = @_; |
|
706 |
$amount ||= 0; |
|
707 |
$dash ||= ''; |
|
708 |
my $neg = $amount < 0; |
|
709 |
my $force_places = defined $places && $places >= 0; |
|
710 |
|
|
711 |
$amount = $self->round_amount($amount, abs $places) if $force_places; |
|
712 |
$neg = 0 if $amount == 0; # don't show negative zero |
|
713 |
$amount = sprintf "%.*f", ($force_places ? $places : 10), abs $amount; # 6 is default for %fa |
|
714 |
|
|
715 |
# before the sprintf amount was a number, afterwards it's a string. because of the dynamic nature of perl |
|
716 |
# this is easy to confuse, so keep in mind: before this comment no s///, m//, concat or other strong ops on |
|
717 |
# $amount. after this comment no +,-,*,/,abs. it will only introduce subtle bugs. |
|
718 |
|
|
719 |
$amount =~ s/0*$// unless defined $places && $places == 0; # cull trailing 0s |
|
720 |
|
|
721 |
my @d = map { s/\d//g; reverse split // } my $tmp = $myconfig->{numberformat}; # get delim chars |
|
722 |
my @p = split(/\./, $amount); # split amount at decimal point |
|
723 | 702 |
|
724 |
$p[0] =~ s/\B(?=(...)*$)/$d[1]/g if $d[1]; # add 1,000 delimiters |
|
725 |
$amount = $p[0]; |
|
726 |
if ($places || $p[1]) { |
|
727 |
$amount .= $d[0] |
|
728 |
. ( $p[1] || '' ) |
|
729 |
. (0 x max(abs($places || 0) - length ($p[1]||''), 0)); # pad the fraction |
|
730 |
} |
|
731 |
|
|
732 |
$amount = do { |
|
733 |
($dash =~ /-/) ? ($neg ? "($amount)" : "$amount" ) : |
|
734 |
($dash =~ /DRCR/) ? ($neg ? "$amount " . $main::locale->text('DR') : "$amount " . $main::locale->text('CR') ) : |
|
735 |
($neg ? "-$amount" : "$amount" ) ; |
|
736 |
}; |
|
737 |
|
|
738 |
$main::lxdebug->leave_sub(2); |
|
739 |
return $amount; |
|
740 |
} |
|
703 |
sub format_amount { shift; goto &SL::Helper::Number::_number; } |
|
741 | 704 |
|
742 | 705 |
sub format_amount_units { |
743 | 706 |
$main::lxdebug->enter_sub(); |
... | ... | |
824 | 787 |
|
825 | 788 |
# |
826 | 789 |
|
827 |
sub parse_amount { |
|
828 |
$main::lxdebug->enter_sub(2); |
|
829 |
|
|
830 |
my ($self, $myconfig, $amount) = @_; |
|
790 |
sub parse_amount { shift; goto &SL::Helper::Number::_parse_number; } |
|
831 | 791 |
|
832 |
if (!defined($amount) || ($amount eq '')) { |
|
833 |
$main::lxdebug->leave_sub(2); |
|
834 |
return 0; |
|
835 |
} |
|
836 |
|
|
837 |
if ( ($myconfig->{numberformat} eq '1.000,00') |
|
838 |
|| ($myconfig->{numberformat} eq '1000,00')) { |
|
839 |
$amount =~ s/\.//g; |
|
840 |
$amount =~ s/,/\./g; |
|
841 |
} |
|
842 |
|
|
843 |
if ($myconfig->{numberformat} eq "1'000.00") { |
|
844 |
$amount =~ s/\'//g; |
|
845 |
} |
|
846 |
|
|
847 |
$amount =~ s/,//g; |
|
848 |
|
|
849 |
$main::lxdebug->leave_sub(2); |
|
850 |
|
|
851 |
# Make sure no code wich is not a math expression ends up in eval(). |
|
852 |
return 0 unless $amount =~ /^ [\s \d \( \) \- \+ \* \/ \. ]* $/x; |
|
853 |
|
|
854 |
# Prevent numbers from being parsed as octals; |
|
855 |
$amount =~ s{ (?<! [\d.] ) 0+ (?= [1-9] ) }{}gx; |
|
856 |
|
|
857 |
return scalar(eval($amount)) * 1 ; |
|
858 |
} |
|
859 |
|
|
860 |
sub round_amount { |
|
861 |
my ($self, $amount, $places, $adjust) = @_; |
|
862 |
|
|
863 |
return 0 if !defined $amount; |
|
864 |
|
|
865 |
$places //= 0; |
|
866 |
|
|
867 |
if ($adjust) { |
|
868 |
my $precision = $::instance_conf->get_precision || 0.01; |
|
869 |
return $self->round_amount( $self->round_amount($amount / $precision, 0) * $precision, $places); |
|
870 |
} |
|
871 |
|
|
872 |
# We use Perl's knowledge of string representation for |
|
873 |
# rounding. First, convert the floating point number to a string |
|
874 |
# with a high number of places. Then split the string on the decimal |
|
875 |
# sign and use integer calculation for rounding the decimal places |
|
876 |
# part. If an overflow occurs then apply that overflow to the part |
|
877 |
# before the decimal sign as well using integer arithmetic again. |
|
878 |
|
|
879 |
my $int_amount = int(abs $amount); |
|
880 |
my $str_places = max(min(10, 16 - length("$int_amount") - $places), $places); |
|
881 |
my $amount_str = sprintf '%.*f', $places + $str_places, abs($amount); |
|
882 |
|
|
883 |
return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$}; |
|
884 |
|
|
885 |
my ($pre, $post) = ($1, $2); |
|
886 |
my $decimals = '1' . substr($post, 0, $places); |
|
887 |
|
|
888 |
my $propagation_limit = $Config{i32size} == 4 ? 7 : 18; |
|
889 |
my $add_for_rounding = substr($post, $places, 1) >= 5 ? 1 : 0; |
|
890 |
|
|
891 |
if ($places > $propagation_limit) { |
|
892 |
$decimals = Math::BigInt->new($decimals)->badd($add_for_rounding); |
|
893 |
$pre = Math::BigInt->new($decimals)->badd(1) if substr($decimals, 0, 1) eq '2'; |
|
894 |
|
|
895 |
} else { |
|
896 |
$decimals += $add_for_rounding; |
|
897 |
$pre += 1 if substr($decimals, 0, 1) eq '2'; |
|
898 |
} |
|
899 |
|
|
900 |
$amount = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0); |
|
901 |
|
|
902 |
return $amount; |
|
903 |
} |
|
792 |
sub round_amount { shift; goto &SL::Helper::Number::_round_number; } |
|
904 | 793 |
|
905 | 794 |
sub parse_template { |
906 | 795 |
$main::lxdebug->enter_sub(); |
SL/Helper/Number.pm | ||
---|---|---|
1 |
package SL::Helper::Number; |
|
2 |
|
|
3 |
use strict; |
|
4 |
use Exporter qw(import); |
|
5 |
use List::Util qw(max min); |
|
6 |
use Config; |
|
7 |
|
|
8 |
our @EXPORT_OK = qw( |
|
9 |
_total _round_total |
|
10 |
_number _round_number |
|
11 |
_parse_number |
|
12 |
); |
|
13 |
our %EXPORT_TAGS = (all => \@EXPORT_OK); |
|
14 |
|
|
15 |
sub _number { |
|
16 |
my ($myconfig, $amount, $places, $dash) = @_; |
|
17 |
$amount ||= 0; |
|
18 |
$dash ||= ''; |
|
19 |
my $neg = $amount < 0; |
|
20 |
my $force_places = defined $places && $places >= 0; |
|
21 |
|
|
22 |
$amount = _round_number($amount, abs $places) if $force_places; |
|
23 |
$neg = 0 if $amount == 0; # don't show negative zero |
|
24 |
$amount = sprintf "%.*f", ($force_places ? $places : 10), abs $amount; # 6 is default for %fa |
|
25 |
|
|
26 |
# before the sprintf amount was a number, afterwards it's a string. because of the dynamic nature of perl |
|
27 |
# this is easy to confuse, so keep in mind: before this comment no s///, m//, concat or other strong ops on |
|
28 |
# $amount. after this comment no +,-,*,/,abs. it will only introduce subtle bugs. |
|
29 |
|
|
30 |
$amount =~ s/0*$// unless defined $places && $places == 0; # cull trailing 0s |
|
31 |
|
|
32 |
my @d = map { s/\d//g; reverse split // } my $tmp = $myconfig->{numberformat}; # get delim chars |
|
33 |
my @p = split(/\./, $amount); # split amount at decimal point |
|
34 |
|
|
35 |
$p[0] =~ s/\B(?=(...)*$)/$d[1]/g if $d[1]; # add 1,000 delimiters |
|
36 |
$amount = $p[0]; |
|
37 |
if ($places || $p[1]) { |
|
38 |
$amount .= $d[0] |
|
39 |
. ( $p[1] || '' ) |
|
40 |
. (0 x max(abs($places || 0) - length ($p[1]||''), 0)); # pad the fraction |
|
41 |
} |
|
42 |
|
|
43 |
$amount = do { |
|
44 |
($dash =~ /-/) ? ($neg ? "($amount)" : "$amount" ) : |
|
45 |
($dash =~ /DRCR/) ? ($neg ? "$amount " . $main::locale->text('DR') : "$amount " . $main::locale->text('CR') ) : |
|
46 |
($neg ? "-$amount" : "$amount" ) ; |
|
47 |
}; |
|
48 |
|
|
49 |
$amount; |
|
50 |
} |
|
51 |
|
|
52 |
sub _round_number { |
|
53 |
my ($amount, $places, $adjust) = @_; |
|
54 |
|
|
55 |
return 0 if !defined $amount; |
|
56 |
|
|
57 |
$places //= 0; |
|
58 |
|
|
59 |
if ($adjust) { |
|
60 |
my $precision = $::instance_conf->get_precision || 0.01; |
|
61 |
return _round_number( _round_number($amount / $precision, 0) * $precision, $places); |
|
62 |
} |
|
63 |
|
|
64 |
# We use Perl's knowledge of string representation for |
|
65 |
# rounding. First, convert the floating point number to a string |
|
66 |
# with a high number of places. Then split the string on the decimal |
|
67 |
# sign and use integer calculation for rounding the decimal places |
|
68 |
# part. If an overflow occurs then apply that overflow to the part |
|
69 |
# before the decimal sign as well using integer arithmetic again. |
|
70 |
|
|
71 |
my $int_amount = int(abs $amount); |
|
72 |
my $str_places = max(min(10, 16 - length("$int_amount") - $places), $places); |
|
73 |
my $amount_str = sprintf '%.*f', $places + $str_places, abs($amount); |
|
74 |
|
|
75 |
return $amount unless $amount_str =~ m{^(\d+)\.(\d+)$}; |
|
76 |
|
|
77 |
my ($pre, $post) = ($1, $2); |
|
78 |
my $decimals = '1' . substr($post, 0, $places); |
|
79 |
|
|
80 |
my $propagation_limit = $Config{i32size} == 4 ? 7 : 18; |
|
81 |
my $add_for_rounding = substr($post, $places, 1) >= 5 ? 1 : 0; |
|
82 |
|
|
83 |
if ($places > $propagation_limit) { |
|
84 |
$decimals = Math::BigInt->new($decimals)->badd($add_for_rounding); |
|
85 |
$pre = Math::BigInt->new($decimals)->badd(1) if substr($decimals, 0, 1) eq '2'; |
|
86 |
|
|
87 |
} else { |
|
88 |
$decimals += $add_for_rounding; |
|
89 |
$pre += 1 if substr($decimals, 0, 1) eq '2'; |
|
90 |
} |
|
91 |
|
|
92 |
$amount = ("${pre}." . substr($decimals, 1)) * ($amount <=> 0); |
|
93 |
|
|
94 |
return $amount; |
|
95 |
} |
|
96 |
|
|
97 |
sub _parse_number { |
|
98 |
my ($myconfig, $amount) = @_; |
|
99 |
|
|
100 |
return 0 if !defined $amount || $amount eq ''; |
|
101 |
|
|
102 |
if ( ($myconfig->{numberformat} eq '1.000,00') |
|
103 |
|| ($myconfig->{numberformat} eq '1000,00')) { |
|
104 |
$amount =~ s/\.//g; |
|
105 |
$amount =~ s/,/\./g; |
|
106 |
} |
|
107 |
|
|
108 |
if ($myconfig->{numberformat} eq "1'000.00") { |
|
109 |
$amount =~ s/\'//g; |
|
110 |
} |
|
111 |
|
|
112 |
$amount =~ s/,//g; |
|
113 |
|
|
114 |
# Make sure no code wich is not a math expression ends up in eval(). |
|
115 |
return 0 unless $amount =~ /^ [\s \d \( \) \- \+ \* \/ \. ]* $/x; |
|
116 |
|
|
117 |
# Prevent numbers from being parsed as octals; |
|
118 |
$amount =~ s{ (?<! [\d.] ) 0+ (?= [1-9] ) }{}gx; |
|
119 |
|
|
120 |
return scalar(eval($amount)) * 1 ; |
|
121 |
} |
|
122 |
|
|
123 |
sub _total { _number(\%::myconfig, $_[0], 2) } |
|
124 |
|
|
125 |
sub _round_total { _round_number($_[0], 2) } |
|
126 |
|
|
127 |
1; |
|
128 |
|
|
129 |
__END__ |
|
130 |
|
|
131 |
=encoding utf-8 |
|
132 |
|
|
133 |
=head1 NAME |
|
134 |
|
|
135 |
SL::Helper::Number - number formating functions formerly sitting in SL::Form |
|
136 |
|
|
137 |
=head1 SYNOPSIS |
|
138 |
|
|
139 |
use SL::Helper::Number qw(all); |
|
140 |
|
|
141 |
my $str = _number(\%::myconfig, $val, 2); |
|
142 |
my $total = _total($val); # rounded to 2 |
|
143 |
|
|
144 |
my $val = _parse_number(\%::myconfig, $str); |
|
145 |
|
|
146 |
my $str = _round_number(\%::myconfig, $val, 2); |
|
147 |
my $total = _round_total($val); # rounded to 2 |
|
148 |
|
|
149 |
=head1 DESCRIPTION |
|
150 |
|
|
151 |
This package contains all the number parsing/formating functions that were previously in SL::Form. |
|
152 |
|
|
153 |
Instead of invoking them as methods on C<$::form> these are pure functions. |
|
154 |
|
|
155 |
=head1 FUNCTIONS |
|
156 |
|
|
157 |
=over 4 |
|
158 |
|
|
159 |
=item * C<_number MYCONFIG VALUE PLACES DASH> |
|
160 |
|
|
161 |
The old C<SL::Form::format_amount>. C<MYCONFIG> is expected to be a hashref |
|
162 |
with a C<numberformat> entry. Usually C<\%::myconfig> will be passed. |
|
163 |
|
|
164 |
The value is expected to be a numeric value, but undef and empty string will be |
|
165 |
vivified to 0 for convinience. Bigints are supported. |
|
166 |
|
|
167 |
For the semantics of places, see L</PLACES>. |
|
168 |
|
|
169 |
The dash parameter allows to change the formatting of positive and negative |
|
170 |
numbers to alternative ones. If C<-> is given for dash, negative numbers will |
|
171 |
instead be formatted with prentheses. If C<DRCR> is given, the numbers will be |
|
172 |
formatted absolute, but suffixed with the localized versions of C<DR> and |
|
173 |
C<CR>. |
|
174 |
|
|
175 |
=item * _total |
|
176 |
|
|
177 |
A curried version used for formatting ledger entries. C<myconfig> is set from the |
|
178 |
current user, C<places> is set to 2. C<dash> is left empty. |
|
179 |
|
|
180 |
=item * _parse_number MYCONFIG VALUE |
|
181 |
|
|
182 |
Parses expressions into numbers. C<MYCONFIG> is expected to be a hashref |
|
183 |
with a C<numberformat> entry. |
|
184 |
|
|
185 |
Also implements basic arithmetic interprtation, so that C<2 * 1400> is |
|
186 |
interpreted as 2800. |
|
187 |
|
|
188 |
=item * _round_number VALUE PLACES |
|
189 |
|
|
190 |
Rounds a number. Due to the way Perl handles floating point we take a lot of |
|
191 |
precautions that rounding ends up being close to where we want. Usually the |
|
192 |
internal floats have more than enough precision to not have any floating point |
|
193 |
issues, but the cumulative error can interfere with proper formatting later. |
|
194 |
|
|
195 |
For places, see L</PLACES> |
|
196 |
|
|
197 |
=item * _round_total |
|
198 |
|
|
199 |
A curried version used for rounding ledger entries. C<places> is set to 2. |
|
200 |
|
|
201 |
=back |
|
202 |
|
|
203 |
=head1 PLACES |
|
204 |
|
|
205 |
Places can be: |
|
206 |
|
|
207 |
=over 4 |
|
208 |
|
|
209 |
=item * not present |
|
210 |
|
|
211 |
In that case a representation is chosen that looks sufficiently human. For |
|
212 |
example C<1/10> equals C<.1000000000000000555> but will be displayed as the |
|
213 |
localzed version of 0.1. |
|
214 |
|
|
215 |
=item * 0 |
|
216 |
|
|
217 |
The number will be rounded to the nearest integer (towards 0). |
|
218 |
|
|
219 |
=item * a positive integer |
|
220 |
|
|
221 |
The number will be rounded to this many places. Formatting functions will then |
|
222 |
make sure to pad the output to this many places. |
|
223 |
|
|
224 |
=item * a negative inteher |
|
225 |
|
|
226 |
The number will not be rounded, but padded to at least this many places. |
|
227 |
|
|
228 |
=back |
|
229 |
|
|
230 |
=head1 ERROR REPORTING |
|
231 |
|
|
232 |
All of these do not thow exceptions and will simply return undef should |
|
233 |
something unforeseen happen. |
|
234 |
|
|
235 |
=head1 BUGS AND CAVEATS |
|
236 |
|
|
237 |
Beware that the old C<amount> is now called plain C<number>. C<amount> is |
|
238 |
deliberately unused in the new version for that reason. |
|
239 |
|
|
240 |
=head1 AUTHOR |
|
241 |
|
|
242 |
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt> |
|
243 |
|
|
244 |
=cut |
Auch abrufbar als: Unified diff
SL::Helper::Number - format_amount aus SL::Form ausgelagert
- Neue Funktionen können direkt exportiert werden.
- Alte Funktionen sind in SL::Form für Kompatibilität auf die neuen
geproxied.
- Tracing debug statements (enter_sub, leave_sub) entfernt
- Zusätzlich noch eine neue Kategorie eingeführt: total, um Werte für
acc_trans immer auf 2 Stellen zu runden.