File Coverage

blib/lib/Word/Rhymes.pm
Criterion Covered Total %
statement 134 155 86.4
branch 77 90 85.5
condition 26 32 81.2
subroutine 19 21 90.4
pod 10 10 100.0
total 266 308 86.3


line stmt bran cond sub pod time code
1             package Word::Rhymes;
2              
3 11     11   801000 use strict;
  11         112  
  11         323  
4 11     11   57 use warnings;
  11         18  
  11         464  
5              
6             our $VERSION = '1.02';
7              
8 11     11   64 use Carp qw(croak);
  11         17  
  11         588  
9 11     11   5729 use HTTP::Request;
  11         221079  
  11         337  
10 11     11   7965 use JSON;
  11         106230  
  11         62  
11 11     11   9512 use LWP::UserAgent;
  11         297817  
  11         708  
12              
13             use constant {
14             # Core
15 11         25374 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              
24             # print() related
25             MAX_NUM_COLS => 8,
26             MIN_NUM_COLS => 7,
27             COL_DIVIDER => 15,
28             COL_PADDING => 3,
29             ROW_INDENT => ' ',
30              
31             # Sort by
32             SORT_BY_SCORE_DESC => 0x00, # Default
33             SORT_BY_SCORE_ASC => 0x01,
34             SORT_BY_ALPHA_DESC => 0x02,
35             SORT_BY_ALPHA_ASC => 0x03,
36              
37 11     11   101 };
  11         41  
38              
39             my $ua = LWP::UserAgent->new;
40              
41             # Public
42              
43             sub new {
44 36     36 1 20452 my ($class, %args) = @_;
45              
46 36         94 my $self = bless {}, $class;
47              
48 36         135 $self->_args(\%args);
49              
50 25         85 return $self;
51             }
52             sub fetch {
53 28     28 1 766 my ($self, $word, $context) = @_;
54              
55 28 100       68 if (! defined $word) {
56 1         169 croak("fetch() needs a word sent in");
57             }
58              
59 27 100 66     78 if (defined $context && $context !~ /^\w+$/) {
60 1         75 croak("context parameter must be an alpha word only.");
61             }
62              
63 26         39 my ($req, $response);
64              
65 26 50       57 if (! $self->file) {
66 0         0 $req = HTTP::Request->new('GET', $self->_uri($word, $context));
67 0         0 $response = $ua->request($req);
68             }
69              
70 26 50 33     57 if ($self->file || $response->is_success) {
71              
72 26         43 my $json;
73              
74 26 50       49 if ($self->file) {
75             {
76 26         43 local $/;
  26         91  
77 26 50       116 open my $fh, '<', $self->file or croak(
78             sprintf("Can't open the data file '%s': $!", $self->file)
79             );
80 26         2544 $json = <$fh>;
81 26         442 close $fh;
82             }
83             }
84             else {
85 0         0 $json = $response->decoded_content;
86             }
87              
88 26         19251 my $result = decode_json $json;
89              
90 26 100       138 return $result if $self->return_raw;
91              
92 24         74 return $self->_process($result);
93             }
94             else {
95 0         0 print "Invalid response\n\n";
96 0         0 return undef;
97             }
98             }
99             sub file {
100 122     122 1 1162 my ($self, $file) = @_;
101              
102 122 100       226 if (defined $file) {
103 17 100       742 croak("File '$file' does not exist") if ! -e $file;
104 15 100       398 croak("File '$file' is not a valid file") if ! -f $file;
105 13         62 $self->{file} = $file;
106             }
107              
108 118   100     1332 return $self->{file} // '';
109             }
110             sub max_results {
111 1009     1009 1 519879 my ($self, $max) = @_;
112              
113 1009 100       2587 if (defined $max) {
114 1006 100       5714 croak("max_results must be an integer") if $max !~ /^\d+$/;
115 1004 100 100     4000 if ($max < MIN_RESULTS || $max > MAX_RESULTS) {
116 4         375 croak(
117             sprintf(
118             "max_results must be between %d and %d",
119             MIN_RESULTS,
120             MAX_RESULTS
121             )
122             );
123             }
124 1000         1949 $self->{max_results} = $max;
125             }
126              
127 1003   100     4789 return $self->{max_results} // MAX_RESULTS;
128             }
129             sub min_score {
130 10259     10259 1 846800 my ($self, $min) = @_;
131              
132 10259 100       17264 if (defined $min) {
133 1615 100       9035 croak("min_score must be an integer") if $min !~ /^-?\d+$/;
134 1613 100 100     6856 if ($min < MIN_SCORE || $min > MAX_SCORE) {
135 4         355 croak(
136             sprintf(
137             "min_score must be between %d and %d",
138             MIN_SCORE,
139             MAX_SCORE
140             )
141             );
142             }
143 1609         2836 $self->{min_score} = $min;
144             }
145              
146 10253   100     29836 return $self->{min_score} // MIN_SCORE;
147             }
148             sub min_syllables {
149 9619     9619 1 66946 my ($self, $min) = @_;
150              
151 9619 100       14452 if (defined $min) {
152 110 100       922 croak("min_syllables must be an integer") if $min !~ /^-?\d+$/;
153 108 100 100     482 if ($min < MIN_SYLLABLES || $min > MAX_SYLLABLES) {
154 4         369 croak(
155             sprintf(
156             "min_syllables must be between %d and %d",
157             MIN_SYLLABLES,
158             MAX_SYLLABLES
159             )
160             );
161             }
162 104         206 $self->{min_syllables} = $min;
163             }
164              
165 9613   100     27037 return $self->{min_syllables} // MIN_SYLLABLES;
166             }
167             sub multi_word {
168 29     29 1 68 my ($self, $bool) = @_;
169              
170 29 100       71 if (defined $bool) {
171 3         7 $self->{multi_word} = $bool;
172             }
173              
174 29   100     142 return $self->{multi_word} // MULTI_WORD;
175             }
176             sub print {
177 0     0 1 0 my ($self, $word, $context) = @_;
178              
179 0         0 my $rhyming_words = $self->fetch($word, $context);
180              
181 0 0       0 print defined $context
182             ? "\nRhymes with '$word' related to '$context'\n"
183             : "\nRhymes with '$word'\n";
184              
185 0         0 for my $num_syl (reverse sort keys %$rhyming_words) {
186             my $max_word_len = length(
187 0         0 (sort {length $b->{word} <=> length $a->{word}} @{ $rhyming_words->{$num_syl} })[0]->{word}
  0         0  
188 0         0 );
189              
190 0         0 my $column_width = $max_word_len + COL_PADDING;
191 0 0       0 my $columns = $column_width > COL_DIVIDER ? MIN_NUM_COLS : MAX_NUM_COLS;
192              
193 0         0 printf "\nSyllables: $num_syl\n\n%s", ROW_INDENT;
194              
195 0         0 for (0 .. $#{ $rhyming_words->{$num_syl} }) {
  0         0  
196 0 0 0     0 printf "\n%s", ROW_INDENT if $_ % $columns == 0 && $_ != 0;
197 0         0 printf("%-*s", $column_width, $rhyming_words->{$num_syl}[$_]->{word});
198             }
199 0         0 print "\n";
200             }
201              
202 0         0 return 0;
203             }
204             sub return_raw {
205 28     28 1 660 my ($self, $ret) = @_;
206              
207 28 100       87 if (defined $ret) {
208 2         5 $self->{return_raw} = $ret;
209             }
210              
211 28   100     172 return $self->{return_raw} // RETURN_RAW;
212             }
213             sub sort_by {
214 294     294 1 3610 my ($self, $sort_by) = @_;
215              
216 294 100       496 if (defined $sort_by) {
217 7 100       161 if (! grep /^$sort_by$/, qw(score_desc score_asc alpha_desc alpha_asc)) {
218 1         169 croak("sort() needs 'score_desc', 'score_asc', 'alpha_desc' or 'alpha_asc' as param");
219             }
220              
221 6 100       29 if ($sort_by =~ /^alpha/) {
    50          
222 4 100       16 $self->{sort_by} = $sort_by =~ /desc/
223             ? SORT_BY_ALPHA_DESC
224             : SORT_BY_ALPHA_ASC;
225             }
226             elsif ($sort_by =~ /^score/) {
227 2 50       8 $self->{sort_by} = $sort_by =~ /desc/
228             ? SORT_BY_SCORE_DESC
229             : SORT_BY_SCORE_ASC;
230             }
231             }
232              
233 293   100     1010 return $self->{sort_by} // SORT_BY_SCORE_DESC;
234             }
235              
236             # Private
237              
238             sub _args {
239 36     36   79 my ($self, $args) = @_;
240              
241             # file
242 36 100       146 $self->file($args->{file}) if exists $args->{file};
243              
244             # max_results
245 34 100       95 $self->max_results($args->{max_results}) if exists $args->{max_results};
246              
247             # min_score
248 31 100       128 $self->min_score($args->{min_score}) if exists $args->{min_score};
249              
250             # min_syllables
251 28 100       83 $self->min_syllables($args->{min_syllables}) if exists $args->{min_syllables};
252              
253             # multi_word
254 25 100       68 $self->multi_word($args->{multi_word}) if exists $args->{multi_word};
255              
256             # return_raw
257 25 100       70 $self->return_raw($args->{return_raw}) if exists $args->{return_raw};
258              
259             # sort_by
260 25 100       108 $self->sort_by($args->{sort_by}) if exists $args->{sort_by};
261             }
262             sub _process {
263 24     24   48 my ($self, $result) = @_;
264              
265 24         41 my @data;
266              
267             # Dump rhyming words that don't have a score or are multi-word
268 24 100       56 if ($self->multi_word) {
269 1         5 @data = grep { $_->{score} } @$result;
  803         1021  
270             }
271             else {
272 23 100       102 @data = grep { $_->{score} && $_->{word} !~ /\s+/ } @$result;
  18469         51709  
273             }
274              
275             # Dump rhyming words that are outside of min_syllables threshold
276 24         92 @data = grep { $_->{numSyllables} >= $self->min_syllables } @data;
  9508         15310  
277              
278 24         226 my @sorted = sort {$b->{numSyllables} <=> $a->{numSyllables}} @data;
  44266         55271  
279 24         66 my %organized;
280              
281 24         78 for (@sorted) {
282 8643 100       13681 push @{ $organized{$_->{numSyllables}} }, $_ if $_->{score} >= $self->min_score;
  6497         14576  
283             }
284              
285 24         99 for (keys %organized) {
286 100 100       224 if ($self->sort_by == SORT_BY_ALPHA_DESC) {
    100          
    100          
    50          
287 10         17 @{ $organized{$_} } = sort {$b->{word} cmp $a->{word}} @{ $organized{$_} };
  10         94  
  4400         5752  
  10         32  
288             }
289             elsif ($self->sort_by == SORT_BY_ALPHA_ASC) {
290 10         16 @{ $organized{$_} } = sort {$a->{word} cmp $b->{word}} @{ $organized{$_} };
  10         78  
  4422         5696  
  10         29  
291             }
292             elsif ($self->sort_by == SORT_BY_SCORE_DESC) {
293 70         89 @{ $organized{$_} } = sort {$b->{score} <=> $a->{score}} @{ $organized{$_} };
  70         457  
  4232         5337  
  70         205  
294             }
295             elsif ($self->sort_by == SORT_BY_SCORE_ASC) {
296 10         14 @{ $organized{$_} } = sort {$a->{score} <=> $b->{score}} @{ $organized{$_} };
  10         85  
  1312         1687  
  10         32  
297             }
298             }
299 24         3269 return \%organized;
300              
301             }
302             sub _uri {
303 2     2   19 my ($self, $word, $context) = @_;
304              
305 2         8 my $uri;
306              
307 2 100       7 if (defined $context) {
308 1         3 $uri = sprintf(
309             "http://api.datamuse.com/words?max=%d&ml=%s&rel_rhy=%s",
310             $self->max_results,
311             $context,
312             $word
313             );
314             }
315             else {
316 1         3 $uri = sprintf(
317             "http://api.datamuse.com/words?max=%d&rel_rhy=%s",
318             $self->max_results,
319             $word
320             );
321             }
322              
323 2         12 return $uri;
324             }
325       0     sub __placeholder {}
326              
327             1;
328             __END__