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   867903 use strict;
  12         132  
  12         360  
4 12     12   71 use warnings;
  12         25  
  12         565  
5              
6             our $VERSION = '1.04';
7              
8 12     12   118 use Carp qw(croak);
  12         23  
  12         652  
9 12     12   6154 use HTTP::Request;
  12         237880  
  12         449  
10 12     12   7699 use JSON;
  12         116684  
  12         65  
11 12     12   10330 use LWP::UserAgent;
  12         324790  
  12         832  
12              
13             use constant {
14             # Core
15 12         29442 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   101 };
  12         32  
40              
41             my $ua = LWP::UserAgent->new;
42              
43             # Public
44              
45             sub new {
46 41     41 1 23147 my ($class, %args) = @_;
47              
48 41         117 my $self = bless {}, $class;
49              
50 41         157 $self->_args(\%args);
51              
52 27         151 return $self;
53             }
54             sub fetch {
55 28     28 1 834 my ($self, $word, $context) = @_;
56              
57 28 100       79 if (! defined $word) {
58 1         199 croak("fetch() needs a word sent in");
59             }
60              
61 27 100 66     77 if (defined $context && $context !~ /^\w+$/) {
62 1         76 croak("context parameter must be an alpha word only.");
63             }
64              
65 26         45 my ($req, $response);
66              
67 26 50       55 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     59 if ($self->file || $response->is_success) {
73              
74 26         39 my $json;
75              
76 26 50       48 if ($self->file) {
77             {
78 26         42 local $/;
  26         97  
79 26 50       119 open my $fh, '<', $self->file or croak(
80             sprintf("Can't open the data file '%s': $!", $self->file)
81             );
82 26         2515 $json = <$fh>;
83 26         482 close $fh;
84             }
85             }
86             else {
87 0         0 $json = $response->decoded_content;
88             }
89              
90 26         19481 my $result = decode_json $json;
91              
92 26 100       203 return $result if $self->return_raw;
93              
94 24         75 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 1218 my ($self, $file) = @_;
103              
104 122 100       218 if (defined $file) {
105 17 100       714 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         68 $self->{file} = $file;
108             }
109              
110 118   100     1499 return $self->{file} // '';
111             }
112             sub limit {
113 1107     1107 1 514213 my ($self, $limit) = @_;
114              
115 1107 100       2915 if (defined $limit) {
116 1006 100       5417 croak("limit must be an integer") if $limit !~ /^\d+$/;
117 1004 100 100     4000 if ($limit < MIN_LIMIT || $limit > MAX_LIMIT) {
118 4         351 croak(
119             sprintf(
120             "limit must be between %d and %d",
121             MIN_LIMIT,
122             MAX_LIMIT
123             )
124             );
125             }
126 1000         1914 $self->{limit} = $limit;
127             }
128              
129 1101   100     5004 return $self->{limit} // MAX_LIMIT;
130             }
131             sub max_results {
132 1009     1009 1 520170 my ($self, $max) = @_;
133              
134 1009 100       2659 if (defined $max) {
135 1006 100       5749 croak("max_results must be an integer") if $max !~ /^\d+$/;
136 1004 100 100     4102 if ($max < MIN_RESULTS || $max > MAX_RESULTS) {
137 4         351 croak(
138             sprintf(
139             "max_results must be between %d and %d",
140             MIN_RESULTS,
141             MAX_RESULTS
142             )
143             );
144             }
145 1000         1871 $self->{max_results} = $max;
146             }
147              
148 1003   100     4619 return $self->{max_results} // MAX_RESULTS;
149             }
150             sub min_score {
151 10259     10259 1 836137 my ($self, $min) = @_;
152              
153 10259 100       17561 if (defined $min) {
154 1615 100       9366 croak("min_score must be an integer") if $min !~ /^-?\d+$/;
155 1613 100 100     6671 if ($min < MIN_SCORE || $min > MAX_SCORE) {
156 4         348 croak(
157             sprintf(
158             "min_score must be between %d and %d",
159             MIN_SCORE,
160             MAX_SCORE
161             )
162             );
163             }
164 1609         3398 $self->{min_score} = $min;
165             }
166              
167 10253   100     30810 return $self->{min_score} // MIN_SCORE;
168             }
169             sub min_syllables {
170 9619     9619 1 67447 my ($self, $min) = @_;
171              
172 9619 100       14665 if (defined $min) {
173 110 100       1022 croak("min_syllables must be an integer") if $min !~ /^-?\d+$/;
174 108 100 100     455 if ($min < MIN_SYLLABLES || $min > MAX_SYLLABLES) {
175 4         354 croak(
176             sprintf(
177             "min_syllables must be between %d and %d",
178             MIN_SYLLABLES,
179             MAX_SYLLABLES
180             )
181             );
182             }
183 104         202 $self->{min_syllables} = $min;
184             }
185              
186 9613   100     26591 return $self->{min_syllables} // MIN_SYLLABLES;
187             }
188             sub multi_word {
189 29     29 1 67 my ($self, $bool) = @_;
190              
191 29 100       65 if (defined $bool) {
192 3         7 $self->{multi_word} = $bool;
193             }
194              
195 29   100     137 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 739 my ($self, $ret) = @_;
227              
228 28 100       82 if (defined $ret) {
229 2         4 $self->{return_raw} = $ret;
230             }
231              
232 28   100     174 return $self->{return_raw} // RETURN_RAW;
233             }
234             sub sort_by {
235 294     294 1 3781 my ($self, $sort_by) = @_;
236              
237 294 100       497 if (defined $sort_by) {
238 7 100       165 if (! grep /^$sort_by$/, qw(score_desc score_asc alpha_desc alpha_asc)) {
239 1         189 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       24 $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     977 return $self->{sort_by} // SORT_BY_SCORE_DESC;
255             }
256              
257             # Private
258              
259             sub _args {
260 41     41   119 my ($self, $args) = @_;
261              
262             # file
263 41 100       171 $self->file($args->{file}) if exists $args->{file};
264              
265             # limit
266 39 100       107 $self->limit($args->{limit}) if exists $args->{limit};
267              
268             # max_results
269 36 100       126 $self->max_results($args->{max_results}) if exists $args->{max_results};
270              
271             # min_score
272 33 100       90 $self->min_score($args->{min_score}) if exists $args->{min_score};
273              
274             # min_syllables
275 30 100       79 $self->min_syllables($args->{min_syllables}) if exists $args->{min_syllables};
276              
277             # multi_word
278 27 100       71 $self->multi_word($args->{multi_word}) if exists $args->{multi_word};
279              
280             # return_raw
281 27 100       94 $self->return_raw($args->{return_raw}) if exists $args->{return_raw};
282              
283             # sort_by
284 27 100       85 $self->sort_by($args->{sort_by}) if exists $args->{sort_by};
285             }
286             sub _process {
287 24     24   59 my ($self, $result) = @_;
288              
289 24         41 my @data;
290              
291             # Dump rhyming words that don't have a score or are multi-word
292 24 100       59 if ($self->multi_word) {
293 1         5 @data = grep { $_->{score} } @$result;
  803         1000  
294             }
295             else {
296 23 100       90 @data = grep { $_->{score} && $_->{word} !~ /\s+/ } @$result;
  18469         52640  
297             }
298              
299             # Dump rhyming words that are outside of min_syllables threshold
300 24         97 @data = grep { $_->{numSyllables} >= $self->min_syllables } @data;
  9508         15274  
301              
302 24         273 my @sorted = sort {$b->{numSyllables} <=> $a->{numSyllables}} @data;
  44266         56000  
303 24         54 my %organized;
304              
305 24         74 for (@sorted) {
306 8643 100       13922 push @{ $organized{$_->{numSyllables}} }, $_ if $_->{score} >= $self->min_score;
  6497         14981  
307             }
308              
309 24         102 for (keys %organized) {
310 100 100       230 if ($self->sort_by == SORT_BY_ALPHA_DESC) {
    100          
    100          
    50          
311 10         14 @{ $organized{$_} } = sort {$b->{word} cmp $a->{word}} @{ $organized{$_} };
  10         76  
  4400         5629  
  10         30  
312             }
313             elsif ($self->sort_by == SORT_BY_ALPHA_ASC) {
314 10         18 @{ $organized{$_} } = sort {$a->{word} cmp $b->{word}} @{ $organized{$_} };
  10         80  
  4422         5927  
  10         31  
315             }
316             elsif ($self->sort_by == SORT_BY_SCORE_DESC) {
317 70         128 @{ $organized{$_} } = sort {$b->{score} <=> $a->{score}} @{ $organized{$_} };
  70         454  
  4232         5444  
  70         217  
318             }
319             elsif ($self->sort_by == SORT_BY_SCORE_ASC) {
320 10         15 @{ $organized{$_} } = sort {$a->{score} <=> $b->{score}} @{ $organized{$_} };
  10         84  
  1312         1646  
  10         28  
321             }
322             }
323              
324             # Limit the result count in each syllable href
325              
326 24         92 for (keys %organized) {
327 100 50       128 next if scalar @{ $organized{$_} } <= $self->limit;
  100         205  
328 0         0 @{ $organized{$_} } = @{ $organized{$_} }[0..$self->limit -1];
  0         0  
  0         0  
329             }
330              
331 24         3292 return \%organized;
332              
333             }
334             sub _uri {
335 2     2   13 my ($self, $word, $context) = @_;
336              
337 2         4 my $uri;
338              
339 2 100       6 if (defined $context) {
340 1         3 $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         3 $uri = sprintf(
349             "http://api.datamuse.com/words?max=%d&rel_rhy=%s",
350             $self->max_results,
351             $word
352             );
353             }
354              
355 2         11 return $uri;
356             }
357       0     sub __placeholder {}
358              
359             1;
360             __END__