File Coverage

blib/lib/Chess/ELO/FIDE.pm
Criterion Covered Total %
statement 42 149 28.1
branch 0 48 0.0
condition 0 37 0.0
subroutine 14 22 63.6
pod 1 3 33.3
total 57 259 22.0


line stmt bran cond sub pod time code
1             package Chess::ELO::FIDE;
2              
3 1     1   412390 use strict;
  1         2  
  1         43  
4 1     1   5 use warnings;
  1         2  
  1         69  
5              
6 1     1   880 use Moo;
  1         10312  
  1         7  
7 1     1   3273 use DateTime;
  1         487376  
  1         68  
8 1     1   823 use DateTime::Format::Mail;
  1         8469  
  1         66  
9 1     1   644 use DateTime::Format::Strptime;
  1         47514  
  1         6  
10 1     1   1941 use DBI;
  1         16376  
  1         75  
11 1     1   958 use DBD::SQLite;
  1         8885  
  1         42  
12 1     1   414 use File::Spec::Functions;
  1         655  
  1         83  
13 1     1   741 use File::Temp qw/tempdir/;
  1         14177  
  1         66  
14 1     1   737 use HTTP::Tiny;
  1         41382  
  1         68  
15 1     1   1071 use IO::Uncompress::Unzip qw/unzip $UnzipError/;
  1         73098  
  1         200  
16 1     1   1130 use Log::Handler;
  1         34922  
  1         6  
17 1     1   1371 use Types::Standard -types;
  1         164834  
  1         16  
18              
19             #------------------------------------------------------------------------------
20              
21             our $VERSION = "0.01";
22              
23             #------------------------------------------------------------------------------
24              
25             has 'sqlite' => ( is => 'ro', isa=> Str, required=> 1 );
26             has 'federation' => ( is => 'ro', isa=> Str, default=> sub { '' } );
27             has 'fide_url' => ( is => 'ro', isa=> Str, default=> sub { 'https://ratings.fide.com/download/players_list.zip' } );
28             has 'log' => ( is => 'rwp' );
29             has '_dbh' => ( is => 'rwp', isa=> InstanceOf['DBI::db']);
30             has '_temp_dir' => (is => 'rwp', isa=> Str, default => '/tmp');
31              
32             #------------------------------------------------------------------------------
33              
34             sub BUILD {
35 0     0 0   my $self = shift;
36 0           my $dsn = "dbi:SQLite:dbname=" . $self->sqlite;
37 0           my $dbh = DBI->connect($dsn, "", "", { RaiseError=>1, AutoCommit=>1 });
38 0           $self->_set__dbh($dbh);
39 0           $self->_set__temp_dir( tempdir(CLEANUP=> 1) );
40 0           $self->_init_sqlite_;
41             }
42              
43             #------------------------------------------------------------------------------
44              
45             sub DEMOLISH {
46 0     0 0   my $self = shift;
47 0           $self->_dbh->disconnect;
48             }
49              
50             #------------------------------------------------------------------------------
51              
52             sub _trim_ {
53 0     0     my $s = shift;
54 0           $s =~ s/\s+$//;
55 0           $s =~ s/^\s+//;
56 0           return $s;
57             }
58             #------------------------------------------------------------------------------
59              
60             sub _debug_ {
61 0     0     my $self = shift;
62 0           my $log = $self->log;
63 0 0         $log->debug(shift) if $log;
64             }
65              
66             #------------------------------------------------------------------------------
67              
68             sub _db_property_ {
69 0     0     my $self = shift;
70 0           my $name = shift;
71              
72 0 0         if( @_ ) {
73 0           my $value = shift;
74 0           my $stmt = $self->_dbh->prepare("UPDATE properties SET value=? WHERE name=?");
75 0           $stmt->execute($value, $name);
76             } else {
77 0           my ($value) = $self->_dbh->selectrow_array("SELECT value FROM properties WHERE name=?", undef, $name);
78 0           return $value;
79             }
80             }
81              
82             #------------------------------------------------------------------------------
83              
84             sub _init_sqlite_ {
85 0     0     my $self = shift;
86 0           my $dbh = $self->_dbh;
87            
88 0           my $sql_create_fide = <<~'SQL_FIDE';
89             CREATE TABLE IF NOT EXISTS fide (
90             fide_id INTEGER PRIMARY KEY,
91             fed TEXT,
92             gender TEXT,
93             title TEXT,
94             year INTEGER,
95             s_rating INTEGER,
96             r_rating INTEGER,
97             b_rating INTEGER,
98             s_k INTEGER,
99             r_k INTEGER,
100             b_k INTEGER,
101             s_games INTEGER,
102             r_games INTEGER,
103             b_games INTEGER,
104             surname TEXT,
105             name TEXT,
106             flag TEXT
107             );
108             SQL_FIDE
109              
110 0           my $sql_create_properties = <<~'SQL_PROPERTIES';
111             CREATE TABLE IF NOT EXISTS properties (
112             name TEXT PRIMARY KEY,
113             value TEXT
114             );
115             SQL_PROPERTIES
116              
117 0           $dbh->do($sql_create_fide);
118 0           $dbh->do($sql_create_properties);
119              
120 0           my ($last_date) = $dbh->selectrow_array("SELECT count(*) FROM properties WHERE name='last-modified'");
121 0 0         if( ! $last_date ) {
122 0           $dbh->do("INSERT INTO properties (name, value) VALUES ('last-modified', '1970-01-01T00:00:00Z')");
123             }
124             }
125              
126              
127             #------------------------------------------------------------------------------
128              
129             sub _load_combined_file_ {
130 0     0     my $self = shift;
131 0           my $players_file = shift;
132              
133 0           my $dbh = $self->_dbh;
134 0           my $sql_insert = <<~'SQL_INSERT';
135             INSERT INTO fide (
136             fide_id,
137             fed,
138             gender,
139             title,
140             year,
141             s_rating, r_rating, b_rating,
142             s_k, r_k, b_k,
143             s_games, r_games, b_games,
144             surname, name,
145             flag
146             )
147             VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
148             SQL_INSERT
149              
150 0           my $stmt = $dbh->prepare($sql_insert);
151 0           $dbh->begin_work;
152              
153 0           $dbh->do("DELETE FROM fide");
154              
155 0 0         open my $fh , '<', $players_file or die "Can't open file [$players_file]: $!";
156              
157 0           my $header = <$fh>;
158 0           my $expected = 'ID Number Name Fed Sex Tit WTit OTit FOA SRtng SGm SK RRtng RGm Rk BRtng BGm BK B-day Flag';
159              
160             #ID Number Name Fed Sex Tit WTit OTit FOA SRtng SGm SK RRtng RGm Rk BRtng BGm BK B-day Flag
161 0 0         if( substr($header, 0, length($expected)) ne $expected) {
162 0           $self->log->error("FIDE file format is not valid");
163 0           return 0;
164             }
165              
166 0           my $count = 0;
167 0           while (my $line = <$fh>) {
168 0           my $fed = substr($line, 76, 3);
169 0 0 0       next if ($self->federation && ($fed ne $self->federation));
170 0 0         next if length($line) < 156;
171 0           my $fide_id = _trim_ substr($line, 0, 15);
172 0           my $s_rating = _trim_ substr($line, 113, 4);
173 0           my $r_rating = _trim_ substr($line, 126, 4);
174 0           my $b_rating = _trim_ substr($line, 139, 4);
175 0           my $year = _trim_ substr($line, 152, 4);
176 0           my $gender = _trim_ substr($line, 80, 1);
177 0           my $title = _trim_ substr($line, 84, 5);
178 0           my $full_name = _trim_ substr($line, 15, 60);
179              
180 0           my ($surname, $name) = split(/ *, */, $full_name);
181              
182 0 0 0       $s_rating = undef if defined($s_rating) && ($s_rating eq '');
183 0 0 0       $r_rating = undef if defined($r_rating) && ($r_rating eq '');
184 0 0 0       $b_rating = undef if defined($b_rating) && ($b_rating eq '');
185            
186 0           my $s_K = _trim_ substr($line, 123, 2);
187 0           my $r_K = _trim_ substr($line, 136, 2);
188 0           my $b_K = _trim_ substr($line, 149, 2);
189 0 0 0       $s_K = undef if defined($s_K) && ($s_K eq '');
190 0 0 0       $r_K = undef if defined($r_K) && ($r_K eq '');
191 0 0 0       $b_K = undef if defined($b_K) && ($b_K eq '');
192              
193 0           my $s_games = _trim_ substr($line, 119, 3);
194 0           my $r_games = _trim_ substr($line, 132, 3);
195 0           my $b_games = _trim_ substr($line, 145, 3);
196 0 0 0       $s_games = undef if defined($s_games) && ($s_games eq '');
197 0 0 0       $r_games = undef if defined($r_games) && ($r_games eq '');
198 0 0 0       $b_games = undef if defined($b_games) && ($b_games eq '');
199              
200 0           my $flag = substr($line, 158, 4);
201 0 0 0       $flag = undef if defined($flag) && ($flag eq '');
202              
203 0   0       $stmt -> execute(
      0        
204             $fide_id,
205             $fed,
206             $gender,
207             $title,
208             $year,
209             $s_rating, $r_rating, $b_rating,
210             $s_K, $r_K, $b_K,
211             $s_games, $r_games, $b_games,
212             uc($surname // ''), uc($name // ''),
213             $flag
214             );
215 0           $count++;
216             }
217              
218 0           $dbh->commit;
219 0           close $fh;
220 0           return $count;
221             }
222              
223             #------------------------------------------------------------------------------
224              
225             sub load {
226 0     0 1   my $self = shift;
227              
228 0           my $count = 0;
229 0           my $strp = DateTime::Format::Strptime->new(pattern=> '%T');
230 0           my $last_download = $self->_db_property_('last-modified');
231 0           my $dt_last_modified = $strp->parse_datetime($last_download);
232              
233 0           my $http = HTTP::Tiny->new;
234 0           my $response = $http->head($self->fide_url);
235 0 0         return 0 unless $response->{success};
236            
237 0           my $dt_fide_last_modified = DateTime::Format::Mail->parse_datetime($response->{'headers'}->{'last-modified'});
238              
239 0 0         if ( DateTime->compare( $dt_last_modified, $dt_fide_last_modified ) == 0 ) {
240 0           $self->_debug_("No new ratings available from $last_download");
241 0           return 0;
242             }
243              
244 0           my $zip_file = catfile($self->_temp_dir, "fide_players_list.zip");
245 0           my $players_file = catfile($self->_temp_dir, "players_list_foa.txt");
246            
247 0 0         unlink $zip_file if -e $zip_file;
248 0 0         unlink $players_file if -e $players_file;
249            
250 0           $self->_debug_("download: " . $self->fide_url . " => $zip_file");
251 0           $response = $http->mirror($self->fide_url, $zip_file);
252 0 0         if( $response->{success} ) {
253 0           $self->_debug_("unzip: $zip_file [$dt_fide_last_modified]");
254            
255 0           unzip($zip_file => $players_file);
256 0 0         if (-e $players_file) {
257 0           $self->_debug_("OK: $players_file [" . $dt_fide_last_modified->iso8601 ."]");
258 0           $count = $self->_load_combined_file_($players_file);
259 0 0         $self->_db_property_('last-modified', $strp->format_datetime($dt_fide_last_modified)) if $count;
260             }
261             }
262 0           return $count;
263             }
264              
265             #------------------------------------------------------------------------------
266              
267             1;
268              
269             __END__
270              
271             =encoding utf-8
272              
273             =head1 NAME
274              
275             Chess::ELO::FIDE - Download and store FIDE ratings
276              
277             =head1 SYNOPSIS
278              
279             use Chess::ELO::FIDE;
280             my $ratings = Chess::ELO::FIDE->new(
281             federation=> 'ESP',
282             sqlite => 'elo.sqlite'
283             );
284             my $count = $ratings->load;
285             print "Loaded $count players\n";
286              
287             =head1 DESCRIPTION
288              
289             Chess::ELO::FIDE is a module to download and store FIDE ratings in a SQLite database.
290             It is intended to be used as a backend for chess applications.
291             There are 3 main phases:
292              
293             =over 4
294              
295             =item 1. Download the FIDE ratings file from the L<FIDE website|https://ratings.fide.com/download/players_list.zip>
296              
297             =item 2. Unzip the file and load the ratings into a SQLite database
298              
299             =item 3. Store the last download date to avoid downloading the same file again
300              
301             =back
302              
303             =head1 LICENSE
304              
305             Copyright (C) Miguel PRZ - NICEPERL
306              
307             This library is free software; you can redistribute it and/or modify
308             it under the same terms as Perl itself.
309              
310             =head1 AUTHOR
311              
312             NICEPERL L<https://metacpan.org/author/NICEPERL>
313              
314             =cut
315