File Coverage

blib/lib/Biblio/COUNTER/Report.pm
Criterion Covered Total %
statement 108 549 19.6
branch 0 156 0.0
condition 0 38 0.0
subroutine 36 126 28.5
pod 0 53 0.0
total 144 922 15.6


line stmt bran cond sub pod time code
1             package Biblio::COUNTER::Report;
2              
3 1     1   8 use strict;
  1         2  
  1         52  
4 1     1   7 use warnings;
  1         2  
  1         55  
5              
6 1     1   6 use Biblio::COUNTER;
  1         2  
  1         161  
7              
8             require Exporter;
9 1     1   6 use vars qw(@ISA @EXPORT_OK);
  1         2  
  1         103  
10             @ISA = qw(Exporter);
11             @EXPORT_OK = qw(
12             MAY_BE_BLANK
13             NOT_BLANK
14             EXACT_MATCH
15             REQUESTS
16             SEARCHES
17             SESSIONS
18             TURNAWAYS
19             );
20              
21             # --- Constants
22              
23             # Scope -- where are we in the report?
24 1     1   6 use constant REPORT => 'report';
  1         2  
  1         78  
25 1     1   6 use constant RECORD => 'record'; # In the records that the report contains
  1         2  
  1         56  
26              
27             # Field names
28 1     1   6 use constant NAME => 'name';
  1         1  
  1         55  
29 1     1   5 use constant CODE => 'code';
  1         2  
  1         52  
30 1     1   5 use constant RELEASE => 'release';
  1         2  
  1         52  
31 1     1   6 use constant DESCRIPTION => 'description';
  1         1  
  1         51  
32 1     1   4 use constant DATE_RUN => 'date_run';
  1         2  
  1         48  
33 1     1   5 use constant CRITERIA => 'criteria';
  1         1  
  1         44  
34 1     1   5 use constant PERIOD_COVERED => 'period_covered'; # JR1a
  1         2  
  1         50  
35 1     1   5 use constant LABEL => 'label';
  1         2  
  1         50  
36 1     1   6 use constant PERIOD_LABEL => 'period_label';
  1         2  
  1         42  
37 1     1   4 use constant BLANK => 'blank_field';
  1         2  
  1         40  
38 1     1   5 use constant PERIODS => 'periods';
  1         1  
  1         34  
39 1     1   18 use constant COUNT => 'count';
  1         7  
  1         44  
40 1     1   5 use constant TITLE => 'title';
  1         1  
  1         43  
41 1     1   6 use constant PUBLISHER => 'publisher';
  1         1  
  1         38  
42 1     1   5 use constant PLATFORM => 'platform';
  1         1  
  1         43  
43 1     1   5 use constant PRINT_ISSN => 'print_issn';
  1         2  
  1         48  
44 1     1   5 use constant ONLINE_ISSN => 'online_issn';
  1         1  
  1         47  
45 1     1   21 use constant YTD_HTML => 'ytd_html';
  1         3  
  1         43  
46 1     1   5 use constant YTD_PDF => 'ytd_pdf';
  1         2  
  1         57  
47 1     1   5 use constant YTD_TOTAL => 'ytd';
  1         2  
  1         36  
48              
49             # Metrics
50 1     1   4 use constant REQUESTS => 'requests';
  1         2  
  1         47  
51 1     1   6 use constant SEARCHES => 'searches';
  1         2  
  1         55  
52 1     1   5 use constant SESSIONS => 'sessions';
  1         2  
  1         39  
53 1     1   4 use constant TURNAWAYS => 'turnaways';
  1         589  
  1         59  
54              
55             # Field matching
56 1     1   6 use constant MAY_BE_BLANK => 0;
  1         2  
  1         40  
57 1     1   5 use constant NOT_BLANK => 1;
  1         1  
  1         44  
58 1     1   5 use constant EXACT_MATCH => 2;
  1         2  
  1         43  
59              
60             # Useful constants
61 1     1   5 use constant INVALID => 0;
  1         1  
  1         37  
62 1     1   4 use constant VALID => 1;
  1         2  
  1         35  
63 1     1   5 use constant FIXED => 2;
  1         1  
  1         6581  
64              
65             # --- Variables
66              
67             my %mon2num = qw(
68             jan 01
69             feb 02
70             mar 03
71             apr 04
72             may 05
73             jun 06
74             jul 07
75             aug 08
76             sep 09
77             oct 10
78             nov 11
79             dec 12
80             );
81              
82             my @num2mon = qw(
83             ---
84             jan
85             feb
86             mar
87             apr
88             may
89             jun
90             jul
91             aug
92             sep
93             oct
94             nov
95             dec
96             );
97              
98             my $rx_mon = qr/(?i)jan|feb|mar|apr|may|june?|july?|aug|sept?|oct|nov|dec|0[1-9]|1[0-2]/;
99             my $rx_year = qr/(?:2[012])?\d\d/; # Good through 2299
100              
101             # ------------------------------------------------------------ PUBLIC METHODS --
102              
103             sub new {
104 0     0 0   my ($cls, %args) = @_;
105 0           bless {
106             'treat_blank_counts_as_zero' => 0,
107             'change_not_available_to_blank' => 0,
108             'dont_reread_next_row' => 0,
109             %args,
110             }, $cls;
111             }
112              
113             sub process {
114 0     0 0   my ($self) = @_;
115 0           $self->begin_file
116             ->begin_report
117             ->process_header
118             ->process_body
119             ->end_report
120             ->end_file;
121             }
122              
123             # ---------------------------------------------- TOP-LEVEL STRUCTURAL METHODS --
124              
125             sub begin_file {
126 0     0 0   my ($self) = @_;
127 0           $self->trigger_callback('begin_file', $self->{'file'});
128             }
129              
130             sub end_file {
131 0     0 0   my ($self) = @_;
132 0           $self->trigger_callback('end_file', $self->{'file'});
133             }
134              
135             sub begin_report {
136 0     0 0   my ($self) = @_;
137 0           $self->trigger_callback('begin_report');
138 0           $self->_orient;
139             }
140              
141             sub end_report {
142 0     0 0   my ($self) = @_;
143 0           $self->{'is_valid'} = !$self->{'errors'};
144 0           $self->trigger_callback('end_report');
145 0           undef $self->{'fh'};
146 0           return $self;
147             }
148              
149             sub process_header {
150 0     0 0   my ($self) = @_;
151 0           $self->begin_header;
152 0           $self->process_header_rows;
153 0           $self->end_header;
154             }
155              
156             sub process_header_rows {
157 0     0 0   die "Every report must have its own header-processing code";
158             }
159              
160             sub process_body {
161 0     0 0   my ($self) = @_;
162 0           $self->_in_scope(RECORD);
163 0           $self->begin_body;
164 0           while (!$self->_eof) {
165 0           $self->begin_record;
166 0           $self->process_record;
167 0           $self->end_record;
168             }
169 0           $self->end_body;
170 0           return $self;
171             }
172              
173             sub begin_body {
174 0     0 0   my ($self) = @_;
175 0           $self->trigger_callback('begin_body');
176 0           return $self;
177             }
178              
179             sub end_body {
180 0     0 0   my ($self) = @_;
181 0           $self->trigger_callback('end_body');
182 0           return $self;
183             }
184              
185             sub process_record {
186 0     0 0   die "Every report must have its own record-parsing code";
187             }
188              
189             # ----------------------------------------------------------------- ACCESSORS --
190              
191 0 0   0 0   sub name { @_ > 1 ? $_[0]->{'report'}->{NAME() } = $_[1] : $_[0]->{'report'}->{NAME() } }
192 0 0   0 0   sub code { @_ > 1 ? $_[0]->{'report'}->{CODE() } = $_[1] : $_[0]->{'report'}->{CODE() } }
193 0 0   0 0   sub release { @_ > 1 ? $_[0]->{'report'}->{RELEASE() } = $_[1] : $_[0]->{'report'}->{RELEASE() } }
194 0 0   0 0   sub description { @_ > 1 ? $_[0]->{'report'}->{DESCRIPTION()} = $_[1] : $_[0]->{'report'}->{DESCRIPTION()} }
195 0 0   0 0   sub date_run { @_ > 1 ? $_[0]->{'report'}->{DATE_RUN() } = $_[1] : $_[0]->{'report'}->{DATE_RUN() } }
196 0 0   0 0   sub criteria { @_ > 1 ? $_[0]->{'report'}->{CRITERIA() } = $_[1] : $_[0]->{'report'}->{CRITERIA() } }
197 0 0   0 0   sub publisher { @_ > 1 ? $_[0]->{'report'}->{PUBLISHER() } = $_[1] : $_[0]->{'report'}->{PUBLISHER() } }
198 0 0   0 0   sub platform { @_ > 1 ? $_[0]->{'report'}->{PLATFORM() } = $_[1] : $_[0]->{'report'}->{PLATFORM() } }
199 0 0   0 0   sub periods { @_ > 1 ? $_[0]->{'report'}->{PERIODS() } = $_[1] : $_[0]->{'report'}->{PERIODS() } }
200              
201 0   0 0 0   sub records { @{ $_[0]->{'records'} ||= [] } }
  0            
202              
203 0     0 0   sub is_valid { $_[0]->{'is_valid'} }
204 0     0 0   sub warnings { $_[0]->{'warnings'} }
205 0     0 0   sub errors { $_[0]->{'errors'} }
206              
207             # ---------------------------- METHODS THAT SUBCLASSES MIGHT WANT TO OVERRIDE --
208              
209             # --- Position setting
210              
211             sub begin_row {
212 0     0 0   my ($self) = @_;
213 0           $self->trigger_callback('begin_row');
214 0           my $fh = $self->{'fh'};
215 0           while (!eof $fh) {
216 0           my $row = $self->_read_next_row;
217 0           my $row_str = join('', @$row);
218 0 0         last if $row_str =~ /\S/;
219             # Oops -- blank row where one wasn't expected
220 0           $self->trigger_callback('fixed', '', '', '');
221 0           $self->{'warnings'}++;
222             }
223 0           return $self;
224             }
225              
226             # --- Field methods
227              
228             sub check_blank {
229             # Any blank field
230 0     0 0   my ($self) = @_;
231 0           my $cur = $self->_ref_to_cur_cell;
232 0           $self->_in_field(BLANK)->_trim($cur);
233 0 0         if ($$cur eq '') {
234 0           $self->_ok($cur);
235             }
236             else {
237 0           $self->_fix('');
238             }
239 0           $self->_next;
240             }
241              
242             sub check_report_name {
243 0     0 0   my ($self) = @_;
244 0           my $name = $self->canonical_report_name;
245 0           $self->_check_field(NAME, _force_exact_match_sub($name))->_next;
246             }
247              
248             sub check_report_description {
249 0     0 0   my ($self) = @_;
250 0           my $description = $self->canonical_report_description;
251 0           $self->_check_field(DESCRIPTION, _force_exact_match_sub($description))->_next;
252             }
253              
254             sub check_date_run {
255 0     0 0   my ($self) = @_;
256 0           $self->_check_field(DATE_RUN, \&_is_yyyymmdd)->_next;
257             }
258              
259             sub check_count_by_periods {
260 0     0 0   my ($self, $metric) = @_;
261 0           my $periods = $self->{'periods'};
262 0           $self->_in_field(COUNT);
263 0           foreach my $period (@$periods) {
264 0           $self->_check_count($metric, $period);
265             }
266 0           return $self;
267             }
268              
269             sub check_report_criteria {
270 0     0 0   my ($self) = @_;
271 0           $self->_check_free_text_field(CRITERIA, NOT_BLANK)->_next;
272             }
273              
274             sub check_period_covered {
275 0     0 0   my ($self) = @_;
276 0           $self->_check_free_text_field(PERIOD_COVERED, NOT_BLANK)->_next;
277             }
278              
279             sub check_title {
280 0     0 0   my ($self, $mode, $str) = @_;
281 0           $self->_check_free_text_field(TITLE, $mode, $str);
282             }
283              
284             sub check_publisher {
285 0     0 0   my ($self, $mode, $str) = @_;
286 0           $self->_check_free_text_field(PUBLISHER, $mode, $str);
287             }
288              
289             sub check_platform {
290 0     0 0   my ($self, $mode, $str) = @_;
291 0           $self->_check_free_text_field(PLATFORM, $mode, $str);
292             }
293              
294             sub check_print_issn {
295 0     0 0   my ($self) = @_;
296 0           $self->_check_field(PRINT_ISSN, \&_is_issn)->_next;
297             }
298              
299             sub check_online_issn {
300 0     0 0   my ($self) = @_;
301 0           $self->_check_field(ONLINE_ISSN, \&_is_issn)->_next;
302             }
303              
304             sub check_ytd_total {
305 0     0 0   my ($self, $metric) = @_;
306 0           $self->_check_count($metric, YTD_TOTAL);
307             }
308              
309             sub check_ytd_html {
310 0     0 0   my ($self, $metric) = @_;
311 0           $self->_check_count($metric, YTD_HTML);
312             }
313              
314             sub check_ytd_pdf {
315 0     0 0   my ($self, $metric) = @_;
316 0           $self->_check_count($metric, YTD_PDF);
317             }
318              
319             sub check_period_labels {
320 0     0 0   my ($self) = @_;
321 0           my @periods;
322 0           $self->_in_field(PERIOD_LABEL);
323 0           while (my $period = $self->_period_label) {
324 0           push @periods, $period;
325             }
326 0 0         if (@periods == 0) {
    0          
327             # Too few periods
328 0           $self->_cant_fix('');
329             }
330             elsif (@periods > 12) {
331 0           $self->_cant_fix('');
332             }
333 0           $self->{'periods'} = \@periods;
334 0           return $self;
335             }
336              
337             sub _period_label {
338 0     0     my ($self, $cur) = @_;
339 0   0       $cur ||= $self->_ref_to_cur_cell;
340             # If the current cell has two digits in a row, we assume it's meant to be a period label
341 0 0         return unless $$cur =~ /\d\d/;
342 0           $self->_trim($cur);
343 0           my ($result, $period) = $self->parse_period($$cur);
344 0 0         if ($result == VALID) {
    0          
345 0           $self->_ok($cur);
346             }
347             elsif ($result == FIXED) {
348 0           $self->_fix($period);
349             }
350             else {
351 0           $self->_cant_fix('');
352             }
353 0           $self->_next;
354 0           return $period;
355             }
356              
357             sub end_row {
358             # Make sure we've reached the end of the row
359 0     0 0   my ($self) = @_;
360 0           my $row = $self->{'row'};
361 0           my $c = $self->{'c'};
362 0           my $ci = _col2idx($c);
363 0 0         if (@$row > $ci) {
364             # Oops -- we're not at the end of the row
365 0           my $n = @$row - $ci;
366 0           my $to_delete = join('', @$row[-$n..-1]);
367 0 0         if ($to_delete =~ /\S/) {
368             # Double oops -- there's at least non-blank cell beyond where
369             # the row should end
370 0           foreach (1..$n) {
371 0           my $cur = $self->_ref_to_cur_cell;
372 0 0         if ($$cur =~ /\S/) {
373 0           $self->trigger_callback('cant_fix', '', $$cur, '');
374 0           $self->{'errors'}++;
375             }
376             else {
377 0           $self->_trim($cur);
378 0           $self->{'warnings'}++;
379             }
380 0           $self->_next;
381             }
382             }
383             else {
384             # No big deal, we'll just strip off the blank cells
385 0           foreach (1..$n) {
386 0           my $cur = $self->_ref_to_cur_cell;
387 0           $self->_trim($cur);
388 0           $self->trigger_callback('deleted', $$cur);
389 0           $self->_next;
390             }
391 0           splice @$row, -$n;
392             }
393             }
394             # Output the row
395 0           $self->trigger_callback('output', join("\t", @$row));
396 0           $self->trigger_callback('end_row', $row);
397 0           return $self;
398             }
399              
400             sub blank_row {
401 0     0 0   my ($self) = @_;
402 0 0         return $self if $self->_eof;
403 0           $self->_read_next_row; # This is probably a blank row
404 0           my $row = $self->{row};
405 0           my $row_str = join('', @$row);
406 0 0         if (@$row == 0) {
    0          
    0          
407             # No cells at all -- perfect!
408             # ??? $self->_read_next_row;
409             }
410             elsif ($row_str eq '') {
411             # All cells are empty -- ok
412             # XXX Callback??
413             # ??? $self->_read_next_row;
414             }
415             elsif ($row_str =~ /\S/) {
416             # Hmm, no blank row
417 0           $self->{'warnings'}++;
418             # XXX Need a callback for inserted blank lines
419             # *Don't* read the next row
420             }
421             else {
422             # Cells are blank but not empty
423 0           foreach my $i (1..@$row) {
424 0           my $cur = $self->_ref_to_cur_cell;
425 0           $self->_in_field(BLANK)->_trim($cur)->_next;
426             }
427             # ??? $self->_read_next_row;
428             }
429             # Output a blank line regardless of what we found
430 0           $self->trigger_callback('output', '');
431 0           return $self;
432             }
433              
434             # --- Generic data checking methods
435              
436             sub check_label {
437 0     0 0   my ($self, $str, $rx) = @_;
438 0           $self->_in_field(LABEL)->_must_match($str, $rx);
439             }
440              
441             sub begin_header {
442 0     0 0   my ($self) = @_;
443 0           my $hdr = $self->{'container'} = $self->{'header'} = {
444             'name' => $self->canonical_report_name,
445             'description' => $self->canonical_report_description,
446             'code' => $self->canonical_report_code,
447             'release' => $self->release_number,
448             };
449 0           $self->trigger_callback('begin_header', $hdr);
450 0           return $self;
451             }
452              
453             sub end_header {
454 0     0 0   my ($self) = @_;
455 0           my $hdr = $self->{'header'};
456 0           $self->trigger_callback('end_header', $hdr);
457 0           return $self;
458             }
459              
460             sub begin_record {
461 0     0 0   my ($self) = @_;
462 0           my $rec = $self->{'container'} = $self->{'record'} = {};
463 0           $self->trigger_callback('begin_record', $rec);
464 0           return $self;
465             }
466              
467             sub end_record {
468 0     0 0   my ($self) = @_;
469 0           my $rec = $self->{'record'};
470 0   0       push @{ $self->{'records'} ||= [] }, $rec;
  0            
471 0           $self->trigger_callback('end_record', $rec);
472 0           return $self;
473             }
474              
475             # ----------------------------------------------------------- PRIVATE METHODS --
476              
477             # --- Record field checking methods
478              
479             sub _check_field {
480 0     0     my ($self, $field, $check) = @_;
481 0           $self->_in_field($field);
482 0           my $container = $self->{'container'};
483 0           my $cur = $self->_ref_to_cur_cell;
484 0           $self->_trim($cur);
485 0 0         if ($check->($self, $field, $cur)) {
486 0           $container->{$field} = $$cur;
487             }
488 0           return $self;
489             }
490              
491             sub _check_free_text_field {
492 0     0     my ($self, $field, $mode, $str) = @_;
493 0 0         if ($mode == EXACT_MATCH) {
    0          
494 0 0         $str = '' unless defined $str;
495 0           $self->_check_field($field, _exact_match_sub($str));
496             }
497             elsif ($mode == NOT_BLANK) {
498 0           $self->_check_field($field, \&_is_not_blank);
499             }
500             else {
501 0           $self->_check_field($field, \&_is_anything);
502             }
503 0           $self->_next;
504             }
505              
506             sub _not_available {
507 0     0     my ($self) = @_;
508 0 0         if ($self->{'change_not_available_to_blank'}) {
509 0           $self->_fix('');
510             }
511             else {
512 0           $self->_cant_fix('');
513             }
514 0           return $self;
515             }
516              
517             sub _check_count {
518 0     0     my ($self, $field, $period) = @_;
519 0           my $cur = $self->_ref_to_cur_cell;
520 0           $self->_trim($cur);
521 0           my $val = $$cur;
522 0           my $container = $self->{'container'};
523 0 0         if (defined $period) {
524             # Usage for a particular period
525 0           my ($result, $normalized_period);
526 0           ($result, $period, $normalized_period) = $self->parse_period($period);
527 0 0         if ($val =~ /^\d+$/) {
    0          
    0          
528 0 0         if ($result != INVALID) {
529 0           $container->{'count'}->{$normalized_period}->{$field} = $val;
530 0           $self->trigger_callback('count', $self->{'scope'}, $field, $period, $val);
531             }
532             }
533             elsif ($val eq '') {
534 0 0         if ($self->{'treat_blank_counts_as_zero'}) {
535 0           $container->{'count'}->{$normalized_period}->{$field} = $val;
536 0           $self->trigger_callback('count', $self->{'scope'}, $field, $period, 0);
537             }
538             }
539             elsif ($val =~ m{^n/a$}i) {
540 0           $self->_not_available;
541             }
542             else {
543 0           $self->_cant_fix('');
544             }
545             }
546             else {
547             # YTD usage
548 0 0         if ($val =~ /^\d+$/) {
    0          
    0          
549 0           $container->{'count'}->{$field} = $val;
550 0           $self->trigger_callback("count_$field", $self->{'scope'}, $field, $val);
551             }
552             elsif ($val eq '') {
553 0 0         if ($self->{'treat_blank_counts_as_zero'}) {
554 0           $container->{'count'}->{$field} = $val;
555 0           $self->trigger_callback("count_$field", $self->{'scope'}, $field, 0);
556             }
557             }
558             elsif ($val =~ m{^n/a$}i) {
559 0           $self->_not_available;
560             }
561             else {
562 0           $self->_cant_fix('');
563             }
564             }
565 0           $self->_next;
566             }
567              
568             sub _exact_match_sub {
569             # Return a ref to code that compares the current cell's value to the given string
570 0     0     my ($str) = @_;
571             return sub {
572 0     0     my ($self, $field, $cur) = @_;
573 0   0       $cur ||= $self->_ref_to_cur_cell;
574 0 0         if ($$cur eq $str) {
575 0           $self->_ok($cur);
576             }
577             else {
578 0           $self->_cant_fix($str);
579             }
580 0           return $self;
581 0           };
582             }
583              
584             sub _force_exact_match_sub {
585             # Return a ref to code that forces the current cell's value to the given string
586 0     0     my ($str) = @_;
587             return sub {
588 0     0     my ($self, $field, $cur) = @_;
589 0 0         if ($$cur eq $str) {
590 0           $self->_ok($cur);
591             }
592             else {
593 0           $self->_fix($str);
594             }
595 0           return $self;
596 0           };
597             }
598              
599             sub _is_yyyymmdd {
600 0     0     my ($self) = @_;
601 0           my $cur = $self->_ref_to_cur_cell;
602 0           my $val = $$cur;
603 0 0         if ($val =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/) {
    0          
604             # Nothing to do
605 0           return $self->_ok($cur);
606             }
607             elsif ($val =~ m{^(\d\d?)/(\d\d?)/(\d\d)?(\d\d)$}) {
608             # Ack! Try to fix
609 0 0 0       if ($1 < 13 && $2 >= 13) {
    0 0        
610             # mm/dd/(cc)?yy
611 0   0       return $self->_fix(sprintf('%02d%02d-%02d-%02d', $3 || 20, $4, $1, $2));
612             }
613             elsif ($2 < 13 && $1 >= 13) {
614             # dd/mm/(cc)?yy
615 0   0       return $self->_fix(sprintf('%02d%02d-%02d-%02d', $3 || 20, $4, $2, $1));
616             }
617             }
618 0           return $self->_cant_fix('');
619             }
620              
621             sub _is_anything {
622 0     0     my ($self) = @_;
623 0           $self->_trim;
624             }
625              
626             sub _is_issn {
627 0     0     my ($self, $field, $cur) = @_;
628 0           $self->_trim;
629 0           my $val = $$cur;
630 0 0         if (length $val) {
631 0 0         if ($val =~ /^\d{4}-\d{3}[\dX]$/) {
    0          
632 0           $self->_ok($cur);
633             }
634             elsif ($val =~ /^(\d{3,4})-?(\d{3})([\dXx])$/) {
635 0           $self->_fix(sprintf("%04d-%03d%s", $1, $2, lc $3));
636             }
637             else {
638 0           $self->_cant_fix('');
639             }
640             }
641 0           return $self;
642             }
643              
644             sub _is_count {
645 0     0     my ($self, $field, $cur) = @_;
646 0 0         if ($$cur =~ /^\d+$/) {
647 0           $self->_ok($cur);
648             }
649             else {
650 0           $self->_cant_fix('');
651             }
652 0           return $self;
653             }
654              
655             sub _is_not_blank {
656 0     0     my ($self, $field, $cur) = @_;
657 0 0         if ($$cur eq '') {
658 0           $self->_cant_fix('');
659 0           return;
660             }
661             else {
662 0           $self->_ok($cur);
663             }
664 0           return $self;
665             }
666              
667             sub _must_match {
668 0     0     my ($self, $str, $rx) = @_;
669 0   0       $rx ||= _str2rx($str);
670 0           my $cur = $self->_ref_to_cur_cell;
671 0           $self->_trim($cur);
672 0 0         if ($$cur eq $str) {
    0          
673 0           $self->_ok($cur);
674             }
675             elsif ($$cur =~ /$rx/) {
676 0           $self->_fix($str);
677             }
678             else {
679 0           $self->_cant_fix($str);
680             }
681 0           $self->_next;
682             }
683              
684             sub _read_next_line {
685 0     0     my ($self) = @_;
686             # Fetch the next line
687 0           my $fh = $self->{'fh'};
688 0           my $line = <$fh>;
689 0 0         return unless defined $line;
690 0           chomp $line;
691 0           $self->trigger_callback('line', $.);
692 0           $self->trigger_callback('input', $line);
693 0           return $line;
694             }
695              
696             sub _read_next_row {
697 0     0     my ($self) = @_;
698 0 0         if ($self->{'dont_reread_next_row'}) {
699 0           $self->{'dont_reread_next_row'} = 0;
700 0           return $self->{'row'};
701             }
702 0           my $line = $self->_read_next_line;
703 0 0         return unless defined $line;
704 0           $line =~s/\x0d$//; # Strip CR at end of line
705 0           my $begin_row = $self->{'row'} = [ $self->_parse_line($line) ];
706 0           push @{ $self->{'rows'} }, $begin_row;
  0            
707 0           $self->{'r'}++;
708 0           $self->{'c'} = 'A';
709 0           return $begin_row;
710             }
711              
712             sub _parse_line {
713 0     0     my ($self, $line) = @_;
714 0           chomp $line;
715 0 0         if ($line =~ /\t/) {
    0          
    0          
716 0           return split /\t/, $line;
717             }
718             elsif ($line =~ /,/) {
719 0           my $csv = $self->{'csv_parser'};
720 0 0         if (!defined $csv) {
721 0 0         eval "use Text::CSV; 1" or die "Can't use Text::CSV";
722 0   0       $csv = $self->{'csv_parser'} ||= Text::CSV->new({'binary' => 1});
723             }
724 0           my @cells;
725 0           my $status = $csv->parse($line); # parse a CSV string into fields
726 0 0         if ($status) {
727 0           @cells = $csv->fields; # get the parsed fields
728             }
729             else {
730             # Text::CSV can't handle it -- fall back to simplistic CSV parsing
731 0           @cells = split /,/, $line;
732 0           s/^"|"$//g for @cells;
733 0           s/""/"/g for @cells;
734             }
735 0           return @cells;
736             }
737             elsif ($line =~ s/^"|"$//g) {
738             # XXX All this needs some tweaking to account for extremely unlikely edge cases
739 0           $line =~ s/""/"/g;
740 0           $line =~ s/\\"/"/g;
741             }
742 0           return $line;
743             }
744              
745             sub _drop_row {
746 0     0     my ($self) = @_;
747 0           return $self;
748             }
749              
750             sub _orient {
751 0     0     my ($self) = @_;
752 0           $self->_in_scope(REPORT);
753 0           my $rows = $self->{'rows'};
754 0           my ($row, $line);
755 0           while (!$self->_eof) {
756 0           $row = $self->_read_next_row;
757 0           $line = join("\t", @$row);
758 0 0         if ($line =~ /\S/) {
759             # Not a blank line
760 0           $self->{'dont_reread_next_row'} = 1; # Don't re-read this row
761 0           last;
762             }
763             else {
764             # Blank line
765 0           $self->trigger_callback('skip_blank_row');
766 0           shift @$rows;
767             }
768             }
769 0 0         die "Not a COUNTER report?" unless $row;
770 0 0         die "Totally malformed report" unless @$row >= 2;
771 0           my ($name, $title) = @$row;
772 0 0         if (@$row > 2) {
773             # XXX Just silently fix the problem?
774 0           @$row = ($name, $title);
775             }
776 0           return Biblio::COUNTER->report($name, %$self);
777             }
778              
779             # --- Cursor moving and reading
780              
781             sub current_position {
782 0     0 0   my ($self) = @_;
783 0           my ($r, $c) = $self->_pos;
784 0           return $c . $r;
785             }
786              
787             sub current_value {
788 0     0 0   my ($self) = @_;
789 0           my $cur = $self->_ref_to_cur_cell;
790 0           return $$cur;
791             }
792              
793             sub _pos {
794 0     0     my ($self) = @_;
795 0           return ($self->{'r'}, $self->{'c'});
796             }
797              
798             sub _eof {
799 0     0     my ($self) = @_;
800 0           eof $self->{'fh'};
801             }
802              
803             sub _sr {
804 0     0     my ($self) = @_;
805             # Show row -- for debugging purposes
806 0           my $row = $self->{row};
807 0           my ($rcur, $ccur) = $self->_pos;
808 0           my $c = 'A';
809 0           foreach my $val (@$row) {
810 0 0         print STDERR $c eq $ccur ? "\e[32m-> " : ' ';
811 0           printf STDERR "%s%d %s\e[0m\n", $c++, $rcur, _hilite_for_debugging($val, $c eq $ccur);
812             }
813 0 0         if ($ccur eq $c) {
814 0           print STDERR "\e[32m->\e[0m\n";
815             }
816             }
817              
818             sub _hilite_for_debugging {
819 0     0     my ($str, $is_cur) = @_;
820 0 0         my $reset = $is_cur ? "\e[32m" : "\e[0m";
821 0 0         if ($str eq '') {
822 0           $str = "\e[31m$reset";
823             }
824             else {
825 0           $str =~ s/(^\s+|\s+$)/"\e[31m" . ('_' x length($1)) . $reset/eg;
  0            
826             }
827 0           return $str;
828             }
829              
830              
831             sub _next {
832             # Move to the next column in the current row
833 0     0     my ($self) = @_;
834 0           my $new_col = ++$self->{'c'};
835 0 0         if (length($new_col) > 1) {
836             # XXX Deal with AA, AB, etc.
837 0           die "Biblio::COUNTER only supports reports with 26 columns or fewer";
838             }
839 0           return $self;
840             }
841              
842             sub _in_scope {
843 0     0     my ($self, $scope) = @_;
844 0           $self->{'scope'} = $scope;
845 0           return $self;
846             }
847              
848             sub _in_field {
849 0     0     my ($self, $field) = @_;
850 0           $self->{'field'} = $field;
851 0           return $self;
852             }
853              
854             # --- Data fetching functions
855              
856             sub _ref_to_cur_cell {
857             # Return a reference to the datum in the current cell
858 0     0     my ($self) = @_;
859 0           my $c = $self->{'c'};
860 0           my $row = $self->{'row'};
861 0           my $ci = _col2idx($c);
862 0           while ($ci >= @$row) {
863 0           push @$row, '';
864 0           $self->_cant_fix('');
865             }
866 0           return \$row->[$ci];
867             }
868              
869             # --- Callback-invoking methods
870              
871             sub trigger_callback {
872 0     0 0   my ($self, $name, @args) = @_;
873 0           my $cb = $self->{'callback'};
874 0 0         if ($cb->{$name}) {
    0          
875             # Regular callback
876 0           $cb->{$name}->($self, @args);
877             }
878             elsif ($cb->{'*'}) {
879             # Fallback callback (got that?)
880 0           $cb->{'*'}->($self, $name, @args);
881             }
882 0           return $self;
883             }
884              
885             sub _ok {
886 0     0     my ($self, $cur) = @_;
887 0   0       $cur ||= $self->_ref_to_cur_cell;
888 0           $self->trigger_callback('ok', $self->{'field'}, $$cur);
889 0           return $self;
890             }
891              
892             sub _fix {
893 0     0     my ($self, $str) = @_;
894 0           my $cur = $self->_ref_to_cur_cell;
895 0           $self->trigger_callback('fixed', $self->{'field'}, $$cur, $str);
896 0           $$cur = $str;
897 0           $self->{'warnings'}++;
898 0           return $self;
899             }
900              
901             sub _cant_fix {
902 0     0     my ($self, $expected) = @_;
903 0           my $cur = $self->_ref_to_cur_cell;
904 0           my $field = $self->{'field'};
905 0 0         $expected = "<$field>" unless defined $expected;
906 0           $self->trigger_callback('cant_fix', $field, $$cur, $expected);
907 0           $self->{'errors'}++;
908 0           return $self;
909             }
910              
911             sub _trim {
912 0     0     my ($self, $cur) = @_;
913 0   0       $cur ||= $self->_ref_to_cur_cell;
914 0 0         if ($$cur =~ s/^\s+|\s+$//g) {
915 0           $self->_trimmed($cur);
916             }
917 0           return $self;
918             }
919              
920             sub _trimmed {
921 0     0     my ($self, $cur) = @_;
922 0   0       $cur ||= $self->_ref_to_cur_cell;
923 0           $self->trigger_callback('trimmed', $self->{'field'}, $$cur);
924 0           $self->{'warnings'}++;
925 0           return $self;
926             }
927              
928             sub parse_period {
929 0     0 0   my ($self, $str) = @_;
930 0 0         if ($str =~ /^(?:($rx_mon)-($rx_year)|($rx_year)-($rx_mon))$/ig) {
931 0 0         my ($m, $y) = $1 ? ($1, $2) : ($4, $3);
932 0           my $period = _normalize_mon($m) . '-' . _normalize_yyyy($y);
933 0   0       my $normalized_period = $y . '-' . ($mon2num{lc $m} || $m);
934 0 0         if ($period eq $str) {
935 0           return (VALID, $period, $normalized_period);
936             }
937             else {
938 0           return (FIXED, $period, $normalized_period);
939             }
940             }
941 0           return (INVALID);
942             }
943              
944             sub _normalize_mon {
945 0     0     my ($m) = @_;
946 0 0         if ($m =~ /^\d/) {
947 0           return $num2mon[$m];
948             }
949             else {
950 0           return ucfirst lc substr($m, 0, 3);
951             }
952             }
953              
954             sub _normalize_yyyy {
955 0     0     my ($y) = @_;
956 0 0         if (length($y) == 2) {
957             # We ignore 1999 and earlier
958 0           return 2000 + $y;
959             }
960             else {
961 0           return $y;
962             }
963             }
964              
965             # --- Utility functions
966              
967             sub _str2rx {
968 0     0     my ($str) = @_;
969 0           my $rx = quotemeta lc $str;
970 0           return qr/$rx/;
971             }
972              
973             sub _col2idx {
974 0     0     my ($c) = @_;
975 0           return ord($c) - ord('A');
976             }
977              
978              
979             1;
980              
981              
982             =pod
983              
984             =head1 NAME
985              
986             Biblio::COUNTER::Report - a COUNTER-compliant (or not) report
987              
988             =head1 SYNOPSIS
989              
990             $report = Biblio::COUNTER::Report->new(
991             'file' => $file,
992             );
993              
994             =head1 DESCRIPTION
995              
996             =head1 PUBLIC METHODS
997              
998             =cut
999