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   552647 use 5.010_001;
  9         102  
4 9     9   55 use strict;
  9         18  
  9         236  
5 9     9   63 use warnings;
  9         29  
  9         331  
6 9     9   3886 use autodie;
  9         129648  
  9         41  
7              
8 9     9   56891 use Carp;
  9         29  
  9         693  
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   4043 use version 0.77; our $VERSION = version->declare("v2.2.5");
  9         15642  
  9         62  
13              
14              
15             sub new {
16 68     68 1 46234 my $class = shift;
17 68 50       194 $class = ref($class) if ref($class);
18 68 100       396 croak("Odd number of arguments") if @_ % 2;
19 67         191 my %args = @_;
20 67   100     316 my $inc = delete $args{inc} // "X";
21 67   100     226 my $noinc = delete $args{noinc} // "";
22 67         136 my $set = delete $args{set};
23 67         123 my $eqs = delete $args{eqs};
24 67         116 my $ext = delete $args{ext};
25 67         111 my $elem_ids = delete $args{elem_ids};
26 67 100       257 croak(join(", ", sort(keys(%args))) . ": unexpected argument") if %args;
27 66 100       263 croak("inc: must be a scalar") if ref($inc);
28 65 100       260 croak("noinc: must be a scalar") if ref($noinc);
29 64         438 s/^\s+// for ($inc, $noinc);
30 64         230 s/\s+$// for ($inc, $noinc);
31 64 100       263 croak("inc and noinc must be different") if $inc eq $noinc;
32 63 100 100     422 croak("'|' is not allowed for inc or noinc") if $inc eq '|' || $noinc eq '|';
33 61         217 my $self = {inc => $inc,
34             noinc => $noinc,
35             };
36 61 100       148 if (defined($set)) {
37 48         81 my %seen;
38 48 100       216 croak("set: must be an array reference") if ref($set) ne 'ARRAY';
39 47         81 my $cnt = 1;
40 47         100 foreach my $e (@$set) {
41 152 100       269 if (ref($e)) {
42 21 100       137 croak("set: entry $cnt: invalid") if ref($e) ne 'ARRAY';
43 20 100       134 croak("set: entry $cnt: array not allowed if eqs is specified") if $eqs;
44 19 100       25 croak("set: entry $cnt: array entry must not be empty") if !@{$e};
  19         265  
45 17         31 foreach my $sub_e (@$e) {
46 35 100 100     406 croak("set: entry $cnt: subarray contains invalid entry")
47             if ref($sub_e) || !defined($sub_e);
48 32 100       475 croak("set: '$sub_e': duplicate element") if exists($seen{$sub_e});
49 28         69 $seen{$sub_e} = undef;
50             }
51             } else {
52 131 100       341 croak("set: entry $cnt: invalid") if !defined($e);
53 130 100       357 croak("set: '$e': duplicate element") if exists($seen{$e});
54 129         231 $seen{$e} = undef;
55             }
56 139         211 ++$cnt;
57             }
58 34         96 $self->{prespec} = 1;
59             } else {
60 13 100       118 croak("eqs: not allowed without argument 'set'") if defined($eqs);
61 12         31 $self->{prespec} = "";
62             }
63 46 100       111 if (defined($elem_ids)) {
64 13 100 100     326 croak("elem_ids: not allowed without arguments 'set' and 'ext'") if !(defined($ext) &&
65             defined($set));
66 10 100       120 croak("elem_ids: must be a hash ref") if ref($elem_ids) ne 'HASH';
67             }
68 42         109 my $elems;
69             my $tabElems; # elems to be used in table --> indes in @elems
70 42         0 my $eqIds;
71 42 100       119 if ($ext) {
    100          
72 12 100       27 if ($set) {
73 11         23 foreach my $e (@$set) {
74 42 100       175 croak("set: no subarray allowed if 'ext' is specified") if ref($e);
75             }
76 10 100       23 if ($elem_ids) {
77 8 100       143 croak("elem_ids: wrong number of entries") if keys(%$elem_ids) != @$set;
78 7         17 foreach my $e (@$set) {
79 24         53 my $e_id = $elem_ids->{$e};
80 24 100       227 croak("elem_ids: '$e': missing value") if !defined($e_id);
81 22 100 100     452 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         11 my $idx = 0;
87 2         7 $elem_ids = {map {$_ => $idx++} @$set};
  14         32  
88             }
89 4         8 $elems = $set;
90             } else {
91 1         89 croak("ext: not allowed without argument 'set'")
92             }
93 4         25 %$tabElems = %$elem_ids;
94             } elsif (ref($set)) {
95 21         59 my @elems; # elems
96             my %ids; # indices in basic elems
97 21         0 my @eqs_tmp;
98              
99 21         42 foreach my $entry (@$set) {
100 75 100       152 if (ref($entry)) {
101 5         10 push(@elems, $entry->[0]);
102 5         8 $ids{$entry->[0]} = $#elems;
103 5         12 for (my $j = 1; $j < @$entry; ++$j) {
104 4         6 my $ent_j = $entry->[$j];
105 4         6 push(@elems, $ent_j);
106 4         9 $ids{$ent_j} = $#elems;
107             }
108 5 100       12 push(@eqs_tmp, $entry) if @$entry > 1;
109             } else {
110 70         125 push(@elems, $entry);
111 70         141 $ids{$entry} = $#elems;
112             }
113             }
114 21 50 66     86 croak("Internal error") if (defined($eqs) && @eqs_tmp); # Should never happen.
115 21 100       50 $eqs = \@eqs_tmp if @eqs_tmp;
116 21         134 ($elems, $elem_ids, $tabElems, $eqIds) = (\@elems, \%ids, {%ids}, {});
117             }
118 34 100       104 if (defined($eqs)) {
119 14 100       140 croak("eqs: must be an array ref") if ref($eqs) ne 'ARRAY';
120 13         25 my %eqIds; # idx => array of equivalent idxes
121             my %seen;
122 13         23 foreach my $eqArray (@{$eqs}) {
  13         30  
123 28 100       198 croak("eqs: each entry must be an array ref") if ref($eqArray) ne 'ARRAY';
124 27 100       39 next if !@{$eqArray};
  27         64  
125 25         43 foreach my $entry (@{$eqArray}) {
  25         44  
126 57 100       240 croak("eqs: subentry contains a non-scalar") if ref($entry);
127 56 100       193 croak("eqs: subentry undefined") if !defined($entry);
128 55 100       301 croak("eqs: '$entry': unknown element") if !exists($elem_ids->{$entry});
129 53 100       299 croak("eqs: '$entry': duplicate element") if exists($seen{$entry});
130 51         97 $seen{$entry} = undef;
131             }
132 19 100       40 next if @{$eqArray} == 1;
  19         51  
133 14         22 my @tmp = @{$eqArray};
  14         37  
134 14         25 my @eqArray;
135 14         40 $eqIds{$tabElems->{shift(@tmp)}} = \@eqArray;
136 14         28 foreach my $e (@tmp) {
137 27         69 push(@eqArray, delete $tabElems->{$e});
138             }
139             }
140 6         28 $eqIds = \%eqIds;
141             }
142 26         56 @{$self}{qw(elems elem_ids tab_elems eq_ids)} = ($elems, $elem_ids, $tabElems, $eqIds);
  26         81  
143 26         108 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   35 my ($str) = @_;
161 17         26 my @rule_pos;
162 17         34 my $idx = index($str, '|');
163 17         37 while($idx != -1) {
164 84         132 push(@rule_pos, $idx);
165 84         149 $idx = index($str, '|', $idx + 1);
166             }
167 17         55 return \@rule_pos;
168             }
169              
170             # just a function, not a method.
171             sub _int_array_cmp {
172 9     9   20 my ($arr1, $arr2) = @_;
173 9 100       188 return !1 if @$arr1 != @$arr2;
174 7         17 for (my $i = 0; $i < @$arr1; ++$i) {
175 36 100       173 return !1 if $arr1->[$i] != $arr2->[$i];
176             }
177 6         15 return 1;
178             }
179              
180             # just a function, not a method.
181             sub _parse_header_f {
182 67     67   158 my ($header, $pedantic) = @_;
183 67         227 $header =~ s/\s+$//;
184 67         112 my @rule_pos;
185 67 100       143 if ($pedantic) {
186 9 100       148 substr($header, -1, 1) eq '|' or croak("'$header': Wrong header format");
187             }
188 66 100       416 $header =~ s/^\s*\|.*?\|\s*// or croak("'$header': Wrong header format");
189 65 100       422 my @elem_array = $header eq "|" ? ('') : split(/\s*\|\s*/, $header);
190 65 100       181 return ([], {}) if $header eq "";
191 63         100 my $index = 0;
192 63         99 my %elem_ids;
193 63         124 foreach my $name (@elem_array) {
194 183 100       572 croak("'$name': duplicate name in header") if exists($elem_ids{$name});
195 181         363 $elem_ids{$name} = $index++;
196             }
197 61         211 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 27048 my $self = shift;
341 79 100       454 croak("Odd number of arguments") if @_ % 2;
342 78         227 my %args = @_;
343 78         153 my $allow_subset = delete $args{allow_subset};
344 78         142 my $pedantic = delete $args{pedantic};
345 78 100       261 croak("Missing argument 'src'") if !@_;
346 77   66     314 my $src = delete $args{src} // croak("Invalid value argument for 'src'");
347 76 100       315 croak(join(", ", sort(keys(%args))) . ": unexpected argument") if %args;
348 74         112 my $inputArray;
349 74 100       263 if (ref($src)) {
    100          
350 39 100       178 croak("Invalid value argument for 'src'") if ref($src) ne 'ARRAY';
351 38         55 foreach my $e (@{$src}) {
  38         89  
352 170 100 66     766 croak("src: each entry must be a defined scalar") if (ref($e) || !defined($e));
353             }
354 37         71 $inputArray = $src;
355             } elsif ($src !~ /\n/) {
356 4         13 open(my $h, '<', $src);
357 4         2750 $inputArray = [<$h>];
358 4         23 close($h);
359             } else {
360 31         181 $inputArray = [split(/\n/, $src)];
361             }
362 72 100       1159 $self->$_reset() if !$self->{prespec};
363 72         244 $self->$_parse_table($inputArray, $allow_subset, $pedantic);
364 43 100       145 return wantarray ? @{$self}{qw(matrix elems elem_ids)} : $self;
  20         132  
365             }
366              
367              
368 3 100   3 1 1698 sub inc {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{inc};}
  2         13  
369 3 100   3 1 663 sub noinc {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{noinc};}
  2         6  
370 12 100   12 1 799 sub prespec {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{prespec};}
  11         70  
371 32 100   32 1 8680 sub elems {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{elems};}
  31         144  
372 40 100   40 1 804 sub elem_ids {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{elem_ids};}
  39         154  
373 29 50   29 1 10129 sub tab_elems {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{tab_elems};}
  29         161  
374 19 50   19 1 65 sub eq_ids {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{eq_ids};}
  19         83  
375              
376              
377             sub matrix {
378 26     26 1 1541 my $self = shift;
379 26 100       139 croak("Odd number of arguments") if @_ % 2;
380 25         52 my %args = @_;
381 25         41 my $bless = delete $args{bless};
382 25 100       121 croak("Unexpected argument(s)") if %args;
383 24 100       55 return if !$self->{matrix};
384 22 100       47 bless($self->{matrix}, "Text::Table::Read::RelationOn::Tiny::_Relation_Matrix") if $bless;
385 22         140 return $self->{matrix};
386             }
387              
388              
389             sub matrix_named {
390 11     11 1 2026 my $self = shift;
391 11 100       187 croak("Odd number of arguments") if @_ % 2;
392 10         26 my %args = @_;
393 10         21 my $bless = delete $args{bless};
394 10 100       94 croak("Unexpected argument(s)") if %args;
395              
396 9         25 my ($matrix, $elems) = @{$self}{qw(matrix elems)};
  9         22  
397 9 100       28 return if !$matrix;
398 7         15 my $matrix_named = {};
399 7 100       20 bless($matrix_named, "Text::Table::Read::RelationOn::Tiny::_Relation_Matrix") if $bless;
400 7         14 while (my ($rowElemIdx, $rowContents) = each(%{$matrix})) {
  25         75  
401 18         29 $matrix_named->{$elems->[$rowElemIdx]} = {map {$elems->[$_] => undef} keys(%{$rowContents})};
  77         212  
  18         41  
402             }
403 7         36 return $matrix_named;
404             }
405              
406              
407              
408             {
409             package Text::Table::Read::RelationOn::Tiny::_Relation_Matrix;
410              
411 4   66 4   1039 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__