File Coverage

blib/lib/Math/Currency.pm
Criterion Covered Total %
statement 148 164 90.2
branch 74 90 82.2
condition 19 57 33.3
subroutine 21 21 100.0
pod 12 13 92.3
total 274 345 79.4


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.51';
18             # ABSTRACT: Exact Currency Math with Formatting and Rounding
19              
20 14     14   164756 use strict;
  14         24  
  14         348  
21 14     14   3695 use utf8;
  14         69  
  14         55  
22 14     14   336 use base qw(Exporter Math::BigFloat);
  14         18  
  14         12572  
23 14     14   223499 use Math::BigFloat 1.60;
  14         206  
  14         69  
24 14     14   385328 use POSIX qw(locale_h);
  14         50039  
  14         79  
25 14     14   16672 use Encode::Locale;
  14         136751  
  14         538  
26 14     14   83 use Encode ();
  14         18  
  14         347  
27              
28 14     14   47 use overload '""' => \&bstr;
  14         18  
  14         137  
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 112     112 1 15802 my $proto = shift;
70 112   66     385 my $class = ref($proto) || $proto;
71 112 100       239 my $parent = $proto if ref($proto);
72              
73 112   100     176 my $value = shift || 0;
74              
75 112 50       5764 if (eval("'1.01' ne 1.01")) { # this might be a comma locale
76 0         0 $value =~ tr/,/./;
77             }
78 112         442 $value =~ tr/-()0-9.//cd; #strip any formatting characters
79 112 100       370 $value = "-$value" if $value =~ s/(^\()|(\)$)//g; # handle parens
80              
81 112 100       388 if ( (caller)[0] =~ /Math\::BigInt/ ) # only when called from objectify()
82             {
83 21         58 return Math::BigFloat->new($value);
84             }
85              
86 91         90 my $self;
87 91         85 my $currency = shift;
88 91         70 my $format;
89              
90 91 100 66     399 if ( not defined $currency and $class->isa('Math::Currency') ) {
91             # must be one of our subclasses
92 82 100       1272 $currency = $1 if ($class =~ /Math::Currency::(\w+)/);
93             }
94              
95 91 100       170 if ( defined $currency ) #override default currency type
96             {
97 29 100       77 unless ( defined $LC_MONETARY->{$currency} ) {
98 20         851 eval "require Math::Currency::$currency";
99 20 50       113 unknown_currency($currency) if $@;
100             }
101 29         43 $format = $LC_MONETARY->{$currency};
102             }
103              
104 91 100 66     243 if ($format) {
    50          
105             $self =
106 9         49 Math::BigFloat->new( $value, undef, -( $format->{FRAC_DIGITS} + 2 ) );
107 9         1613 bless $self, $class;
108 9         25 $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 82         369 Math::BigFloat->new( $value, undef, -( $FORMAT->{FRAC_DIGITS} + 2 ) );
122 82         12114 bless $self, $class;
123             }
124 91         223 return $self;
125             } ##new
126              
127              
128             sub Money {
129 2     2 1 7 return __PACKAGE__->new(@_);
130             }
131              
132             sub bstr {
133 37     37 1 10815 my $self = shift;
134 37         70 my $myformat = $self->format();
135 37         68 my $value = $self->as_float();
136 37         62 my $neg = ( $value =~ tr/-//d );
137 37         58 my $dp = index( $value, "." );
138             my $sign = $neg
139             ? $myformat->{NEGATIVE_SIGN}
140 37 100       75 : $myformat->{POSITIVE_SIGN};
141             my $curr = $use_int
142             ? $myformat->{INT_CURR_SYMBOL}
143 37 100       68 : $myformat->{CURRENCY_SYMBOL};
144             my $digits = $use_int
145             ? $myformat->{INT_FRAC_DIGITS}
146 37 100       59 : $myformat->{FRAC_DIGITS};
147 37         300 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 37 100       118 if ( $dp < 0 ) {
    50          
165 8         11 $value .= '.' . '0' x $digits;
166             }
167             elsif ( ( length($value) - $dp - 1 ) < $digits ) {
168 0         0 $value .= '0' x ( $digits - $dp );
169             }
170              
171 37         80 ( $value = reverse "$value" ) =~ s/\+//;
172              
173             # make sure there is a leading 0 for values < 1
174 37 50       75 if ( substr( $value, -1, 1 ) eq '.' ) {
175 0         0 $value .= "0";
176             }
177 37         142 $value =~ s/\./$myformat->{MON_DECIMAL_POINT}/;
178 37         269 $value =~
179             s/(\d{$myformat->{MON_GROUPING}})(?=\d)(?!\d*\.)/$1$myformat->{MON_THOUSANDS_SEP}/g;
180 37         68 $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 37 100       2338 [ $myformat->{P_SEP_BY_SPACE} ]
189             )
190             . '"';
191              
192 37 100       158 if ( substr( $value, -1, 1 ) eq '.' ) { # trailing bare decimal
193 8         12 chop($value);
194             }
195              
196 37         492 return $value;
197             } ##stringify
198              
199              
200              
201             sub format {
202 322     322 1 13107 my $self = shift;
203 322         277 my $key = shift; # do they want to display or set?
204 322         226 my $value = shift; # did they supply a value?
205 322 50       497 localize() if $always_init; # always reset the global format?
206 322         271 my $source = \$FORMAT; # default format rules
207              
208 322 100       437 if ( ref($self) ) {
209 244 100       654 if ( defined $self->{format} ) {
    100          
210 40 100 100     98 if ( defined $key and $key eq '' ) {
211 2         8 delete $self->{format};
212 2         3 $source = \$FORMAT;
213             }
214             else {
215 38         48 $source = \$self->{format};
216             }
217             }
218             elsif ( defined $key ) # get/set a parameter
219             {
220 35 100 100     156 if ( defined $value
221             or ref($key) eq "HASH" ) # have to copy global format
222             {
223 33         33 while ( my ( $k, $v ) = each %{$FORMAT} ) {
  528         865  
224 495         613 $self->{format}{$k} = $v;
225             }
226 33         50 $source = \$self->{format};
227             }
228             }
229             }
230             else { # called as class method to set the default currency
231 78 100 66     292 if ( defined $key && not exists $FORMAT->{$key} ) {
232 18 100       54 unless ( defined $LC_MONETARY->{$key} ) {
233 10         652 eval "require Math::Currency::$key";
234 10 100       56 unknown_currency($key) if $@;
235             }
236 18         34 $FORMAT = $LC_MONETARY->{$key};
237 18         74 return $FORMAT;
238             }
239             }
240              
241              
242 304 100       406 if ( defined $key ) # otherwise just return
243             {
244 101 100       134 if ( ref($key) eq "HASH" ) # must be trying to replace all
245             {
246 29         44 $$source = $key;
247             }
248             else # get/set just one parameter
249             {
250 72 100       267 return $$source->{$key} unless defined $value;
251 6         12 $$source->{$key} = $value;
252             }
253             }
254 238         313 return $$source;
255             } ##format
256              
257              
258             sub as_float {
259 166     166 1 141 my $self = shift;
260 166         229 my $format = $self->format;
261 166         229 my $string = $self->copy->bfround( -$format->{FRAC_DIGITS} )->SUPER::bstr();
262 166         18318 return $string;
263             }
264              
265              
266             sub copy {
267 183     183 1 2847 my $self = shift;
268              
269             # grab the builtin formatting
270 183 100       237 my $myformat = ( defined $self->{format} ? $self->{format} : undef );
271              
272             # let Math::BigFloat do it's thing
273 183         408 my $new = $self->SUPER::copy(@_);
274              
275 183 100       2323 if ($myformat) {
276              
277             # make sure we keep the original formatting
278 20         33 $new->format($myformat);
279             }
280              
281             # done...
282 183         460 return $new;
283             }
284              
285              
286              
287             sub as_int {
288 4     4 1 13 my $self = shift;
289 4         9 (my $str = $self->as_float) =~ s/\.//o;
290 4         14 $str =~ s/^(\-?)0+/$1/o;
291 4 50       24 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 8979 my $class = (@_ == 3) ? shift : __PACKAGE__;
306              
307             # make sure we're dealing with two Math::Currency objects
308             my ( $x, $y ) =
309 61 100       114 map { ref $_ ne $class ? $class->new($_) : $_ } @_[ 0, 1 ];
  122         263  
310              
311 61         107 return $x->as_float <=> $y->as_float;
312             }
313              
314              
315             sub localize {
316 14     14 1 19 my $self = shift;
317 14   50     93 my $format = shift || \$FORMAT;
318              
319 14         116 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         34 Encode::Locale::reinit(); # neede in case locale was changed with setlocale()
324              
325 14         602 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       55 unless (utf8::is_utf8($$localeconv{$key})) {
331 14         41 $$localeconv{$key} = Encode::decode(locale => $$localeconv{$key});
332             }
333             }
334              
335             # so you can test to see if locale was effective
336 14 50       836 return 0 if ! exists $localeconv->{'currency_symbol'};
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 27 50   27 1 121 return if $^O =~ /Win32/; # cant run locale -a on windows
371              
372 27 100       54 unless (defined $locales) {
373 2         3 $locales = [];
374              
375 2 50       3980 open my $fh, '-|', 'locale -a' or die $!;
376              
377 2         1228 while (my $locale = <$fh>) {
378 6         14 chomp $locale;
379 6         99 push @$locales, $locale;
380             }
381              
382 2         79 close $fh;
383             }
384              
385 27         88 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 27     27 0 39 my ($currency) = @_;
393              
394             # remember current locale
395 27         90 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 27         43 my $version = "$Math::Currency::VERSION";
400              
401 27         40 for my $LOCALE (available_locales()) {
402 81         21120704 setlocale( LC_ALL, $LOCALE );
403 81         525 my $localeconv = POSIX::localeconv();
404 81 50 50     725 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 27         193 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 4 my $class = shift;
443              
444 1 50       3 if (@_) {
445 0         0 $always_init = shift;
446             }
447              
448 1         3 return $always_init;
449             }
450              
451              
452             sub use_int {
453 1     1 1 53 my $class = shift;
454              
455 1 50       4 if (@_) {
456 0         0 $use_int = shift;
457             }
458              
459 1         2 return $use_int;
460             }
461              
462             1;
463              
464             __END__