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   615820 use 5.010_001;
  9         102  
4 9     9   66 use strict;
  9         21  
  9         198  
5 9     9   94 use warnings;
  9         30  
  9         337  
6 9     9   4346 use autodie;
  9         142163  
  9         39  
7              
8 9     9   62049 use Carp;
  9         21  
  9         662  
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   4050 use version 0.77; our $VERSION = version->declare("v2.2.6");
  9         17317  
  9         69  
13              
14              
15             sub new {
16 68     68 1 49341 my $class = shift;
17 68 50       204 $class = ref($class) if ref($class);
18 68 100       434 croak("Odd number of arguments") if @_ % 2;
19 67         219 my %args = @_;
20 67   100     291 my $inc = delete $args{inc} // "X";
21 67   100     251 my $noinc = delete $args{noinc} // "";
22 67         138 my $set = delete $args{set};
23 67         124 my $eqs = delete $args{eqs};
24 67         113 my $ext = delete $args{ext};
25 67         120 my $elem_ids = delete $args{elem_ids};
26 67 100       262 croak(join(", ", sort(keys(%args))) . ": unexpected argument") if %args;
27 66 100       276 croak("inc: must be a scalar") if ref($inc);
28 65 100       249 croak("noinc: must be a scalar") if ref($noinc);
29 64         462 s/^\s+// for ($inc, $noinc);
30 64         254 s/\s+$// for ($inc, $noinc);
31 64 100       267 croak("inc and noinc must be different") if $inc eq $noinc;
32 63 100 100     442 croak("'|' is not allowed for inc or noinc") if $inc eq '|' || $noinc eq '|';
33 61         197 my $self = {inc => $inc,
34             noinc => $noinc,
35             };
36 61 100       160 if (defined($set)) {
37 48         84 my %seen;
38 48 100       216 croak("set: must be an array reference") if ref($set) ne 'ARRAY';
39 47         88 my $cnt = 1;
40 47         98 foreach my $e (@$set) {
41 152 100       261 if (ref($e)) {
42 21 100       137 croak("set: entry $cnt: invalid") if ref($e) ne 'ARRAY';
43 20 100       133 croak("set: entry $cnt: array not allowed if eqs is specified") if $eqs;
44 19 100       28 croak("set: entry $cnt: array entry must not be empty") if !@{$e};
  19         219  
45 17         38 foreach my $sub_e (@$e) {
46 35 100 100     380 croak("set: entry $cnt: subarray contains invalid entry")
47             if ref($sub_e) || !defined($sub_e);
48 32 100       431 croak("set: '$sub_e': duplicate element") if exists($seen{$sub_e});
49 28         59 $seen{$sub_e} = undef;
50             }
51             } else {
52 131 100       385 croak("set: entry $cnt: invalid") if !defined($e);
53 130 100       363 croak("set: '$e': duplicate element") if exists($seen{$e});
54 129         231 $seen{$e} = undef;
55             }
56 139         237 ++$cnt;
57             }
58 34         109 $self->{prespec} = 1;
59             } else {
60 13 100       119 croak("eqs: not allowed without argument 'set'") if defined($eqs);
61 12         28 $self->{prespec} = "";
62             }
63 46 100       115 if (defined($elem_ids)) {
64 13 100 100     317 croak("elem_ids: not allowed without arguments 'set' and 'ext'") if !(defined($ext) &&
65             defined($set));
66 10 100       146 croak("elem_ids: must be a hash ref") if ref($elem_ids) ne 'HASH';
67             }
68 42         120 my $elems;
69             my $tabElems; # elems to be used in table --> indes in @elems
70 42         0 my $eqIds;
71 42 100       133 if ($ext) {
    100          
72 12 100       29 if ($set) {
73 11         27 foreach my $e (@$set) {
74 42 100       172 croak("set: no subarray allowed if 'ext' is specified") if ref($e);
75             }
76 10 100       26 if ($elem_ids) {
77 8 100       227 croak("elem_ids: wrong number of entries") if keys(%$elem_ids) != @$set;
78 7         18 foreach my $e (@$set) {
79 24         49 my $e_id = $elem_ids->{$e};
80 24 100       233 croak("elem_ids: '$e': missing value") if !defined($e_id);
81 22 100 100     404 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         14 my $idx = 0;
87 2         7 $elem_ids = {map {$_ => $idx++} @$set};
  14         38  
88             }
89 4         8 $elems = $set;
90             } else {
91 1         90 croak("ext: not allowed without argument 'set'")
92             }
93 4         23 %$tabElems = %$elem_ids;
94             } elsif (ref($set)) {
95 21         56 my @elems; # elems
96             my %ids; # indices in basic elems
97 21         0 my @eqs_tmp;
98              
99 21         44 foreach my $entry (@$set) {
100 75 100       132 if (ref($entry)) {
101 5         11 push(@elems, $entry->[0]);
102 5         12 $ids{$entry->[0]} = $#elems;
103 5         13 for (my $j = 1; $j < @$entry; ++$j) {
104 4         6 my $ent_j = $entry->[$j];
105 4         8 push(@elems, $ent_j);
106 4         10 $ids{$ent_j} = $#elems;
107             }
108 5 100       16 push(@eqs_tmp, $entry) if @$entry > 1;
109             } else {
110 70         130 push(@elems, $entry);
111 70         145 $ids{$entry} = $#elems;
112             }
113             }
114 21 50 66     84 croak("Internal error") if (defined($eqs) && @eqs_tmp); # Should never happen.
115 21 100       62 $eqs = \@eqs_tmp if @eqs_tmp;
116 21         126 ($elems, $elem_ids, $tabElems, $eqIds) = (\@elems, \%ids, {%ids}, {});
117             }
118 34 100       103 if (defined($eqs)) {
119 14 100       167 croak("eqs: must be an array ref") if ref($eqs) ne 'ARRAY';
120 13         28 my %eqIds; # idx => array of equivalent idxes
121             my %seen;
122 13         20 foreach my $eqArray (@{$eqs}) {
  13         31  
123 28 100       163 croak("eqs: each entry must be an array ref") if ref($eqArray) ne 'ARRAY';
124 27 100       38 next if !@{$eqArray};
  27         63  
125 25         45 foreach my $entry (@{$eqArray}) {
  25         44  
126 57 100       211 croak("eqs: subentry contains a non-scalar") if ref($entry);
127 56 100       195 croak("eqs: subentry undefined") if !defined($entry);
128 55 100       299 croak("eqs: '$entry': unknown element") if !exists($elem_ids->{$entry});
129 53 100       305 croak("eqs: '$entry': duplicate element") if exists($seen{$entry});
130 51         98 $seen{$entry} = undef;
131             }
132 19 100       33 next if @{$eqArray} == 1;
  19         68  
133 14         28 my @tmp = @{$eqArray};
  14         35  
134 14         23 my @eqArray;
135 14         40 $eqIds{$tabElems->{shift(@tmp)}} = \@eqArray;
136 14         36 foreach my $e (@tmp) {
137 27         68 push(@eqArray, delete $tabElems->{$e});
138             }
139             }
140 6         27 $eqIds = \%eqIds;
141             }
142 26         64 @{$self}{qw(elems elem_ids tab_elems eq_ids)} = ($elems, $elem_ids, $tabElems, $eqIds);
  26         81  
143 26         118 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         27 my @rule_pos;
162 17         43 my $idx = index($str, '|');
163 17         40 while($idx != -1) {
164 84         136 push(@rule_pos, $idx);
165 84         168 $idx = index($str, '|', $idx + 1);
166             }
167 17         66 return \@rule_pos;
168             }
169              
170             # just a function, not a method.
171             sub _int_array_cmp {
172 9     9   23 my ($arr1, $arr2) = @_;
173 9 100       178 return !1 if @$arr1 != @$arr2;
174 7         31 for (my $i = 0; $i < @$arr1; ++$i) {
175 36 100       183 return !1 if $arr1->[$i] != $arr2->[$i];
176             }
177 6         18 return 1;
178             }
179              
180             # just a function, not a method.
181             sub _parse_header_f {
182 67     67   151 my ($header, $pedantic) = @_;
183 67         248 $header =~ s/\s+$//;
184 67         111 my @rule_pos;
185 67 100       168 if ($pedantic) {
186 9 100       134 substr($header, -1, 1) eq '|' or croak("'$header': Wrong header format");
187             }
188 66 100       457 $header =~ s/^\s*\|.*?\|\s*// or croak("'$header': Wrong header format");
189 65 100       461 my @elem_array = $header eq "|" ? ('') : split(/\s*\|\s*/, $header);
190 65 100       189 return ([], {}) if $header eq "";
191 63         106 my $index = 0;
192 63         102 my %elem_ids;
193 63         141 foreach my $name (@elem_array) {
194 183 100       613 croak("'$name': duplicate name in header") if exists($elem_ids{$name});
195 181         375 $elem_ids{$name} = $index++;
196             }
197 61         219 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 28063 my $self = shift;
341 79 100       332 croak("Odd number of arguments") if @_ % 2;
342 78         265 my %args = @_;
343 78         160 my $allow_subset = delete $args{allow_subset};
344 78         135 my $pedantic = delete $args{pedantic};
345 78 100       279 croak("Missing argument 'src'") if !@_;
346 77   66     279 my $src = delete $args{src} // croak("Invalid value argument for 'src'");
347 76 100       326 croak(join(", ", sort(keys(%args))) . ": unexpected argument") if %args;
348 74         138 my $inputArray;
349 74 100       277 if (ref($src)) {
    100          
350 39 100       203 croak("Invalid value argument for 'src'") if ref($src) ne 'ARRAY';
351 38         58 foreach my $e (@{$src}) {
  38         102  
352 170 100 66     615 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         23 open(my $h, '<', $src);
357 4         3518 $inputArray = [<$h>];
358 4         31 close($h);
359             } else {
360 31         200 $inputArray = [split(/\n/, $src)];
361             }
362 72 100       1445 $self->$_reset() if !$self->{prespec};
363 72         252 $self->$_parse_table($inputArray, $allow_subset, $pedantic);
364 43 100       196 return wantarray ? @{$self}{qw(matrix elems elem_ids)} : $self;
  20         184  
365             }
366              
367              
368 3 100   3 1 1873 sub inc {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{inc};}
  2         15  
369 3 100   3 1 662 sub noinc {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{noinc};}
  2         9  
370 12 100   12 1 712 sub prespec {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{prespec};}
  11         79  
371 32 100   32 1 8906 sub elems {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{elems};}
  31         158  
372 40 100   40 1 791 sub elem_ids {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{elem_ids};}
  39         195  
373 29 50   29 1 12710 sub tab_elems {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{tab_elems};}
  29         157  
374 19 50   19 1 72 sub eq_ids {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{eq_ids};}
  19         96  
375              
376              
377             sub matrix {
378 26     26 1 1626 my $self = shift;
379 26 100       156 croak("Odd number of arguments") if @_ % 2;
380 25         55 my %args = @_;
381 25         57 my $bless = delete $args{bless};
382 25 100       132 croak("Unexpected argument(s)") if %args;
383 24 100       63 return if !$self->{matrix};
384 22 100       56 bless($self->{matrix}, "Text::Table::Read::RelationOn::Tiny::_Relation_Matrix") if $bless;
385 22         179 return $self->{matrix};
386             }
387              
388              
389             sub matrix_named {
390 11     11 1 2163 my $self = shift;
391 11 100       116 croak("Odd number of arguments") if @_ % 2;
392 10         28 my %args = @_;
393 10         22 my $bless = delete $args{bless};
394 10 100       94 croak("Unexpected argument(s)") if %args;
395              
396 9         19 my ($matrix, $elems) = @{$self}{qw(matrix elems)};
  9         26  
397 9 100       30 return if !$matrix;
398 7         27 my $matrix_named = {};
399 7 100       25 bless($matrix_named, "Text::Table::Read::RelationOn::Tiny::_Relation_Matrix") if $bless;
400 7         15 while (my ($rowElemIdx, $rowContents) = each(%{$matrix})) {
  25         116  
401 18         33 $matrix_named->{$elems->[$rowElemIdx]} = {map {$elems->[$_] => undef} keys(%{$rowContents})};
  77         215  
  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   1264 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__