File Coverage

blib/lib/Math/Currency.pm
Criterion Covered Total %
statement 148 164 90.2
branch 74 90 82.2
condition 20 60 33.3
subroutine 21 21 100.0
pod 12 13 92.3
total 275 348 79.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2             # vim:ts=4:sw=4:et:at:
3             #
4             # PROGRAM: Math::Currency.pm # - 04/26/00 9:10:AM
5             # PURPOSE: Perform currency calculations without floating point
6             #
7             #------------------------------------------------------------------------------
8             # Copyright (c) 2001-2008 John Peacock
9             #
10             # You may distribute under the terms of either the GNU General Public
11             # License or the Artistic License, as specified in the Perl README file,
12             # with the exception that it cannot be placed on a CD-ROM or similar media
13             # for commercial distribution without the prior approval of the author.
14             #------------------------------------------------------------------------------
15              
16             package Math::Currency;
17             $Math::Currency::VERSION = '0.53';
18             # ABSTRACT: Exact Currency Math with Formatting and Rounding
19              
20 14     14   738698 use strict;
  14         118  
  14         415  
21 14     14   4311 use utf8;
  14         119  
  14         99  
22 14     14   584 use base qw(Exporter Math::BigFloat);
  14         26  
  14         17080  
23 14     14   809975 use Math::BigFloat 1.60;
  14         419  
  14         90  
24 14     14   347392 use POSIX qw(locale_h);
  14         75597  
  14         104  
25 14     14   24891 use Encode::Locale;
  14         204500  
  14         669  
26 14     14   117 use Encode ();
  14         44  
  14         445  
27              
28 14     14   79 use overload '""' => \&bstr;
  14         35  
  14         138  
29              
30             our $LC_MONETARY = {
31             en_US => {
32             INT_CURR_SYMBOL => 'USD ',
33             CURRENCY_SYMBOL => '$',
34             MON_DECIMAL_POINT => '.',
35             MON_THOUSANDS_SEP => ',',
36             MON_GROUPING => '3',
37             POSITIVE_SIGN => '',
38             NEGATIVE_SIGN => '-',
39             INT_FRAC_DIGITS => '2',
40             FRAC_DIGITS => '2',
41             P_CS_PRECEDES => '1',
42             P_SEP_BY_SPACE => '0',
43             N_CS_PRECEDES => '1',
44             N_SEP_BY_SPACE => '0',
45             P_SIGN_POSN => '1',
46             N_SIGN_POSN => '1',
47             },
48             };
49             $LC_MONETARY->{USD} = $LC_MONETARY->{en_US};
50              
51             our $FORMAT = $LC_MONETARY->{en_US} unless localize();
52              
53             our @EXPORT_OK = qw(
54             $LC_MONETARY
55             $FORMAT
56             Money
57             );
58              
59             # Set class constants
60             our $round_mode = 'even'; # Banker's rounding obviously
61             our $accuracy = undef;
62             our $precision = $FORMAT->{FRAC_DIGITS} > 0 ? -$FORMAT->{FRAC_DIGITS} : 0;
63             our $div_scale = 40;
64             our $use_int = 0;
65             our $always_init = 0; # should the localize() happen every time?
66              
67              
68             sub new {
69 122     122 1 31725 my $proto = shift;
70 122   66     475 my $class = ref($proto) || $proto;
71 122 100       271 my $parent = $proto if ref($proto);
72              
73 122   100     283 my $value = shift || 0;
74              
75 122 50       7278 if (eval("'1.01' ne 1.01")) { # this might be a comma locale
76 0         0 $value =~ tr/,/./;
77             }
78 122         640 $value =~ tr/-()0-9.//cd; #strip any formatting characters
79 122 100       535 $value = "-$value" if $value =~ s/(^\()|(\)$)//g; # handle parens
80              
81 122 100       558 if ( (caller)[0] =~ /Math\::BigInt/ ) # only when called from objectify()
82             {
83 21         72 return Math::BigFloat->new($value);
84             }
85              
86 101         186 my $self;
87 101         167 my $currency = shift;
88 101         138 my $format;
89              
90 101 100 66     481 if ( not defined $currency and $class->isa('Math::Currency') ) {
91             # must be one of our subclasses
92 92 100       1102 $currency = $1 if ($class =~ /Math::Currency::(\w+)/);
93             }
94              
95 101 100       222 if ( defined $currency ) #override default currency type
96             {
97 21 100       69 unless ( defined $LC_MONETARY->{$currency} ) {
98 12         580 eval "require Math::Currency::$currency";
99 12 50       99 unknown_currency($currency) if $@;
100             }
101 21         47 $format = $LC_MONETARY->{$currency};
102             }
103              
104 101 100 66     311 if ($format) {
    50          
105             $self =
106 9         50 Math::BigFloat->new( $value, undef, -( $format->{FRAC_DIGITS} + 2 ) );
107 9         2694 bless $self, $class;
108 9         32 $self->format($format);
109             }
110             elsif ( $parent
111             and defined $parent->{format} ) # if we are cloning an existing instance
112             {
113             $self =
114             Math::BigFloat->new( $value, undef,
115 0         0 -( $parent->format->{FRAC_DIGITS} + 2 ) );
116 0         0 bless $self, $class;
117 0         0 $self->format( $parent->format );
118             }
119             else {
120             $self =
121 92         467 Math::BigFloat->new( $value, undef, -( $FORMAT->{FRAC_DIGITS} + 2 ) );
122 92         22882 bless $self, $class;
123             }
124 101         389 return $self;
125             } ##new
126              
127              
128             sub Money {
129 2     2 1 7 return __PACKAGE__->new(@_);
130             }
131              
132             sub bstr {
133 57     57 1 17372 my $self = shift;
134 57         138 my $myformat = $self->format();
135 57         133 my $value = $self->as_float();
136 57         126 my $neg = ( $value =~ tr/-//d );
137 57         123 my $dp = index( $value, "." );
138             my $sign = $neg
139             ? $myformat->{NEGATIVE_SIGN}
140 57 100       156 : $myformat->{POSITIVE_SIGN};
141             my $curr = $use_int
142             ? $myformat->{INT_CURR_SYMBOL}
143 57 100       133 : $myformat->{CURRENCY_SYMBOL};
144             my $digits = $use_int
145             ? $myformat->{INT_FRAC_DIGITS}
146 57 100       112 : $myformat->{FRAC_DIGITS};
147 57         462 my $formtab = [
148             [
149             [ '($value$curr)', '($value $curr)', '($value $curr)' ],
150             [ '$sign$value$curr', '$sign$value $curr', '$sign$value $curr' ],
151             [ '$value$curr$sign', '$value $curr$sign', '$value$curr $sign' ],
152             [ '$value$sign$curr', '$value $sign$curr', '$value$sign $curr' ],
153             [ '$value$curr$sign', '$value $curr$sign', '$value$curr $sign' ],
154             ],
155             [
156             [ '($curr$value)', '($curr $value)', '($curr $value)' ],
157             [ '$sign$curr$value', '$sign$curr $value', '$sign $curr$value' ],
158             [ '$curr$value$sign', '$curr $value$sign', '$curr$value $sign' ],
159             [ '$sign$curr$value', '$sign$curr $value', '$sign $curr$value' ],
160             [ '$curr$sign$value', '$curr$sign $value', '$curr $sign$value' ],
161             ],
162             ];
163              
164 57 100       190 if ( $dp < 0 ) {
    50          
165 12         26 $value .= '.' . '0' x $digits;
166             }
167             elsif ( ( length($value) - $dp - 1 ) < $digits ) {
168 0         0 $value .= '0' x ( $digits - $dp );
169             }
170              
171 57         160 ( $value = reverse "$value" ) =~ s/\+//;
172              
173             # make sure there is a leading 0 for values < 1
174 57 50       152 if ( substr( $value, -1, 1 ) eq '.' ) {
175 0         0 $value .= "0";
176             }
177 57         268 $value =~ s/\./$myformat->{MON_DECIMAL_POINT}/;
178 57         471 $value =~
179             s/(\d{$myformat->{MON_GROUPING}})(?=\d)(?!\d*\.)/$1$myformat->{MON_THOUSANDS_SEP}/g;
180 57         168 $value = reverse $value;
181              
182             eval '$value = "'
183             . (
184             $neg
185             ? $formtab->[ $myformat->{N_CS_PRECEDES} ][ $myformat->{N_SIGN_POSN} ]
186             [ $myformat->{N_SEP_BY_SPACE} ]
187             : $formtab->[ $myformat->{P_CS_PRECEDES} ][ $myformat->{P_SIGN_POSN} ]
188 57 100       4001 [ $myformat->{P_SEP_BY_SPACE} ]
189             )
190             . '"';
191              
192 57 100       322 if ( substr( $value, -1, 1 ) eq '.' ) { # trailing bare decimal
193 12         27 chop($value);
194             }
195              
196 57         733 return $value;
197             } ##stringify
198              
199              
200              
201             sub format {
202 364     364 1 18645 my $self = shift;
203 364         515 my $key = shift; # do they want to display or set?
204 364         477 my $value = shift; # did they supply a value?
205 364 50       748 localize() if $always_init; # always reset the global format?
206 364         562 my $source = \$FORMAT; # default format rules
207              
208 364 100       716 if ( ref($self) ) {
209 286 100       968 if ( defined $self->{format} ) {
    100          
210 46 100 100     128 if ( defined $key and $key eq '' ) {
211 2         8 delete $self->{format};
212 2         29 $source = \$FORMAT;
213             }
214             else {
215 44         72 $source = \$self->{format};
216             }
217             }
218             elsif ( defined $key ) # get/set a parameter
219             {
220 37 100 100     181 if ( defined $value
221             or ref($key) eq "HASH" ) # have to copy global format
222             {
223 35         76 while ( my ( $k, $v ) = each %{$FORMAT} ) {
  560         1356  
224 525         958 $self->{format}{$k} = $v;
225             }
226 35         75 $source = \$self->{format};
227             }
228             }
229             }
230             else { # called as class method to set the default currency
231 78 100 66     315 if (defined $key && not exists $FORMAT->{$key}) {
232 18 100       78 unless (defined $LC_MONETARY->{$key}) {
233 10         648 eval "require Math::Currency::$key";
234 10 100       71 unknown_currency($key) if $@;
235             }
236 18         44 $FORMAT = $LC_MONETARY->{$key};
237 18         70 return $FORMAT;
238             }
239             }
240              
241              
242 346 100       632 if ( defined $key ) # otherwise just return
243             {
244 103 100       189 if ( ref($key) eq "HASH" ) # must be trying to replace all
245             {
246 31         61 $$source = $key;
247             }
248             else # get/set just one parameter
249             {
250 72 100       310 return $$source->{$key} unless defined $value;
251 6         11 $$source->{$key} = $value;
252             }
253             }
254 280         543 return $$source;
255             } ##format
256              
257              
258             sub as_float {
259 186     186 1 291 my $self = shift;
260 186         356 my $format = $self->format;
261 186         386 my $string = $self->copy->bfround( -$format->{FRAC_DIGITS} )->SUPER::bstr();
262 186         33810 return $string;
263             }
264              
265              
266             sub copy {
267 203     203 1 4412 my $self = shift;
268              
269             # grab the builtin formatting
270 203 100       413 my $myformat = ( defined $self->{format} ? $self->{format} : undef );
271              
272             # let Math::BigFloat do it's thing
273 203         557 my $new = $self->SUPER::copy(@_);
274              
275 203 100       5805 if ($myformat) {
276              
277             # make sure we keep the original formatting
278 22         42 $new->format($myformat);
279             }
280              
281             # done...
282 203         628 return $new;
283             }
284              
285              
286              
287             sub as_int {
288 4     4 1 17 my $self = shift;
289 4         13 (my $str = $self->as_float) =~ s/\.//o;
290 4         19 $str =~ s/^(\-?)0+/$1/o;
291 4 50       25 return $str eq '' ? '0' : $str;
292             }
293              
294             # we override the default here because we only want to compare the precision of
295             # the currency we're dealing with, not the precision of the underlying object
296             sub bcmp {
297             # See RT #115247, #115761
298             # bcmp() might get called in comparison overoad as an object method with
299             # one arg, or, as a class method with two args depending on the version of
300             # Math::BigInt that is installed. Workaround is to check if @_ has three
301             # args, and if so, the first one is the class name. Otherwise, the first
302             # arg is the left side is an object that bcmp() is called on.
303             # An alternate solution to this is to require Math::BigInt >= 1.999718
304             # which always uses bcmp() as an object method.
305 61 100   61 1 13549 my $class = (@_ == 3) ? shift : __PACKAGE__;
306              
307             # make sure we're dealing with two Math::Currency objects
308             my ( $x, $y ) =
309 61 100       150 map { ref $_ ne $class ? $class->new($_) : $_ } @_[ 0, 1 ];
  122         356  
310              
311 61         150 return $x->as_float <=> $y->as_float;
312             }
313              
314              
315             sub localize {
316 14     14 1 35 my $self = shift;
317 14   50     126 my $format = shift || \$FORMAT;
318              
319 14         158 my $localeconv = POSIX::localeconv();
320              
321             # localeconv()'s character encoding depends on the current locale setting,
322             # so it is necessary to decode the currency symbol.
323 14         68 Encode::Locale::reinit(); # neede in case locale was changed with setlocale()
324              
325 14         1094 for my $key (keys %$localeconv) {
326             # POSIX::localeconv() changed behaviour between 5.20 and 5.22. As of
327             # 5.22, it sets the UTF-8 flag for localeconv() returned data if hte
328             # data is in UTF-8 format, so if POSIX::localeconv() already turned on
329             # the UTF-8 flag, we should not decode the data.
330 14 50       95 unless (utf8::is_utf8($$localeconv{$key})) {
331 14         86 $$localeconv{$key} = Encode::decode(locale => $$localeconv{$key});
332             }
333             }
334              
335             # so you can test to see if locale was effective
336 14 50 33     1488 return 0 if ! exists $localeconv->{'currency_symbol'} || $localeconv->{'currency_symbol'} eq '';
337              
338             $$format = {
339             INT_CURR_SYMBOL => $localeconv->{'int_curr_symbol'} || '',
340             CURRENCY_SYMBOL => $localeconv->{'currency_symbol'} || '',
341             MON_DECIMAL_POINT => $localeconv->{'mon_decimal_point'} || '',
342             MON_THOUSANDS_SEP => $localeconv->{'mon_thousands_sep'} || '',
343             MON_GROUPING => (
344             exists $localeconv->{'mon_grouping'}
345             and defined $localeconv->{'mon_grouping'}
346             and ord( $localeconv->{'mon_grouping'} ) < 47
347             ? ord( $localeconv->{'mon_grouping'} )
348             : $localeconv->{'mon_grouping'}
349             )
350             || 0,
351             POSITIVE_SIGN => $localeconv->{'positive_sign'} || '',
352             NEGATIVE_SIGN => $localeconv->{'negative_sign'} || '-',
353             INT_FRAC_DIGITS => $localeconv->{'int_frac_digits'} || 0,
354             FRAC_DIGITS => $localeconv->{'frac_digits'} || 0,
355             P_CS_PRECEDES => $localeconv->{'p_cs_precedes'} || 0,
356             P_SEP_BY_SPACE => $localeconv->{'p_sep_by_space'} || 0,
357             N_CS_PRECEDES => $localeconv->{'n_cs_precedes'} || 0,
358             N_SEP_BY_SPACE => $localeconv->{'n_sep_by_space'} || 0,
359             P_SIGN_POSN => $localeconv->{'p_sign_posn'} || 1,
360 0   0     0 N_SIGN_POSN => $localeconv->{'n_sign_posn'} || 0,
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
361             };
362              
363 0         0 return 1;
364             }
365              
366              
367             {
368             my $locales;
369             sub available_locales {
370 19 50   19 1 103 return if $^O =~ /Win32/; # cant run locale -a on windows
371              
372 19 100       46 unless (defined $locales) {
373 2         5 $locales = [];
374              
375 2 50       9132 open my $fh, '-|', 'locale -a' or die $!;
376              
377 2         1672 while (my $locale = <$fh>) {
378 6         32 chomp $locale;
379 6         78 push @$locales, $locale;
380             }
381              
382 2         133 close $fh;
383             }
384              
385 19         98 return @$locales;
386             }
387             }
388              
389             # if no currency module exists for the requested currency, this function tries
390             # to find the currency settings from the available locales
391             sub unknown_currency {
392 19     19 0 59 my ($currency) = @_;
393              
394             # remember current locale
395 19         82 my $original_locale = setlocale( LC_ALL );
396              
397             # we need to save a copy of $VERSION here becuase the effective locale can
398             # render $VERSION as X,YY instead of Y.YY for exmaple
399 19         40 my $version = "$Math::Currency::VERSION";
400              
401 19         56 for my $LOCALE (available_locales()) {
402 57         2114 setlocale( LC_ALL, $LOCALE );
403 57         458 my $localeconv = POSIX::localeconv();
404 57 50 50     549 if ( $LOCALE eq $currency or
      33        
405             ($localeconv->{int_curr_symbol} || '') =~ /$currency/ )
406             {
407 0         0 my $format = \$LC_MONETARY->{$currency};
408 0         0 Math::Currency->localize($format);
409 0         0 (my $int_curr = $$format->{INT_CURR_SYMBOL}) =~ s/ //g;
410             $LC_MONETARY->{$int_curr} = $LC_MONETARY->{$currency}
411 0 0       0 unless exists $LC_MONETARY->{$int_curr};
412 0         0 eval <<"EOP";
413             package Math::Currency::${LOCALE};
414              
415             use base 'Math::Currency';
416             our \$VERSION = $version;
417             our \$LANG = '$LOCALE';
418              
419             1;
420              
421             package Math::Currency::${int_curr};
422              
423             use base 'Math::Currency';
424              
425             our \$VERSION = $version;
426             our \$LANG = '$LOCALE';
427              
428             1;
429             EOP
430 0         0 last;
431             }
432             }
433              
434             # restore the original locale
435 19         163 setlocale( LC_ALL, $original_locale );
436             }
437              
438             # additional methods needed to get/set package globals
439              
440              
441             sub always_init {
442 1     1 1 5 my $class = shift;
443              
444 1 50       3 if (@_) {
445 0         0 $always_init = shift;
446             }
447              
448 1         2 return $always_init;
449             }
450              
451              
452             sub use_int {
453 1     1 1 79 my $class = shift;
454              
455 1 50       4 if (@_) {
456 0         0 $use_int = shift;
457             }
458              
459 1         3 return $use_int;
460             }
461              
462             1;
463              
464             __END__