File Coverage

blib/lib/Data/Freq/Field.pm
Criterion Covered Total %
statement 188 192 97.9
branch 115 134 85.8
condition 32 46 69.5
subroutine 26 26 100.0
pod 13 13 100.0
total 374 411 91.0


line stmt bran cond sub pod time code
1 5     5   79290 use 5.006;
  5         22  
  5         218  
2 5     5   30 use strict;
  5         13  
  5         187  
3 5     5   26 use warnings;
  5         9  
  5         238  
4              
5             package Data::Freq::Field;
6              
7             =head1 NAME
8              
9             Data::Freq::Field - Controls counting with Data::Freq at each level
10              
11             =cut
12              
13 5     5   30 use Carp qw(croak);
  5         10  
  5         320  
14 5     5   4937 use Date::Parse qw(str2time);
  5         43770  
  5         455  
15 5     5   48 use Scalar::Util qw(looks_like_number);
  5         11  
  5         15432  
16             require POSIX;
17              
18             =head1 METHODS
19              
20             =head2 new
21              
22             Usage:
23              
24             Data::Freq::Field->new({
25             type => 'text' , # { 'text' | 'number' | 'date' }
26             sort => 'count', # { 'value' | 'count' | 'first' | 'last' }
27             order => 'desc' , # { 'asc' | 'desc' }
28             pos => 0 , # { 0 | 1 | 2 | -1 | -2 | .. | [0, 1, 2] | .. }
29             key => 'mykey', # { any key(s) for input hash refs }
30             convert => sub {...},
31             });
32              
33             Constructs a field object.
34              
35             See L for details.
36              
37             =cut
38              
39             sub new {
40 287     287 1 87377 my ($class, $input) = @_;
41 287         948 my $self = bless {}, $class;
42            
43 287 100       975 if (!ref $input) {
    100          
    50          
44 102 50       256 $self->_extract_any($input) or croak "invalid argument: $input";
45             } elsif (ref $input eq 'HASH') {
46 107         205 for my $target (qw(type aggregate sort order pos key)) {
47 642 100       1777 if (defined $input->{$target}) {
48 206         543 my $method = "_extract_$target";
49            
50 206 50       563 $self->$method($input->{$target})
51             or croak "invalid $target: $input->{$target}";
52             }
53             }
54            
55 107         204 for my $target (qw(offset limit)) {
56 214 100       564 if (defined $input->{$target}) {
57 44         125 $self->{$target} = int($input->{$target});
58             }
59             }
60            
61 107         193 for my $target (qw(convert)) {
62 107 100       336 if (defined $input->{$target}) {
63 1         4 $self->{$target} = $input->{$target};
64            
65 1 50       7 if (ref $input->{$target} ne 'CODE') {
66 0         0 croak "$target must be a CODE ref";
67             }
68             }
69             }
70             } elsif (ref $input eq 'ARRAY') {
71 78         147 for my $item (@$input) {
72 173 50       359 $self->_extract_any($item) or croak "invalid argument: $item";
73             }
74             } else {
75 0         0 croak "invalid field: $input";
76             }
77            
78 287 100       654 $self->{type} = 'text' unless defined $self->type;
79 287   100     1358 $self->{aggregate} ||= 'count';
80            
81 287 100       507 if ($self->type eq 'text') {
82 175   100     644 $self->{sort} ||= 'score';
83             } else {
84 112   100     602 $self->{sort} ||= 'value';
85             }
86            
87 287 100       1003 if ($self->{sort} =~ /^(count|score|last)$/) {
88 146   100     679 $self->{order} ||= 'desc';
89             } else {
90 141   100     1347 $self->{order} ||= 'asc';
91             }
92            
93 287         1031 return $self;
94             }
95              
96             =head2 evaluate_record
97              
98             Usage:
99              
100             my $field = Data::Freq::Field->new(...);
101             my $record = Data::Freq::Record->new(...);
102             my $normalized_text = $field->evaluate_record($record);
103              
104             Evaluates an input record as a normalized text that will be used for frequency counting,
105             depending on the parameters passed to the L method.
106              
107             This is intended to be an internal method for L.
108              
109             =cut
110              
111             sub evaluate_record {
112 249     249 1 317 my ($self, $record) = @_;
113 249         283 my $result = undef;
114            
115             TRY: {
116 249 100       262 if (defined $self->pos) {
  249 100       643  
    100          
    100          
117 129         234 my $pos = $self->pos;
118 129 50       318 my $array = $record->array or last TRY;
119 129         294 $result = "@$array[@$pos]";
120             } elsif (defined $self->key) {
121 2         5 my $key = $self->key;
122 2 50       9 my $hash = $record->hash or last TRY;
123 2         8 $result = "@$hash{@$key}";
124             } elsif ($self->type eq 'date') {
125 15         45 $result = $record->date;
126             } elsif ($self->type eq 'number') {
127 8 100       26 my $array = $record->array or last TRY;
128 7         22 $result = $array->[0];
129             } else {
130 95         242 $result = $record->text;
131             }
132            
133 248 100       557 last TRY unless defined $result;
134            
135 247 100       484 if ($self->type eq 'date') {
136 15 50       50 $result = looks_like_number($result) ? $result : str2time($result);
137 15 50       33 last TRY unless defined $result;
138 15         38 $result = POSIX::strftime($self->strftime, localtime $result);
139             }
140             }
141            
142 249 100       517 if ($self->convert) {
143 1         4 $result = $self->convert->($result);
144             }
145            
146 249         760 return $result;
147             }
148              
149             =head2 select_nodes
150              
151             Usage:
152              
153             my $raw_node_list = [values %{$parent_node->children}];
154             my $sorted_node_list = $field->select_nodes($raw_node_list);
155              
156             Sorts and reduces a list of nodes (Data::Freq::Node) at the corresponding depth
157             in the L,
158             depending on the parameters passed to the L method.
159              
160             This is intended to be an internal method for L.
161              
162             =cut
163              
164             sub select_nodes {
165 48     48 1 259 my ($self, $nodes, $subfield) = @_;
166 48         115 my $type = $self->type;
167 48         99 my $sort = $self->sort;
168 48         91 my $order = $self->order;
169            
170 48 100       119 if ($sort eq 'score') {
171 11 100       31 if ($subfield) {
172 8         15 $sort = $subfield->aggregate;
173             } else {
174 3         5 $sort = 'count';
175             }
176             }
177            
178 48         98 my @tuples = map {[$_, $_->$sort, $_->first]} @$nodes;
  168         467  
179            
180 48 100 100     222 if ($type ne 'number' && $sort eq 'value') {
181 28 100       54 if ($order eq 'asc') {
182 26 50       94 @tuples = CORE::sort {$a->[1] cmp $b->[1] || $a->[2] <=> $b->[2]} @tuples;
  111         296  
183             } else {
184 2 50       7 @tuples = CORE::sort {$b->[1] cmp $a->[1] || $a->[2] <=> $b->[2]} @tuples;
  6         20  
185             }
186             } else {
187 20 100       50 if ($order eq 'asc') {
188 9 50       36 @tuples = CORE::sort {$a->[1] <=> $b->[1] || $a->[2] <=> $b->[2]} @tuples;
  28         86  
189             } else {
190 11 50       33 @tuples = CORE::sort {$b->[1] <=> $a->[1] || $a->[2] <=> $b->[2]} @tuples;
  30         101  
191             }
192             }
193            
194 48         78 my @result = map {$_->[0]} @tuples;
  168         292  
195            
196 48 100 100     110 if (defined $self->offset || defined $self->limit) {
197 22 100       40 my $offset = defined $self->offset ? $self->offset : 0;
198 22 100       45 my $length = defined $self->limit ? $self->limit : scalar(@result);
199 22         64 @result = splice(@result, $offset, $length);
200             }
201            
202 48         285 return \@result;
203             }
204              
205             =head2 type
206              
207             Retrieves the C parameter.
208              
209             =head2 aggregate
210              
211             Retrieves the C parameter.
212              
213             =head2 sort
214              
215             Retrieves the C parameter.
216              
217             =head2 order
218              
219             Retrieves the C parameter.
220              
221             =head2 pos
222              
223             Retrieves the C parameter as an array ref.
224              
225             =head2 key
226              
227             Retrieves the C parameter as an array ref.
228              
229             =head2 limit
230              
231             Retrieves the C parameter.
232              
233             =head2 offset
234              
235             Retrieves the C parameter.
236              
237             =head2 strftime
238              
239             Retrieves the C parameter (L).
240              
241             =head2 convert
242              
243             Retrieves the C parameter.
244              
245             =cut
246              
247 1150     1150 1 3515 sub type {$_[0]{type }}
248 26     26 1 115 sub aggregate {$_[0]{aggregate}}
249 94     94 1 319 sub sort {$_[0]{sort }}
250 84     84 1 385 sub order {$_[0]{order }}
251 394     394 1 1020 sub pos {$_[0]{pos }}
252 126     126 1 410 sub key {$_[0]{key }}
253 76     76 1 253 sub limit {$_[0]{limit }}
254 89     89 1 300 sub offset {$_[0]{offset }}
255 33     33 1 884 sub strftime {$_[0]{strftime}}
256 250     250 1 746 sub convert {$_[0]{convert }}
257              
258             sub _extract_any {
259 275     275   381 my ($self, $input) = @_;
260            
261 275         484 for my $target (qw(pos type aggregate sort order)) {
262 754         1414 my $method = "_extract_$target";
263 754 100       1795 return $self if $self->$method($input);
264             }
265            
266 0         0 return undef;
267             }
268              
269             sub _extract_type {
270 308     308   551 my ($self, $input) = @_;
271 308 50       767 return undef if ref($input);
272            
273 308 100 66     3749 if (!defined $input || $input eq '' || $input =~ /^texts?$/i) {
    100 100        
    100          
    100          
    100          
    100          
    100          
    100          
    100          
274 91         225 $self->{type} = 'text';
275 91         456 return $self;
276             } elsif ($input =~ /^num(ber)?s?$/i) {
277 38         110 $self->{type} = 'number';
278 38         201 return $self;
279             } elsif ($input =~ /\%/) {
280 3         9 $self->{type} = 'date';
281 3         8 $self->{strftime} = $input;
282 3         18 return $self;
283             } elsif ($input =~ /^years?$/i) {
284 6         17 $self->{type} = 'date';
285 6         11 $self->{strftime} = '%Y';
286 6         32 return $self;
287             } elsif ($input =~ /^month?s?$/i) {
288 12         33 $self->{type} = 'date';
289 12         23 $self->{strftime} = '%Y-%m';
290 12         69 return $self;
291             } elsif ($input =~ /^(date|day)s?$/i) {
292 39         105 $self->{type} = 'date';
293 39         73 $self->{strftime} = '%Y-%m-%d';
294 39         371 return $self;
295             } elsif ($input =~ /^hours?$/i) {
296 4         11 $self->{type} = 'date';
297 4         7 $self->{strftime} = '%Y-%m-%d %H';
298 4         45 return $self;
299             } elsif ($input =~ /^minutes?$/i) {
300 4         11 $self->{type} = 'date';
301 4         8 $self->{strftime} = '%Y-%m-%d %H:%M';
302 4         23 return $self;
303             } elsif ($input =~ /^(seconds?|time)?$/i) {
304 6         89 $self->{type} = 'date';
305 6         8 $self->{strftime} = '%Y-%m-%d %H:%M:%S';
306 6         33 return $self;
307             }
308            
309 105         365 return undef;
310             }
311              
312             sub _extract_aggregate {
313 113     113   156 my ($self, $input) = @_;
314 113 50 33     698 return undef if !defined $input || ref($input) || $input eq '';
      33        
315            
316 113 100       561 if ($input =~ /^uniq(ue)?$/) {
    100          
    100          
    100          
317 4         14 $self->{aggregate} = 'unique';
318 4         23 return $self;
319             } elsif ($input =~ /^max(imum)?$/) {
320 4         12 $self->{aggregate} = 'max';
321 4         19 return $self;
322             } elsif ($input =~ /^min(imum)?$/) {
323 4         11 $self->{aggregate} = 'min';
324 4         23 return $self;
325             } elsif ($input =~ /^av(g|e(rage)?)?$/) {
326 6         14 $self->{aggregate} = 'average';
327 6         34 return $self;
328             }
329            
330 95         305 return undef;
331             }
332              
333             sub _extract_sort {
334 160     160   218 my ($self, $input) = @_;
335 160 50 33     1049 return undef if !defined $input || ref($input) || $input eq '';
      33        
336            
337 160 100       835 if ($input =~ /^values?$/i) {
    100          
    100          
    100          
    100          
338 49         103 $self->{sort} = 'value';
339 49         224 return $self;
340             } elsif ($input =~ /^counts?$/i) {
341 25         51 $self->{sort} = 'count';
342 25         130 return $self;
343             } elsif ($input =~ /^scores?$/i) {
344 17         40 $self->{sort} = 'score';
345 17         99 return $self;
346             } elsif ($input =~ /^(first|occur(rence)?s?)$/i) {
347 21         51 $self->{sort} = 'first';
348 21         112 return $self;
349             } elsif ($input =~ /^last$/i) {
350 15         32 $self->{sort} = 'last';
351 15         89 return $self;
352             }
353            
354 33         106 return undef;
355             }
356              
357             sub _extract_order {
358 83     83   114 my ($self, $input) = @_;
359 83 50 33     535 return undef if !defined $input || ref($input) || $input eq '';
      33        
360            
361 83 100       350 if ($input =~ /^asc(end(ing)?)?$/i) {
    50          
362 55         126 $self->{order} = 'asc';
363 55         259 return $self;
364             } elsif ($input =~ /^desc(end(ing)?)?$/i) {
365 28         59 $self->{order} = 'desc';
366 28         154 return $self;
367             }
368            
369 0         0 return undef;
370             }
371              
372             sub _extract_pos {
373 287     287   365 my ($self, $input) = @_;
374 287 100       622 return undef if !defined $input;
375            
376 281 100       1337 if (ref $input eq 'ARRAY') {
    100          
377 11   100     45 $self->{pos} ||= [];
378 11         13 push @{$self->{pos}}, @$input;
  11         29  
379 11         58 return $self;
380             } elsif ($input =~ /^-?\d+$/) {
381 30   100     153 $self->{pos} ||= [];
382 30         41 push @{$self->{pos}}, $input;
  30         72  
383 30         157 return $self;
384             }
385            
386 240         845 return undef;
387             }
388              
389             sub _extract_key {
390 9     9   14 my ($self, $input) = @_;
391 9 50       23 return undef if !defined $input;
392            
393 9   50     71 $self->{key} ||= [];
394 9 100       12 push @{$self->{key}}, (ref($input) eq 'ARRAY' ? @$input : ($input));
  9         52  
395 9         82 return $self;
396             }
397              
398             =head1 AUTHOR
399              
400             Mahiro Ando, C<< >>
401              
402             =head1 LICENSE AND COPYRIGHT
403              
404             Copyright 2012 Mahiro Ando.
405              
406             This program is free software; you can redistribute it and/or modify it
407             under the terms of either: the GNU General Public License as published
408             by the Free Software Foundation; or the Artistic License.
409              
410             See http://dev.perl.org/licenses/ for more information.
411              
412             =cut
413              
414             1;