File Coverage

blib/lib/Lingua/TH/Numbers.pm
Criterion Covered Total %
statement 80 84 95.2
branch 32 40 80.0
condition 17 21 80.9
subroutine 12 12 100.0
pod 4 4 100.0
total 145 161 90.0


line stmt bran cond sub pod time code
1             =encoding utf8
2              
3             =cut
4              
5             package Lingua::TH::Numbers;
6              
7 5     5   84273 use 5.008;
  5         19  
  5         213  
8 5     5   30 use strict;
  5         8  
  5         152  
9 5     5   27 use warnings;
  5         21  
  5         135  
10 5     5   1071 use utf8;
  5         17  
  5         23  
11              
12 5     5   141 use Carp;
  5         9  
  5         374  
13 5     5   7876 use Data::Dumper;
  5         69468  
  5         7173  
14              
15              
16             =head1 NAME
17              
18             Lingua::TH::Numbers - Convert and spell Thai numbers.
19              
20              
21             =head1 VERSION
22              
23             Version 1.0.8
24              
25             =cut
26              
27             our $VERSION = '1.0.8';
28              
29             # Digits from 1 to 9.
30             our $DIGITS =
31             {
32             # Thai RTGS
33             0 => [ "ศูนย์", 'sun', ],
34             1 => [ "หนึ่ง", 'nueng', ],
35             2 => [ "สอง", 'song', ],
36             3 => [ "สาม", 'sam', ],
37             4 => [ "สี่", 'si', ],
38             5 => [ "ห้า", 'ha', ],
39             6 => [ "หก", 'hok', ],
40             7 => [ "เจ็ด", 'chet', ],
41             8 => [ "แปด", 'paet', ],
42             9 => [ "เก้า", 'kao', ],
43             };
44              
45             # Powers of 10, from 1 to 1 million. Numbers above one million are formed using
46             # numbers below one million as a multiplier for 'lan'.
47             our $POWERS_OF_TEN =
48             {
49             # Thai RTGS
50             0 => [ '', '' ], # 1
51             1 => [ "สิบ", 'sip', ], # 10
52             2 => [ "ร้อย", 'roi', ], # 100
53             3 => [ "พัน", 'phan', ], # 1,000
54             4 => [ "หมื่น", 'muen', ], # 10,000
55             5 => [ "แสน", 'saen', ], # 100,000
56             6 => [ "ล้าน", 'lan', ], # 1,000,000
57             };
58              
59             # Minus sign for negative numbers.
60             # Thai RTGS
61             our $MINUS_SIGN = [ "ลบ", 'lop', ];
62              
63             # The '20' part of numbers from 20 to 29 is an exception.
64             # Thai RTGS
65             our $TWO_FOR_TWENTY = [ "ยี่", 'yi', ];
66              
67             # 11, 21, ..., 91 use 'et' instead of 'neung' for the trailing 1.
68             # Thai RTGS
69             our $TRAILING_ONE = [ "เอ็ด", 'et', ];
70              
71             # Decimal separator.
72             # Thai RTGS
73             our $DECIMAL_SEPARATOR = [ "จุด", 'chut', ];
74              
75             # Spelling output modes supported.
76             our $SPELLING_OUTPUT_MODES =
77             {
78             # Name Position in arrays of translations
79             'thai' => 0,
80             'rtgs' => 1,
81             };
82              
83              
84             =head1 SYNOPSIS
85              
86             use Lingua::TH::Numbers;
87              
88             # Input.
89             my $ten = Lingua::TH::Numbers->new( '10' );
90             my $sip = Lingua::TH::Numbers->new( '๑๐' );
91             my $lop_sip = Lingua::TH::Numbers->new( '-๑๐' );
92             my $three_point_one_four = Lingua::TH::Numbers->new( '3.14' );
93             my $nueng_chut_sun_song = Lingua::TH::Numbers->new( '๑.๐๒' );
94              
95             # Output.
96             print $ten->thai_numerals(), "\n";
97             print $sip->arabic_numerals(), "\n";
98             print $lop_sip->arabic_numerals(), "\n";
99             print $three_point_one_four->thai_numerals(), "\n";
100             print $nueng_chut_sun_song->arabic_numerals(), "\n";
101              
102             # Spell.
103             print $three_point_one_four->spell(), "\n";
104             print $three_point_one_four->spell( output_type => 'thai' ), "\n";
105             print $nueng_chut_sun_song->spell( output_type => 'rtgs' ), "\n";
106             print $nueng_chut_sun_song->spell( output_type => 'rtgs', informal => 1 ), "\n";
107              
108              
109             =head1 METHODS
110              
111             =head2 new()
112              
113             Create a new Lingua::TH::Numbers object.
114              
115             my $ten = Lingua::TH::Numbers->new( '10' );
116             my $sip = Lingua::TH::Numbers->new( '๑๐' );
117             my $lop_sip = Lingua::TH::Numbers->new( '-๑๐' );
118             my $three_point_one_four = Lingua::TH::Numbers->new( '3.14' );
119             my $nueng_chut_sun_song = Lingua::TH::Numbers->new( '๑.๐๒' );
120              
121             The input can use either Thai or Arabic numerals, but not both at the same time.
122              
123             =cut
124              
125             sub new
126             {
127 68     68 1 117462 my ( $class, $input ) = @_;
128              
129             # Required parameters.
130 68 50       334 croak 'Input number is missing'
131             unless defined( $input );
132              
133             # Find the type of the input.
134             # Note: \d includes thai numbers with the utf8 pragma, so we can't use it here.
135 68         98 my ( $arabic, $thai );
136 68 100       461 if ( $input =~ m/^-?[0-9]+\.?[0-9]*$/ )
    100          
137             {
138 48         79 $arabic = $input;
139             }
140             elsif ( $input =~ m/^-?[\x{e50}-\x{e59}]+\.?[\x{e50}-\x{e59}]*$/ )
141             {
142 18         39 $thai = $input;
143             }
144             else
145             {
146 2         35 croak 'The input must use either Thai or Arabic numerals and be a number';
147             }
148              
149             # Create the object.
150 66         382 my $self = bless(
151             {
152             arabic => $arabic,
153             thai => $thai,
154             },
155             $class,
156             );
157              
158 66         296 return $self;
159             }
160              
161              
162             =head2 thai_numerals()
163              
164             Output the number stored in the object using thai numerals.
165              
166             my $ten = Lingua::TH::Numbers->new( '10' );
167             print $ten->thai_numerals(), "\n";
168              
169             =cut
170              
171             sub thai_numerals
172             {
173 13     13 1 19 my ( $self ) = @_;
174              
175 13 100       45 unless ( defined( $self->{'thai'} ) )
176             {
177             # Convert to Thai numerals.
178 12         27 $self->{'thai'} = $self->{'arabic'};
179 5     5   61 $self->{'thai'} =~ tr/0123456789/๐๑๒๓๔๕๖๗๘๙/;
  5         12  
  5         89  
  12         63  
180             }
181              
182 13         72 return $self->{'thai'};
183             }
184              
185              
186             =head2 arabic_numerals()
187              
188             Output the number stored in the object using arabic numerals.
189              
190             my $lop_sip = Lingua::TH::Numbers->new( '-๑๐' );
191             print $lop_sip->arabic_numerals(), "\n";
192              
193             =cut
194              
195             sub arabic_numerals
196             {
197 73     73 1 101 my ( $self ) = @_;
198              
199 73 100       199 unless ( defined( $self->{'arabic'} ) )
200             {
201             # Convert to Thai numerals.
202 12         29 $self->{'arabic'} = $self->{'thai'};
203 12         57 $self->{'arabic'} =~ tr/๐๑๒๓๔๕๖๗๘๙/0123456789/;
204             }
205              
206 73         242 return $self->{'arabic'};
207             }
208              
209              
210             =head2 spell()
211              
212             Spell the number stored in the object.
213              
214             By default, spelling is done using Thai script, but the method also supports
215             the spelling of the Royal Thai General System with the parameter I
216             set to I.
217              
218             This method also supports spelling shortcuts for informal language, using the
219             parameter I.
220              
221             # Spell using Thai script.
222             print Lingua::TH::Numbers->new( '10' )->spell(), "\n";
223              
224             # Spell using the Royal Thai General System.
225             print Lingua::TH::Numbers->new( '10' )->spell( output_mode => 'rtgs' ), "\n";
226              
227             # Spell using Thai script, with informal shortcuts.
228             print Lingua::TH::Numbers->new( '10' )->spell( informal => 1 ), "\n";
229              
230             # Spell using the Royal Thai General System, with informal shortcuts.
231             print Lingua::TH::Numbers->new( '10' )->spell( output_mode => 'rtgs', informal => 1 ), "\n";
232              
233             =cut
234              
235             sub spell
236             {
237 60     60 1 297 my ( $self, %args ) = @_;
238 60         139 my $informal = delete( $args{'informal'} );
239 60         115 my $output_mode = delete( $args{'output_mode'} );
240              
241             # Check parameters.
242 60 50       152 $output_mode = 'thai'
243             unless defined( $output_mode );
244 60 50       179 croak 'Output mode is not valid'
245             unless defined( $SPELLING_OUTPUT_MODES->{ $output_mode } );
246 60 50       122 $informal = 0
247             unless defined( $informal );
248              
249 60         92 my $output_mode_index = $SPELLING_OUTPUT_MODES->{ $output_mode };
250              
251             # Parse the number.
252 60         140 my $number = $self->arabic_numerals();
253 60         343 my ( $sign, $integer, $decimals ) = $number =~ /^(-?)(\d+)\.?(\d*)$/;
254 60 50       186 croak 'Can only spell numbers up to ( 10**13 - 1 )'
255             if length( $integer ) > 13;
256              
257             # Put all the words in an array, as the word separator varies depending on the
258             # output mode.
259 60         95 my @spelling = ();
260              
261             # Convert the sign of the number.
262 60 50 33     292 if ( defined( $sign ) && ( $sign eq '-' ) )
263             {
264 0         0 push( @spelling, $MINUS_SIGN->[ $output_mode_index ] );
265             }
266              
267             # Convert the integer part of the number.
268 60 100       129 if ( length( $integer ) > 7 )
269             {
270 4         5 my $millions;
271 4         30 ( $millions, $integer ) = $integer =~ /^(\d*)(\d{6})$/;
272              
273 4         12 push( @spelling, _spell_integer( $millions, $output_mode_index, $informal ) );
274 4         13 push( @spelling, $POWERS_OF_TEN->{'6'}->[ $output_mode_index ] );
275             }
276 60         124 push( @spelling, _spell_integer( $integer, $output_mode_index, $informal ) );
277              
278             # Convert the decimal part of the number.
279 60 50 33     282 if ( defined( $decimals ) && ( $decimals ne '' ) )
280             {
281 0         0 push( @spelling, $DECIMAL_SEPARATOR->[ $output_mode_index ] );
282 0         0 foreach my $decimal ( split( //, $decimals ) )
283             {
284 0         0 push( @spelling, $DIGITS->{ $decimal }->[ $output_mode_index ] );
285             }
286             }
287              
288             # Join the words and return the final string.
289 60 100       152 my $separator = $output_mode eq 'thai'
290             ? ''
291             : ' ';
292 60         97 return join( $separator, grep { $_ ne '' } @spelling );
  148         782  
293             }
294              
295              
296             =head1 INTERNAL FUNCTIONS
297              
298             =head2 _spell_integer()
299              
300             Spell the integer passed as parameter.
301              
302             This internal function should not be used, as it is designed to handle a
303             sub-case of C only in order to spell integers lesser than 10,000,000.
304              
305             my @spelling = Lingua::TH::Numbers::_spell_integer( 10, $output_mode_index, $is_informal );
306              
307             =cut
308              
309             sub _spell_integer
310             {
311 64     64   101 my ( $integer, $output_mode_index, $is_informal ) = @_;
312 64         88 my @spelling = ();
313              
314 64 50       164 croak 'Integer is too large for the internal function to spell'
315             if length( $integer ) > 7;
316              
317 64         214 my @integer_digits = reverse split( //, $integer );
318              
319 64         240 for ( my $power_of_ten = scalar( @integer_digits ) - 1; $power_of_ten >= 0; $power_of_ten-- )
320             {
321 216         277 my $digit = $integer_digits[ $power_of_ten ];
322              
323             # If there's no digit for this power of 10, skip it (except for 0 itself).
324 216 100 100     948 next if $digit eq '0' && $integer ne '0';
325              
326             # 11, 21, ..., 91 use 'et' instead of 'neung' for the trailing 1.
327 84 100 100     574 if ( $power_of_ten == 0 && $digit eq '1' && $integer ne '1' )
    100 100        
    100 100        
      100        
328             {
329 4         8 push( @spelling, $TRAILING_ONE->[ $output_mode_index ] );
330 4         15 $power_of_ten = 0;
331             }
332             # 10 to 99 may have exceptions.
333             elsif ( $power_of_ten == 1 )
334             {
335 16 100       49 if ( $digit eq '1' )
    100          
336             {
337             # Just 'sip', not 'neung sip'
338             }
339             elsif ( $digit eq '2' )
340             {
341             # 'yi' instead of 'song' of 20 to 29.
342 6         16 push( @spelling, $TWO_FOR_TWENTY->[ $output_mode_index ] );
343             }
344             else
345             {
346 4         13 push( @spelling, $DIGITS->{ $digit }->[ $output_mode_index ] );
347             }
348 16         62 push( @spelling, $POWERS_OF_TEN->{ $power_of_ten }->[ $output_mode_index ] );
349             }
350             # For numbers >= 100, '1' is implicit.
351             elsif ( $is_informal && $power_of_ten >= 2 && $digit eq '1' )
352             {
353 14         64 push( @spelling, $POWERS_OF_TEN->{ $power_of_ten }->[ $output_mode_index ] );
354             }
355             else
356             # Normal rules apply.
357             {
358 50         152 push( @spelling, $DIGITS->{ $digit }->[ $output_mode_index ] );
359 50         217 push( @spelling, $POWERS_OF_TEN->{ $power_of_ten }->[ $output_mode_index ] );
360             }
361             }
362              
363 64         271 return @spelling;
364             }
365              
366              
367             =head1 CAVEAT
368              
369             There's too many Unicode issues in Perl 5.6 (in particular with tr/// which
370             this module uses) and Perl 5.6 is 10 year old at this point, so I decided to
371             make Perl 5.8 the minimum requirement for this module after a lot of time
372             spent jumping through pre-5.8 hoops.
373              
374             If you really need this module and you are still using a version of Perl that
375             predates 5.8, please let me know although I would really encourage you to
376             upgrade.
377              
378              
379             =head1 BUGS
380              
381             Please report any bugs or feature requests through the web interface at
382             L.
383             I will be notified, and then you'll automatically be notified of progress on
384             your bug as I make changes.
385              
386              
387             =head1 SUPPORT
388              
389             You can find documentation for this module with the perldoc command.
390              
391             perldoc Lingua::TH::Numbers
392              
393              
394             You can also look for information at:
395              
396             =over
397              
398             =item * GitHub's request tracker
399              
400             L
401              
402             =item * AnnoCPAN: Annotated CPAN documentation
403              
404             L
405              
406             =item * CPAN Ratings
407              
408             L
409              
410             =item * MetaCPAN
411              
412             L
413              
414             =back
415              
416              
417             =head1 AUTHOR
418              
419             L,
420             C<< >>.
421              
422              
423             =head1 COPYRIGHT & LICENSE
424              
425             Copyright 2011-2014 Guillaume Aubert.
426              
427             This program is free software: you can redistribute it and/or modify it under
428             the terms of the GNU General Public License version 3 as published by the Free
429             Software Foundation.
430              
431             This program is distributed in the hope that it will be useful, but WITHOUT ANY
432             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
433             PARTICULAR PURPOSE. See the GNU General Public License for more details.
434              
435             You should have received a copy of the GNU General Public License along with
436             this program. If not, see http://www.gnu.org/licenses/
437              
438             =cut
439              
440             1;