File Coverage

blib/lib/Games/Go/AGA/Parse/TDList.pm
Criterion Covered Total %
statement 90 119 75.6
branch 25 52 48.0
condition 26 38 68.4
subroutine 21 22 95.4
pod 3 12 25.0
total 165 243 67.9


line stmt bran cond sub pod time code
1             #===============================================================================
2             #
3             # FILE: Games::Go::AGA::Parse::TDList.pm
4             #
5             # PODNAME: Games::Go::AGA::Parse::TDList
6             # ABSTRACT: Parses lines from an AGA TDLISTlist
7             #
8             # AUTHOR: Reid Augustin (REID),
9             # COMPANY: LucidPort Technology, Inc.
10             # CREATED: Tue Jan 18 16:12:35 PST 2011
11             #===============================================================================
12              
13 1     1   1180 use 5.008;
  1         3  
  1         52  
14 1     1   8 use strict;
  1         3  
  1         50  
15 1     1   7 use warnings;
  1         1  
  1         75  
16              
17             package Games::Go::AGA::Parse::TDList;
18 1     1   691 use parent 'Games::Go::AGA::Parse';
  1         496  
  1         10  
19              
20 1     1   63 use Carp;
  1         2  
  1         97  
21 1     1   709 use Readonly;
  1         4411  
  1         72  
22 1     1   710 use String::Tokenizer;
  1         1992  
  1         43  
23 1     1   8 use Scalar::Util qw( looks_like_number );
  1         1  
  1         103  
24 1     1   720 use Games::Go::AGA::Parse::Util qw( is_Rating is_Rank_or_Rating normalize_ID );
  1         2  
  1         85  
25 1     1   710 use Games::Go::AGA::Parse::Exceptions;
  1         2  
  1         1639  
26              
27             our $VERSION = '0.042'; # VERSION
28              
29             sub last_name {
30 9     9 0 14 my ($self, $new) = @_;
31              
32 9 50       19 if (@_ > 1) {
33 0         0 $self->{last_name} = $new;
34             }
35 9   50     33 return $self->{last_name} || '';
36             }
37              
38             sub first_name {
39 9     9 0 10 my ($self, $new) = @_;
40              
41 9 50       19 if (@_ > 1) {
42 0         0 $self->{first_name} = $new;
43             }
44 9   100     31 return $self->{first_name} || '';
45             }
46              
47             sub id {
48 9     9 0 12 my ($self, $new) = @_;
49              
50 9 50       22 if (@_ > 1) {
51 0         0 $self->{id} = $new;
52             }
53 9   100     32 return $self->{id} || '';
54             }
55              
56             sub membership {
57 9     9 0 8 my ($self, $new) = @_;
58              
59 9 50       25 if (@_ > 1) {
60 0         0 $self->{membership} = $new;
61             }
62 9   100     23 return $self->{membership} || '';
63             }
64              
65             sub rank {
66 27     27 0 30 my ($self, $new) = @_;
67              
68 27 100       41 if (@_ > 1) {
69 9         11 $self->{rank} = $new;
70 9 100       19 $self->{rank} += 0 if (is_Rating($new)); # numify
71             }
72 27   100     125 return $self->{rank} || '';
73             }
74              
75             sub date {
76 9     9 0 13 my ($self, $new) = @_;
77              
78 9 50       21 if (@_ > 1) {
79 0         0 $self->{date} = $new;
80             }
81 9   100     25 return $self->{date} || '';
82             }
83              
84             sub club {
85 9     9 0 10 my ($self, $new) = @_;
86              
87 9 50       18 if (@_ > 1) {
88 0         0 $self->{club} = $new;
89             }
90 9   100     25 return $self->{club} || '';
91             }
92              
93             sub state {
94 9     9 0 11 my ($self, $new) = @_;
95              
96 9 50       22 if (@_ > 1) {
97 0         0 $self->{state} = $new;
98             }
99 9   100     28 return $self->{state} || '';
100             }
101              
102             sub extra {
103 9     9 0 11 my ($self, $new) = @_;
104              
105 9 50       18 if (@_ > 1) {
106 0         0 $self->{extra} = $new;
107             }
108 9   50     93 return $self->{extra} || '';
109             }
110              
111             sub as_array {
112 0     0 1 0 my ($self, $new) = @_;
113              
114 0         0 my @ret = map
115 0         0 { $self->$_ }
116             # fields, in order
117             qw(
118             last_name
119             first_name
120             id
121             membership
122             rank
123             date
124             club
125             state
126             extra
127             );
128 0 0       0 return wantarray ? @ret : \@ret;
129             }
130              
131             sub as_hash {
132 9     9 1 13 my ($self, $new) = @_;
133              
134 81         156 my %ret = map
135 9         37 { $_, $self->$_ }
136             # fields
137             qw(
138             last_name
139             first_name
140             id
141             membership
142             rank
143             date
144             club
145             state
146             extra
147             );
148 9 50       89 return wantarray ? %ret : \%ret;
149             }
150              
151             Readonly my $LAST_NAME => 0;
152             Readonly my $FIRST_NAME => 1;
153             Readonly my $ID => 2;
154             Readonly my $MEMBERSHIP => 3;
155             Readonly my $RANK => 4;
156             Readonly my $DATE => 5;
157             Readonly my $CLUB => 6;
158             Readonly my $STATE => 7;
159             Readonly my $EXTRA => 8;
160              
161             Readonly my %name_of_state => (
162             $FIRST_NAME => 'first_name',
163             $ID => 'id',
164             $MEMBERSHIP => 'membership',
165             $RANK => 'rank',
166             $DATE => 'date',
167             $CLUB => 'club',
168             $STATE => 'state',
169             $EXTRA => 'extra',
170             );
171              
172             Readonly my %MEMBERSHIP_TYPES => (
173             Full => 1,
174             Youth => 1,
175             Limit => 1,
176             Non => 1,
177             Sust => 1,
178             Spons => 1,
179             Forgn => 1,
180             Comp => 1,
181             Life => 1,
182             Donar => 1,
183             );
184              
185             Readonly my %state_functions => (
186             $LAST_NAME => sub {
187             my ($self, $token) = @_;
188              
189             if ($token eq ',') {
190             return $FIRST_NAME;
191             }
192             if ($token =~ m/^\w*\d+$/ and # IDs are numbers with possible alpha prefix
193             @{$self->{last_name}}) { # but only if we already have a name
194             $self->{id} = $token;
195             return $MEMBERSHIP;
196             }
197             push @{$self->{last_name}}, $token;
198             return $LAST_NAME;
199             },
200             $FIRST_NAME => sub {
201             my ($self, $token) = @_;
202              
203             if ($token eq ',') {
204             # Oops. transfer anything we saved in first_name back to
205             # last_name
206             push @{$self->{last_name}}, @{$self->{first_name}};
207             $self->{first_name} = [];
208             return $FIRST_NAME;
209             }
210             if ($token =~ m/^\w*\d+$/) { # IDs are numbers with possible alpha prefix
211             $self->{id} = $token;
212             return $MEMBERSHIP;
213             }
214             if (exists $MEMBERSHIP_TYPES{$token}) { # skipped over ID?
215             $self->{membership} = $token;
216             return $RANK;
217             }
218             if (is_Rank_or_Rating($token)) {
219             Games::Go::AGA::Parse::Exception->throw(
220             error => "Invalid membership",
221             filename => 'fake_file',
222             source => $self->{source},
223             line_number => 321,
224             );
225             }
226             push @{$self->{first_name}}, $token;
227             return $FIRST_NAME; # first name may have several parts
228             },
229             $ID => sub {
230             my ($self, $token) = @_;
231              
232             if (exists $MEMBERSHIP_TYPES{$token}) {
233             $self->{membership} = $token;
234             return $RANK;
235             }
236             $self->{id} = $token;
237             return $MEMBERSHIP;
238             },
239             $MEMBERSHIP => sub {
240             my ($self, $token) = @_;
241              
242             if ($token eq ',') {
243             # Oops. We got here because something looked like an ID
244             # but was really part of the last name. transfer
245             # anything we saved in first_name and id back to last_name
246             push @{$self->{last_name}}, @{$self->{first_name}}, $self->{id};
247             $self->{first_name} = [];
248             $self->{id} = '';
249             return $FIRST_NAME;
250             }
251             if (not exists $MEMBERSHIP_TYPES{$token}) {
252             if ($token eq '0.0' or
253             is_Rank_or_Rating($token)) {
254             $self->{membership} = ''; # shrug
255             $self->rank($token);
256             return $DATE;
257             }
258             $self->_parse_error(
259             error => "Invalid membership: $token",
260             source => $self->{source},
261             );
262             }
263             $self->{membership} = $token;
264             return $RANK;
265             },
266             $RANK => sub {
267             my ($self, $token) = @_;
268              
269             if ($token eq '0.0' or
270             is_Rank_or_Rating($token)) {
271             $self->rank($token);
272             return $DATE;
273             }
274             # grrr: AGA changed the format; rank can be blank or 0.0
275             if ($token =~ m/^\d\d?\/\d\d?\/\d{2,4}$/) {
276             $self->rank(0);
277             $self->{date} = $token;
278             return $CLUB;
279             }
280             $self->_parse_error(
281             error => "Invalid rank $token",
282             source => $self->{source},
283             );
284             },
285             $DATE => sub {
286             my ($self, $token) = @_;
287              
288             if ($token =~ m/^\d\d?\D\d\d?\D\d{2,4}$/) {
289             $self->{date} = $token;
290             return $CLUB;
291             }
292             if ($token =~ m/^[A-Z][A-Z]$/) {
293             $self->{state} = $token;
294             return $EXTRA;
295             }
296             $self->{club} = $token; # shrug - best guess is this is a club
297             return $STATE;
298             },
299             $CLUB => sub {
300             my ($self, $token) = @_;
301              
302             if ($token =~ m/^[A-Z][A-Z]$/) {
303             $self->{state} = $token;
304             return $EXTRA;
305             }
306             $self->{club} = $token;
307             return $STATE;
308             },
309             $STATE => sub {
310             my ($self, $token) = @_;
311              
312             if (($token eq '-') and
313             ($self->{state} eq '-')) {
314             $self->{state} = '';
315             return $LAST_NAME;
316             }
317             $self->{state} = $token;
318             if ($token eq '-') {
319             return $STATE; # wait for second '-';
320             }
321             return $EXTRA; # done with 'official' TDList format,
322             # anything left over is extra
323             },
324             );
325              
326             *parse = \&parse_line; # alias parse to parse_line
327             sub parse_line {
328 12     12 1 22834 my ($self, $string) = @_;
329              
330 12         29 $self->{source} = $string;
331 12         23 map { $self->{$_} = [] } qw( last_name first_name ); # empty arrays
  24         58  
332 12         17 map { $self->{$_} = '' } qw( id membership rank date club state ); # empty strings
  72         99  
333              
334 12 50       34 return $self->as_hash if (not $string);
335              
336 12         76 my $tokenizer = String::Tokenizer->new(
337             $string, # source string
338             ",\n", # delimiters
339             String::Tokenizer->RETAIN_WHITESPACE,
340             );
341 12         1572 my $iter = $tokenizer->iterator;
342 12         405 my $state = $LAST_NAME;
343              
344             TOKEN:
345 12         57 while ($iter->hasNextToken) {
346 156         733 my $token = $iter->nextToken;
347 156 50 66     1273 if ($token eq "\n") { # a carriage return?
    100          
    50          
    50          
    0          
348 0         0 last TOKEN;
349             }
350             elsif ($token !~ m/\S/ or # only whitespace
351             $token eq '') { # empty
352 67         110 next TOKEN;
353             }
354             elsif ($token eq '#') { # comment
355 0 0       0 if ($state ne $FIRST_NAME) {
356 0         0 $self->_parse_error(
357             error => "got comment, expected $name_of_state{$state}",
358             source => $self->{source},
359             );
360             }
361             # dump the rest of the line
362 0 0       0 $iter->collectTokensUntil("\n") if ($iter->hasNextToken);
363 0         0 next TOKEN;
364             }
365             elsif (exists $state_functions{$state}) {
366 89         740 $state = $state_functions{$state}($self, $token);
367 86         401 next TOKEN;
368             }
369             elsif ($state == $EXTRA) {
370 0 0       0 if ($iter->hasNextToken) {
371 0   0     0 $token .= $iter->collectTokensUntil("\n") || ''; # slurp the rest
372             }
373 0         0 chomp $token;
374 0         0 $self->{extra} = $token;
375 0         0 next TOKEN;
376             }
377             else {
378 0         0 $self->_parse_error(
379             error => "Unknown state: $state",
380             source => $self->{source},
381             );
382             }
383             }
384 9 50 66     66 if (length $self->{state} == 0 and
      33        
385             length $self->{club} == 2 and
386             uc $self->{club} eq $self->{club}) { # state is in club field
387 0         0 $self->{state} = $self->{club};
388 0         0 $self->{club} = '';
389             }
390 9 50 66     30 if (not $self->{id} and
  1   33     7  
391             @{$self->{first_name}} > 1 and
392             $self->{first_name}[-1] =~ m/\d/) { # digit(s) present?
393 0         0 $self->{id} = pop @{$self->{first_name}}; # probably
  0         0  
394             }
395 9         12 $self->{last_name} = join(q{ }, @{$self->{last_name}});
  9         20  
396 9         15 $self->{first_name} = join(q{ }, @{$self->{first_name}});
  9         15  
397 9 50 66     15 if ($self->rank eq '' and
398             is_Rank_or_Rating($self->{id})) {
399             # hmm, this is more likely:
400 0         0 $self->rank = $self->{id};
401 0         0 $self->{id} = '';
402             }
403 9 100       45 $self->{id} = normalize_ID($self->{id}) if ($self->{id});
404 9 50       28 $self->{club} = '' if (lc $self->{club} eq 'none');
405 9         19 return $self->as_hash;
406             }
407              
408             1;
409              
410             =pod
411              
412             =encoding UTF-8
413              
414             =head1 NAME
415              
416             Games::Go::AGA::Parse::TDList - Parses lines from an AGA TDLISTlist
417              
418             =head1 VERSION
419              
420             version 0.042
421              
422             =head1 SYNOPSIS
423              
424             use Games::Go::Parse::TDList;
425              
426             my $parser = Games::Go::AGA::Parse::TDList->new;
427             while (my $line = <$tdlist_fh>) {
428             my $hash = $parser->parse($line); # ref to a hash
429             ...
430             }
431              
432             =head1 DESCRIPTION
433              
434             A parser to break out and return fields of a line from an AGA TDListN.txt
435             file. The fields are usually:
436              
437             last_name
438             first_name
439             id
440             membership
441             rank
442             date
443             club
444             state
445             extra
446              
447             but some fields could be empty ('').
448              
449             Note that the B field may be either rank format (3K, 4d, etc) or a
450             numerical rating (from -100 to +20 with a gap from -1 to +1). Rank
451             format implies less certainty in the accuracy of the rank. The
452             B function in Games::Go::AGA::Parse::Utils can be used
453             to force the numerical format.
454              
455             =head1 METHODS
456              
457             =over
458              
459             =item $parser = Games::Go::AGA::Parse::TDList->new;
460              
461             Creates a new parser object.
462              
463             =item $parser = $parser->filename( ['new_name'])
464              
465             Get/set a filename (used in error messages)
466              
467             =item $file_handle = $parser->filename( [$new_file_handle ])
468              
469             Get/set a file handle (used in error messages)
470              
471             =item %result_hash = $parser->parse_line('a TDListN line')
472              
473             =item %result_hash = $parser->parse()
474              
475             Parses a single line from the TDListN.txt file and returns a hash with the
476             fields as listed in B. Calling this function removes any field
477             results from previous lines.
478              
479             =item %as_hash = $parser->as_hash()
480              
481             Retuns the parsed line as a hash. Missing fields will be empty strings
482             (''). The hash keys are
483              
484             (
485             last_name => 'last name of player',
486             first_name => 'first name of player',
487             id => 'player ID',
488             membership => 'AGA membership type (if any)',
489             rank => 'player's rank or rating',
490             date => 'date player's membership is valid until',
491             club => 'club player usually plays at',
492             state => 'state player lives in',
493             extra => 'any extra stuff',
494             )
495              
496             In scalar context, returns a reference to the hash.
497              
498             =item @as_array = $parser->as_array()
499              
500             Retuns the parsed line as an array. Missing fields will be empty strings
501             (''). The order is:
502              
503             (
504             last_name
505             first_name
506             id
507             membership
508             rank
509             date
510             club
511             state
512             extra
513             )
514              
515             In scalar context, returns a reference to the array.
516              
517             =item $field_by_name = $parser-> < name >
518              
519             Individual fields may be set or retrieved by name. E.g:
520              
521             $last_name = $parser->last_name
522             . . .
523             $parser->rank('4d');
524              
525             =back
526              
527             =head1 OPTIONS
528              
529             Options to the B<-Enew> method are:
530              
531             =over
532              
533             =item filename => 'file name'
534              
535             =item handle => $file_handle
536              
537             =back
538              
539             These are not required to create a parser, but if supplied, error
540             exceptions will include more useful information.
541              
542             =head1 AUTHOR
543              
544             Reid Augustin
545              
546             =head1 COPYRIGHT AND LICENSE
547              
548             This software is copyright (c) 2011 by Reid Augustin.
549              
550             This is free software; you can redistribute it and/or modify it under
551             the same terms as the Perl 5 programming language system itself.
552              
553             =cut
554              
555             __END__