File Coverage

blib/lib/Text/Table/Read/RelationOn/Tiny.pm
Criterion Covered Total %
statement 236 237 99.5
branch 161 166 96.9
condition 27 31 87.1
subroutine 23 23 100.0
pod 11 11 100.0
total 458 468 97.8


line stmt bran cond sub pod time code
1             package Text::Table::Read::RelationOn::Tiny;
2              
3 9     9   1069130 use 5.010_001;
  9         39  
4 9     9   111 use strict;
  9         55  
  9         291  
5 9     9   55 use warnings;
  9         17  
  9         485  
6 9     9   4253 use autodie;
  9         165287  
  9         57  
7              
8 9     9   69580 use Carp;
  9         21  
  9         995  
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   6241 use version 0.77; our $VERSION = version->declare("v3.0.3");
  9         19686  
  9         79  
13              
14              
15             sub new {
16 68     68 1 1454814 my $class = shift;
17 68 50       302 $class = ref($class) if ref($class);
18 68 100       426 croak("Odd number of arguments") if @_ % 2;
19 67         259 my %args = @_;
20 67   100     399 my $inc = delete $args{inc} // "X";
21 67   100     338 my $noinc = delete $args{noinc} // "";
22 67         156 my $set = delete $args{set};
23 67         145 my $eqs = delete $args{eqs};
24 67         128 my $ext = delete $args{ext};
25 67         145 my $elem_ids = delete $args{elem_ids};
26 67 100       344 croak(join(", ", sort(keys(%args))) . ": unexpected argument") if %args;
27 66 100       320 croak("inc: must be a scalar") if ref($inc);
28 65 100       399 croak("noinc: must be a scalar") if ref($noinc);
29 64         442 s/^\s+// for ($inc, $noinc);
30 64         365 s/\s+$// for ($inc, $noinc);
31 64 100       347 croak("inc and noinc must be different") if $inc eq $noinc;
32 63 100 100     632 croak("'|' is not allowed for inc or noinc") if $inc eq '|' || $noinc eq '|';
33 61         276 my $self = {inc => $inc,
34             noinc => $noinc,
35             };
36 61 100       195 if (defined($set)) {
37 48         101 my %seen;
38 48 100       306 croak("set: must be an array reference") if ref($set) ne 'ARRAY';
39 47         84 my $cnt = 1;
40 47         111 foreach my $e (@$set) {
41 152 100       291 if (ref($e)) {
42 21 100       184 croak("set: entry $cnt: invalid") if ref($e) ne 'ARRAY';
43 20 100       279 croak("set: entry $cnt: array not allowed if eqs is specified") if $eqs;
44 19 100       29 croak("set: entry $cnt: array entry must not be empty") if !@{$e};
  19         485  
45 17         32 foreach my $sub_e (@$e) {
46 35 100 100     552 croak("set: entry $cnt: subarray contains invalid entry")
47             if ref($sub_e) || !defined($sub_e);
48 32 100       641 croak("set: '$sub_e': duplicate element") if exists($seen{$sub_e});
49 28         65 $seen{$sub_e} = undef;
50             }
51             } else {
52 131 100       419 croak("set: entry $cnt: invalid") if !defined($e);
53 130 100       483 croak("set: '$e': duplicate element") if exists($seen{$e});
54 129         288 $seen{$e} = undef;
55             }
56 139         258 ++$cnt;
57             }
58 34         127 $self->{prespec} = 1;
59             } else {
60 13 100       182 croak("eqs: not allowed without argument 'set'") if defined($eqs);
61 12         43 $self->{prespec} = "";
62             }
63 46 100       125 if (defined($elem_ids)) {
64 13 100 100     521 croak("elem_ids: not allowed without arguments 'set' and 'ext'") if !(defined($ext) &&
65             defined($set));
66 10 100       226 croak("elem_ids: must be a hash ref") if ref($elem_ids) ne 'HASH';
67             }
68 42         147 my $elems;
69             my $tabElems; # elems to be used in table --> indes in @elems
70 42         0 my $eqIds;
71 42 100       184 if ($ext) {
    100          
72 12 100       55 if ($set) {
73 11         26 foreach my $e (@$set) {
74 42 100       249 croak("set: no subarray allowed if 'ext' is specified") if ref($e);
75             }
76 10 100       38 if ($elem_ids) {
77 8 100       179 croak("elem_ids: wrong number of entries") if keys(%$elem_ids) != @$set;
78 7         37 foreach my $e (@$set) {
79 24         58 my $e_id = $elem_ids->{$e};
80 24 100       365 croak("elem_ids: '$e': missing value") if !defined($e_id);
81 22 100 100     690 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         5 $elem_ids = {map {$_ => $idx++} @$set};
  14         27  
88             }
89 4         10 $elems = $set;
90             } else {
91 1         134 croak("ext: not allowed without argument 'set'")
92             }
93 4         30 %$tabElems = %$elem_ids;
94             } elsif (ref($set)) {
95 21         62 my @elems; # elems
96             my %ids; # indices in basic elems
97 21         0 my @eqs_tmp;
98              
99 21         56 foreach my $entry (@$set) {
100 75 100       157 if (ref($entry)) {
101 5         7 push(@elems, $entry->[0]);
102 5         8 $ids{$entry->[0]} = $#elems;
103 5         16 for (my $j = 1; $j < @$entry; ++$j) {
104 4         5 my $ent_j = $entry->[$j];
105 4         5 push(@elems, $ent_j);
106 4         8 $ids{$ent_j} = $#elems;
107             }
108 5 100       9 push(@eqs_tmp, $entry) if @$entry > 1;
109             } else {
110 70         139 push(@elems, $entry);
111 70         153 $ids{$entry} = $#elems;
112             }
113             }
114 21 50 66     94 croak("Internal error") if (defined($eqs) && @eqs_tmp); # Should never happen.
115 21 100       52 $eqs = \@eqs_tmp if @eqs_tmp;
116 21         136 ($elems, $elem_ids, $tabElems, $eqIds) = (\@elems, \%ids, {%ids}, {});
117             }
118 34 100       161 if (defined($eqs)) {
119 14 100       195 croak("eqs: must be an array ref") if ref($eqs) ne 'ARRAY';
120 13         30 my %eqIds; # idx => array of equivalent idxes
121             my %seen;
122 13         27 foreach my $eqArray (@{$eqs}) {
  13         35  
123 28 100       223 croak("eqs: each entry must be an array ref") if ref($eqArray) ne 'ARRAY';
124 27 100       42 next if !@{$eqArray};
  27         61  
125 25         45 foreach my $entry (@{$eqArray}) {
  25         46  
126 57 100       277 croak("eqs: subentry contains a non-scalar") if ref($entry);
127 56 100       260 croak("eqs: subentry undefined") if !defined($entry);
128 55 100       477 croak("eqs: '$entry': unknown element") if !exists($elem_ids->{$entry});
129 53 100       449 croak("eqs: '$entry': duplicate element") if exists($seen{$entry});
130 51         105 $seen{$entry} = undef;
131             }
132 19 100       38 next if @{$eqArray} == 1;
  19         59  
133 14         25 my @tmp = @{$eqArray};
  14         53  
134 14         21 my @eqArray;
135 14         58 $eqIds{$tabElems->{shift(@tmp)}} = \@eqArray;
136 14         31 foreach my $e (@tmp) {
137 27         70 push(@eqArray, delete $tabElems->{$e});
138             }
139             }
140 6         25 $eqIds = \%eqIds;
141             }
142 26         65 @{$self}{qw(elems elem_ids tab_elems eq_ids)} = ($elems, $elem_ids, $tabElems, $eqIds);
  26         148  
143 26         133 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   38 my ($str) = @_;
161 17         28 my @rule_pos;
162 17         36 my $idx = index($str, '|');
163 17         40 while($idx != -1) {
164 84         143 push(@rule_pos, $idx);
165 84         203 $idx = index($str, '|', $idx + 1);
166             }
167 17         49 return \@rule_pos;
168             }
169              
170             # just a function, not a method.
171             sub _int_array_cmp {
172 9     9   19 my ($arr1, $arr2) = @_;
173 9 100       357 return !1 if @$arr1 != @$arr2;
174 7         23 for (my $i = 0; $i < @$arr1; ++$i) {
175 36 100       215 return !1 if $arr1->[$i] != $arr2->[$i];
176             }
177 6         19 return 1;
178             }
179              
180             # just a function, not a method.
181             sub _parse_header_f {
182 70     70   170 my ($header, $pedantic) = @_;
183 70         341 $header =~ s/\s+$//;
184 70         164 my @rule_pos;
185 70 100       218 if ($pedantic) {
186 9 100       147 substr($header, -1, 1) eq '|' or croak("'$header': Wrong header format");
187             }
188 69 100       730 $header =~ s/^\s*\|.*?\|\s*// or croak("'$header': Wrong header format");
189 68 100       648 my @elem_array = $header eq "|" ? ('') : split(/\s*\|\s*/, $header);
190 68 100       244 return ([], {}) if $header eq "";
191 66         145 my $index = 0;
192 66         125 my %elem_ids;
193 66         162 foreach my $name (@elem_array) {
194 192 100       823 croak("'$name': duplicate name in header") if exists($elem_ids{$name});
195 190         475 $elem_ids{$name} = $index++;
196             }
197 64         266 return (\@elem_array, \%elem_ids);
198             }
199              
200              
201              
202             #
203             # _ret_hash(HASHREF [, BOOL])
204             #
205             # Returns a duplicate of HASHREF if BOOL is true. Otherwise return HASHREF.
206             #
207             sub _ret_hash {
208 76 100   76   227 return undef if !$_[0];
209 70 100       503 return $_[1] ? {%{$_[0]}} : $_[0];
  2         22  
210             }
211              
212              
213              
214             my $_parse_row = sub {
215             my $self = shift;
216             my $row = shift;
217             my ($inc, $noinc) = @{$self}{qw(inc noinc)};
218             $row =~ s/^\|\s*([^|]*?)\s*\|\s*// or croak("Wrong row format: '$row'");
219             my $rowElem = $1;
220             my @rowContents;
221             if ($row ne "") {
222             $row =~ s/\s*\|\s*$//;
223             my @entries = $row eq "" ? ("") : split(/\s*\|\s*/, $row, -1);
224             foreach my $entry (@entries) {
225             if ($entry eq $inc) {
226             push(@rowContents, 1);
227             } elsif ($entry eq $noinc) {
228             push(@rowContents, "");
229             } else {
230             croak("'$entry': unexpected entry");
231             }
232             }
233             }
234             return ($rowElem, \@rowContents);
235             };
236              
237              
238             my $_parse_table = sub {
239             my $self = shift;
240             my ($lines, $allow_subset, $pedantic) = @_;
241             my $index = 0;
242             for (; $index < @$lines; ++$index) { # skip heading empty lines
243             last if $lines->[$index] =~ /\S/;
244             }
245             if ($index == @$lines) {
246             $self->$_reset(1);
247             return;
248             }
249             my ($h_elems, $h_ids) = _parse_header_f($lines->[$index], $pedantic);
250             my ($sep_line, $rule_pos);
251             if ($pedantic) {
252             ($sep_line = $lines->[$index]) =~ s/\s+$//;
253             $rule_pos = _rule_pos_array_f($sep_line);
254             for (my $i = 0; $i < @$rule_pos - 1; ++$i) {
255             my ($b, $e) = @{$rule_pos}[$i, $i + 1];
256             substr($sep_line, $b, 1, '+') if $i;
257             my $d = $e - $b;
258             next unless $d > 1;
259             --$d;
260             substr($sep_line, $b + 1, $d, '-' x $d);
261             }
262             }
263             my $elem_ids;
264             my %rows;
265             my @rowElems; # To keep order of additional row elements, if any.
266             for (++$index; $index < @$lines; ++$index) {
267             (my $line = $lines->[$index]) =~ s/\s+$//;
268             last if $line eq q{};
269             if ($pedantic) {
270             $line =~ /\S/;
271             $-[0] == $rule_pos->[0] or croak("Wrong indentation at line " . ($index + 1));
272             }
273             if ($line =~ /^\s*\|-/) {
274             if ($pedantic) {
275             $line eq $sep_line or croak("Invalid row separator at line " . ($index + 1));
276             }
277             next;
278             }
279             if ($pedantic) {
280             _int_array_cmp(_rule_pos_array_f($line), $rule_pos) or
281             croak("Wrong row format at line " . ($index + 1));
282             }
283             $line =~ s/^\s*//;
284             my ($rowElem, $rowContent) = $self->$_parse_row($line);
285             croak("'$rowElem': duplicate element in first column") if exists($rows{$rowElem});
286             $rows{$rowElem} = $rowContent;
287             push(@rowElems, $rowElem);
288             }
289             if ($self->{prespec}) {
290             my $tab_elems = $self->{tab_elems};
291             $elem_ids = $self->{elem_ids};
292             foreach my $elem (keys(%{$h_ids})) {
293             croak("'$elem': unknown element in table") if !exists($tab_elems->{$elem});
294             }
295             foreach my $elem (keys(%rows)) {
296             croak("'$elem': unknown element in table") if !exists($tab_elems->{$elem});
297             }
298             if (!$allow_subset) {
299             foreach my $elem (keys(%{$tab_elems})) {
300             croak("'$elem': column missing for element") if !exists($h_ids->{$elem});
301             croak("'$elem': row missing for element" ) if !exists($rows{$elem});
302             }
303             }
304             } else {
305             if ($allow_subset) {
306             foreach my $rowElem (@rowElems) {
307             if (!exists($h_ids->{$rowElem})) {
308             $h_ids->{$rowElem} = @{$h_elems};
309             push(@{$h_elems}, $rowElem);
310             }
311             }
312             } else {
313             croak("Number of elements in header does not match number of elemens in row")
314             if keys(%{$h_ids}) != keys(%rows);
315             foreach my $elem (keys(%{$h_ids})) {
316             croak("'$elem': row missing for element") if !exists($rows{$elem});
317             }
318             }
319             my %tmp = %{$h_ids};
320             @{$self}{qw(elems elem_ids tab_elems eq_ids)} = ($h_elems, $h_ids, \%tmp, {});
321             $elem_ids = $h_ids;
322             }
323             my $eq_ids = $self->{eq_ids};
324             my %matrix;
325             while (my ($rowElem, $rowContents) = each(%rows)) {
326             my %new_row;
327             for (my $i = 0; $i < @{$rowContents}; $i++) {
328             if ($rowContents->[$i]) {
329             my $e_id = $elem_ids->{$h_elems->[$i]};
330             $new_row{$e_id} = undef;
331             if (exists($eq_ids->{$e_id})) {
332             foreach my $eq_id (@{$eq_ids->{$e_id}}) {
333             $new_row{$eq_id} = undef
334             }
335             }
336             }
337             }
338             if (%new_row) {
339             $matrix{$elem_ids->{$rowElem}} = \%new_row;
340             if (exists($eq_ids->{$rowElem})) {
341             foreach my $eq_id (@{$eq_ids->{$rowElem}}) {
342             $matrix{$eq_id} = {%new_row};
343             }
344             }
345             }
346             }
347             $self->{matrix} = \%matrix;
348             return;
349             };
350              
351              
352             # just a function, not a method.
353             sub _get_from_str {
354 38 100   38   75 if (${$_[0]} !~ /\n/) {
  38         199  
355 5         14 open(my $h, '<', ${$_[0]});
  5         35  
356 5         3319 my $inputArray = [<$h>];
357 5         39 close($h);
358 5         1679 return $inputArray;
359             } else {
360 33         75 return [split(/\n/, ${$_[0]})];
  33         263  
361             }
362             }
363              
364             sub get {
365 82     82 1 55809 my $self = shift;
366 82 100       502 croak("Odd number of arguments") if @_ % 2;
367 81         290 my %args = @_;
368 81         182 my $allow_subset = delete $args{allow_subset};
369 81         174 my $pedantic = delete $args{pedantic};
370 81 100       382 croak("Missing argument 'src'") if !@_;
371 80   66     416 my $src = delete $args{src} // croak("Invalid value argument for 'src'");
372 79 100       644 croak(join(", ", sort(keys(%args))) . ": unexpected argument") if %args;
373 77         141 my $inputArray;
374 77 100       274 if (my $ref_str = ref($src)) {
375 41 100       143 if ($ref_str eq 'ARRAY') {
    50          
    100          
376 38         72 foreach my $e (@{$src}) {
  38         96  
377 170 100 66     671 croak("src: each entry must be a defined scalar") if (ref($e) || !defined($e));
378             }
379 37         70 $inputArray = $src;
380             } elsif ($ref_str eq 'GLOB') {
381 0         0 $inputArray = [<$src>];
382             } elsif ($ref_str eq 'SCALAR') {
383 2         7 $inputArray = _get_from_str($src);
384             }
385             else {
386 1         146 croak("Invalid value argument for 'src'");
387             }
388             } else {
389 36         126 $inputArray = _get_from_str(\$src);
390             }
391 75 100       329 $self->$_reset() if !$self->{prespec};
392 75         348 $self->$_parse_table($inputArray, $allow_subset, $pedantic);
393 46 100       193 return wantarray ? @{$self}{qw(matrix elems elem_ids)} : $self;
  22         213  
394             }
395              
396              
397 3 100   3 1 2760 sub inc {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{inc};}
  2         19  
398 3 100   3 1 1014 sub noinc {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{noinc};}
  2         12  
399 12 100   12 1 1068 sub prespec {croak("Unexpected argument(s)") if @_ > 1; $_[0]->{prespec};}
  11         80  
400              
401              
402             sub elems {
403 36 100   36 1 11976 croak("Unexpected argument(s)") if @_ > 2;
404              
405 35 100       115 if ($_[1]) {
406 2 100       13 return defined($_[0]->{elems}) ? [@{$_[0]->{elems}}] : undef;
  1         9  
407             } else {
408 33         273 return $_[0]->{elems};
409             }
410             }
411              
412              
413             sub elem_ids {
414 44 100   44 1 4308 croak("Unexpected argument(s)") if @_ > 2;
415 43         203 _ret_hash($_[0]->{elem_ids}, $_[1]);
416             }
417              
418              
419             sub tab_elems {
420 33 50   33 1 27582 croak("Unexpected argument(s)") if @_ > 2;
421 33         152 _ret_hash($_[0]->{tab_elems}, $_[1]);
422             }
423              
424              
425             #hash of array refs
426             sub eq_ids {
427 24 50   24 1 3034 croak("Unexpected argument(s)") if @_ > 2;
428              
429 24         71 my ($self, $dup) = @_;
430 24 100       81 if ($dup) {
431 2 100       14 return undef if !$self->{eq_ids};
432 1         3 return {map {$_ => [@{$self->{eq_ids}{$_}}]} keys(%{$self->{eq_ids}})};
  2         5  
  2         14  
  1         7  
433             } else {
434 22         141 return $self->{eq_ids};
435             }
436             }
437              
438              
439             sub matrix {
440 30     30 1 8968 my $self = shift;
441 30 100       274 croak("Odd number of arguments") if @_ % 2;
442 29         104 my %args = @_;
443 29         71 my $bless = delete $args{bless};
444 29         72 my $dup = delete $args{dup};
445 29 100       237 croak("Unexpected argument(s)") if %args;
446 28 100       116 return if !$self->{matrix};
447 25         46 my $matrix;
448 25 100       79 if ($dup) {
449 1         4 while (my ($key, $value) = each(%{$self->{matrix}})) {
  5         23  
450 4         44 $matrix->{$key} = {%$value}
451             }
452             } else {
453 24         57 $matrix = $self->{matrix};
454             }
455 25 100       75 bless($matrix, "Text::Table::Read::RelationOn::Tiny::_Relation_Matrix") if $bless;
456 25         187 return $matrix;
457             }
458              
459              
460             sub matrix_named {
461 11     11 1 6467 my $self = shift;
462 11 100       182 croak("Odd number of arguments") if @_ % 2;
463 10         33 my %args = @_;
464 10         30 my $bless = delete $args{bless};
465 10 100       175 croak("Unexpected argument(s)") if %args;
466              
467 9         20 my ($matrix, $elems) = @{$self}{qw(matrix elems)};
  9         32  
468 9 100       35 return if !$matrix;
469 7         15 my $matrix_named = {};
470 7 100       30 bless($matrix_named, "Text::Table::Read::RelationOn::Tiny::_Relation_Matrix") if $bless;
471 7         17 while (my ($rowElemIdx, $rowContents) = each(%{$matrix})) {
  25         82  
472 18         26 $matrix_named->{$elems->[$rowElemIdx]} = {map {$elems->[$_] => undef} keys(%{$rowContents})};
  77         211  
  18         42  
473             }
474 7         66 return $matrix_named;
475             }
476              
477              
478              
479             {
480             package # hide from pause
481             Text::Table::Read::RelationOn::Tiny::_Relation_Matrix;
482              
483 4   66 4   1946 sub related { return exists($_[0]->{$_[1]}) && exists($_[0]->{$_[1]}->{$_[2]}); }
484             }
485              
486              
487             1; # End of Text::Table::Read::RelationOn::Tiny
488              
489              
490              
491             __END__