File Coverage

blib/lib/Number/Format/SouthAsian.pm
Criterion Covered Total %
statement 76 79 96.2
branch 19 24 79.1
condition 7 10 70.0
subroutine 11 11 100.0
pod 2 2 100.0
total 115 126 91.2


line stmt bran cond sub pod time code
1 4     4   87138 use strict;
  4         10  
  4         169  
2 4     4   26 use warnings;
  4         9  
  4         251  
3              
4             package Number::Format::SouthAsian;
5             {
6             $Number::Format::SouthAsian::VERSION = '0.09';
7             }
8              
9 4     4   34 use Carp;
  4         6  
  4         634  
10 4     4   5136 use English qw(-no_match_vars);
  4         19280  
  4         25  
11 4     4   2005 use Scalar::Util qw(looks_like_number);
  4         9  
  4         5110  
12              
13             =head1 NAME
14              
15             Number::Format::SouthAsian - format numbers in the South Asian style
16              
17             =head1 VERSION
18              
19             version 0.09
20              
21             =head1 SYNOPSIS
22              
23             Formats numbers in the South Asian style. You can read more on Wikipedia here:
24              
25             http://en.wikipedia.org/wiki/South_Asian_numbering_system
26              
27             The format_number() method has a words parameter which tells it to use
28             words rather than simply separating the numbers with commas.
29              
30             my $formatter = Number::Format::SouthAsian->new();
31             say $formatter->format_number(12345678); # 1,23,45,678
32             say $formatter->format_number(12345678, words => 1); # 1.2345678 crores
33              
34             You can also specify words to new(), which has the effect of setting a
35             default value to be used.
36              
37             my $formatter = Number::Format::SouthAsian->new(words => 1);
38             say $formatter->format_number(12345678); # 1.2345678 crores
39             say $formatter->format_number(12345678, words => 0); # 1,23,45,678
40              
41             You can also specify "decimals" to either new() or format_number(), which has
42             the effect of rounding any decimals found. Note that this means slightly
43             different things depending on wordiness.
44              
45             my $rounding_formatter = Number::Format::SouthAsian->new(decimals => 2);
46             say $rounding_formatter->format_number(1234.5678); # 1,234.57
47             say $rounding_formatter->format_number(12345678, words => 1); # 1.23 crores
48              
49             =head1 METHODS
50              
51             =head2 new
52              
53             Optionally takes a named parameter 'words' which sets the default of the
54             'words' parameter to format_number.
55              
56             my $normal_formatter = Number::Format::SouthAsian->new();
57             my $wordy_formatter = Number::Format::SouthAsian->new(words => 1);
58             my $rounding_formatter = Number::Format::SouthAsian->new(decimals => 2);
59              
60             =cut
61              
62             sub new {
63 6     6 1 1845 my $class = shift;
64 6         18 my %opts = @_;
65 6         23 my $self = bless {}, $class;
66              
67 6         96 $self->_init_defaults(%opts);
68              
69 6         20 return $self;
70             }
71              
72             sub _init_defaults {
73 6     6   10 my $self = shift;
74 6         15 my %opts = @_;
75              
76 6   100     57 $self->{'defaults'}{'words'} = $opts{'words'} || 0;
77 6   100     35 $self->{'defaults'}{'decimals'} = $opts{'decimals'} || 0;
78              
79 6         14 return;
80             }
81              
82             =head2 format_number
83              
84             Takes a positional parameter which should just be a number. Optionally takes
85             a named parameter 'words' which turns on or off the wordy behaviour. Returns
86             a string containing the number passed in formatted in the South Asian style.
87              
88             my $formatted_number = $formatter->format_number(12345678);
89              
90             my $formatted_number = $formatter->format_number(12345678, words => 1);
91              
92             =cut
93              
94             sub format_number {
95 142     142 1 80260 my $self = shift;
96 142         179 my $number = shift;
97 142         468 my %opts = @_;
98              
99 142 50 33     672 if (!defined($number) || !looks_like_number($number)) {
100 0         0 croak "First parameter to format_number() must be a number";
101             }
102              
103 142 100       419 my $want_words = exists($opts{'words'})
104             ? $opts{'words'}
105             : $self->{'defaults'}{'words'};
106              
107 142         143 my $result;
108              
109 142 100       217 if ($want_words) {
110 86         212 return $self->_format_number_wordy($number, %opts);
111             }
112             else {
113 56         347 return $self->_format_number_separators_only($number, %opts);
114             }
115             }
116              
117             my %zeroes_to_words = (
118             '5' => 'lakh',
119             '7' => 'crore',
120             '9' => 'arab',
121             '11' => 'kharab',
122             '13' => 'neel',
123             '15' => 'padma',
124             '17' => 'shankh',
125             '19' => 'maha shankh',
126             '21' => 'ank',
127             '23' => 'jald',
128             '25' => 'madh',
129             '27' => 'paraardha',
130             '29' => 'ant',
131             '31' => 'maha ant',
132             '33' => 'shisht',
133             '35' => 'singhar',
134             '37' => 'maha singhar',
135             '39' => 'adant singhar',
136             );
137              
138             sub _format_number_wordy {
139 86     86   90 my $self = shift;
140 86         102 my $number = shift;
141 86         235 my %opts = @_;
142              
143 86         233 my $zeroes = length($number) - 1;
144              
145             # scientific notation kicks in at a certain size...
146             # we have to get around that.
147 86 100       297 if ($number =~ m/^ ( \d+ (?: [.]\d+)?) e[+] (\d+) $/msx) {
148 20         61 my ($mantissa, $exponent) = ($1, $2);
149              
150             ## in MSWin32 the exponent has an extra 0 on the front...
151 20 50       89 if ($OSNAME eq 'MSWin32') {
152 0         0 $exponent =~ s/^0+//;
153             }
154              
155 20 50       40 if ($mantissa <= 1) {
156 20         32 $zeroes = $exponent;
157             }
158             else {
159 0         0 $zeroes = $exponent + 1;
160             }
161             }
162              
163 86 100       144 if ($zeroes < 5) {
164 10         25 return $self->_format_number_separators_only($number);
165             }
166              
167 76         161 my $divisor = "1" . ("0" x $zeroes);
168              
169 76   66     465 while (!$zeroes_to_words{$zeroes} || (($number / $divisor) < 1)) {
170 12         14 $zeroes -= 1;
171 12         70 $divisor /= 10;
172             }
173              
174 76         651 my $fraction = sprintf("%f", ($number / $divisor)); # force no scientific notation
175 76 50       215 if ($fraction =~ m/[.]/) {
176 76         234 $fraction =~ s/0+$//;
177 76         238 $fraction =~ s/[.]$//;
178             }
179              
180 76         120 my $word = $zeroes_to_words{$zeroes};
181              
182 76         179 $fraction = $self->_correct_decimals($fraction, %opts);
183              
184 76 100       190 my $pluralization = $fraction eq '1' ? '' : 's';
185              
186 76         183 my $words = sprintf('%s %s%s', $fraction, $word, $pluralization);
187              
188 76         748 return $words;
189             }
190              
191             sub _format_number_separators_only {
192 66     66   82 my $self = shift;
193 66         78 my $number = shift;
194 66         290 my %opts = @_;
195              
196 66         2896 $number =~ s{
197             (?:
198             (?<= \d{2})
199             (?= \d{3}$)
200             )
201             |
202             (?:
203             (?<= ^\d{1} )
204             (?= \d{3}$)
205             )
206             }{,}gmsx;
207              
208 66         1634 1 while $number =~ s{
209             (?
210             (?
211             (?= \d{2},)
212             }{,}gmsx;
213              
214 66         725 1 while $number =~ s{([.].*),}{$1}gmsx;
215              
216 66         166 $number = $self->_correct_decimals($number, %opts);
217              
218 66         1338 return $number;
219             }
220              
221             sub _correct_decimals {
222 142     142   314 my ($self, $number, %opts) = @_;
223              
224 142 100       369 my $decimals = exists($opts{'decimals'})
225             ? $opts{'decimals'}
226             : $self->{'defaults'}{'decimals'};
227              
228 142 100       273 if ($decimals) {
229 16         25 my $pattern = "%.${decimals}f";
230              
231 16         71 $number =~ s{
232             (\d+[.]\d+)
233             }{
234 16         89 sprintf($pattern, $1);
235             }egmsx;
236              
237 16 50       47 if ($number =~ m/[.]/) {
238 16         35 $number =~ s/0+$//;
239 16         29 $number =~ s/[.]$//;
240             }
241             }
242              
243 142         560 return $number;
244             }
245              
246             =head1 Copyright
247              
248             Copyright (C) 2010 Lokku Ltd.
249              
250             =head1 Author
251              
252             Alex Balhatchet (alex@lokku.com)
253              
254             =cut
255              
256             1;