File Coverage

blib/lib/Graph/Graph6.pm
Criterion Covered Total %
statement 298 331 90.0
branch 155 196 79.0
condition 50 68 73.5
subroutine 23 23 100.0
pod 2 2 100.0
total 528 620 85.1


line stmt bran cond sub pod time code
1             # Copyright 2015, 2016, 2017 Kevin Ryde
2             #
3             # This file is part of Graph-Graph6.
4             #
5             # Graph-Graph6 is free software; you can redistribute it and/or modify it under
6             # the terms of the GNU General Public License as published by the Free
7             # Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Graph-Graph6 is distributed in the hope that it will be useful, but WITHOUT
11             # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12             # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13             # more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Graph-Graph6. If not, see .
17              
18             package Graph::Graph6;
19 9     9   7279 use 5.006; # for 3-arg open
  9         32  
20 9     9   45 use strict;
  9         16  
  9         162  
21 9     9   66 use warnings;
  9         25  
  9         266  
22 9     9   85 use List::Util 'max';
  9         22  
  9         572  
23 9     9   52 use Carp 'croak';
  9         16  
  9         369  
24              
25 9     9   92 use Exporter;
  9         28  
  9         614  
26             our @ISA = ('Exporter');
27             our @EXPORT_OK = ('read_graph','write_graph',
28             'HEADER_GRAPH6','HEADER_SPARSE6','HEADER_DIGRAPH6');
29              
30             our $VERSION = 7;
31              
32             # uncomment this to run the ### lines
33             # use Smart::Comments;
34              
35              
36 9     9   56 use constant HEADER_GRAPH6 => '>>graph6<<';
  9         18  
  9         509  
37 9     9   60 use constant HEADER_SPARSE6 => '>>sparse6<<';
  9         22  
  9         337  
38 9     9   44 use constant HEADER_DIGRAPH6 => '>>digraph6<<';
  9         19  
  9         17348  
39              
40             sub _read_header {
41 2     2   5 my ($fh, $str) = @_;
42 2         3 for (;;) {
43 5         12 my $s2 = getc $fh;
44 5 50       104 if (! defined $str) { return; }
  0         0  
45              
46 5         6 $str .= $s2;
47 5 100       19 if ($str eq substr(HEADER_GRAPH6, 0, length($str))) {
    50          
    50          
48 3 50       10 if (length($str) == length(HEADER_GRAPH6)) {
49             ### header: $str
50             # $format = 'graph6';
51 0         0 return;
52             }
53              
54             } elsif ($str eq substr(HEADER_SPARSE6, 0, length($str))) {
55 0 0       0 if (length($str) == length(HEADER_SPARSE6)) {
56             ### header: $str
57             # $format = 'sparse6';
58 0         0 return;
59             }
60              
61             } elsif ($str eq substr(HEADER_DIGRAPH6, 0, length($str))) {
62 0 0       0 if (length($str) == length(HEADER_DIGRAPH6)) {
63             ### header: $str
64             # $format = 'digraph6';
65 0         0 return;
66             }
67             } else {
68 2         4 return $str;
69             }
70             }
71             }
72              
73             sub read_graph {
74 39     39 1 11535 my %options = @_;
75              
76 39         82 my $fh = $options{'fh'};
77 39 100       108 if (defined $options{'str'}) {
78 28         1318 require IO::String;
79 28         8899 $fh = IO::String->new($options{'str'});
80             }
81              
82 39         1085 my $skip_newlines = 1;
83 39         64 my $allow_header = 1;
84 39         75 my $format = 'graph6';
85 39         57 my $initial = 1;
86 39         45 my $error;
87              
88             # Return: byte 0 to 63
89             # or -1 and $error=undef if end of file
90             # or -1 and $error=string if something bad
91             my $read_byte = sub {
92 98     98   136 for (;;) {
93 129         197 my $str;
94 129         367 my $len = read($fh, $str, 1);
95 129 50       1612 if (! defined $len) {
96 0         0 $error = "Error reading: $!";
97 0         0 return -1;
98             }
99             ### read byte: $str
100              
101 129 100 100     400 if ($skip_newlines && $str eq "\n") {
102             # secret undocumented skipping of newlines, so skip blank lines
103             # rather than reckoning one newline as immediate end of file
104             ### skip initial newline ...
105 9         12 next;
106             }
107 120         168 $skip_newlines = 0;
108              
109 120 100 100     328 if ($allow_header && $str eq '>') {
110 2         6 $str = _read_header($fh, $str);
111 2 50       5 if (defined $str) {
112 2         5 $error = "Incomplete header: $str";
113 2         2 return -1;
114             }
115 0         0 $allow_header = 0;
116 0         0 next;
117             }
118 118         172 $allow_header = 0;
119              
120 118         184 my $n = ord($str) - 63;
121 118 100 66     404 if ($n >= 0 && $n <= 63) {
122 81         201 return $n;
123             }
124              
125 37 100 100     139 if ($str eq '' || $str eq "\n") {
126             ### end of file or end of line ...
127 8         39 return -1;
128             }
129              
130 29 100 100     118 if ($initial && $str eq '&') {
131 3         4 $format = 'digraph6';
132             ### $format
133 3         4 $initial = 0;
134 3         6 next;
135             }
136 26 100 100     94 if ($initial && $str eq ':') {
137 19         46 $format = 'sparse6';
138             ### $format
139 19         27 $initial = 0;
140 19         36 next;
141             }
142 7 50       19 if ($str eq "\r") {
143             ### skip CR ...
144 0         0 next;
145             }
146              
147 7         13 $error = "Unrecognised character: $str";
148 7         13 return -1;
149             }
150 39         157 };
151              
152             # Return: number 0 to 2^36-1
153             # -1 and $error=undef if end of file before any part of number
154             # -1 and $error if something bad, including partial number
155             my $read_number = sub {
156 38     38   64 my $n = $read_byte->();
157 38         58 $initial = 0;
158 38 100       80 if ($n <= 62) {
159 37         62 return $n;
160             }
161 1         3 $n = $read_byte->();
162 1 50       4 if ($n < 0) {
163 1   50     6 $error ||= "Unexpected EOF";
164 1         2 return -1;
165             }
166 0         0 my $len;
167 0 0       0 if ($n <= 62) {
168 0         0 $len = 2;
169             } else {
170 0         0 $n = 0;
171 0         0 $len = 6;
172             }
173 0         0 foreach (1 .. $len) {
174 0         0 my $n2 = $read_byte->();
175 0 0       0 if ($n2 < 0) {
176 0   0     0 $error ||= "Unexpected EOF";
177 0         0 return -1;
178             }
179 0         0 $n = ($n << 6) + $n2;
180             }
181 0         0 return $n;
182 39         116 };
183              
184             # Return true if good.
185             # Return false and $error=string if something bad.
186             # Return false and $error=undef if EOF.
187             my $read = sub {
188              
189 39 100   39   93 if (! defined $fh) {
190 8 50       21 if (defined(my $filename = $options{'filename'})) {
191             open $fh, '<', $filename
192 8 100       168 or do {
193 1         22 $error = "Cannot open file $filename: $!";
194 1         4 return;
195             };
196             }
197             }
198              
199 38         77 my $num_vertices = $read_number->();
200             ### $num_vertices
201              
202 38 100       93 if (my $format_func = $options{'format_func'}) {
203 13         36 $format_func->($format);
204             }
205 38 50       587 if (my $format_ref = $options{'format_ref'}) {
206 0         0 $$format_ref = $format;
207             }
208              
209 38 100       85 if ($num_vertices < 0) {
210 11         31 return; # eof or possible error
211             }
212 27 100       64 if (my $num_vertices_func = $options{'num_vertices_func'}) {
213 18         52 $num_vertices_func->($num_vertices);
214             }
215 27 100       475 if (my $num_vertices_ref = $options{'num_vertices_ref'}) {
216 8         14 $$num_vertices_ref = $num_vertices;
217             }
218              
219 27         40 my $edge_func = $options{'edge_func'};
220 27         40 my $edge_aref = $options{'edge_aref'};
221 27 100       60 if ($edge_aref) { @$edge_aref = (); }
  7         14  
222              
223             ### $format
224 27 100       63 if ($format eq 'sparse6') {
225             ### sparse6 ...
226 18         27 my $v = 0;
227              
228             # number of bits required to represent $num_vertices - 1
229 18         22 my $width = 0;
230 18         50 while (($num_vertices-1) >> $width) { $width++; }
  38         80  
231              
232 18         26 my $bits = 0;
233 18         27 my $n = 0;
234 18         32 my $mask = (1 << $width) - 1;
235              
236 18         49 while ($v < $num_vertices) {
237 71 100       149 if ($bits < 1) {
238 29         59 $n = $read_byte->();
239 29 100       67 if ($n < 0) {
240             ### end n ...
241             ### $error
242 5         17 return ! defined $error;
243             }
244 24         33 $bits = 6;
245             }
246 66         104 $bits--;
247 66         107 my $b = ($n >> $bits) & 1; # first bit from $n
248 66         93 $v += $b; # propagate possible taintedness of $n,$b to $v
249             ### $b
250             ### to v: $v
251              
252 66         136 while ($bits < $width) {
253 15         31 my $n2 = $read_byte->();
254 15 100       34 if ($n2 < 0) {
255             ### end n2 ...
256             ### $error
257 1         4 return ! defined $error;
258             }
259 14         20 $bits += 6;
260 14         32 $n <<= 6;
261 14         33 $n |= $n2;
262             }
263 65         90 $bits -= $width;
264 65         94 my $x = ($n >> $bits) & $mask;
265             ### $x
266              
267 65 100       168 if ($x > $v) {
    100          
268             ### set v: $x
269 11         28 $v = $x;
270             } elsif ($v < $num_vertices) { # padding can make v>n-1
271             ### edge: "$x - $v"
272 42 100       85 if ($edge_func) { $edge_func->($x, $v); }
  25         64  
273 42 100       1387 if ($edge_aref) { push @$edge_aref, [$x, $v]; }
  16         41  
274             }
275             }
276             ### end ...
277              
278             } else {
279             ### graph6 or digraph6 ...
280 9         34 my $n;
281             my $mask;
282 9         0 my $from;
283 9         0 my $to;
284             my $output_edge = sub {
285 67 100       147 if ($n & $mask) {
286 22         39 my $taint0 = $n & 0;
287 22         39 my $from_taint = $from + $taint0;
288 22         70 my $to_taint = $to + $taint0;
289 22 100       53 if ($edge_func) { $edge_func->( $from_taint, $to_taint); }
  17         40  
290 22 100       693 if ($edge_aref) { push @$edge_aref, [$from_taint, $to_taint]; }
  1         3  
291             }
292 9         34 };
293              
294 9 100       24 if ($format eq 'graph6') {
295             # graph6 goes by columns of "to" within which "from" runs 0 though to-1
296             # first column is to=1
297 8         14 $from = 0;
298 8         11 $to = 1;
299 8         21 while ($to < $num_vertices) {
300 10 50       24 if (($n = $read_byte->()) < 0) {
301 0   0     0 $error ||= "Unexpected EOF"; # end of file is not ok
302 0         0 return;
303             }
304 10         29 for ($mask = 1 << 5; $mask != 0; $mask >>= 1) {
305 42         88 $output_edge->();
306 42         57 $from++;
307 42 100       104 if ($from >= $to) {
308 18         22 $to++;
309 18 100       48 last unless $to < $num_vertices;
310 12         30 $from = 0;
311             }
312             }
313             }
314             } else {
315             # graph6 goes by rows of "from", within which "to" runs 0 to n-1
316 1         2 $from = 0;
317 1         2 $to = 0;
318 1         3 while ($from < $num_vertices) {
319 5 50       9 if (($n = $read_byte->()) < 0) {
320 0   0     0 $error ||= "Unexpected EOF"; # end of file is not ok
321 0         0 return;
322             }
323 5         12 for ($mask = 1 << 5; $mask != 0; $mask >>= 1) {
324 25         43 $output_edge->();
325 25         29 $to++;
326 25 100       64 if ($to >= $num_vertices) {
327 5         9 $from++;
328 5 100       10 last unless $from < $num_vertices;
329 4         8 $to = 0;
330             }
331             }
332             }
333             }
334              
335             # read \n or \r\n, so can take successive graphs from file handle
336 9         16 for (;;) {
337 10         16 my $str;
338 10         32 my $len = read($fh, $str, 1);
339 10 50       127 if (! defined $len) {
340 0         0 $error = "Error reading: $!";
341 0         0 last;
342             }
343 10 100       36 if ($str eq "\r") {
344 1         2 next; # skip CR in case reading MS-DOS file as bytes
345             }
346 9 50 66     39 if ($str eq '' || $str eq "\n") {
347 9         45 last; # EOF or EOL, good
348             }
349             }
350             }
351              
352 21         57 return 1;
353 39         170 };
354              
355              
356 39 100       84 if ($read->()) {
357 26         623 return 1; # successful read
358             }
359 13 100       34 if (defined $error) {
360             ### $error
361 11   100     29 my $error_func = $options{'error_func'} || \&Carp::croak;
362 11         206 $error_func->($error);
363 10         225 return undef;
364             }
365 2         40 return 0; # EOF
366             }
367              
368             #------------------------------------------------------------------------------
369              
370             # For internal use.
371             # Biggest shift is by (6-1)*6 = 30 bits, so ok in 32-bit Perls circa 5.8 and
372             # earlier (where counts were taken modulo 32, not full value).
373             sub _number_to_string {
374 73     73   28032 my ($n) = @_;
375 73         110 my $str;
376             my $bitpos;
377 73 100       188 if ($n > 258047) { # binary 0b_111110_111111_111111 octal 0767777
    100          
378 7         389 $str = '~~';
379 7         11 $bitpos = (6-1)*6;
380             } elsif ($n > 62) {
381 1         3 $str = '~';
382 1         1 $bitpos = (3-1)*6;
383             } else {
384 65         104 $str = '';
385 65         90 $bitpos = 0;
386             }
387 73         191 for ( ; $bitpos >= 0; $bitpos -= 6) { # big endian, high to low
388 110         12211 $str .= chr( (($n >> $bitpos) & 0x3F) + 63 );
389             }
390 73         2820 return $str;
391             }
392              
393             sub _edges_iterator_none {
394 2     2   7 return;
395             }
396             sub _edge_predicate_none {
397 1     1   7 return 0;
398             }
399              
400             sub write_graph {
401 64     64 1 6049 my %options = @_;
402             ### %options
403              
404 64         132 my $fh = $options{'fh'};
405 64 100 100     292 if (! $fh
406             && defined(my $str_ref = $options{'str_ref'})) {
407             ### str_ref ...
408 44         962 require IO::String;
409 44         5537 $fh = IO::String->new($$str_ref);
410             }
411 64 100 66     1842 if (! $fh
412             && defined(my $filename = $options{'filename'})) {
413             ### $filename
414 2 50       159 open $fh, '>', $filename
415             or return 0;
416             }
417              
418 64         122 my $format = $options{'format'};
419 64 100       152 if (! defined $format) { $format = 'graph6'; }
  8         12  
420              
421 64         98 my $num_vertices = $options{'num_vertices'};
422 64 100 66     165 if (! defined $num_vertices
423             && (my $edge_aref = $options{'edge_aref'})) {
424             # from maximum in edge_aref
425 2         5 $num_vertices = -1;
426 2         5 foreach my $edge (@$edge_aref) {
427 2         15 $num_vertices = max($num_vertices, @$edge);
428             }
429 2         3 $num_vertices += 1;
430             }
431 64 50       136 if (! defined $num_vertices) {
432 0         0 croak 'Missing num_vertices';
433             }
434             ### $num_vertices
435              
436             print $fh
437 64 100       261 ($options{'header'} ? ">>$format<<" : ()),
    100          
    100          
    50          
438             ($format eq 'sparse6' ? ':'
439             : $format eq 'digraph6' ? '&'
440             : ()),
441             _number_to_string($num_vertices)
442             or return 0;
443              
444 64         1102 my $bitpos = 5;
445 64         92 my $word = 0;
446             my $put_bit = sub {
447 654     654   5940 my ($bit) = @_;
448 654         925 $word |= $bit << $bitpos;
449 654 100       1057 if ($bitpos > 0) {
450 545         730 $bitpos--;
451             } else {
452 109 50       334 print $fh chr($word + 63) or return 0;
453 109         1660 $bitpos = 5;
454 109         148 $word = 0;
455             }
456 654         1769 return 1;
457 64         259 };
458              
459 64 100       150 if ($format eq 'sparse6') {
460 30         42 my $edge_iterator;
461              
462 30 100       80 if (my $edge_aref = $options{'edge_aref'}) {
463             ### edge_aref ...
464             # swap to [from <= to]
465 27 100       51 my @edges = map { $_->[0] > $_->[1]
  55         143  
466             ? [ $_->[1], $_->[0] ]
467             : $_
468             } @$edge_aref;
469             # sort to ascending "to", and within those ascending "from"
470 27 50       68 @edges = sort { ($a->[1] <=> $b->[1]) || ($a->[0] <=> $b->[0]) } @edges;
  42         125  
471             $edge_iterator = sub {
472 82 100   82   95 return @{(shift @edges) || []};
  82         345  
473 27         75 };
474             }
475              
476 30 100 100     96 if (! $edge_iterator
477             && (my $edge_predicate = $options{'edge_predicate'})) {
478             ### edge_predicate ...
479 1         2 my $from = 0;
480 1         3 my $to = -1;
481             $edge_iterator = sub {
482 2     2   4 for (;;) {
483 497         2368 $from++;
484 497 100       944 if ($from > $to) {
485 32         45 $to++;
486 32 100       64 if ($to >= $num_vertices) {
487 1         6 return;
488             }
489 31         46 $from = 0;
490             }
491 496 100       854 if ($edge_predicate->($from,$to)) {
492 1         10 return ($from,$to);
493             }
494             }
495 1         3 };
496             }
497              
498 30   100     77 $edge_iterator ||= \&_edges_iterator_none;
499              
500             # $width = number of bits required to represent $num_vertices - 1
501 30         46 my $width = 0;
502 30 100       68 if ($num_vertices > 0) {
503 29         89 while (($num_vertices-1) >> $width) { $width++; }
  59         119  
504             }
505             ### $width
506              
507             my $put_n = sub {
508 63     63   95 my ($n) = @_;
509 63         139 for (my $i = $width-1; $i >= 0; $i--) {
510 145 50       255 $put_bit->(($n >> $i) & 1) or return 0;
511             }
512 63         142 return 1;
513 30         87 };
514              
515 30         44 my $v = 0;
516 30         66 while (my ($from, $to) = $edge_iterator->()) {
517             ### edge: "$from $to"
518              
519 56 100       116 if ($to == $v + 1) {
520             ### increment v ...
521 30 50       54 $put_bit->(1) or return 0;
522              
523             } else {
524 26 100       74 if ($to != $v) { # $to >= $v+2
525             ### set v ...
526 7 50 33     15 ($put_bit->(1) # set v done with b[i]=1
527             && $put_n->($to))
528             or return 0;
529             }
530 26 50       50 $put_bit->(0) or return 0; # v unchanged
531             }
532             ### write: $from
533 56 50       100 $put_n->($from) or return 0; # edge ($from, $v)
534              
535 56         111 $v = $to;
536             }
537              
538 30 100       94 if ($bitpos != 5) {
539             ### pad: $bitpos+1
540             ### $v
541              
542             # Rule for padding so not to look like self-loop n-1 to n-1.
543             # There are $bitpos+1 many bits to pad.
544             # b[i]=0 bit if num_vertices = 2,4,8,16 so width=1,2,3,4
545             # and pad >= width+1
546             # and edge involving n-2 so final v=n-2
547             # 0 111 is set v=n-1 provided prev <= n-2
548             # 1 111 is a v+1 and edge n-1,v which is n-1,n out of range
549 22 50 33     181 if (($width >= 1 && $width <= 4)
      66        
      66        
      66        
550             && $num_vertices == (1 << $width) # 1,2,4,8
551             && $bitpos >= $width # room for final b[i] and x[i]
552             && $v == $num_vertices - 2) {
553             ### pad 0 ...
554 0 0       0 $put_bit->(0) or return 0;
555             }
556              
557             ### pad with 1s: $bitpos
558 22         54 until ($bitpos == 5) {
559 74 50       121 $put_bit->(1) or return 0;
560             }
561             }
562              
563             } else {
564 34         64 my $edge_predicate = $options{'edge_predicate'};
565              
566 34 100 100     113 if (! $edge_predicate
567             && (my $edge_aref = $options{'edge_aref'})) {
568             ### edge_predicate from edge_aref ...
569 7         17 my %edge_hash;
570 7         17 foreach my $edge (@$edge_aref) {
571 17         35 my ($from, $to) = @$edge;
572 17 100 100     62 if ($from > $to && $format eq 'graph6') { ($from,$to) = ($to,$from); }
  2         13  
573 17         59 $edge_hash{$from}->{$to} = undef;
574             }
575             $edge_predicate = sub {
576 65     65   111 my ($from, $to) = @_;
577 65         185 return exists $edge_hash{$from}->{$to};
578 7         33 };
579             }
580              
581 34   100     91 $edge_predicate ||= \&_edge_predicate_none;
582              
583 34 100       84 if ($format eq 'graph6') {
    50          
584 22         52 foreach my $to (1 .. $num_vertices-1) {
585 47         84 foreach my $from (0 .. $to-1) {
586 92 100       201 $put_bit->($edge_predicate->($from,$to) ? 1 : 0) or return 0;
    50          
587             }
588             }
589             } elsif ($format eq 'digraph6') {
590 12         47 foreach my $from (0 .. $num_vertices-1) {
591 44         87 foreach my $to (0 .. $num_vertices-1) {
592 172 100       363 $put_bit->($edge_predicate->($from,$to) ? 1 : 0) or return 0;
    50          
593             }
594             }
595             } else {
596 0         0 croak 'Unrecognised format: ',$format;
597             }
598              
599 34         97 until ($bitpos == 5) {
600 108 50       173 $put_bit->(0) or return 0;
601             }
602             }
603              
604 64 50       175 print $fh "\n" or return 0;
605 64         1228 return 1;
606             }
607              
608             # if (! $edge_predicate
609             # && (my $edge_matrix = $options{'edge_matrix'})) {
610             # $edge_predicate = sub {
611             # my ($from, $to) = @_;
612             # return $edge_matrix->[$from]->[$to];
613             # };
614             # }
615              
616             1;
617             __END__