File Coverage

blib/lib/Games/Go/AGATourn.pm
Criterion Covered Total %
statement 269 425 63.2
branch 102 250 40.8
condition 11 33 33.3
subroutine 29 41 70.7
pod 34 34 100.0
total 445 783 56.8


line stmt bran cond sub pod time code
1             # $Id: AGATourn.pm,v 1.35 2005/01/24 04:32:17 reid Exp $
2              
3             # AGATourn
4             #
5             # Copyright (C) 1999, 2004, 2005 Reid Augustin reid@netchip.com
6             # 1000 San Mateo Dr.
7             # Menlo Park, CA 94025 USA
8             #
9             # This library is free software; you can redistribute it and/or modify it
10             # under the same terms as Perl itself, either Perl version 5.8.5 or, at your
11             # option, any later version of Perl 5 you may have available.
12             #
13             # This program is distributed in the hope that it will be useful, but
14             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15             # or FITNESS FOR A PARTICULAR PURPOSE.
16             #
17              
18             =head1 NAME
19              
20             AGATourn - Perl extensions to ease the pain of using AGA tournament data files.
21              
22             =head1 SYNOPSIS
23              
24             use Games::Go::AGATourn;
25              
26             my $agaTourn = Bnew> (options);
27              
28             =head1 DESCRIPTION
29              
30             An AGATourn object represents a round or several rounds of an American Go
31             Association tournament. There are methods for parsing several type of AGA
32             file format:
33              
34             =over 4
35              
36             =item tdlist
37              
38             The entire list of AGA members including playing strength, club affiliation,
39             and some other stuff.
40              
41             =item register.tde
42              
43             The starting point for a tournament. All players in a tournament must be
44             entered in the register.tde file.
45              
46             =item round results: 1.tde, 2.tde, etc.
47              
48             Game results for each round of the tournament.
49              
50             =back
51              
52             A note on IDs: in general, hashes in an AGATourn object are keyed by the AGA
53             ID. An AGA ID consists of a three letter country specifier (like USA or TMP
54             for temporary IDs) concatenated to an integer. Here we specify the three
55             letter country specifier as the 'country' and the integer part as the
56             'agaNum'. The country concatenated with the agaNum is the ID. My ID for
57             example is USA2122. IDs should be normalized (capitalize the country part and
58             remove preceding 0s from the agaNum part) with the B method
59             (below).
60              
61             Note also that some programs may accept limited integers in the agaNum part of
62             the ID. Accelerat, for example, seems to accept only up to 32K (someone used
63             a signed short somewhere?)
64              
65             =cut
66              
67 1     1   56906 use strict;
  1         3  
  1         63  
68             require 5.001;
69              
70             package Games::Go::AGATourn;
71 1     1   7 use Carp;
  1         2  
  1         76  
72 1     1   6 use IO::File;
  1         7  
  1         292  
73              
74             our @ISA = qw(Exporter);
75              
76             # Items to export into callers namespace by default. Note: do not export
77             # names by default without a very good reason. Use EXPORT_OK instead.
78             # Do not simply export all your public functions/methods/constants.
79              
80             # This allows declaration use PackageName ':all';
81             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
82             # will save memory.
83             our %EXPORT_TAGS = ( 'all' => [ qw(
84             ) ] );
85              
86             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
87              
88             our @EXPORT = qw(
89             );
90              
91             BEGIN {
92 1     1   43 our $VERSION = sprintf "%d.%03d", '$Revision: 1.35 $' =~ /(\d+)/g;
93             }
94              
95             ######################################################
96             #
97             # Class Variables
98             #
99             #####################################################
100              
101 1     1   6 use constant NOTARANK => -99.9; # illegal rank or rating
  1         2  
  1         13688  
102              
103             ######################################################
104             #
105             # Public methods
106             #
107             #####################################################
108              
109             =head1 METHODS
110              
111             =over 4
112              
113             =item my $agaTourn = Bnew> (options)
114              
115             A B AGATourn by default reads the B file to get the name,
116             rank, and AGA numbers for all the players in the tournament. It then reads
117             all available game results (B files: 1.tde, 2.tde, etc.) and the game
118             data is incorporated into the AGATourn object.
119              
120             =head2 Options:
121              
122             =over 4
123              
124             =item B
125              
126             Round file number to read. If B is 0, no round files are read. If
127             B is 1 or greater, only the one round file will be read. If B
128             is undef (or not specified), all existing round files are read. Round files
129             should be named I<1.tde>, I<2.tde>, etc.
130              
131             Default: undef
132              
133             =item B
134              
135             Name of register.tde file. Use undef to prevent reading the register.tde
136             file. Changing the name of this file is probably a bad idea.
137              
138             Default 'register.tde' (in the current directory)
139              
140             =item B
141              
142             Starting length of name field. While reading the register file (see
143             B below), B grows to reflect the longest name
144             seen so far (see B method below).
145              
146             Default: 0
147              
148             =item B
149              
150             Default three-letter country name.
151              
152             The tdlist file does not include country information in the ID, so the
153             B method returns country => B.
154              
155             Default: 'USA'
156              
157             =back
158              
159             =cut
160              
161             sub new {
162 2     2 1 29 my ($proto, %args) = @_;
163              
164 2         5 my $self = {};
165 2   33     18 bless($self, ref($proto) || $proto);
166 2         13 $self->{defaultCountry} = 'USA';
167 2         10 $self->Clear;
168             # transfer user args
169 2         9 foreach (keys(%args)) {
170 4         10 $self->{$_} = $args{$_};
171             }
172 2 100       9 if (defined($self->{register_tde})) {
173 1 50       6 return(undef) unless($self->ReadRegisterFile($self->{register_tde}));
174             }
175 2 100       7 if (defined($self->{register_tde})) {
176 1 50       4 if (defined($self->{Round})) {
177 1 50       3 if ($self->{Round} > 0) {
178 0         0 $self->ReadRoundFile("$self->{Round}.tde");
179             }
180             } else {
181 0         0 my $round = 1;
182 0         0 while (-f "$round.tde") {
183 0         0 $self->{Round} = $round;
184 0         0 $self->ReadRoundFile("$self->{Round}.tde");
185 0         0 $round++;
186             }
187             }
188             }
189 2         11 return($self);
190             }
191              
192             =item $agaTourn-EB
193              
194             Clears AGATourn database.
195              
196             =cut
197              
198             sub Clear {
199 2     2 1 4 my ($self) = @_;
200              
201             # set defaults
202 2         6 $self->{Round} = undef;
203 2         3 $self->{register_tde} = "register.tde"; # default
204 2         9 $self->{Directive}{ROUNDS}[0] = 1; # I hope there's at least one!
205 2         6 $self->{Directive}{TOURNEY}[0] = "Unknown tournament";
206 2         7 $self->{nameLength} = 0;
207 2         4 $self->{Name} = {}; # empty hash
208 2         4 $self->{Rating} = {};
209 2         5 $self->{Rank} = {};
210 2         5 $self->{Comment} = {};
211 2         6 $self->{Wins} = {};
212 2         4 $self->{Losses} = {};
213 2         4 $self->{NoResults} = {};
214 2         4 $self->{Played} = {};
215 2         3 $self->{gameAllList} = []; # empty array
216 2         5 $self->{error} = 0;
217             }
218              
219             =item my $hash = $agaTourn-EB ($line)
220              
221             Parses a single line from the TDLIST file (the latest TDLIST file
222             should be downloaded from the AGA at http://usgo.org shortly before
223             the tournament, and either the tab-delimited tdlista or the
224             space-delimited versions are accepted). The return value is a
225             reference to a hash of the following values:
226             agaNum => the number part if the ID
227             country => the country part of the ID (always the default
228             country)
229             name => complains if there is no a comma
230             memType => membership type or '' if none
231             agaRating => rating in decimal form, or '' if none
232             agaRank => undef unless rating is a D/K style rank
233             expire => date membership expires or '' if none
234             club => club affiliation or '' if none
235             state => state or '' if none
236              
237             If the line is not parsable, prints a warning and returns undef.
238              
239             =cut
240              
241             # sadly, we need to deal with two formats
242             # old tdlist input looks like this:
243             # name AGA# MmbrTyp Rank expires Club State
244             #Abe, Shozo 2443 L 8603 NJ
245             #Abe, Y. 2043 8312 GA
246             #Abell, John 3605 -1.4 9105 MHGA CO
247             #Abrahms, Judy 1253 L 8012 MGA MA
248             #Abrams, Michael 6779 L -27.4 9411 MIAM FL
249             #Abramson, Allan 101 3.5 9504 NOVA VA
250             # the new format is like this:
251             #Abe, Shozo 2443 Limit 03/28/1986 NJ
252             #Abe, Y. 2043 Full 12/28/1983 GA
253             #Abell, John 3605 Full -1.4 05/28/1991 MHGA CO
254             #Abrahms, Judy 1253 Limit 12/28/1980 MGA MA
255             #
256             # There's also a tab-delimited version
257              
258             sub ParseTdListLine {
259 1     1 1 3 my ($self, $string) = @_;
260              
261 1         18 $string =~ s/[\n\r]*$/\t/s; # remove crlf, and tack on an extra tab
262 1         6 my @fields = $string =~ m/(.*?)\t/g; # is it the tab-delimited version?
263 1 50       5 if (@fields == 9) {
264             return {
265 0         0 name => $fields[0], # return ref to hash
266             agaNum => $fields[1],
267             memType => $fields[2],
268             agaRating => $fields[3],
269             expire => $fields[4],
270             club => $fields[5],
271             state => $fields[6],
272             sigma => $fields[7],
273             ratingDate => $fields[8],
274             country => $self->{defaultCountry},
275             };
276             }
277             # else parse a space-delimited version:
278 1         2 my ($name, $agaNum, $agaRank, $misc);
279 1         4 my ($agaRating, $memType, $club, $state, $expire) = (-99, '', '', '', '');
280              
281 1 50       11 unless($string =~ m/^\s*(.*?)\s*(\d+) (.*)/) { # break into manageble groups
282 0         0 carp("Error: can't extract AGA number from \"$string\"\n");
283 0         0 return(undef);
284             }
285 1         3 $name = $1; # part before is name
286 1         3 $agaNum = $2; # middle part is the AGA number
287 1         3 $misc = $3; # part after match
288 1 50       11 if ($misc =~ m/([\w ]{6}?) ([-\d\. ]{5}) ([\d\/ ]{10}) ([\w ]{4}) (.*?)\s*$/) {
289             # parse by character positions (blanks lined up in the right places)
290 0         0 $memType = _ws_clean($1);
291 0         0 $agaRating = _ws_clean($2);
292 0         0 $expire = _ws_clean($3);
293 0         0 $club = _ws_clean($4);
294 0         0 $state = _ws_clean($5);
295 0 0       0 if ($agaRating =~ m/(\d+)([dk])/i) {
296 0         0 $agaRank = uc($agaRating);
297 0         0 $agaRating = $1 + 0.5;
298 0 0       0 $agaRating = -$agaRating if (uc($2) eq 'K');
299             }
300             } else { # try to parse free-form style
301 1 50       9 if ($misc =~ s/^\s*([^\s\d-]+) //) { # membership type, if any
    0          
302 1         3 $memType = $1;
303             } elsif (not $misc =~ s/^ //) {
304 0         0 carp("Uh oh, no membership type space in: '$misc'");
305             }
306 1 50       8 if ($misc =~ s/^\s*(-?\d+\.\d) //) { # find rank, if any
    0          
    0          
    0          
307 1         3 $agaRating = $1;
308             } elsif ($misc =~ s/^\s*(\d+)([dkDK]) //) { # 4D or 15k type rank
309 0         0 $agaRank = uc("$1$2");
310 0         0 $agaRating = $1 + 0.5;
311 0 0       0 $agaRating = -$agaRating if (uc($2) eq 'K');
312             } elsif ($misc =~ s/^\s*(-?\d\d?) //) { # one or two digit number, no decimal point?
313 0         0 $agaRating = $1; # it's another way of indicating rank
314             } elsif (not $misc =~ s/^ //) {
315 0         0 carp("Uh oh, no rating space in: '$misc'");
316             }
317 1 50       6 if ($misc =~ s/^\s*([\d\/]+) //) { # expiration date, if any
    0          
318 1         3 $expire = $1;
319             } elsif (not $misc =~ s/ //) {
320 0         0 carp("Uh oh, no expire space in: '$misc'");
321             }
322 1 50 33     5 unless(defined($expire) or defined($memType)) {
323 0         0 carp "Uh oh";
324             }
325 1 50       6 if ($misc =~ s/^(\w+)\s*//) { # club
    0          
326 1         2 $club = $1;
327 1         4 $club =~ s/\W//g; # remove all non-word chars
328             } elsif (not $misc =~ s/ //) {
329 0         0 carp("Uh oh, no expire space in: '$misc'");
330             }
331 1 50       8 if ($misc =~ s/^\s*(.*?)\s*$//) { # state
332 1         4 $state = $1;
333             }
334 1 50       4 if ($misc ne '') {
335 0         0 carp("Error: \"$misc\" was left over after parsing \"$string\"\n",
336             "name=$name, id=$agaNum, mem=$memType, rating=$agaRating, ",
337             "expire=$expire, club=$club, state=$state\n");
338             }
339             }
340             return {
341 1         22 agaNum => $agaNum, # return ref to hash
342             country => $self->{defaultCountry},
343             name => $name,
344             memType => $memType,
345             agaRating => $agaRating,
346             agaRank => $agaRank,
347             expire => $expire,
348             club => $club,
349             state => $state,
350             };
351             }
352              
353             sub _ws_clean {
354 0     0   0 my $str = shift @_;
355 0         0 $str =~ m/^\s*(.*?)\s*$/;
356 0         0 return $1;
357             }
358              
359             =item my $result = $agaTourn-EB ($fileName)
360              
361             Reads a register.tde file and calls B on each line of the file.
362              
363             Returns 0 if $fileName couldn't be opened for reading, 1 otherwise.
364              
365             =cut
366              
367             sub ReadRegisterFile {
368 1     1 1 3 my ($self, $fName) = @_;
369              
370 1         5 $self->{fileName} = $fName; # set global name
371 1         12 my $inFP = new IO::File("<$fName");
372 1 50       151 unless ($inFP) {
373 0         0 carp("Error: can't open $fName for reading\n"),
374             $self->{error} = 1,
375             return(0);
376             }
377 1         132 while(my $line = <$inFP>) {
378 17         36 $self->AddRegisterLine($line);
379             }
380 1         13 $inFP->close();
381 1         30 return(1);
382             }
383              
384             =item $agaTourn-EB ($line)
385              
386             Calls B on $line. Information extracted about players and
387             directives is added to the $agaTourn object. Comments and blank lines are
388             ignored.
389              
390             =cut
391              
392             sub AddRegisterLine {
393 17     17 1 27 my ($self, $line) = @_;
394              
395 17 50 33     104 my $fileMsg = (ref ($self) and exists ($self->{fileName})) ?
396             " at line $. in $self->{fileName} " :
397             '';
398 17         35 my $h = $self->ParseRegisterLine($line);
399 17 100       46 return unless(defined($h));
400 16 100       36 if (exists($h->{directive})) {
401 9         16 foreach (qw(HANDICAPS ROUNDS RULES TOURNEY)) { # non-array directives
402 30 100       78 if ($h->{directive} eq $_) {
403 4         14 $self->{Directive}{$h->{directive}} = [$h->{value}]; # single value
404 4         29 return;
405             }
406             }
407 5         7 push(@{$self->{Directive}{$h->{directive}}}, $h->{value});
  5         15  
408 5         22 return;
409             }
410 7 100       25 return unless(exists($h->{agaNum})); # probably a comment
411 4         9 my $id = "$h->{country}$h->{agaNum}";
412 4 50       11 if (defined($self->{Name}{$id})) {
413 0         0 carp("Error: Player ID $id is duplicated$fileMsg\n");
414 0         0 $self->{error} = 1;
415             }
416 4         16 $self->{Name}{$id} = $h->{name};
417 4         8 $self->{Rating}{$id} = $h->{agaRating};
418 4         8 $self->{Rank}{$id} = $h->{agaRank};
419 4         138 $self->{Comment}{$id} = $h->{comment};
420 4         10 $self->{Club}{$id} = $h->{club};
421 4         10 $self->{Flags}{$id} = $h->{flags};
422 4 50       16 $self->{Played}{$id} = [] unless exists($self->{Played}{$id});
423 4         10 foreach (qw(Wins Losses NoResults)) {
424 12 50       41 $self->{$_}{$id} = 0 unless exists($self->{$_}{$id});
425             }
426 4         9 my $len = length($h->{name});
427 4 100       31 $self->{nameLength} = $len if ($len > $self->{nameLength});
428             }
429              
430             =item my $hash = $agaTourn-EB ($line)
431              
432             Parses a single line from the register.tde file (name lines). Here are some
433             examples lines from register.tde file:
434              
435             # this line is a comment. the following line is a directive:
436             ## HANDICAPS MAX
437             # the following line is a name line:
438             USA02122 Augustin, Reid 5.0 CLUB=PALO # 12/31/2004 CA
439              
440             The return value is a reference to a hash of the following values:
441             agaNum => just the number part of the ID
442             country => just the country part of the ID (default ='USA')
443             name => complains if name doesn't contain a comma
444             agaRating => rating for the player
445             agaRank => undef if line contains a rating and not a rank
446             club => if there is a club association, '' if not
447             flags => anything left over (excluding comment)
448             comment => everything after the #, '' if none
449              
450             If the line is a directive, the return hash reference contains only:
451             directive => the directive name
452             value => the directive value ('' if none)
453              
454             If the line is a comment, leading and trailing whitespace is removed and the
455             hash contains only:
456             comment => comment contents (may be '')
457              
458             If the line is empty, returns undef.
459              
460             If the line is not parsable, prints a warning and returns undef.
461              
462             =cut
463              
464             sub ParseRegisterLine {
465 18     18 1 30 my ($self, $line) = @_;
466              
467 18         134 $line =~ s/\s*$//s; # delete trailing spaces
468 18 100       45 return undef if ($line eq ''); # nothing left? return undef
469              
470 17 100       57 if ($line =~ s/^\s*##\s*//) {
471 9         40 $line =~ m/(\S+)\s*(.*?)\s*$/;
472             return {
473 9         39 directive => $1,
474             value => $2
475             };
476             }
477 8         11 my $comment = '';
478 8 50       73 if ($line =~ s/\s*#\s*(.*?)\s*$//) {
479 8         18 $comment = $1;
480             }
481 8 100       19 if ($line eq '') {
482             return {
483 3         8 comment => $comment,
484             };
485             }
486              
487 5 50 33     39 my $fileMsg = (ref ($self) and exists ($self->{fileName})) ?
488             " at line $. in $self->{fileName} " :
489             '';
490 5         7 my $club = '';
491 5 50       53 if ($line =~ s/\s*CLUB=(\S*)\s*//) {
492 5         8 $club = $1;
493 5         10 $club =~ s/\W//g; # remove all non-word chars
494             }
495 5         18 my ($agaRating, $agaRank);
496 5 100       89 if($line =~ s/^\s*(\S*)\s+(.*?)\s+(\d+[dDkK])\s*//) { # look for dan or kyu rank
    50          
    0          
497 2         4 $agaRank = $3;
498 2         7 $agaRating = $self->RankToRating($3);
499             } elsif($line =~ s/^\s*(\S*)\s+(.*?)\s+(-*\d+\.\d+)\s*//) { # look for 5.4 or -13.6 type of rank
500 3         9 $agaRating = $3; # ok as is
501             } elsif($line =~ s/^\s*(\S*)\s+(.*?)\s+(-*\d+)\s*//) { # look for 5 or -13 type of rank
502 0         0 carp("Warning: rank is non-decimalized:\n$line\n");
503 0         0 $agaRating = "$3.0";
504             } else {
505 0         0 carp("Error: Can't parse name$fileMsg:\n$line\n");
506 0         0 $self->{error} = 1;
507 0         0 return;
508             }
509              
510 5         11 my $name = $2;
511 5         23 my $agaNum = $self->NormalizeID($1);
512 5         11 my $country = $self->{defaultCountry};
513 5 50       25 if ($agaNum =~ s/^(\D+)//) {
514 5         10 $country = uc($1);
515             }
516 5 50       18 unless ($name =~ m/,/) {
517 0         0 carp("Warning: no comma in name \"$name\"$fileMsg\n");
518             }
519             return { # return ref to hash of:
520 5         51 agaNum => $agaNum,
521             name => $name,
522             agaRating => $agaRating,
523             agaRank => $agaRank,
524             club => $club,
525             country => $country,
526             flags => $line, # whatever's left over
527             comment => $comment,
528             };
529             }
530              
531             =item my $result = $agaTourn-EB ($fileName)
532              
533             Reads a round file and calls B on each line of the file.
534             Complains if filename is not in the form I<1.tde>, I<2.tde>, etc.
535              
536             Sets the current B number to the digit part of fileName.
537              
538             Returns 0 if fileName couldn't be opened for reading, 1 otherwise.
539              
540             =cut
541              
542             sub ReadRoundFile {
543 1     1 1 3 my ($self, $fName) = @_;
544              
545 1 50       9 if ($fName =~ m/^\d+$/) { # no TDE extension?
546 0         0 $fName .= '.tde';
547             }
548 1         4 $self->{fileName} = $fName; # set global name
549 1 50       8 if ($fName =~ m/(\d+).tde/) {
550 1         5 $self->{Round} = $1;
551             } else {
552 0         0 carp "Round filename not in normal ('1.tde', '2.tde', etc) format\n";
553             }
554 1         11 my $inFP = new IO::File("<$fName");
555 1 50       144 unless ($inFP) {
556 0         0 carp("Error: can't open $fName for reading\n");
557 0         0 $self->{error} = 1;
558 0         0 return(0);
559             }
560 1         31 while (my $line = <$inFP>) {
561 4         12 $self->AddRoundLine($line);
562             }
563 1         8 $inFP->close();
564 1         28 return(1);
565             }
566              
567             =item $agaTourn-EB ($line)
568              
569             Parses $line (by calling B) and adds the information to the
570             B. Games without a result ('?') increment both players' NoResults
571             list scores, and games with a result ('b' or 'w') increment the two players'
572             Wins and Losses scores. If the game result is 'b' or 'w', the black player is
573             added to the white player's B list and vica-versa. Note that
574             B is not affected by games that are not complete.
575              
576             Complains if either player, or both, are not registered via
577             B.
578              
579             =cut
580              
581             sub AddRoundLine {
582 4     4 1 9 my ($self, $line) = @_;
583              
584 4         10 my $g = $self->ParseRoundLine($line); # get game result
585 4 100 66     29 return unless(defined($g) and exists($g->{result}));
586 2         11 my $wId = $self->NormalizeID("$g->{wcountry}$g->{wagaNum}");
587 2         8 my $bId = $self->NormalizeID("$g->{bcountry}$g->{bagaNum}");
588 2 50       8 carp("Game $wId.vs.$bId, $wId is not registered\n") unless (exists($self->{Rating}{$wId}));
589 2 50       8 carp("Game $wId.vs.$bId, $bId is not registered\n") unless (exists($self->{Rating}{$bId}));
590 2         5 foreach (qw(Wins Losses NoResults)) {
591 6 50       14 $self->{$_}{$wId} = 0 unless exists($self->{$_}{$wId});
592 6 50       26 $self->{$_}{$bId} = 0 unless exists($self->{$_}{$bId});
593             }
594 2 50       24 if ($g->{result} eq 'w') {
    100          
    50          
595 0         0 $self->{Wins}{$wId}++;
596 0         0 $self->{Losses}{$bId}++;
597 0         0 push(@{$self->{Played}{$bId}}, $wId);
  0         0  
598 0         0 push(@{$self->{Played}{$wId}}, $bId);
  0         0  
599             } elsif ($g->{result} eq 'b') {
600 1         3 $self->{Wins}{$bId}++;
601 1         3 $self->{Losses}{$wId}++;
602 1         3 push(@{$self->{Played}{$bId}}, $wId);
  1         4  
603 1         2 push(@{$self->{Played}{$wId}}, $bId);
  1         3  
604             } elsif ($g->{result} eq '?') {
605 1         2 $self->{NoResults}{$bId}++;
606 1         2 $self->{NoResults}{$wId}++;
607             } else {
608 0         0 carp("Unknown game result:$g->{result}"); # probably can't happen
609             }
610 2         10 my $game = "$wId,$bId,$g->{result},$g->{handi},$g->{komi},$self->{Round}";
611 2         3 push(@{$self->{gameAllList}}, $game);
  2         6  
612 2         2 push(@{$self->{gameIDList}{$wId}}, $game);
  2         8  
613 2         3 push(@{$self->{gameIDList}{$bId}}, $game);
  2         24  
614             }
615              
616             =item my $hash = $agaTourn-EB ($line)
617              
618             Parses a single line from a results file (I<1.tde>, I<2.tde>, etc). Here's an
619             example line from a results file:
620              
621             TMP18 TMP10 b 0 7 # Lee, Ken -28.5 : Yang, John -28.5
622             # wID bID result handi komi comment
623              
624             The return value is a reference to a hash of the following values:
625             wcountry => combine with wagaNum to get complete ID
626             wagaNum => the number part of white's AGA number
627             bcountry => combine with bagaNum to get complete ID
628             bagaNum => the number part of black's AGA number
629             result => winner: 'b', 'w' or '?'
630             handi => handicap game was played with
631             komi => komi game was played with
632             comment => everything after the #
633              
634             If $line is empty, returns undef.
635              
636             If $line is a comment, returns only:
637             comment => everything after the #
638              
639             If the line is not parsable, prints a warning and returns undef.
640              
641             =cut
642              
643             sub ParseRoundLine {
644 4     4 1 7 my ($self, $line) = @_;
645              
646 4         39 $line =~ s/\s*$//s; # delete trailing spaces
647 4 50       15 return undef if ($line eq ''); # nothing left? return undef
648              
649 4 100       19 if ($line =~ s/^\s*##\s*//) {
650 2         6 $line =~ m/(\S+)\s*(.*?)\s*/;
651             return {
652 2         11 directive => $1,
653             value => $2
654             };
655             }
656 2         4 my $comment = '';
657 2 50       24 if ($line =~ s/\s*#\s*(.*?)\s*$//) {
658 2         7 $comment = $1;
659             }
660 2 50       5 if ($line eq '') {
661             return {
662 0         0 comment => $comment,
663             };
664             }
665              
666 2 50       15 if ($line =~ m/^\s*(\w+)(\d+)\s+(\w+)(\d+)\s+([bwBW\?])\s+(\d+)\s+(-?\d+)$/) {
667             return {
668 2         47 wcountry => uc($1),
669             wagaNum => $2,
670             bcountry => uc($3),
671             bagaNum => $4,
672             result => lc($5),
673             handi => $6,
674             komi => $7,
675             comment => $comment,
676             };
677             }
678 0 0 0     0 my $fileMsg = (ref ($self) and exists ($self->{fileName})) ?
679             " at line $. in $self->{fileName} " :
680             '';
681 0         0 carp("Can't parse round line $.$fileMsg:\n$line\n");
682 0         0 $self->{error} = 1;
683 0         0 return undef;
684             }
685              
686             =item my $tourney = $agaTourn-EB
687              
688             Returns the name of the tournament from a ##TOURNEY directive added via
689             B, or 'Unknown Tournament' if no TOURNEY directive has been
690             added.
691              
692             =cut
693              
694             sub Tourney {
695 2     2 1 4103 my ($self) = @_;
696 2         17 return ($self->{Directive}{TOURNEY}[0]); # last TOURNEY directive
697             }
698              
699             =item my $directive = $agaTourn-EB ($directive)
700              
701             Returns a list (or a reference to the list in scalar context) of directives
702             added via calls to B. Directive names are always turned into
703             upper case (but the case of the directive value, if any, is preserved).
704              
705             Since some directives (like BAND) may occur several times, all directives are
706             stored as a list in the order added (either from B or
707             B). Certain directives (HANDICAPS ROUNDS RULES TOURNEY) keep
708             only the last directive added.
709              
710             Some directives have no associated value.
711              
712             B returns undef if $directive has not been added, or a list
713             (possibly empty) if $directive has been added.
714              
715             If called with no arguments (or $directive is undef), returns a reference to a
716             hash of all the current directives.
717              
718             =cut
719              
720             sub Directive {
721 2     2 1 7 my ($self, $directive) = @_;
722              
723 2 50       10 if (defined($directive)) {
724 0         0 $directive = uc($directive); # force to upper case
725 0 0       0 if (exists($self->{Directive}{$directive})) {
726 0 0       0 return wantarray ? @{$self->{Directive}{$directive}} : $self->{Directive}{$directive};
  0         0  
727             }
728 0         0 return(undef);
729             }
730 2         33 return($self->{Directive}); # the whole shebang...
731             }
732              
733             =item my $rounds = $agaTourn-EB
734              
735             Returns the total number of rounds the $agaTourn object knows about. If there
736             has been a ##ROUNDS directive in a call to B file, this will
737             return that number. If not, it will return the number part of the last
738             I.tde file read or undef.
739              
740             =cut
741              
742             sub Rounds {
743 2     2 1 6 my ($self) = @_;
744              
745 2 50       18 return $self->{Directive}{ROUNDS}[0] # fetch ROUNDS directive
746             if(defined($self->{Directive}{ROUNDS}[0]));
747 0         0 return($self->{Round});
748             }
749              
750             =item my $round = $agaTourn-EB
751              
752             Returns the number of the current round (based on the last I.tde
753             file read).
754              
755             =cut
756              
757             sub Round {
758 2     2 1 5 my ($self) = @_;
759 2         10 return($self->{Round});
760             }
761              
762             =item my $name = $agaTourn-EB ($id)
763              
764             Returns the the name for $id.
765              
766             If $id is undef, returns a reference to the entire B hash (keyed by ID).
767              
768             =cut
769              
770             sub Name {
771 2     2 1 5 my ($self, $id) = @_;
772              
773 2 50       9 return ($self->{Name}{$id}) if (defined($id));
774 2         24 return ($self->{Name});
775             }
776              
777             =item my $name_length = $agaTourn-EB
778              
779             Returns the length of the longest name.
780              
781             =cut
782              
783             sub NameLength {
784 2     2 1 6 my ($self) = @_;
785 2         13 return ($self->{nameLength});
786             }
787              
788             =item my $rating = $agaTourn-EB ($id, $newRating)
789              
790             Sets (if $newRating is defined) or returns the rating for $id. If $id is not
791             defined, returns a reference to the entire B hash (keyed by IDs).
792              
793             $id can also be a rank ('4d', or '5k'), or a rating (4.2 or -5.3, but not
794             between 1.0 and -1.0). This form is simply a converter - $newRating is not
795             accepted.
796              
797             If $id is defined but not registered (via B), complains and
798             returns undef.
799              
800             =cut
801              
802             sub Rating {
803 2     2 1 6 my ($self, $id, $newRating) = @_;
804              
805 2 50       10 $self->{Rating}{$id} = $newRating if (defined($newRating));
806 2 50       43 if (defined($id)) {
807 0 0       0 return ($self->{Rating}{$id}) if (exists($self->{Rating}{$id}));
808 0 0       0 if ($id =~ m/^(-?\d+\.\d)\s*/) { # find rank
809 0         0 return $1; # rating format
810             }
811 0 0       0 if ($id =~ m/^\s*(\d+)([dkDK])\b/) { # 4D or 15k type rank
812 0         0 my $rating = $1;
813 0 0       0 $rating = -$rating if (lc($2) eq 'k');
814 0         0 return $rating;
815             }
816 0 0       0 if ($id =~ m/^\s*(-?\d\d?)\b/) { # one or two digit number, no decimal point?
817 0         0 return $1; # it's another way of indicating rank
818             }
819 0         0 carp ("Invalid Rating argument:$id\n");
820 0         0 return undef; # eh?
821             }
822 2         18 return ($self->{Rating});
823             }
824              
825             =item my $rank = $agaTourn-EB ($id)
826              
827             Returns the rank for $id. This field is undef unless the B
828             contained a rank field of the form '4k' or '3d' as opposed to a rating of the
829             form '-4.5' or '3.4'.
830              
831             If $id is not defined, returns a reference to the entire B hash (keyed
832             by IDs).
833              
834             =cut
835              
836             sub Rank {
837 0     0 1 0 my ($self, $id) = @_;
838              
839 0 0       0 return ($self->{Rank}{$id}) if(defined($id));
840 0         0 return ($self->{Rank});
841             }
842              
843             =item my $sigma = $agaTourn-EB ($id)
844              
845             Returns the sigma for $id. Sigma is determined by the rating/rank in the
846             B. If the line contains a rank field of the form '4k' or '3d',
847             sigma is 1.2 for 7k and stronger, and
848              
849             (k - 0.3) / 6
850              
851             for 8k and weaker. If the line contains a rating of the form '-4.5' or '3.4',
852             sigma is 0.6 for -8.0 and stronger, and
853              
854             (-rating - 4.4) / 6
855              
856             for weaker than -8.0.
857              
858             Complains and returns undef if $id is undefined or unregistered.
859              
860             =cut
861              
862             sub Sigma {
863 0     0 1 0 my ($self, $id) = @_;
864              
865 0 0       0 if (defined($id)) {
866 0 0       0 if (defined($self->{Rank}{$id})) {
    0          
867 0         0 $self->{Rank}{$id} =~ m/^([\d]+)([kdKD])$/;
868 0         0 my $r = $1;
869 0 0       0 $r = -$r if (lc($2) eq 'k');
870 0         0 my $sigma = (-$r - 0.3) / 6;
871 0 0       0 return ($sigma > 1.2) ? $sigma : 1.2;
872             } elsif (defined($self->{Rating}{$id})) {
873 0         0 my $sigma = (-$self->{Rating}{$id} - 4.4) / 6;
874 0 0       0 return ($sigma > 0.6) ? $sigma : 0.6;
875             } else {
876 0         0 carp("$id is not registered\n");
877             }
878             } else {
879 0         0 carp("called Sigma(\$id) without a valid ID\n");
880             }
881 0         0 return(undef);
882             }
883              
884             =item my $club = $agaTourn-EB ($id)
885              
886             Returns the club for $id or '' if no club is known. Returns undef if $id is
887             not registered (via B).
888              
889             If no $id parameter is passed, returns a reference to the entire B hash
890             (keyed by IDs).
891              
892             =cut
893              
894             sub Club {
895 0     0 1 0 my ($self, $id) = @_;
896              
897 0 0       0 return ($self->{Club}{$id}) if (defined($id));
898 0         0 return($self->{Club});
899             }
900              
901             =item my $flags = $agaTourn-EB ($id)
902              
903             Returns the flags for $id or '' if no flags are known. Flags are anything
904             left over (excluding the comment) after the ID, name, rating, and club have
905             been parsed by B. It might include (for example) BYE or
906             Drop. The case is preserved from the original line parsed.
907              
908             Returns undef if $id is not registered (via B). If no $id
909             parameter is passed, returns a reference to the entire B hash (keyed by
910             IDs).
911              
912             =cut
913              
914             sub Flags {
915 0     0 1 0 my ($self, $id) = @_;
916              
917 0 0       0 if (defined($id)) {
918 0 0       0 return ($self->{Flags}{$id}) if (exists($self->{Flags}{$id}));
919 0 0       0 return ('') if exists($self->{Rating}{$id});
920             return (undef)
921 0         0 }
922 0         0 return($self->{Flags});
923             }
924              
925             =item $comment = $agaTourn-EB ($id)
926              
927             Returns the comment associated with $id line as added via B.
928              
929             If no $id argument is passed, returns a reference to the entire B
930             hash (keyed by IDs).
931              
932             =cut
933              
934             sub Comment {
935 2     2 1 8 my ($self, $id) = @_;
936              
937 2 50       12 if (defined($id)) {
938 0 0       0 return ($self->{Comment}{$id}) if (exists($self->{Comment}{$id}));
939 0 0       0 return ('') if exists($self->{Rating}{$id});
940             return (undef)
941 0         0 }
942 2         18 return ($self->{Comment});
943             }
944              
945             =item my $error = $agaTourn-EB
946              
947             If called with an argument, sets the error flag to the new value.
948             Returns the current (or new) value of the error flag.
949              
950             =cut
951              
952             sub Error {
953 2     2 1 7 my ($self, $error) = @_;
954              
955 2 50       13 $self->{error} = $error if (defined($error));
956 2         12 return ($self->{error});
957             }
958              
959             =item my $gamesList = $agaTourn-EB ($id, ...)
960              
961             Returns a list (or a reference to the list in scalar context) of games played
962             by B(s). If no B argument is passed, returns the list of all
963             games.
964              
965             Games are added via the B or the B methods.
966              
967             Entries in the returned list are comma separated strings. They can be parsed
968             with:
969              
970             my ($whiteID, $blackID, $result,
971             $handicap, $komi, $round) = split(',', $agaTourn->GamesList[$index]);
972              
973             =cut
974              
975             sub GamesList {
976 3     3 1 10 my ($self, @arg) = @_;
977              
978 3 50       31 return($self->{gameAllList}) unless (@arg);
979 0         0 my @games;
980 0         0 foreach (@arg) {
981 0         0 push(@games, @{$self->{gameIDList}{$_}});
  0         0  
982             }
983 0 0       0 return(wantarray ? @games : \@games);
984             }
985              
986             =item my $wins = $agaTourn-EB ($id)
987              
988             Returns the number of winning games recorded for $id. Wins are recorded
989             via the B method.
990              
991             If no $id argument is passed, returns a reference to the entire B hash
992             (keyed by IDs).
993              
994             =cut
995              
996             sub Wins {
997 0     0 1 0 my ($self, $id) = @_;
998              
999 0 0       0 return($self->{Wins}{$id}) if (defined($id));
1000 0         0 return($self->{Wins});
1001             }
1002              
1003             =item my $losses = $agaTourn-EB ($id)
1004              
1005             Returns the number of losing games recorded for $id. Losses are
1006             recorded via the B method.
1007              
1008             If no $id argument is passed, returns a reference to the entire B hash
1009             (keyed by IDs).
1010              
1011             =cut
1012              
1013             sub Losses {
1014 0     0 1 0 my ($self, $id) = @_;
1015              
1016 0 0       0 return($self->{Losses}{$id}) if (defined($id));
1017 0         0 return($self->{Losses});
1018             }
1019              
1020             =item my $no_results = $agaTourn-EB ($id)
1021              
1022             Returns the number of no-result games recorded for $id. No-results are
1023             recorded via the B method.
1024              
1025             If no $id argument is passed, returns a reference to the entire B
1026             hash (keyed by IDs).
1027              
1028             =cut
1029              
1030             sub NoResults {
1031 0     0 1 0 my ($self, $id) = @_;
1032              
1033 0 0       0 return($self->{NoResults}{$id}) if (defined($id));
1034 0         0 return($self->{NoResults});
1035             }
1036              
1037             =item my @played = $agaTourn-EB ($id)
1038              
1039             Returns a list (or a reference to the list in scalar context) of $id's
1040             opponents. The list is ordered as they were added by B method.
1041              
1042             If no $id argument is passed, returns a reference to the entire B hash
1043             (keyed by IDs).
1044              
1045             =cut
1046              
1047             sub Played {
1048 0     0 1 0 my ($self, $id) = @_;
1049              
1050 0 0       0 if (defined($id)) {
1051 0 0       0 return wantarray ? @{$self->{Played}{$id}} : $self->{Played}{$id};
  0         0  
1052             }
1053 0         0 return $self->{Played};
1054             }
1055              
1056             =item my $rating = $agaTourn-EB ($rank | $rating)
1057              
1058             Returns a value guaranteed to be in a correct AGA Rating format. The format
1059             is a number with a tenths decimal, where the number represents the dan rank
1060             (if positive) or the kyu rank (if negative). A rating of 3.5 represents
1061             squarely in the middle of the 3 dan rank, and -1.9 represents a weak 1 kyu
1062             rank. The range from 1.0 to -1.0 is not used (see
1063             B/B below).
1064              
1065             =cut
1066              
1067             sub RankToRating {
1068 32     32 1 53 my ($self, $rating) = @_;
1069              
1070 32 50 33     187 return (NOTARANK) if (not defined($rating) or ($rating eq ''));
1071 32 100       132 return "$rating.0" if ($rating =~ m/^-?\d+$/); # not in decimalized format?
1072 29 100       79 unless ($rating =~ m/^-?\d+\.\d+$/) { # not in rating format?
1073 24 50       91 return(NOTARANK) unless($rating =~ m/^(\d+)([dDkK])$/); # not in rank format either?
1074 24         52 $rating = "$1.5"; # it's in rank format (like 5D or 2k), convert to rating
1075 24 100       85 $rating = -$rating if (uc($2) eq "K"); # kyus are negative
1076             }
1077 29         87 return($rating);
1078             }
1079              
1080             =item my $band_idx = $agaTourn-EB ($rank | $rating)
1081              
1082             Returns the band index for a B or B. Returns NOTARANK if
1083             rank/rating is not in any bands.
1084              
1085             See also B below.
1086              
1087             =cut
1088              
1089             sub WhichBandIs {
1090 8     8 1 15 my ($self, $r) = @_;
1091              
1092 8 100       24 unless (exists($self->{bandTop})) {
1093 2         10 $self->_setBands();
1094             }
1095 8         22 $r = $self->RankToRating($r);
1096 8         14 my $ii;
1097 8         11 for ($ii = 0; $ii < @{$self->{bandTop}}; $ii++) {
  21         52  
1098 15 50       44 next if ($r > $self->{bandTop}[$ii]);
1099 15 100       39 if ($r >= $self->{bandBot}[$ii]) {
1100 2         14 return($ii); # this is it
1101             }
1102             }
1103 6         12 return(NOTARANK);
1104             }
1105              
1106             =item my $band_name = $agaTourn-EB ($bandIndex)
1107              
1108             Returns the name of a band specified by the B or undef of not known.
1109              
1110             Scoring bands are specified via B with ##BAND directives.
1111              
1112             AGATourn complains if bands are specified with holes between them.
1113              
1114             The bands are sorted (by strength) and indexed. B returns the
1115             original name (as specified in the ##BAND directive) from a band index.
1116              
1117             =cut
1118              
1119             sub BandName {
1120 4     4 1 10 my ($self, $idx) = @_;
1121              
1122 4         7 my ($band, $top, $bot);
1123 4         7 foreach $band (@{$self->{Directive}{'BAND'}}) {
  4         12  
1124 6         16 ($top, $bot) = split(/\s+/, $band);
1125 6         17 $top = int($self->RankToRating($top));
1126 6 100       24 return undef unless defined($self->{bandTop}[$idx]);
1127 5 100       17 if ($top == int($self->{bandTop}[$idx])) {
1128 3         16 return($band);
1129             }
1130             }
1131 0         0 return(undef);
1132             }
1133              
1134             =item my ($handicap, $komi) = $agaTourn-EB ($player1, $player2)
1135              
1136             Returns the appropriate handicap and komi for two players. Players can be in
1137             any form acceptable to B.
1138              
1139             If player1 is stronger than player two, the handicap is a
1140             positive number. If player1 is weaker than player2, (players need to be
1141             swapped), the returned handicap is a negative number. If the handicap would
1142             normally be 0 and the players need to be swapped, the returned handicap is -1.
1143              
1144             A handicap of 1 is never returned. The returned handicap and komi are always
1145             integers (you may assume that komi needs a additional half-point if you like).
1146              
1147             If either player1 or player2 is invalid, B complains (during the
1148             call to B for the player) and returns (-1, -1).
1149              
1150             B uses the following table (same as the AGA handicap practice):
1151              
1152             rating handi Ing AGA
1153             diff Komi Komi
1154             0.000-0.650 0 7 6 even, normal komi
1155             0.651-1.250 0 -1* 0 no komi (* black wins ties under Ing)
1156             1.251-2.200 0 -7 -6 reverse komi
1157             2.201-3.300 2 -2 0 two stones
1158             3.301-4.400 3 -3 0 three stones ...
1159              
1160             =cut
1161              
1162             sub Handicap {
1163 0     0 1 0 my ($self, $p1, $p2) = @_;
1164              
1165 0         0 $p1 = $self->CollapseRating($self->Rating($p1));
1166 0         0 $p2 = $self->CollapseRating($self->Rating($p2));
1167 0 0 0     0 return (-1, -1) unless(defined($p1) and defined($p2));
1168 0         0 my $diff = $p1 - $p2;
1169 0         0 my $ing = $self->{Directive}{RULES}[0] eq 'ING';
1170 0         0 my $swap = 1;
1171 0         0 my ($handi, $komi) = (0, 0);
1172 0 0       0 if ($diff < 0) {
1173 0         0 $swap = $handi = -1;
1174 0         0 $diff = -$diff;
1175             }
1176 0 0       0 if ($diff <= .650) {
    0          
    0          
1177 0 0       0 $komi = $ing ? 7 : 6; # normal komi game
1178             } elsif ($diff <= 1.25) {
1179 0 0       0 $komi = $ing ? -1 : 0; # no komi game
1180             } elsif ($diff <= 2.2) {
1181 0 0       0 $komi = $ing ? -7 : -6; # reverse komi game
1182             } else {
1183 0         0 $handi = $swap * int($diff / 1.1);
1184 0         0 $komi = 0;
1185             }
1186 0         0 return (int($handi), int($komi));
1187             }
1188              
1189             =item my $collapsed_rating = $agaTourn-EB ($aga_rating)
1190              
1191             AGA ratings have a hole between 1.0 and -1.0. This method fills the hole by
1192             adding 1 to kyu ratings and subtracting 1 from dan ratings. If $aga_rating is
1193             between 1.0 and -1.0, complains and returns the original $rating.
1194              
1195             =cut
1196              
1197             sub CollapseRating {
1198 0     0 1 0 my ($self, $rating) = @_;
1199              
1200 0 0       0 if ($rating >= 1) {
    0          
1201 0         0 $rating -= 1; # pull dan ratings down to 0
1202             } elsif ($rating <= -1) {
1203 0         0 $rating += 1; # pull kyu ratings up to 0
1204             } else {
1205 0         0 carp "CollapseRating called on a rating between -1 and +1: $rating\n";
1206             }
1207 0         0 return $rating;
1208             }
1209              
1210             =item my $AGA_rating = $agaTourn-EB ($collapsed_rating)
1211              
1212             AGA ratings have a hole between 1.0 and -1.0. This method converts a
1213             continuous rating with no hole into a valid AGA rating by adding 1 to ratings
1214             greater than 0 and subtracting 1 from ratings less than 0.
1215              
1216             =cut
1217              
1218             sub ExpandRating {
1219 0     0 1 0 my ($self, $rating) = @_;
1220              
1221 0 0       0 if ($rating >= 0) {
1222 0         0 $rating += 1; # dan ratings are upwards from 1
1223             } else {
1224 0         0 $rating -= 1; # kyu ratings are downwards from -1
1225             }
1226 0         0 return $rating;
1227             }
1228              
1229             =item my $normalized_id = $agaTourn-EB ($id)
1230              
1231             Performs normalization of $id so the we can compare variations of $id without
1232             considering them as different. Normalization consists of turning the country
1233             part of $id to all upper-case and removing leading zeros from the number part.
1234              
1235             All $ids used as hash keys should be normalized.
1236              
1237             =cut
1238              
1239             sub NormalizeID {
1240 9     9 1 23 my ($self, $id) = @_;
1241              
1242 9         14 $id = uc ($id); # make all letters upper case
1243 9         56 $id =~ s/^([A-Z]*)0*([1-9].*)/$1$2/; # remove leading zeros from number part
1244 9         25 return($id);
1245             }
1246              
1247             ######################################################
1248             #
1249             # Private methods
1250             #
1251             #####################################################
1252              
1253             sub _setBands {
1254 2     2   4 my ($self) = @_;
1255              
1256 2 100       12 unless(exists($self->{Directive}{'BAND'})) {
1257             # carp("Note: no bands selected, assuming one band.\n");
1258 1         2 unshift(@{$self->{Directive}{'BAND'}}, '99D 99K');
  1         7  
1259             }
1260 2         9 $self->{bandTop} = []; # ref to empty array (to prevent infinite recursion)
1261 2         4 my ($band, $ovBand, $top, $bot);
1262 2         6 foreach $band (@{$self->{Directive}{'BAND'}}) {
  2         7  
1263 6         20 ($top, $bot) = split(/\s+/, $band);
1264 6         18 $top = int($self->RankToRating($top));
1265 6 100       20 $top += 0.99999 if ($top > 0);
1266 6         17 $bot = int($self->RankToRating($bot));
1267 6 100       31 $bot -= 0.99999 if ($bot < 0);
1268 6 50 33     51 if (($top > 9999) || ($bot < -9999) || ($bot >= $top)) {
      33        
1269 0         0 carp("Error: can't parse BAND directive at line $. in $self->{fileName}: $band\n");
1270 0         0 $self->{error} = 1;
1271             return
1272 0         0 }
1273 6         20 $ovBand = $self->WhichBandIs($top); # check for overlapped bands
1274 6 50       83 $ovBand = $self->WhichBandIs($bot) unless ($ovBand eq NOTARANK);
1275 6 50       32 unless ($ovBand eq NOTARANK) {
1276 0         0 carp("Warning: band conflict: $band\n (overlaps $self->{Directive}{'BAND'}[$ovBand])\n");
1277             }
1278 6         6 push(@{$self->{bandTop}}, $top);
  6         16  
1279 6         7 push(@{$self->{bandBot}}, $bot);
  6         18  
1280             }
1281 2         5 my (@tops) = sort({ $b <=> $a; } @{$self->{bandTop}}); # now check for holes
  8         15  
  2         12  
1282 2         4 my (@bots) = sort({ $b <=> $a; } @{$self->{bandBot}});
  8         12  
  2         5  
1283 2         4 my $ii;
1284 2         16 for ($ii = 0; $ii < @tops - 1; $ii++) {
1285 4 100 66     21 next if (($bots[$ii] == 1) && ($tops[$ii + 1] == -1)); # 1d to 1k is a legitimate hole
1286 3 50       22 if ($bots[$ii] - $tops[$ii + 1] > 0.001) {
1287 0         0 carp( "Warning: hole between bands\n");
1288             }
1289             }
1290 2         7 $self->{bandTop} = \@tops; # use sorted bands
1291 2         7 $self->{bandBot} = \@bots;
1292             }
1293              
1294             1;
1295              
1296             __END__