File Coverage

blib/lib/Text/Table/Read/RelationOn/Tiny.pm
Criterion Covered Total %
statement 208 208 100.0
branch 144 148 97.3
condition 27 31 87.1
subroutine 21 21 100.0
pod 11 11 100.0
total 411 419 98.0


line stmt bran cond sub pod time code
1             package Text::Table::Read::RelationOn::Tiny;
2              
3 9     9   523441 use 5.010_001;
  9         98  
4 9     9   51 use strict;
  9         17  
  9         181  
5 9     9   58 use warnings;
  9         44  
  9         298  
6 9     9   3662 use autodie;
  9         120770  
  9         38  
7              
8 9     9   51999 use Carp;
  9         20  
  9         683  
9              
10             # The following must be on the same line to ensure that $VERSION is read
11             # correctly by PAUSE and installer tools. See docu of 'version'.
12 9     9   4203 use version 0.77; our $VERSION = version->declare("v2.2.4");
  9         14878  
  9         57  
13              
14              
15             sub new {
16 68     68 1 40482 my $class = shift;
17 68 50       186 $class = ref($class) if ref($class);
18 68 100       410 croak("Odd number of arguments") if @_ % 2;
19 67         175 my %args = @_;
20 67   100     299 my $inc = delete $args{inc} // "X";
21 67   100     220 my $noinc = delete $args{noinc} // "";
22 67         129 my $set = delete $args{set};
23 67         107 my $eqs = delete $args{eqs};
24 67         114 my $ext = delete $args{ext};
25 67         121 my $elem_ids = delete $args{elem_ids};
26 67 100       235 croak(join(", ", sort(keys(%args))) . ": unexpected argument") if %args;
27 66 100       271 croak("inc: must be a scalar") if ref($inc);
28 65 100       252 croak("noinc: must be a scalar") if ref($noinc);
29 64         420 s/^\s+// for ($inc, $noinc);
30 64         223 s/\s+$// for ($inc, $noinc);
31 64 100       232 croak("inc and noinc must be different") if $inc eq $noinc;
32 63 100 100     366 croak("'|' is not allowed for inc or noinc") if $inc eq '|' || $noinc eq '|';
33 61         168 my $self = {inc => $inc,
34             noinc => $noinc,
35             };
36 61 100       137 if (defined($set)) {
37 48         65 my %seen;
38 48 100       218 croak("set: must be an array reference") if ref($set) ne 'ARRAY';
39 47         65 my $cnt = 1;
40 47         90 foreach my $e (@$set) {
41 152 100       229 if (ref($e)) {
42 21 100       124 croak("set: entry $cnt: invalid") if ref($e) ne 'ARRAY';
43 20 100       162 croak("set: entry $cnt: array not allowed if eqs is specified") if $eqs;
44 19 100       24 croak("set: entry $cnt: array entry must not be empty") if !@{$e};
  19         198  
45 17         29 foreach my $sub_e (@$e) {
46 35 100 100     325 croak("set: entry $cnt: subarray contains invalid entry")
47             if ref($sub_e) || !defined($sub_e);
48 32 100       376 croak("set: '$sub_e': duplicate element") if exists($seen{$sub_e});
49 28         50 $seen{$sub_e} = undef;
50             }
51             } else {
52 131 100       361 croak("set: entry $cnt: invalid") if !defined($e);
53 130 100       317 croak("set: '$e': duplicate element") if exists($seen{$e});
54 129         201 $seen{$e} = undef;
55             }
56 139         191 ++$cnt;
57             }
58 34         94 $self->{prespec} = 1;
59             } else {
60 13 100       108 croak("eqs: not allowed without argument 'set'") if defined($eqs);
61 12         37 $self->{prespec} = "";
62             }
63 46 100       118 if (defined($elem_ids)) {
64 13 100 100     269 croak("elem_ids: not allowed without arguments 'set' and 'ext'") if !(defined($ext) &&
65             defined($set));
66 10 100       96 croak("elem_ids: must be a hash ref") if ref($elem_ids) ne 'HASH';
67             }
68 42         104 my $elems;
69             my $tabElems; # elems to be used in table --> indes in @elems
70 42         0 my $eqIds;
71 42 100       141 if ($ext) {
    100          
72 12 100       25 if ($set) {
73 11         22 foreach my $e (@$set) {
74 42 100       144 croak("set: no subarray allowed if 'ext' is specified") if ref($e);
75             }
76 10 100       23 if ($elem_ids) {
77 8 100       113 croak("elem_ids: wrong number of entries") if keys(%$elem_ids) != @$set;
78 7         16 foreach my $e (@$set) {
79 24         46 my $e_id = $elem_ids->{$e};
80 24 100       190 croak("elem_ids: '$e': missing value") if !defined($e_id);
81 22 100 100     339 croak("elem_ids: '$e': entry has wrong value") if ($e_id !~ /^\d$/ ||
      100        
82             !defined($set->[$e_id]) ||
83             $set->[$e_id] ne $e);
84             }
85             } else {
86 2         9 my $idx = 0;
87 2         6 $elem_ids = {map {$_ => $idx++} @$set};
  14         26  
88             }
89 4         8 $elems = $set;
90             } else {
91 1         74 croak("ext: not allowed without argument 'set'")
92             }
93 4         19 %$tabElems = %$elem_ids;
94             } elsif (ref($set)) {
95 21         48 my @elems; # elems
96             my %ids; # indices in basic elems
97 21         0 my @eqs_tmp;
98              
99 21         41 foreach my $entry (@$set) {
100 75 100       115 if (ref($entry)) {
101 5         10 push(@elems, $entry->[0]);
102 5         9 $ids{$entry->[0]} = $#elems;
103 5         11 for (my $j = 1; $j < @$entry; ++$j) {
104 4         6 my $ent_j = $entry->[$j];
105 4         7 push(@elems, $ent_j);
106 4         8 $ids{$ent_j} = $#elems;
107             }
108 5 100       14 push(@eqs_tmp, $entry) if @$entry > 1;
109             } else {
110 70         114 push(@elems, $entry);
111 70         138 $ids{$entry} = $#elems;
112             }
113             }
114 21 50 66     71 croak("Internal error") if (defined($eqs) && @eqs_tmp); # Should never happen.
115 21 100       42 $eqs = \@eqs_tmp if @eqs_tmp;
116 21         102 ($elems, $elem_ids, $tabElems, $eqIds) = (\@elems, \%ids, {%ids}, {});
117             }
118 34 100       126 if (defined($eqs)) {
119 14 100       123 croak("eqs: must be an array ref") if ref($eqs) ne 'ARRAY';
120 13         22 my %eqIds; # idx => array of equivalent idxes
121             my %seen;
122 13         20 foreach my $eqArray (@{$eqs}) {
  13         27  
123 28 100       138 croak("eqs: each entry must be an array ref") if ref($eqArray) ne 'ARRAY';
124 27 100       31 next if !@{$eqArray};
  27         51  
125 25         37 foreach my $entry (@{$eqArray}) {
  25         40  
126 57 100       181 croak("eqs: subentry contains a non-scalar") if ref($entry);
127 56 100       159 croak("eqs: subentry undefined") if !defined($entry);
128 55 100       254 croak("eqs: '$entry': unknown element") if !exists($elem_ids->{$entry});
129 53 100       251 croak("eqs: '$entry': duplicate element") if exists($seen{$entry});
130 51         84 $seen{$entry} = undef;
131             }
132 19 100       28 next if @{$eqArray} == 1;
  19         39  
133 14         21 my @tmp = @{$eqArray};
  14         32  
134 14         19 my @eqArray;
135 14         41 $eqIds{$tabElems->{shift(@tmp)}} = \@eqArray;
136 14         21 foreach my $e (@tmp) {
137 27         66 push(@eqArray, delete $tabElems->{$e});
138             }
139             }
140 6         15 $eqIds = \%eqIds;
141             }
142 26         66 @{$self}{qw(elems elem_ids tab_elems eq_ids)} = ($elems, $elem_ids, $tabElems, $eqIds);
  26         87  
143 26         106 return bless($self, $class);
144             }
145              
146              
147             #
148             # $self->$_reset() - set (matrix elems elem_ids tab_elems eq_ids) to
149             # empty structures
150             # $self->$_reset(1) - set (matrix elems elem_ids tab_elems eq_ids) to
151             # undef
152             my $_reset = sub {
153             @{$_[0]}{qw(matrix elems elem_ids tab_elems eq_ids)} =
154             $_[1] ? ( {}, [], {}, {}, {}) : ((undef) x 5);
155             };
156              
157              
158             # just a function, not a method.
159             sub _rule_pos_array_f {
160 17     17   34 my ($str) = @_;
161 17         20 my @rule_pos;
162 17         33 my $idx = index($str, '|');
163 17         40 while($idx != -1) {
164 84         117 push(@rule_pos, $idx);
165 84         146 $idx = index($str, '|', $idx + 1);
166             }
167 17         50 return \@rule_pos;
168             }
169              
170             # just a function, not a method.
171             sub _int_array_cmp {
172 9     9   22 my ($arr1, $arr2) = @_;
173 9 100       195 return !1 if @$arr1 != @$arr2;
174 7         22 for (my $i = 0; $i < @$arr1; ++$i) {
175 36 100       211 return !1 if $arr1->[$i] != $arr2->[$i];
176             }
177 6         16 return 1;
178             }
179              
180             # just a function, not a method.
181             sub _parse_header_f {
182 67     67   155 my ($header, $pedantic) = @_;
183 67         248 $header =~ s/\s+$//;
184 67         119 my @rule_pos;
185 67 100       151 if ($pedantic) {
186 9 100       129 substr($header, -1, 1) eq '|' or croak("'$header': Wrong header format");
187             }
188 66 100       428 $header =~ s/^\s*\|.*?\|\s*// or croak("'$header': Wrong header format");
189 65 100       385 my @elem_array = $header eq "|" ? ('') : split(/\s*\|\s*/, $header);
190 65 100       171 return ([], {}) if $header eq "";
191 63         96 my $index = 0;
192 63         98 my %elem_ids;
193 63         122 foreach my $name (@elem_array) {
194 183 100       496 croak("'$name': duplicate name in header") if exists($elem_ids{$name});
195 181         341 $elem_ids{$name} = $index++;
196             }
197 61         193 return (\@elem_array, \%elem_ids);
198             }
199              
200              
201             my $_parse_row = sub {
202             my $self = shift;
203             my $row = shift;
204             my ($inc, $noinc) = @{$self}{qw(inc noinc)};
205             $row =~ s/^\|\s*([^|]*?)\s*\|\s*// or croak("Wrong row format: '$row'");
206             my $rowElem = $1;
207             my @rowContents;
208             if ($row ne "") {
209             $row =~ s/\s*\|\s*$//;
210             my @entries = $row eq "" ? ("") : split(/\s*\|\s*/, $row, -1);
211             foreach my $entry (@entries) {
212             if ($entry eq $inc) {
213             push(@rowContents, 1);
214             } elsif ($entry eq $noinc) {
215             push(@rowContents, "");
216             } else {
217             croak("'$entry': unexpected entry");
218             }
219             }
220             }
221             return ($rowElem, \@rowContents);
222             };
223              
224              
225             my $_parse_table = sub {
226             my $self = shift;
227             my ($lines, $allow_subset, $pedantic) = @_;
228             my $index = 0;
229             for (; $index < @$lines; ++$index) { # skip heading empty lines
230             last if $lines->[$index] =~ /\S/;
231             }
232             if ($index == @$lines) {
233             $self->$_reset(1);
234             return;
235             }
236             my ($h_elems, $h_ids) = _parse_header_f($lines->[$index], $pedantic);
237             my ($sep_line, $rule_pos);
238             if ($pedantic) {
239             ($sep_line = $lines->[$index]) =~ s/\s+$//;
240             $rule_pos = _rule_pos_array_f($sep_line);
241             for (my $i = 0; $i < @$rule_pos - 1; ++$i) {
242             my ($b, $e) = @{$rule_pos}[$i, $i + 1];
243             substr($sep_line, $b, 1, '+') if $i;
244             my $d = $e - $b;
245             next unless $d > 1;
246             --$d;
247             substr($sep_line, $b + 1, $d, '-' x $d);
248             }
249             }
250             my $elem_ids;
251             my %rows;
252             my @rowElems; # To keep oder of additional row elements, if any.
253             for (++$index; $index < @$lines; ++$index) {
254             (my $line = $lines->[$index]) =~ s/\s+$//;
255             last if $line eq q{};
256             if ($pedantic) {
257             $line =~ /\S/;
258             $-[0] == $rule_pos->[0] or croak("Wrong indentation at line " . ($index + 1));
259             }
260             if ($line =~ /^\s*\|-/) {
261             if ($pedantic) {
262             $line eq $sep_line or croak("Invalid row separator at line " . ($index + 1));
263             }
264             next;
265             }
266             if ($pedantic) {
267             _int_array_cmp(_rule_pos_array_f($line), $rule_pos) or
268             croak("Wrong row format at line " . ($index + 1));
269             }
270             $line =~ s/^\s*//;
271             my ($rowElem, $rowContent) = $self->$_parse_row($line);
272             croak("'$rowElem': duplicate element in first column") if exists($rows{$rowElem});
273             $rows{$rowElem} = $rowContent;
274             push(@rowElems, $rowElem);
275             }
276             if ($self->{prespec}) {
277             my $tab_elems = $self->{tab_elems};
278             $elem_ids = $self->{elem_ids};
279             foreach my $elem (keys(%{$h_ids})) {
280             croak("'$elem': unknown element in table") if !exists($tab_elems->{$elem});
281             }
282             foreach my $elem (keys(%rows)) {
283             croak("'$elem': unknown element in table") if !exists($tab_elems->{$elem});
284             }
285             if (!$allow_subset) {
286             foreach my $elem (keys(%{$tab_elems})) {
287             croak("'$elem': column missing for element") if !exists($h_ids->{$elem});
288             croak("'$elem': row missing for element" ) if !exists($rows{$elem});
289             }
290             }
291             } else {
292             if ($allow_subset) {
293             foreach my $rowElem (@rowElems) {
294             if (!exists($h_ids->{$rowElem})) {
295             $h_ids->{$rowElem} = @{$h_elems};
296             push(@{$h_elems}, $rowElem);
297             }
298             }
299             } else {
300             croak("Number of elements in header does not match number of elemens in row")
301             if keys(%{$h_ids}) != keys(%rows);
302             foreach my $elem (keys(%{$h_ids})) {
303             croak("'$elem': row missing for element") if !exists($rows{$elem});
304             }
305             }
306             my %tmp = %{$h_ids};
307             @{$self}{qw(elems elem_ids tab_elems eq_ids)} = ($h_elems, $h_ids, \%tmp, {});
308             $elem_ids = $h_ids;
309             }
310             my $eq_ids = $self->{eq_ids};
311             my %matrix;
312             while (my ($rowElem, $rowContents) = each(%rows)) {
313             my %new_row;
314             for (my $i = 0; $i < @{$rowContents}; $i++) {
315             if ($rowContents->[$i]) {
316             my $e_id = $elem_ids->{$h_elems->[$i]};
317             $new_row{$e_id} = undef;
318             if (exists($eq_ids->{$e_id})) {
319             foreach my $eq_id (@{$eq_ids->{$e_id}}) {
320             $new_row{$eq_id} = undef
321             }
322             }
323             }
324             }
325             if (%new_row) {
326             $matrix{$elem_ids->{$rowElem}} = \%new_row;
327             if (exists($eq_ids->{$rowElem})) {
328             foreach my $eq_id (@{$eq_ids->{$rowElem}}) {
329             $matrix{$eq_id} = {%new_row};
330             }
331             }
332             }
333             }
334             $self->{matrix} = \%matrix;
335             return;
336             };
337              
338              
339             sub get {
340 79     79 1 23613 my $self = shift;
341 79 100       364 croak("Odd number of arguments") if @_ % 2;
342 78         231 my %args = @_;
343 78         157 my $allow_subset = delete $args{allow_subset};
344 78         139 my $pedantic = delete $args{pedantic};
345 78 100       261 croak("Missing argument 'src'") if !@_;
346 77   66     279 my $src = delete $args{src} // croak("Invalid value argument for 'src'");
347 76 100       312 croak(join(", ", sort(keys(%args))) . ": unexpected argument") if %args;
348 74         109 my $inputArray;
349 74 100       286 if (ref($src)) {
    100          
350 39 100       165 croak("Invalid value argument for 'src'") if ref($src) ne 'ARRAY';
351 38         62 foreach my $e (@{$src}) {
  38         98  
352 170 100 66     528 croak("src: each entry must be a defined scalar") if (ref($e) || !defined($e));
353             }
354 37         73 $inputArray = $src;
355             } elsif ($src !~ /\n/) {
356 4         26 open(my $h, '<', $src);
357 4         4007 $inputArray = [<$h>];
358 4         30 close($h);
359             } else {
360 31         197 $inputArray = [split(/\n/, $src)];
361             }
362 72 100       1407 $self->$_reset() if !$self->{prespec};
363 72         263 $self->$_parse_table($inputArray, $allow_subset, $pedantic);
364 43 100       169 return wantarray ? @{$self}{qw(matrix elems elem_ids)} : $self;
  20         136  
365             }
366              
367              
368 3 100   3 1 1598 sub inc {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{inc};}
  2         16  
369 3 100   3 1 631 sub noinc {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{noinc};}
  2         10  
370 12 100   12 1 595 sub prespec {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{prespec};}
  11         68  
371 32 100   32 1 7728 sub elems {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{elems};}
  31         163  
372 40 100   40 1 673 sub elem_ids {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{elem_ids};}
  39         144  
373 29 50   29 1 11165 sub tab_elems {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{tab_elems};}
  29         138  
374 19 50   19 1 83 sub eq_ids {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{eq_ids};}
  19         91  
375              
376              
377             sub matrix {
378 26     26 1 1342 my $self = shift;
379 26 100       156 croak("Odd number of arguments") if @_ % 2;
380 25         56 my %args = @_;
381 25         54 my $bless = delete $args{bless};
382 25 100       126 croak("Unexpected argument(s)") if %args;
383 24 100       76 return if !$self->{matrix};
384 22 100       72 bless($self->{matrix}, "Text::Table::Read::RelationOn::Tiny::_Relation_Matrix") if $bless;
385 22         155 return $self->{matrix};
386             }
387              
388              
389             sub matrix_named {
390 11     11 1 1895 my $self = shift;
391 11 100       95 croak("Odd number of arguments") if @_ % 2;
392 10         31 my %args = @_;
393 10         23 my $bless = delete $args{bless};
394 10 100       85 croak("Unexpected argument(s)") if %args;
395              
396 9         25 my ($matrix, $elems) = @{$self}{qw(matrix elems)};
  9         25  
397 9 100       29 return if !$matrix;
398 7         13 my $matrix_named = {};
399 7 100       29 bless($matrix_named, "Text::Table::Read::RelationOn::Tiny::_Relation_Matrix") if $bless;
400 7         15 while (my ($rowElemIdx, $rowContents) = each(%{$matrix})) {
  25         81  
401 18         28 $matrix_named->{$elems->[$rowElemIdx]} = {map {$elems->[$_] => undef} keys(%{$rowContents})};
  77         182  
  18         37  
402             }
403 7         67 return $matrix_named;
404             }
405              
406              
407              
408             {
409             package Text::Table::Read::RelationOn::Tiny::_Relation_Matrix;
410              
411 4   66 4   1213 sub related { return exists($_[0]->{$_[1]}) && exists($_[0]->{$_[1]}->{$_[2]}); }
412             }
413              
414              
415             1; # End of Text::Table::Read::RelationOn::Tiny
416              
417              
418              
419             __END__