File Coverage

blib/lib/Number/Format/SouthAsian.pm
Criterion Covered Total %
statement 81 84 96.4
branch 21 26 80.7
condition 13 18 72.2
subroutine 11 11 100.0
pod 2 2 100.0
total 128 141 90.7


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