File Coverage

blib/lib/Lingua/EO/Numbers.pm
Criterion Covered Total %
statement 75 76 98.6
branch 30 34 88.2
condition 23 24 95.8
subroutine 12 12 100.0
pod 2 2 100.0
total 142 148 95.9


line stmt bran cond sub pod time code
1             package Lingua::EO::Numbers;
2              
3 2     2   26510 use 5.008_001;
  2         6  
  2         81  
4 2     2   9 use strict;
  2         5  
  2         59  
5 2     2   9 use warnings;
  2         4  
  2         87  
6 2     2   9 use utf8;
  2         4  
  2         12  
7 2     2   1753 use Readonly;
  2         6894  
  2         132  
8 2     2   1920 use Regexp::Common qw( number );
  2         11184  
  2         11  
9              
10 2     2   7403 use base qw( Exporter );
  2         3  
  2         2388  
11             our @EXPORT_OK = qw( num2eo num2eo_ordinal );
12             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
13              
14             our $VERSION = '0.03';
15              
16             # up to 999,999 vigintillion (long scale) supported
17             Readonly my $MAX_INT_DIGITS => 126;
18              
19             Readonly my $EMPTY_STR => q{};
20             Readonly my $SPACE => q{ };
21             Readonly my $ORDINAL_SUFFIX => q{a};
22             Readonly my $PLURAL_SUFFIX => q{j};
23              
24             Readonly my @NAMES1 => qw< nul unu du tri kvar kvin ses sep ok naĆ­ >;
25             Readonly my @NAMES2 => $EMPTY_STR, qw< dek cent >;
26             Readonly my @GROUPS => (
27             undef, qw< mil miliono miliardo >,
28             map { $_ . 'iliono' } qw<
29             b tr kvadr kvint sekst sept okt non dec undec duodec tredec
30             kvatuordec kvindec seksdec septendec oktodec novemdec vigint
31             >
32             );
33              
34             Readonly my %WORDS => (
35             ',' => 'komo',
36             '-' => 'negativa',
37             '+' => 'positiva',
38             inf => 'senfineco',
39             NaN => 'ne nombro',
40             );
41              
42             # convert number to words
43             sub num2eo {
44 110     110 1 20429 my ($number) = @_;
45 110         123 my @names;
46              
47 110 100       249 return unless defined $number;
48 107 100       278 return $WORDS{NaN} if $number eq 'NaN';
49              
50 105 100       617 if ($number =~ m/^ ( [-+] )? inf $/ix) {
    100          
51             # infinity
52 3 100       22 push @names, $1 ? $WORDS{$1} : (), $WORDS{inf};
53             }
54             elsif ($number =~ m/^ $RE{num}{real}{-radix=>'[,.]'}{-keep} $/x) {
55 96         17181 my ($sign, $int, $frac) = ($2, $4, $6);
56              
57 96 50       289 return if length $int > $MAX_INT_DIGITS;
58              
59             # sign and integer
60 96   66     702 unshift @names, $WORDS{$sign} || (), _convert_int($int);
61              
62             # fraction
63 96 100 100     298 if (defined $frac && $frac ne $EMPTY_STR) {
64 22         222 push @names, (
65             $WORDS{','},
66 16         118 map { $NAMES1[$_] } split $EMPTY_STR, $frac,
67             );
68             }
69             }
70             else {
71 6         975 return;
72             }
73              
74 99         1087 return join $SPACE, @names;
75             }
76              
77             # convert number to ordinal words
78             sub num2eo_ordinal {
79 33     33 1 9620 my ($number) = @_;
80 33         67 my $name = num2eo($number);
81              
82 33 50       215 return unless defined $name;
83              
84 33         45 for ($name) {
85 33         162 s{ (?: oj? | a ) \b }{}gx; # remove word suffixes
86 33         81 tr{ }{-};
87             }
88              
89 33         83 return $name . $ORDINAL_SUFFIX;
90             }
91              
92             # convert integers to words
93             sub _convert_int {
94 96     96   745 my ($int) = @_;
95 96         157 my @number_groups = _split_groups($int);
96 96         99 my @name_groups;
97 96         99 my $group_count = 0;
98              
99             GROUP:
100 96         131 for my $group (reverse @number_groups) {
101             # skip zeros unless it is the only digit
102 160 100 100     458 next GROUP if $group == 0 && $int != 0;
103              
104 106         324 my $type = $GROUPS[$group_count];
105              
106             # pluralize nouns
107 106 100 100     684 if ($type && $type ne $GROUPS[1] && $group > 1) {
      100        
108 10         88 $type .= $PLURAL_SUFFIX;
109             }
110              
111 106         357 my @names = do {
112             # use thousand instead of one thousand
113 106 100 100     326 if ($group == 1 && $type eq $GROUPS[1]) { () }
  2 50       13  
114              
115             # groups for billions and greater contain thousands sub-groups
116 0         0 elsif (length $group > 3) { _convert_int( $group ) }
117 104         189 else { _convert_group( $group ) }
118             };
119              
120 106 100       270 unshift @name_groups, @names, $type ? $type : ();
121             }
122             continue {
123 160         248 $group_count++;
124             }
125              
126 96         256 return @name_groups;
127             }
128              
129             # split integer into groups for use with thousands, millions, etc.
130             # the first 3 groups contain 3 digits and the rest contain 6 digits
131             sub _split_groups {
132 96     96   113 my ($int) = @_;
133 96         109 my $group_length = 3;
134 96         91 my @groups;
135              
136 96         575 while ($int =~ s[ ( .{1,$group_length} ) $ ][]x) {
137 160         319 unshift @groups, $1;
138             }
139             continue {
140 160 100       765 if (@groups == 4) {
141 8         105 $group_length = 6;
142             }
143             }
144              
145 96         276 return @groups;
146             }
147              
148             # the actual integer to word conversion
149             # this expects an integer group of 1 to 3 digits
150             sub _convert_group {
151 104     104   116 my ($int) = @_;
152 104 50       242 my @digits = split $EMPTY_STR, defined $int ? $int : $EMPTY_STR;
153 104         614 my $digit_count = 0;
154 104         89 my @names;
155              
156             DIGIT:
157 104         155 for my $digit (reverse @digits) {
158             # skip zero unless it is the only digit
159 186 100 100     498 next DIGIT if $digit == 0 && $int != 0;
160              
161             # leave off one for ten and hundred
162 148 100 100     613 unshift @names, (
163             $digit == 1 && $digit_count ? $EMPTY_STR : $NAMES1[$digit]
164             ) . $NAMES2[$digit_count];
165             }
166             continue {
167 186         1392 $digit_count++;
168             }
169              
170 104         305 return @names;
171             }
172              
173             1;
174              
175             __END__