File Coverage

blib/lib/Lingua/EN/Inflexion/Term.pm
Criterion Covered Total %
statement 123 174 70.6
branch 19 30 63.3
condition 19 43 44.1
subroutine 34 60 56.6
pod 0 7 0.0
total 195 314 62.1


line stmt bran cond sub pod time code
1             package Lingua::EN::Inflexion::Term;
2              
3 24     24   370 use 5.010; use warnings; use Carp;
  24     24   66  
  24     24   104  
  24         35  
  24         566  
  24         104  
  24         46  
  24         1295  
4 24     24   9399 no if $] >= 5.018, warnings => "experimental::smartmatch";
  24         292  
  24         120  
5              
6 24     24   8466 use Hash::Util 'fieldhash';
  24         50892  
  24         129  
7              
8             fieldhash my %term_of;
9              
10             # Inside-out constructor...
11             sub new {
12 25086     25086 0 45162 my ($class, $term) = @_;
13              
14 25086         34607 my $object = bless do{ \my $scalar }, $class;
  25086         55311  
15              
16 25086   33     143396 $term_of{$object} = $term // croak "Missing arg to $class ctor";
17              
18 25086         105948 return $object;
19             }
20              
21             # Replicate casing...
22             my $encase = sub {
23             my ($original, $target) = @_;
24              
25             # Special case for 'I' <-> 'we'...
26             return $target if $original =~ /\A(?:I|we)\Z/i;
27              
28             # Construct word-by-word case transformations...
29             my @transforms
30             = map { /\A[[:lower:][:^alpha:]]+\Z/ ? sub { lc shift }
31             : /\A[[:upper:]][[:lower:][:^alpha:]]+\Z/ ? sub { ucfirst lc shift }
32             : /\A[[:upper:][:^alpha:]]+\Z/ ? sub { uc shift }
33             : sub { shift }
34             }
35             split /\s+/, $original;
36              
37             if (!@transforms) {
38             @transforms = sub {shift};
39             }
40              
41             # Apply to target...
42             $target =~ s{(\S+)}
43             { my $transform = @transforms > 1 ? shift @transforms : $transforms[0];
44             $transform->($1);
45             }xmseg;
46              
47             return $target;
48             };
49              
50 0     0 0 0 # Report part-of-speech...
51 0     0 0 0 sub is_noun { 0 }
52 0     0 0 0 sub is_verb { 0 }
53             sub is_adj { 0 }
54              
55 0     0 0 0 # Default classical/unassimilated mode does nothing...
56 0     0 0 0 sub classical { return shift; }
57             sub unassimilated { return shift->classical; }
58              
59 24     24   39878 # Coerce to original...
  24         50  
  24         6235  
60             use Scalar::Util qw< refaddr blessed >;
61 0     0   0 use overload (
62 0     0   0 q[qr] => sub { return shift->as_regex(); },
63 0     0   0 q[""] => sub { return "$term_of{shift()}"; },
64 2     2   18 q[0+] => sub { return refaddr(shift); },
65 0     0   0 q[bool] => sub { return 1; },
66 0     0   0 q[${}] => sub { croak "Can't coerce ", ref(shift), ' object to scalar reference'; },
67 0     0   0 q[@{}] => sub { croak "Can't coerce ", ref(shift), ' object to array reference'; },
68 0     0   0 q[%{}] => sub { croak "Can't coerce ", ref(shift), ' object to hash reference'; },
69 0     0   0 q[&{}] => sub { croak "Can't coerce ", ref(shift), ' object to subroutine reference'; },
70             q[*{}] => sub { croak "Can't coerce ", ref(shift), ' object to typeglob reference'; },
71              
72 141     141   276 q[~~] => sub {
73             my ($term, $other_arg) = @_;
74              
75 141 100 66     654 # Handle TERM ~~ TERM...
76 47   66     127 if (blessed($other_arg) && $other_arg->isa(__PACKAGE__)) {
77             return lc($term->singular) eq lc($other_arg->singular)
78             || lc($term->plural) eq lc($other_arg->plural)
79             || lc($term->classical->plural) eq lc($other_arg->classical->plural);
80             }
81              
82             # Otherwise just smartmatch against TERM as regex....
83 94         223 else {
84             return $other_arg ~~ $term->as_regex;
85             }
86             },
87              
88 24         340  
89 24     24   18866 fallback => 1,
  24         17237  
90             );
91              
92             # Treat as regex...
93 94     94 0 175 sub as_regex {
94 94         151 my ($self) = @_;
95 94         233 my %seen;
  212         487  
  282         963  
96             my $pattern = join '|', map { quotemeta } reverse sort grep { !$seen{$_}++ }
97 94         3366 ($self->singular, $self->plural, $self->classical->plural);
98             return qr{$pattern}i;
99             }
100              
101              
102             package Lingua::EN::Inflexion::Noun;
103             our @ISA = 'Lingua::EN::Inflexion::Term';
104 24     24   34255  
  24         101  
  24         7888  
105 24     24   9026 use Lingua::EN::Inflexion::Nouns;
  24         62  
  24         23448  
106             use Lingua::EN::Inflexion::Indefinite;
107              
108             # Report number of the noun...
109 2978     2978   5366 sub is_plural {
110 2978         9175 my ($self) = @_;
111             return Lingua::EN::Inflexion::Nouns::is_plural( $term_of{$self} );
112             }
113              
114 2405     2405   3989 sub is_singular {
115 2405         6055 my ($self) = @_;
116             return Lingua::EN::Inflexion::Nouns::is_singular( $term_of{$self} );
117             }
118              
119 0     0   0 # Report part-of-speech...
120             sub is_noun { 1 }
121              
122             # Return plural and singular forms of the noun...
123 6019     6019   10568 sub plural {
124             my ($self) = @_;
125             return $encase->(
126 6019         18910 $term_of{$self},
127             Lingua::EN::Inflexion::Nouns::convert_to_modern_plural( $term_of{$self} )
128             );
129             }
130              
131 11771     11771   138074 sub singular {
132             my ($self) = @_;
133             return $encase->(
134 11771         39850 $term_of{$self},
135             Lingua::EN::Inflexion::Nouns::convert_to_singular( $term_of{$self} )
136             );
137             }
138              
139 0     0   0 sub indef_article {
140             my ($self) = @_;
141 0         0  
142             return Lingua::EN::Inflexion::Indefinite::select_indefinite_article($self->singular);
143             }
144              
145 956     956   3062 sub indefinite {
146 956   100     2817 my ($self, $count) = @_;
147             $count //= 1;
148 956 100       2221  
149 478         1165 if ($count == 1 ) {
150             return Lingua::EN::Inflexion::Indefinite::prepend_indefinite_article($self->singular);
151             }
152 478         1496 else {
153             return "$count " . $self->plural;
154             }
155             }
156              
157              
158             # Conversions to ordinal and cardinal numbers (with module loaded on demand)...
159             my $num2word = sub {
160             state $load = require Lingua::EN::Nums2Words && Lingua::EN::Nums2Words::set_case('lower');
161             Lingua::EN::Nums2Words::num2word(@_);
162             };
163              
164             my $num2word_short_ordinal = sub {
165             state $load = require Lingua::EN::Nums2Words && Lingua::EN::Nums2Words::set_case('lower');
166             Lingua::EN::Nums2Words::num2word_short_ordinal(@_);
167             };
168              
169             my $num2word_ordinal = sub {
170             state $load = require Lingua::EN::Nums2Words && Lingua::EN::Nums2Words::set_case('lower');
171             Lingua::EN::Nums2Words::num2word_ordinal(@_);
172             };
173              
174             # These words may need an "and" before them...
175             my $LAST_WORD = qr{
176             one | two | three | four | five | six | seven | eight | nine | ten
177             | eleven | twelve | teen | ty
178             | first | second | third | [rfxnhe]th
179             }x;
180              
181             # These words may need an "and" after them...
182             my $POWER_WORD = qr{
183             hundred | thousand | \S+illion
184             }x;
185              
186 2     2   10 sub cardinal {
187 2         6 my $value = $term_of{ shift() };
188             my $max_trans = shift();
189              
190 2         417 # Load the necessary module, and compensate for its persnicketiness...
191 0     0   0 state $load = require Lingua::EN::Words2Nums;
192             local $SIG{__WARN__} = sub{};
193              
194 0   0     0 # Make sure we have a number...
195             $value = Lingua::EN::Words2Nums::words2nums($value) // $value;
196              
197 0 0 0     0 # If it's above threshold, return it as a number...
198             return $value
199             if defined $max_trans && $value >= $max_trans;
200              
201 0         0 # Otherwise, convert it to words...
202             my $words = $num2word->($value);
203              
204 0 0       0 # Correct for proper English pronunciation...
205 0         0 if ($value > 100) {
206 0         0 $words =~ s{ ($POWER_WORD) \s+ (\S*$LAST_WORD) \b } {$1 and $2}gx;
207 0         0 $words =~ s{ (?
208             $words =~ s{ ^ ([^,]+),([^,]+) $ } {$1$2}x;
209             }
210 0         0  
211             return $words;
212             }
213              
214 0     0   0 sub ordinal {
215 0         0 my $value = $term_of{ shift() };
216             my $max_trans = shift();
217              
218 0         0 # Load the necessary module, and compensate for its persnicketiness...
219 0     0   0 state $load = require Lingua::EN::Words2Nums;
220             local $SIG{__WARN__} = sub{};
221              
222 0   0     0 # Make sure we have a number...
223             $value = Lingua::EN::Words2Nums::words2nums($value) // $value;
224              
225 0 0 0     0 # If it's above threshold, return it as a number...
226             return $num2word_short_ordinal->($value)
227             if defined $max_trans && $value >= $max_trans;
228              
229 0         0 # Otherwise, convert it to words...
230             my $words = $num2word_ordinal->( $value );
231              
232 0 0       0 # Correct for proper English pronunciation...
233 0         0 if ($value > 100) {
234 0         0 $words =~ s{ ($POWER_WORD) \s+ (\S*$LAST_WORD) \b } {$1 and $2}gx;
235 0         0 $words =~ s{ (?
236             $words =~ s{ ^ ([^,]+),([^,]+) $ } {$1$2}x;
237             }
238 0         0  
239             return $words;
240             }
241              
242              
243 4951     4951   13011 # Return a classical version of the term...
244             sub classical { Lingua::EN::Inflexion::Noun::Classical->new(shift) }
245              
246              
247             package Lingua::EN::Inflexion::Noun::Classical;
248             our @ISA = 'Lingua::EN::Inflexion::Noun';
249              
250             # Inside-out ctor expects a base-class object to clone...
251 4951     4951   9597 sub new {
252             my ($class, $orig_object) = @_;
253 4951         7106  
  4951         9911  
254             my $new_object = bless do{ \my $scalar }, $class;
255 4951         11319  
256             $term_of{$new_object} = $orig_object->singular;
257 4951         20587  
258             return $new_object;
259             }
260              
261 0     0   0 # Already a classical noun, so this is now idempotent...
262             sub classical { return shift }
263              
264             # Classical plurals are different...
265 4779     4779   9076 sub plural {
266             my ($self) = @_;
267             return $encase->(
268 4779         15399 $term_of{$self},
269             Lingua::EN::Inflexion::Nouns::convert_to_classical_plural($term_of{$self})
270             );
271             }
272              
273             package Lingua::EN::Inflexion::Verb;
274             our @ISA = 'Lingua::EN::Inflexion::Term';
275 24     24   16798  
  24         110  
  24         30603  
276             use Lingua::EN::Inflexion::Verbs;
277              
278             # Utility sub that adjusts final consonants when they need to be doubled in inflexions...
279             my $truncate = sub {
280             my ($term) = @_;
281              
282             # Apply the first relevant transform...
283             $term =~ s{ ie \Z }{y}x
284             or $term =~ s{ ue \Z }{u}x
285             or $term =~ s{ ([auy])e \Z }{$1}x
286              
287             or $term =~ s{ ski \Z }{ski}x
288             or $term =~ s{ [^b]i \Z }{}x
289              
290             or $term =~ s{ ([^e])e \Z }{$1}x
291              
292             or $term =~ m{ er \Z }x
293             or $term =~ s{ (.[bdghklmnprstz][o]([n])) \Z }{$1}x
294              
295             or $term =~ s{ ([^aeiou][aeiouy]([bcdlgmnprstv])) \Z }{$1$2}x
296              
297             or $term =~ s{ e \Z }{}x;
298              
299             return $term;
300             };
301              
302             # Report status of verb...
303 212     212   362 sub is_plural {
304 212         561 my ($self) = @_;
305             return Lingua::EN::Inflexion::Verbs::is_plural( $term_of{$self} );
306             }
307              
308 214     214   348 sub is_singular {
309 214         736 my ($self) = @_;
310             return Lingua::EN::Inflexion::Verbs::is_singular( $term_of{$self} );
311             }
312              
313 0     0   0 sub is_present {
314 0         0 my ($self) = @_;
315             return Lingua::EN::Inflexion::Verbs::is_present( $term_of{$self} );
316             }
317              
318 0     0   0 sub is_past {
319 0         0 my ($self) = @_;
320             return Lingua::EN::Inflexion::Verbs::is_past( $term_of{$self} );
321             }
322              
323 0     0   0 sub is_pres_part {
324 0         0 my ($self) = @_;
325             return Lingua::EN::Inflexion::Verbs::is_pres_part( $term_of{$self} );
326             }
327              
328 0     0   0 sub is_past_part {
329 0         0 my ($self) = @_;
330             return Lingua::EN::Inflexion::Verbs::is_past_part( $term_of{$self} );
331             }
332              
333 0     0   0 # Report part-of-speech...
334             sub is_verb { 1 }
335              
336              
337             # Conversions...
338              
339 532     532   35552 sub singular {
340             my ($self) = @_;
341              
342 532         1530 # Is it a known inflexion???
343             my $inflexion = Lingua::EN::Inflexion::Verbs::convert_to_singular( $term_of{$self} );
344              
345 532 50       1662 # Return with case-following...
346             return $encase->( $term_of{$self}, $inflexion eq '_' ? $term_of{$self} : $inflexion );
347             }
348              
349 3664     3664   5450 sub plural {
350             my ($self) = @_;
351              
352 3664         9358 # Is it a known inflexion???
353             my $inflexion = Lingua::EN::Inflexion::Verbs::convert_to_plural( $term_of{$self} );
354              
355 3664 100       11238 # Return with case-following...
356             return $encase->( $term_of{$self}, $inflexion eq '_' ? $term_of{$self} : $inflexion );
357             }
358              
359 1024     1024   107783 sub past {
360 1024         1833 my ($self) = @_;
361 1024         1931 my $term = $term_of{$self};
362             my $root = $self->plural;
363              
364 1024         2551 # Is it a known inflexion???
365             my $inflexion = Lingua::EN::Inflexion::Verbs::convert_to_past( $term );
366 1024 100       2168  
367 48         107 if ($inflexion eq '_') {
368             $inflexion = Lingua::EN::Inflexion::Verbs::convert_to_past( $root );
369             }
370              
371 1024 100       1733 # Otherwise use the standard pattern...
372 48         101 if ($inflexion eq '_') {
373             $inflexion = $truncate->($root) . 'ed';
374             }
375              
376 1024         1724 # Return with case-following...
377             return $encase->( $term, $inflexion );
378             }
379              
380 1023     1023   108321 sub pres_part {
381 1023         1871 my ($self) = @_;
382 1023         2072 my $term = $term_of{$self};
383             my $root = $self->plural;
384              
385 1023         2672 # Is it a known inflexion???
386             my $inflexion = Lingua::EN::Inflexion::Verbs::convert_to_pres_part( $root );
387              
388 1023 100       2035 # Otherwise use the standard pattern...
389 48         96 if ($inflexion eq '_') {
390             $inflexion = $truncate->($root) . 'ing';
391             }
392              
393 1023         1637 # Return with case-following...
394             return $encase->( $term, $inflexion );
395             }
396              
397 1082     1082   107502 sub past_part {
398 1082         2075 my ($self) = @_;
399 1082         2078 my $term = $term_of{$self};
400             my $root = $self->plural;
401              
402 1082         2765 # Is it a known inflexion???
403             my $inflexion = Lingua::EN::Inflexion::Verbs::convert_to_past_part( $root );
404              
405 1082 100       2194 # Otherwise use the standard pattern...
406 48         101 if ($inflexion eq '_') {
407             $inflexion = $truncate->($root) . 'ed';
408             }
409              
410 1082         1674 # Return with case-following...
411             return $encase->( $term, $inflexion );
412             }
413              
414 0     0   0 sub indefinite {
415 0   0     0 my ($self, $count) = @_;
416             $count //= 1;
417 0 0       0  
418             return $count == 1 ? $self->singular
419             : $self->plural;
420             }
421              
422              
423             package Lingua::EN::Inflexion::Adjective;
424             our @ISA = 'Lingua::EN::Inflexion::Term';
425              
426             # Load adjective tables, always taking first option...
427             my @adjectives = (
428             # Determiners...
429             'a' => 'some',
430             'an' => 'some',
431              
432             # Demonstratives...
433             'that' => 'those',
434             'this' => 'these',
435              
436             # Possessives...
437             'my' => 'our',
438             'your' => 'your',
439             'their' => 'their',
440             'her' => 'their',
441             'his' => 'their',
442             'its' => 'their',
443             );
444              
445             my (%adj_plural_of, %adj_singular_of, %adj_is_plural, %adj_is_singular);
446             while (my ($sing, $plur) = splice @adjectives, 0, 2) {
447             $adj_is_singular{$sing} = 1;
448             $adj_singular_of{$plur} //= $sing;
449              
450             $adj_is_plural{$plur} = 1;
451             $adj_plural_of{$sing} //= $plur;
452             }
453              
454              
455 0     0   0 # Report part-of-speech...
456             sub is_adj { 1 }
457              
458              
459             # Report number of adjective...
460 18     18   38 sub is_plural {
461 18         43 my ($self) = @_;
462             my $term = $term_of{$self};
463 18   66     179 return $adj_is_plural{$term} || $adj_is_plural{lc $term}
464             || !$adj_is_singular{$term} && !$adj_is_singular{lc $term};
465             }
466              
467 22     22   53 sub is_singular {
468 22         56 my ($self) = @_;
469             my $term = $term_of{$self};
470 22   66     253 return $adj_is_singular{$term} || $adj_is_singular{lc $term}
471             || !$adj_is_plural{$term} && !$adj_is_plural{lc $term};
472             }
473              
474              
475             # Conversions...
476              
477 18     18   46 sub singular {
478 18         37 my ($self) = @_;
479 18         37 my $term = $term_of{$self};
480             my $singular = $term;;
481              
482 18 100       79 # Is it a possessive form???
483 6         17 if ($term =~ m{ \A (.*) 's? \Z }ixms) {
484             $singular = Lingua::EN::Inflexion::Noun->new($1)->singular . q{'s};
485             }
486              
487             # Otherwise, it's either a known inflexion, or uninflected...
488 12   66     66 else {
      66        
489             $singular = $adj_singular_of{$term} // $adj_singular_of{lc $term} // $term;
490             }
491 18         61  
492             return $encase->($term, $singular);
493             }
494              
495 22     22   56 sub plural {
496 22         56 my ($self) = @_;
497 22         48 my $term = $term_of{$self};
498             my $plural = $term;;
499              
500 22 100       104 # Is it a possessive form???
501 6         28 if ($term =~ m{ \A (.*) 's? \Z }ixms) {
502 6         48 $plural = Lingua::EN::Inflexion::Noun->new($1)->plural . q{'s};
503             $plural =~ s{ s's \Z }{s'}xms
504             }
505              
506             # Otherwise, it's either a known inflexion, or uninflected...
507 16   66     103 else {
      66        
508             $plural = $adj_plural_of{$term} // $adj_plural_of{lc $term} // $term;
509             }
510 22         64  
511             return $encase->($term, $plural);
512             }
513              
514              
515             1; # Magic true value required at end of module
516             __END__