File Coverage

blib/lib/Word/Rhymes.pm
Criterion Covered Total %
statement 145 169 85.8
branch 86 100 86.0
condition 31 37 83.7
subroutine 20 22 90.9
pod 11 11 100.0
total 293 339 86.4


line stmt bran cond sub pod time code
1             package Word::Rhymes;
2              
3 12     12   870994 use strict;
  12         138  
  12         362  
4 12     12   68 use warnings;
  12         22  
  12         569  
5              
6             our $VERSION = '1.03';
7              
8 12     12   78 use Carp qw(croak);
  12         20  
  12         638  
9 12     12   6292 use HTTP::Request;
  12         248054  
  12         422  
10 12     12   7729 use JSON;
  12         119311  
  12         70  
11 12     12   10279 use LWP::UserAgent;
  12         325007  
  12         845  
12              
13             use constant {
14             # Core
15 12         29424 MIN_SCORE => 0,
16             MAX_SCORE => 1000000,
17             MIN_RESULTS => 1,
18             MAX_RESULTS => 1000,
19             MIN_SYLLABLES => 1,
20             MAX_SYLLABLES => 100,
21             MULTI_WORD => 0,
22             RETURN_RAW => 0,
23             MIN_LIMIT => 1,
24             MAX_LIMIT => 1000,
25              
26             # print() related
27             MAX_NUM_COLS => 8,
28             MIN_NUM_COLS => 7,
29             COL_DIVIDER => 15,
30             COL_PADDING => 3,
31             ROW_INDENT => ' ',
32              
33             # Sort by
34             SORT_BY_SCORE_DESC => 0x00, # Default
35             SORT_BY_SCORE_ASC => 0x01,
36             SORT_BY_ALPHA_DESC => 0x02,
37             SORT_BY_ALPHA_ASC => 0x03,
38              
39 12     12   113 };
  12         27  
40              
41             my $ua = LWP::UserAgent->new;
42              
43             # Public
44              
45             sub new {
46 41     41 1 22569 my ($class, %args) = @_;
47              
48 41         116 my $self = bless {}, $class;
49              
50 41         153 $self->_args(\%args);
51              
52 27         157 return $self;
53             }
54             sub fetch {
55 28     28 1 6152 my ($self, $word, $context) = @_;
56              
57 28 100       73 if (! defined $word) {
58 1         156 croak("fetch() needs a word sent in");
59             }
60              
61 27 100 66     85 if (defined $context && $context !~ /^\w+$/) {
62 1         6689 croak("context parameter must be an alpha word only.");
63             }
64              
65 26         49 my ($req, $response);
66              
67 26 50       64 if (! $self->file) {
68 0         0 $req = HTTP::Request->new('GET', $self->_uri($word, $context));
69 0         0 $response = $ua->request($req);
70             }
71              
72 26 50 33     66 if ($self->file || $response->is_success) {
73              
74 26         42 my $json;
75              
76 26 50       47 if ($self->file) {
77             {
78 26         44 local $/;
  26         106  
79 26 50       124 open my $fh, '<', $self->file or croak(
80             sprintf("Can't open the data file '%s': $!", $self->file)
81             );
82 26         2621 $json = <$fh>;
83 26         481 close $fh;
84             }
85             }
86             else {
87 0         0 $json = $response->decoded_content;
88             }
89              
90 26         20066 my $result = decode_json $json;
91              
92 26 100       151 return $result if $self->return_raw;
93              
94 24         73 return $self->_process($result);
95             }
96             else {
97 0         0 print "Invalid response\n\n";
98 0         0 return undef;
99             }
100             }
101             sub file {
102 122     122 1 1159 my ($self, $file) = @_;
103              
104 122 100       220 if (defined $file) {
105 17 100       680 croak("File '$file' does not exist") if ! -e $file;
106 15 100       390 croak("File '$file' is not a valid file") if ! -f $file;
107 13         66 $self->{file} = $file;
108             }
109              
110 118   100     1447 return $self->{file} // '';
111             }
112             sub limit {
113 1107     1107 1 457624 my ($self, $limit) = @_;
114              
115 1107 100       2390 if (defined $limit) {
116 1006 100       4596 croak("limit must be an integer") if $limit !~ /^\d+$/;
117 1004 100 100     3291 if ($limit < MIN_LIMIT || $limit > MAX_LIMIT) {
118 4         295 croak(
119             sprintf(
120             "limit must be between %d and %d",
121             MIN_LIMIT,
122             MAX_LIMIT
123             )
124             );
125             }
126 1000         1699 $self->{limit} = $limit;
127             }
128              
129 1101   100     4042 return $self->{limit} // MAX_LIMIT;
130             }
131             sub max_results {
132 1009     1009 1 517776 my ($self, $max) = @_;
133              
134 1009 100       2496 if (defined $max) {
135 1006 100       5785 croak("max_results must be an integer") if $max !~ /^\d+$/;
136 1004 100 100     4419 if ($max < MIN_RESULTS || $max > MAX_RESULTS) {
137 4         353 croak(
138             sprintf(
139             "max_results must be between %d and %d",
140             MIN_RESULTS,
141             MAX_RESULTS
142             )
143             );
144             }
145 1000         2236 $self->{max_results} = $max;
146             }
147              
148 1003   100     4514 return $self->{max_results} // MAX_RESULTS;
149             }
150             sub min_score {
151 10259     10259 1 854346 my ($self, $min) = @_;
152              
153 10259 100       17064 if (defined $min) {
154 1615 100       9566 croak("min_score must be an integer") if $min !~ /^-?\d+$/;
155 1613 100 100     6512 if ($min < MIN_SCORE || $min > MAX_SCORE) {
156 4         351 croak(
157             sprintf(
158             "min_score must be between %d and %d",
159             MIN_SCORE,
160             MAX_SCORE
161             )
162             );
163             }
164 1609         3068 $self->{min_score} = $min;
165             }
166              
167 10253   100     30047 return $self->{min_score} // MIN_SCORE;
168             }
169             sub min_syllables {
170 9619     9619 1 66957 my ($self, $min) = @_;
171              
172 9619 100       14836 if (defined $min) {
173 110 100       968 croak("min_syllables must be an integer") if $min !~ /^-?\d+$/;
174 108 100 100     475 if ($min < MIN_SYLLABLES || $min > MAX_SYLLABLES) {
175 4         396 croak(
176             sprintf(
177             "min_syllables must be between %d and %d",
178             MIN_SYLLABLES,
179             MAX_SYLLABLES
180             )
181             );
182             }
183 104         194 $self->{min_syllables} = $min;
184             }
185              
186 9613   100     26333 return $self->{min_syllables} // MIN_SYLLABLES;
187             }
188             sub multi_word {
189 29     29 1 72 my ($self, $bool) = @_;
190              
191 29 100       64 if (defined $bool) {
192 3         6 $self->{multi_word} = $bool;
193             }
194              
195 29   100     143 return $self->{multi_word} // MULTI_WORD;
196             }
197             sub print {
198 0     0 1 0 my ($self, $word, $context) = @_;
199              
200 0         0 my $rhyming_words = $self->fetch($word, $context);
201              
202 0 0       0 print defined $context
203             ? "\nRhymes with '$word' related to '$context'\n"
204             : "\nRhymes with '$word'\n";
205              
206 0         0 for my $num_syl (reverse sort keys %$rhyming_words) {
207             my $max_word_len = length(
208 0         0 (sort {length $b->{word} <=> length $a->{word}} @{ $rhyming_words->{$num_syl} })[0]->{word}
  0         0  
209 0         0 );
210              
211 0         0 my $column_width = $max_word_len + COL_PADDING;
212 0 0       0 my $columns = $column_width > COL_DIVIDER ? MIN_NUM_COLS : MAX_NUM_COLS;
213              
214 0         0 printf "\nSyllables: $num_syl\n\n%s", ROW_INDENT;
215              
216 0         0 for (0 .. $#{ $rhyming_words->{$num_syl} }) {
  0         0  
217 0 0 0     0 printf "\n%s", ROW_INDENT if $_ % $columns == 0 && $_ != 0;
218 0         0 printf("%-*s", $column_width, $rhyming_words->{$num_syl}[$_]->{word});
219             }
220 0         0 print "\n";
221             }
222              
223 0         0 return 0;
224             }
225             sub return_raw {
226 28     28 1 669 my ($self, $ret) = @_;
227              
228 28 100       82 if (defined $ret) {
229 2         13 $self->{return_raw} = $ret;
230             }
231              
232 28   100     186 return $self->{return_raw} // RETURN_RAW;
233             }
234             sub sort_by {
235 294     294 1 3767 my ($self, $sort_by) = @_;
236              
237 294 100       483 if (defined $sort_by) {
238 7 100       167 if (! grep /^$sort_by$/, qw(score_desc score_asc alpha_desc alpha_asc)) {
239 1         201 croak("sort() needs 'score_desc', 'score_asc', 'alpha_desc' or 'alpha_asc' as param");
240             }
241              
242 6 100       29 if ($sort_by =~ /^alpha/) {
    50          
243 4 100       26 $self->{sort_by} = $sort_by =~ /desc/
244             ? SORT_BY_ALPHA_DESC
245             : SORT_BY_ALPHA_ASC;
246             }
247             elsif ($sort_by =~ /^score/) {
248 2 50       10 $self->{sort_by} = $sort_by =~ /desc/
249             ? SORT_BY_SCORE_DESC
250             : SORT_BY_SCORE_ASC;
251             }
252             }
253              
254 293   100     1025 return $self->{sort_by} // SORT_BY_SCORE_DESC;
255             }
256              
257             # Private
258              
259             sub _args {
260 41     41   85 my ($self, $args) = @_;
261              
262             # file
263 41 100       158 $self->file($args->{file}) if exists $args->{file};
264              
265             # limit
266 39 100       98 $self->limit($args->{limit}) if exists $args->{limit};
267              
268             # max_results
269 36 100       142 $self->max_results($args->{max_results}) if exists $args->{max_results};
270              
271             # min_score
272 33 100       98 $self->min_score($args->{min_score}) if exists $args->{min_score};
273              
274             # min_syllables
275 30 100       75 $self->min_syllables($args->{min_syllables}) if exists $args->{min_syllables};
276              
277             # multi_word
278 27 100       73 $self->multi_word($args->{multi_word}) if exists $args->{multi_word};
279              
280             # return_raw
281 27 100       86 $self->return_raw($args->{return_raw}) if exists $args->{return_raw};
282              
283             # sort_by
284 27 100       78 $self->sort_by($args->{sort_by}) if exists $args->{sort_by};
285             }
286             sub _process {
287 24     24   78 my ($self, $result) = @_;
288              
289 24         39 my @data;
290              
291             # Dump rhyming words that don't have a score or are multi-word
292 24 100       62 if ($self->multi_word) {
293 1         7 @data = grep { $_->{score} } @$result;
  803         1013  
294             }
295             else {
296 23 100       102 @data = grep { $_->{score} && $_->{word} !~ /\s+/ } @$result;
  18469         52610  
297             }
298              
299             # Dump rhyming words that are outside of min_syllables threshold
300 24         92 @data = grep { $_->{numSyllables} >= $self->min_syllables } @data;
  9508         14787  
301              
302 24         300 my @sorted = sort {$b->{numSyllables} <=> $a->{numSyllables}} @data;
  44266         55640  
303 24         59 my %organized;
304              
305 24         96 for (@sorted) {
306 8643 100       13706 push @{ $organized{$_->{numSyllables}} }, $_ if $_->{score} >= $self->min_score;
  6497         14951  
307             }
308              
309 24         108 for (keys %organized) {
310 100 100       236 if ($self->sort_by == SORT_BY_ALPHA_DESC) {
    100          
    100          
    50          
311 10         14 @{ $organized{$_} } = sort {$b->{word} cmp $a->{word}} @{ $organized{$_} };
  10         77  
  4400         5651  
  10         31  
312             }
313             elsif ($self->sort_by == SORT_BY_ALPHA_ASC) {
314 10         19 @{ $organized{$_} } = sort {$a->{word} cmp $b->{word}} @{ $organized{$_} };
  10         81  
  4422         5687  
  10         44  
315             }
316             elsif ($self->sort_by == SORT_BY_SCORE_DESC) {
317 70         94 @{ $organized{$_} } = sort {$b->{score} <=> $a->{score}} @{ $organized{$_} };
  70         487  
  4232         5470  
  70         222  
318             }
319             elsif ($self->sort_by == SORT_BY_SCORE_ASC) {
320 10         14 @{ $organized{$_} } = sort {$a->{score} <=> $b->{score}} @{ $organized{$_} };
  10         78  
  1312         1651  
  10         29  
321             }
322             }
323              
324             # Limit the result count in each syllable href
325              
326 24         101 for (keys %organized) {
327 100 50       129 next if scalar @{ $organized{$_} } <= $self->limit;
  100         218  
328 0         0 @{ $organized{$_} } = @{ $organized{$_} }[0..$self->limit -1];
  0         0  
  0         0  
329             }
330              
331 24         3474 return \%organized;
332              
333             }
334             sub _uri {
335 2     2   17 my ($self, $word, $context) = @_;
336              
337 2         5 my $uri;
338              
339 2 100       9 if (defined $context) {
340 1         7 $uri = sprintf(
341             "http://api.datamuse.com/words?max=%d&ml=%s&rel_rhy=%s",
342             $self->max_results,
343             $context,
344             $word
345             );
346             }
347             else {
348 1         4 $uri = sprintf(
349             "http://api.datamuse.com/words?max=%d&rel_rhy=%s",
350             $self->max_results,
351             $word
352             );
353             }
354              
355 2         15 return $uri;
356             }
357       0     sub __placeholder {}
358              
359             1;
360             __END__