File Coverage

blib/lib/Lingua/FR/Ladl/Table.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package Lingua::FR::Ladl::Table;
2              
3 1     1   5959 use warnings;
  1         3  
  1         42  
4 1     1   6 use strict;
  1         2  
  1         40  
5 1     1   1144 use English;
  1         6016  
  1         8  
6              
7 1     1   933 use version; our $VERSION = qv('0.0.1');
  1         2  
  1         8  
8              
9 1     1   102 use Carp;
  1         3  
  1         73  
10              
11 1     1   480 use Readonly;
  0            
  0            
12             use List::Util qw(max first);
13             use List::MoreUtils qw(all);
14              
15              
16             use Lingua::FR::Ladl::Exceptions;
17             use Lingua::FR::Ladl::Parametrizer;
18             use Lingua::FR::Ladl::Util;
19              
20             use Class::Std;
21              
22             {
23             Readonly my %is_implemented_format => ( xls => \&_load_xls,
24             xml => \&_load_gnumeric_xml );
25              
26             my %name_of : ATTR( :default('none') :name);
27             my %table_of : ATTR; # hash of hashes to hold the table data in a perl data structure
28             my %maxCol_of : ATTR( :default(0) :name);
29             my %maxRow_of : ATTR( :default(0) :name);
30             my %verbCol_of : ATTR;
31             my %verbs_of : ATTR;
32             my %colTypes_of : ATTR;
33             my %is_tildaRow_of : ATTR;
34             my %headers_of : ATTR;
35             my %parameters_of : ATTR( :set :get ); # customization parameters
36              
37             ############# Utility subroutines #################################################################
38            
39             sub _find_out_verbCol {
40             my ($id) = @_;
41              
42             my $name = $name_of{$id};
43            
44             my $verbCol = first { $table_of{$id}->{0}->{$_} =~ m{\b$name\b} } (keys %{$table_of{$id}->{0}});
45              
46             if ($verbCol) {
47             $verbCol_of{$id} = $verbCol;
48             return $verbCol_of{$id};
49             } else {
50             carp "Couldn't find out verb column: no column with column header $name\n";
51             return;
52             }
53            
54             }
55              
56             sub _check_row {
57             my ($table, $row, $message) = @_;
58              
59             croak "$row must be greater 0\n" unless $row;
60              
61             my $id = ident $table;
62             X::NoTableData->throw(
63             message => $message,
64             table => $table,
65             ) unless $table_of{$id};
66              
67             croak "$row must be less or equal $maxRow_of{$id}\n" if $row > $maxRow_of{$id};
68              
69             return;
70              
71             }
72              
73             sub _check_col {
74             my ($table, $col, $message) = @_;
75              
76             croak "$col must be greater 0\n" unless $col;
77              
78             my $id = ident $table;
79             X::NoTableData->throw(
80             message => $message,
81             table => $table,
82             ) unless $table_of{$id};
83              
84             croak "$col must be less or equal $maxCol_of{$id}\n" if $col > $maxCol_of{$id};
85              
86             return;
87              
88             }
89              
90             sub _load_xls {
91             my ($file_name) = @_;
92              
93             my $table_ref = {};
94            
95             eval { use Spreadsheet::ParseExcel; };
96             if ($EVAL_ERROR) {
97             croak "Can't load table data from excel file $file_name: Spreadsheed::ParseExcel not installed";
98             }
99            
100             my $excel = Spreadsheet::ParseExcel::Workbook->Parse($file_name) or
101             croak "Error parsing $file_name: Spreadsheet::ParseExcel returned undef\n";
102             my $sheet = $excel->{Worksheet}->[0];
103             $sheet->{MaxRow} ||= $sheet->{MinRow};
104             unless ($sheet->{MaxRow}) {
105             croak "Couldn't load table data: Spreadsheet::ParseExcel returned 0 max row when parsing $file_name\n";
106             }
107             foreach my $row ($sheet->{MinRow} .. $sheet->{MaxRow}) {
108             $sheet->{MaxCol} ||= $sheet->{MinCol};
109             foreach my $col ($sheet->{MinCol} .. $sheet->{MaxCol}) {
110             my $cell = $sheet->{Cells}[$row][$col];
111             if ($cell) {
112             $table_ref->{$row}->{$col} = $cell->{Val};
113             }
114             }
115             }
116             return ($table_ref, $sheet->{MaxRow}, $sheet->{MaxCol});
117             }
118              
119              
120             sub _load_gnumeric_xml {
121             my ($file_name) = @_;
122              
123             my $table_ref = {};
124              
125             eval { use XML::LibXML; };
126             if ($EVAL_ERROR) {
127             croak "Can't load table data from xml file $file_name: XML::LibXML not installed\n";
128             }
129              
130            
131             my $parser = XML::LibXML->new();
132             $parser->keep_blanks(0);
133             my $table_doc = $parser->parse_file($file_name);
134             my @cells = $table_doc->findnodes("//gmr:Cell");
135             foreach my $cell (@cells) {
136             my $row = $cell->getAttribute('Row');
137             my $col = $cell->getAttribute('Col');
138             $table_ref->{$row}->{$col}=$cell->textContent();
139             }
140             my @maxRowCells = $table_doc->findnodes("//gmr:MaxRow");
141             my $maxRow = $maxRowCells[0]->textContent();
142              
143             my @column_nbrs = map { $_->getValue() } $table_doc->findnodes('//gmr:Cell/@Col');
144            
145             my $maxCol = max @column_nbrs;
146              
147             return ($table_ref, $maxRow, $maxCol);
148            
149             }
150              
151             ############ Methods ###################################################################
152              
153             sub BUILD {
154             my ($self, $id, $arg_ref) = @_;
155              
156             # parametrize with new default parametrizer
157             my $param = Lingua::FR::Ladl::Parametrizer->new();
158             $parameters_of{$id} = $param;
159             }
160            
161             sub load {
162             my ($self, $arg_ref) = @_;
163             my $format = $arg_ref->{format};
164             my $file_name = $arg_ref->{file};
165             my $id = ident $self;
166              
167             unless ($is_implemented_format{$format}) {
168             croak 'Format must be one of '.join(', ', keys %is_implemented_format).", not $format\n";
169             }
170              
171             =for Rationale:
172             We set a default table name, inferred from the file name.
173             The table name is needed for inferring the verb column
174             which is the header of the verb column.
175             However the table name can be also set manually.
176             The get_verb_column() method won't work if the table name is
177             not set correctly.
178              
179             =cut
180              
181             $self->set_name(_Name::from_file_name($file_name));
182              
183             ($table_of{$id}, $maxRow_of{$id}, $maxCol_of{$id}) = $is_implemented_format{$format}->($file_name);
184            
185             return;
186             }
187              
188              
189             =for Rationale
190             The header line is line number 0 in the table
191             This sub returns a hash with the headers as keys and the columns as values.
192              
193             =cut
194            
195             sub get_headers {
196             my ($self) = @_;
197             my $id = ident $self;
198              
199             if ($headers_of{$id}) {
200             return $headers_of{$id};
201             }
202            
203             X::NoTableData->throw(
204             message => "Could not get table headers:\n",
205             table => $self)
206             unless $table_of{$id};
207              
208             foreach my $col_num (0..$maxCol_of{$id}) {
209             if (exists $table_of{$id}->{0}->{$col_num}) {
210             my $header = $table_of{$id}->{0}->{$col_num};
211             $headers_of{$id}->{$header} = $col_num;
212             } else {
213             $headers_of{$col_num} = $col_num;
214             }
215             }
216            
217             return $headers_of{$id};
218             }
219              
220             sub get_value_at {
221             my ($self, $row, $col) = @_;
222             my $id = ident($self);
223              
224             _check_row($self, $row, "Can't get value at $row, $col:");
225             _check_col($self, $col, "Can't get value at $row, $col:");
226              
227             return $table_of{$id}->{$row}->{$col};
228             }
229              
230             sub get_col_for_header {
231             my ($self, $header) = @_;
232             my $id = ident($self);
233              
234             X::NoTableData->throw(
235             message => "Couldn't find out col for header:\n",
236             table => $self )
237             unless $table_of{$id};
238              
239             my $col_for_headers = $self->get_headers();
240             if (exists($col_for_headers->{$header})) {
241             return $col_for_headers->{$header};
242             } else {
243             return;
244             }
245              
246             }
247              
248             sub get_header_for_col {
249             my ($self, $col) = @_;
250             my $id = ident($self);
251              
252             _check_col($self, $col, "Can't get header for $col:");
253              
254             my $header = $table_of{$id}->{0}->{$col};
255             my $empty_string_mark = $self->get_parameters()->get_empty_string_mark();
256             $header =~ s{$empty_string_mark}{};
257             return $header;
258             }
259              
260              
261             =for Rationale:
262             we determine the verb column as the header column with the table name
263             this is an empirical observation, it may fail!
264              
265             =cut
266              
267             sub get_verb_column {
268             my ($self) = @_;
269             my $id = ident $self;
270            
271             if ($verbCol_of{$id}) {
272             return $verbCol_of{$id};
273             } else {
274             X::NoTableData->throw(
275             message => "Couldn't find out verb column:\n",
276             table => $self )
277             unless $table_of{$id};
278             return _find_out_verbCol($id);
279             }
280             }
281              
282              
283              
284             sub get_verbs {
285             my ($self) = @_;
286             my $id = ident $self;
287              
288             if ($verbs_of{$id}) {
289             return @{$verbs_of{$id}};
290             } else {
291             croak "Need table to calculate verbs\n" unless $table_of{$id};
292             my $verbCol = $verbCol_of{$id} or $self->get_verb_column();
293             my $maxRow = $maxRow_of{$id} or $self->get_maxRow();
294             croak "Max row must be greater 0" unless $maxRow;
295             $verbs_of{$id} = [ map { $table_of{$id}->{$_}->{$verbCol} } 1 .. $maxRow ];
296             return @{$verbs_of{$id}};
297             }
298             }
299              
300              
301             =for Rationale:
302             the particle column is the column before the verb column
303              
304             =cut
305              
306             sub get_particle_column {
307             my ($self) = @_;
308             my $id = ident $self;
309              
310             my $verbCol = $verbCol_of{$id} or $self->get_verb_column();
311              
312             return $verbCol - 1;
313            
314             }
315              
316             =for Rationale:
317             the example column is the last column
318              
319             =cut
320              
321             sub get_example_column {
322             my ($self) = @_;
323             my $id = ident $self;
324              
325             if ($maxCol_of{$id}) {
326             return $maxCol_of{$id};
327             } else {
328             X::NoTableData->throw(
329             message => "Max column is 0!\n",
330             table => $self,
331             );
332             return;
333             }
334             }
335              
336             =for Rationale:
337             We try to determine the data type of a specific column
338             first we read a sample row (other than 0 which is the header row)
339             if the content of a given column is other than '+' '-' '~' we
340             presume that this column is supposed to contain text data,
341             else the possible values are '+', '-', '~'
342             Returns a hash reference with the column index as keys and either the string `+-~' or the string `text' as values.
343              
344             =cut
345              
346             sub get_column_types {
347             my ($self) = @_;
348             my $id = ident $self;
349            
350             if ($colTypes_of{$id}) {
351             return $colTypes_of{$id};
352             } else {
353            
354             X::NoTableData->throw(
355             message => "Couldn't calculate column types:\n",
356             table => $self,
357             )
358             unless $table_of{$id};
359            
360             while ( my ($col_num, $col_val) = each %{$table_of{$id}->{1}} ) {
361             if ( $col_val =~ m{\A \s* [+-~] \s* \z}xms ) {
362             $colTypes_of{$id}->{$col_num} = '+-~';
363             } else {
364             $colTypes_of{$id}->{$col_num} = 'text';
365             }
366             }
367             return $colTypes_of{$id};
368             }
369             }
370              
371             sub get_column_type_for_col {
372             my ($self, $col) = @_;
373             my $id = ident $self;
374              
375             _check_col($self, $col, "Can't get column type for column $col:");
376              
377             my $colTypes = $colTypes_of{$id} ? $colTypes_of{$id} : $self->get_column_types();
378              
379             return $colTypes->{$col};
380             }
381              
382            
383              
384             =for Rationale:
385             A row of the table is a tilda row when all the columns of `+-~'
386             type contain a `~'. This means, the table doesn't say anything about this verb (usage).
387             The row is identified by number.
388              
389             =cut
390              
391             sub is_tilda_row {
392             my ($self, $row) = @_;
393             my $id = ident $self;
394              
395             if ($is_tildaRow_of{$id}) {
396             return $is_tildaRow_of{$id};
397             }
398            
399             my $err_msg = "Can't check if tilda row: \n";
400             _check_row($self, $row, $err_msg);
401              
402             my $col_types = $self->get_column_types();
403              
404             $is_tildaRow_of{$id} = all { $table_of{$id}->{$row}->{$_} eq '~' } grep { $col_types->{$_} eq '+-~' } keys %{$col_types};
405            
406             return $is_tildaRow_of{$id};
407             }
408              
409             sub get_verb_for_row {
410              
411             my ($self, $row) = @_;
412             my $id = ident $self;
413              
414             my $err_msg = "Can't get verb of row:\n";
415             _check_row($self, $row, $err_msg);
416              
417             return $table_of{$id}->{$row}->{$verbCol_of{$id}};
418             }
419              
420             sub get_rows_for_verb {
421             my ($self, $verb) = @_;
422             my $id = ident $self;
423              
424             croak "No verb\n" unless $verb;
425              
426             X::NoTableData->throw(
427             message => "Couldn't get rows of verb: $verb:\n",
428             table => $self,
429             ) unless $table_of{$id};
430              
431             my $verbCol = $verbCol_of{$id};
432             my @rows = grep { $table_of{$id}->{$_}->{$verbCol} =~ m{\b$verb\b} } 1 .. $maxRow_of{$id};
433              
434             return (@rows);
435             }
436              
437              
438             =for Rationale:
439             A column may be of type `text' or `+-~'.
440             - A `text' column of a row is set if it's different from the `Empty mark', which in this case is .
441             - A `+-~' column of a row is set if it's `+'.
442             The `empty string mark' can be set via the Parametrizer Object
443              
444             =cut
445              
446            
447             sub is_column_set {
448             my ($self, $row, $col) = @_;
449              
450             _check_row($self, $row, "Can't test if column is set:\n");
451             my $col_types = $self->get_column_types();
452             my $empty_mark = $self->get_parameters()->get_empty_string_mark();
453             my $re = qr($empty_mark);
454            
455             if ($col_types->{$col} eq 'text') {
456             $table_of{ident $self}->{$row}->{$col} =~ m{$re} ? return 0 : return 1;
457             } else {
458             $table_of{ident $self}->{$row}->{$col} =~ m{[+]} ? return 1 : return 0;
459             }
460             }
461              
462             sub has_verb {
463             my ($self, $verb) = @_;
464             my $id = ident($self);
465            
466             croak "Can't check on empty verb\n" unless $verb;
467              
468             my $verbCol = $self->get_verb_column();
469              
470             return grep { m{\b$verb\b} } map { $table_of{$id}->{$_}->{$verbCol} } 1 .. $maxCol_of{$id};
471             }
472              
473             sub has_verb_matching {
474             my ($self, $match) = @_; # match is supposed to be a pattern: either a string or a qr//
475             my $id = ident($self);
476              
477             croak "Won't check on empty match\n" unless $match;
478              
479             my $verbCol = $self->get_verb_column();
480              
481             return grep { m{$match} } map { $table_of{$id}->{$_}->{$verbCol} } 1 .. $maxCol_of{$id};
482             }
483              
484             sub create_db_table {
485             my ($self, $arg_ref) = @_;
486             my $id = ident($self);
487              
488             X::NoTableData->throw(
489             message => "Can't create DB handle:\n",
490             table => $self,
491             ) unless $table_of{$id};
492              
493             use DBI;
494             my $dbh=DBI->connect('dbi:AnyData(RaiseError=>1):');
495            
496             my @column_names;
497             if (not ($arg_ref) or $arg_ref->{col_names} eq 'col_numbers') {
498             @column_names = map { "col_$_" } 0 .. $maxCol_of{$id};
499             } else {
500             @column_names =
501             map {
502             my $header = $table_of{$id}->{0}->{$_};
503             if (not $header) {
504             $header = "col_$_";
505             }
506             $header =~ m{\A \p{Alphabetic} }x ? $header : "col_$header";
507             } 0 .. $maxCol_of{$id};
508             my $verbCol = $verbCol_of{$id} or $self->get_verb_column();
509             $column_names[$verbCol] = 'verb';
510             $column_names[$verbCol-1] = 'particle';
511             $column_names[$maxCol_of{$id}] = 'example';
512             }
513              
514             my $empty_string_mark = $parameters_of{$id}->get_empty_string_mark();
515             my $db_array_ref = [
516             [@column_names],
517             map {
518             my $row = $_;
519             [
520             map {
521             my $col_value = $table_of{$id}->{$row}->{$_};
522             $col_value =~ s{$empty_string_mark}{};
523             "$col_value";
524             } 0 .. $maxCol_of{$id}
525             ];
526             } 1 .. $maxRow_of{$id}
527             ];
528            
529             $dbh->func( "table_$name_of{$id}",
530             'ARRAY',
531             $db_array_ref,
532             'ad_import');
533              
534             return $dbh;
535             }
536            
537             }
538              
539             1; # Magic true value required at end of module
540             __END__