File Coverage

blib/lib/Lingua/TH/Numbers.pm
Criterion Covered Total %
statement 76 80 95.0
branch 32 40 80.0
condition 17 21 80.9
subroutine 11 11 100.0
pod 4 4 100.0
total 140 156 89.7


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