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.52';
18             # ABSTRACT: Exact Currency Math with Formatting and Rounding
19              
20 14     14   838642 use strict;
  14         46  
  14         558  
21 14     14   3651 use utf8;
  14         131  
  14         93  
22 14     14   546 use base qw(Exporter Math::BigFloat);
  14         46  
  14         12709  
23 14     14   884611 use Math::BigFloat 1.60;
  14         433  
  14         107  
24 14     14   374030 use POSIX qw(locale_h);
  14         82934  
  14         145  
25 14     14   27811 use Encode::Locale;
  14         216706  
  14         947  
26 14     14   153 use Encode ();
  14         60  
  14         573  
27              
28 14     14   100 use overload '""' => \&bstr;
  14         57  
  14         208  
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 39777 my $proto = shift;
70 122   66     632 my $class = ref($proto) || $proto;
71 122 100       440 my $parent = $proto if ref($proto);
72              
73 122   100     460 my $value = shift || 0;
74              
75 122 50       8831 if (eval("'1.01' ne 1.01")) { # this might be a comma locale
76 0         0 $value =~ tr/,/./;
77             }
78 122         851 $value =~ tr/-()0-9.//cd; #strip any formatting characters
79 122 100       716 $value = "-$value" if $value =~ s/(^\()|(\)$)//g; # handle parens
80              
81 122 100       750 if ( (caller)[0] =~ /Math\::BigInt/ ) # only when called from objectify()
82             {
83 21         125 return Math::BigFloat->new($value);
84             }
85              
86 101         253 my $self;
87 101         236 my $currency = shift;
88 101         184 my $format;
89              
90 101 100 66     739 if ( not defined $currency and $class->isa('Math::Currency') ) {
91             # must be one of our subclasses
92 92 100       1492 $currency = $1 if ($class =~ /Math::Currency::(\w+)/);
93             }
94              
95 101 100       343 if ( defined $currency ) #override default currency type
96             {
97 21 100       97 unless ( defined $LC_MONETARY->{$currency} ) {
98 12         742 eval "require Math::Currency::$currency";
99 12 50       105 unknown_currency($currency) if $@;
100             }
101 21         66 $format = $LC_MONETARY->{$currency};
102             }
103              
104 101 100 66     458 if ($format) {
    50          
105             $self =
106 9         88 Math::BigFloat->new( $value, undef, -( $format->{FRAC_DIGITS} + 2 ) );
107 9         4153 bless $self, $class;
108 9         48 $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         652 Math::BigFloat->new( $value, undef, -( $FORMAT->{FRAC_DIGITS} + 2 ) );
122 92         30177 bless $self, $class;
123             }
124 101         476 return $self;
125             } ##new
126              
127              
128             sub Money {
129 2     2 1 16 return __PACKAGE__->new(@_);
130             }
131              
132             sub bstr {
133 57     57 1 23915 my $self = shift;
134 57         209 my $myformat = $self->format();
135 57         226 my $value = $self->as_float();
136 57         187 my $neg = ( $value =~ tr/-//d );
137 57         160 my $dp = index( $value, "." );
138             my $sign = $neg
139             ? $myformat->{NEGATIVE_SIGN}
140 57 100       203 : $myformat->{POSITIVE_SIGN};
141             my $curr = $use_int
142             ? $myformat->{INT_CURR_SYMBOL}
143 57 100       192 : $myformat->{CURRENCY_SYMBOL};
144             my $digits = $use_int
145             ? $myformat->{INT_FRAC_DIGITS}
146 57 100       162 : $myformat->{FRAC_DIGITS};
147 57         678 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       297 if ( $dp < 0 ) {
    50          
165 12         32 $value .= '.' . '0' x $digits;
166             }
167             elsif ( ( length($value) - $dp - 1 ) < $digits ) {
168 0         0 $value .= '0' x ( $digits - $dp );
169             }
170              
171 57         245 ( $value = reverse "$value" ) =~ s/\+//;
172              
173             # make sure there is a leading 0 for values < 1
174 57 50       203 if ( substr( $value, -1, 1 ) eq '.' ) {
175 0         0 $value .= "0";
176             }
177 57         425 $value =~ s/\./$myformat->{MON_DECIMAL_POINT}/;
178 57         631 $value =~
179             s/(\d{$myformat->{MON_GROUPING}})(?=\d)(?!\d*\.)/$1$myformat->{MON_THOUSANDS_SEP}/g;
180 57         192 $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       4574 [ $myformat->{P_SEP_BY_SPACE} ]
189             )
190             . '"';
191              
192 57 100       372 if ( substr( $value, -1, 1 ) eq '.' ) { # trailing bare decimal
193 12         40 chop($value);
194             }
195              
196 57         966 return $value;
197             } ##stringify
198              
199              
200              
201             sub format {
202 364     364 1 33627 my $self = shift;
203 364         731 my $key = shift; # do they want to display or set?
204 364         621 my $value = shift; # did they supply a value?
205 364 50       928 localize() if $always_init; # always reset the global format?
206 364         711 my $source = \$FORMAT; # default format rules
207              
208 364 100       951 if ( ref($self) ) {
209 286 100       1304 if ( defined $self->{format} ) {
    100          
210 46 100 100     153 if ( defined $key and $key eq '' ) {
211 2         10 delete $self->{format};
212 2         9 $source = \$FORMAT;
213             }
214             else {
215 44         96 $source = \$self->{format};
216             }
217             }
218             elsif ( defined $key ) # get/set a parameter
219             {
220 37 100 100     230 if ( defined $value
221             or ref($key) eq "HASH" ) # have to copy global format
222             {
223 35         77 while ( my ( $k, $v ) = each %{$FORMAT} ) {
  560         1759  
224 525         4041 $self->{format}{$k} = $v;
225             }
226 35         96 $source = \$self->{format};
227             }
228             }
229             }
230             else { # called as class method to set the default currency
231 78 100 66     414 if ( defined $key && not exists $FORMAT->{$key} ) {
232 18 100       88 unless ( defined $LC_MONETARY->{$key} ) {
233 10         718 eval "require Math::Currency::$key";
234 10 100       84 unknown_currency($key) if $@;
235             }
236 18         67 $FORMAT = $LC_MONETARY->{$key};
237 18         84 return $FORMAT;
238             }
239             }
240              
241              
242 346 100       824 if ( defined $key ) # otherwise just return
243             {
244 103 100       269 if ( ref($key) eq "HASH" ) # must be trying to replace all
245             {
246 31         87 $$source = $key;
247             }
248             else # get/set just one parameter
249             {
250 72 100       483 return $$source->{$key} unless defined $value;
251 6         21 $$source->{$key} = $value;
252             }
253             }
254 280         778 return $$source;
255             } ##format
256              
257              
258             sub as_float {
259 186     186 1 370 my $self = shift;
260 186         474 my $format = $self->format;
261 186         517 my $string = $self->copy->bfround( -$format->{FRAC_DIGITS} )->SUPER::bstr();
262 186         44050 return $string;
263             }
264              
265              
266             sub copy {
267 203     203 1 9150 my $self = shift;
268              
269             # grab the builtin formatting
270 203 100       530 my $myformat = ( defined $self->{format} ? $self->{format} : undef );
271              
272             # let Math::BigFloat do it's thing
273 203         792 my $new = $self->SUPER::copy(@_);
274              
275 203 100       7414 if ($myformat) {
276              
277             # make sure we keep the original formatting
278 22         64 $new->format($myformat);
279             }
280              
281             # done...
282 203         884 return $new;
283             }
284              
285              
286              
287             sub as_int {
288 4     4 1 24 my $self = shift;
289 4         13 (my $str = $self->as_float) =~ s/\.//o;
290 4         28 $str =~ s/^(\-?)0+/$1/o;
291 4 50       41 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 20043 my $class = (@_ == 3) ? shift : __PACKAGE__;
306              
307             # make sure we're dealing with two Math::Currency objects
308             my ( $x, $y ) =
309 61 100       207 map { ref $_ ne $class ? $class->new($_) : $_ } @_[ 0, 1 ];
  122         498  
310              
311 61         238 return $x->as_float <=> $y->as_float;
312             }
313              
314              
315             sub localize {
316 14     14 1 53 my $self = shift;
317 14   50     168 my $format = shift || \$FORMAT;
318              
319 14         200 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         96 Encode::Locale::reinit(); # neede in case locale was changed with setlocale()
324              
325 14         1730 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       91 unless (utf8::is_utf8($$localeconv{$key})) {
331 14         82 $$localeconv{$key} = Encode::decode(locale => $$localeconv{$key});
332             }
333             }
334              
335             # so you can test to see if locale was effective
336 14 50       2113 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 19 50   19 1 108 return if $^O =~ /Win32/; # cant run locale -a on windows
371              
372 19 100       59 unless (defined $locales) {
373 2         14 $locales = [];
374              
375 2 50       8275 open my $fh, '-|', 'locale -a' or die $!;
376              
377 2         1474 while (my $locale = <$fh>) {
378 6         26 chomp $locale;
379 6         80 push @$locales, $locale;
380             }
381              
382 2         87 close $fh;
383             }
384              
385 19         87 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 56 my ($currency) = @_;
393              
394             # remember current locale
395 19         282 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         57 my $version = "$Math::Currency::VERSION";
400              
401 19         56 for my $LOCALE (available_locales()) {
402 57         1836 setlocale( LC_ALL, $LOCALE );
403 57         498 my $localeconv = POSIX::localeconv();
404 57 50 50     590 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         172 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 8 my $class = shift;
443              
444 1 50       6 if (@_) {
445 0         0 $always_init = shift;
446             }
447              
448 1         4 return $always_init;
449             }
450              
451              
452             sub use_int {
453 1     1 1 118 my $class = shift;
454              
455 1 50       6 if (@_) {
456 0         0 $use_int = shift;
457             }
458              
459 1         5 return $use_int;
460             }
461              
462             1;
463              
464             __END__