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, 2018 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   8384 use 5.006; # for 3-arg open
  9         41  
20 9     9   47 use strict;
  9         20  
  9         208  
21 9     9   44 use warnings;
  9         17  
  9         265  
22 9     9   48 use List::Util 'max';
  9         26  
  9         749  
23 9     9   60 use Carp 'croak';
  9         33  
  9         457  
24              
25 9     9   98 use Exporter;
  9         29  
  9         870  
26             our @ISA = ('Exporter');
27             our @EXPORT_OK = ('read_graph','write_graph',
28             'HEADER_GRAPH6','HEADER_SPARSE6','HEADER_DIGRAPH6');
29              
30             our $VERSION = 8;
31              
32             # uncomment this to run the ### lines
33             # use Smart::Comments;
34              
35              
36 9     9   92 use constant HEADER_GRAPH6 => '>>graph6<<';
  9         29  
  9         842  
37 9     9   77 use constant HEADER_SPARSE6 => '>>sparse6<<';
  9         18  
  9         466  
38 9     9   56 use constant HEADER_DIGRAPH6 => '>>digraph6<<';
  9         19  
  9         24900  
39              
40             sub _read_header {
41 2     2   5 my ($fh, $str) = @_;
42 2         3 for (;;) {
43 5         16 my $s2 = getc $fh;
44 5 50       103 if (! defined $str) { return; }
  0         0  
45              
46 5         8 $str .= $s2;
47 5 100       31 if ($str eq substr(HEADER_GRAPH6, 0, length($str))) {
    50          
    50          
48 3 50       9 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         5 return $str;
69             }
70             }
71             }
72              
73             sub read_graph {
74 43     43 1 7250 my %options = @_;
75              
76 43         94 my $fh = $options{'fh'};
77 43 100       120 if (defined $options{'str'}) {
78 32         1852 require IO::String;
79 32         12156 $fh = IO::String->new($options{'str'});
80             }
81              
82 43         1325 my $skip_newlines = 1;
83 43         77 my $allow_header = 1;
84 43         67 my $format = 'graph6';
85 43         63 my $initial = 1;
86 43         59 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 117     117   163 for (;;) {
93 152         183 my $str;
94 152         505 my $len = read($fh, $str, 1);
95 152 50       1991 if (! defined $len) {
96 0         0 $error = "Error reading: $!";
97 0         0 return -1;
98             }
99             ### read byte: $str
100              
101 152 100 100     390 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         16 next;
106             }
107 143         180 $skip_newlines = 0;
108              
109 143 100 100     317 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         4 return -1;
114             }
115 0         0 $allow_header = 0;
116 0         0 next;
117             }
118 141         172 $allow_header = 0;
119              
120 141         203 my $n = ord($str) - 63;
121 141 100 66     396 if ($n >= 0 && $n <= 63) {
122 96         216 return $n;
123             }
124              
125 45 100 100     139 if ($str eq '' || $str eq "\n") {
126             ### end of file or end of line ...
127 12         24 return -1;
128             }
129              
130 33 100 100     91 if ($initial && $str eq '&') {
131 3         6 $format = 'digraph6';
132             ### $format
133 3         4 $initial = 0;
134 3         6 next;
135             }
136 30 100 100     91 if ($initial && $str eq ':') {
137 23         37 $format = 'sparse6';
138             ### $format
139 23         39 $initial = 0;
140 23         44 next;
141             }
142 7 50       18 if ($str eq "\r") {
143             ### skip CR ...
144 0         0 next;
145             }
146              
147 7         14 $error = "Unrecognised character: $str";
148 7         13 return -1;
149             }
150 43         196 };
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 42     42   65 my $n = $read_byte->();
157 42         69 $initial = 0;
158 42 100       143 if ($n <= 62) {
159 41         71 return $n;
160             }
161 1         2 $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 43         133 };
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 43 100   43   111 if (! defined $fh) {
190 8 50       26 if (defined(my $filename = $options{'filename'})) {
191             open $fh, '<', $filename
192 8 100       402 or do {
193 1         25 $error = "Cannot open file $filename: $!";
194 1         5 return;
195             };
196             }
197             }
198              
199 42         99 my $num_vertices = $read_number->();
200             ### $num_vertices
201              
202 42 100       105 if (my $format_func = $options{'format_func'}) {
203 13         42 $format_func->($format);
204             }
205 42 50       639 if (my $format_ref = $options{'format_ref'}) {
206 0         0 $$format_ref = $format;
207             }
208              
209 42 100       84 if ($num_vertices < 0) {
210 11         25 return; # eof or possible error
211             }
212 31 100       73 if (my $num_vertices_func = $options{'num_vertices_func'}) {
213 18         55 $num_vertices_func->($num_vertices);
214             }
215 31 100       524 if (my $num_vertices_ref = $options{'num_vertices_ref'}) {
216 12         17 $$num_vertices_ref = $num_vertices;
217             }
218              
219 31         57 my $edge_func = $options{'edge_func'};
220 31         48 my $edge_aref = $options{'edge_aref'};
221 31 100       98 if ($edge_aref) { @$edge_aref = (); }
  11         18  
222              
223             ### $format
224 31 100       78 if ($format eq 'sparse6') {
225             ### sparse6 ...
226 22         32 my $v = 0;
227              
228             # number of bits required to represent $num_vertices - 1
229 22         30 my $width = 0;
230 22         54 while (($num_vertices-1) >> $width) { $width++; }
  48         87  
231              
232 22         26 my $bits = 0;
233 22         29 my $n = 0;
234 22         44 my $mask = (1 << $width) - 1;
235              
236 22         44 while ($v < $num_vertices) {
237 93 100       162 if ($bits < 1) {
238 40         90 $n = $read_byte->();
239 40 100       87 if ($n < 0) {
240             ### end n ...
241             ### $error
242 8         29 return ! defined $error;
243             }
244 32         40 $bits = 6;
245             }
246 85         108 $bits--;
247 85         152 my $b = ($n >> $bits) & 1; # first bit from $n
248 85         98 $v += $b; # propagate possible taintedness of $n,$b to $v
249             ### $b
250             ### to v: $v
251              
252 85         144 while ($bits < $width) { # fill $n,$bits to >= $width many bits
253 19         32 my $n2 = $read_byte->();
254 19 100       39 if ($n2 < 0) {
255             ### end n2 ...
256             ### $error
257 2         6 return ! defined $error;
258             }
259 17         24 $bits += 6;
260 17         22 $n <<= 6;
261 17         34 $n |= $n2;
262             }
263 83         130 $bits -= $width;
264 83         113 my $x = ($n >> $bits) & $mask;
265             ### $x
266              
267 83 100       165 if ($x > $v) {
    100          
268             ### set v: $x
269 19         37 $v = $x;
270             } elsif ($v < $num_vertices) { # padding can make v>n-1
271             ### edge: "$x - $v"
272 52 100       99 if ($edge_func) { $edge_func->($x, $v); }
  25         55  
273 52 100       1519 if ($edge_aref) { push @$edge_aref, [$x, $v]; }
  26         87  
274             }
275             }
276             ### end ...
277              
278             } else {
279             ### graph6 or digraph6 ...
280 9         40 my $n;
281             my $mask;
282 9         0 my $from;
283 9         0 my $to;
284             my $output_edge = sub {
285 67 100       155 if ($n & $mask) {
286 22         47 my $taint0 = $n & 0;
287 22         41 my $from_taint = $from + $taint0;
288 22         35 my $to_taint = $to + $taint0;
289 22 100       49 if ($edge_func) { $edge_func->( $from_taint, $to_taint); }
  17         45  
290 22 100       853 if ($edge_aref) { push @$edge_aref, [$from_taint, $to_taint]; }
  1         3  
291             }
292 9         72 };
293              
294 9 100       31 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         13 $to = 1;
299 8         23 while ($to < $num_vertices) {
300 10 50       23 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         38 for ($mask = 1 << 5; $mask != 0; $mask >>= 1) {
305 42         95 $output_edge->();
306 42         64 $from++;
307 42 100       101 if ($from >= $to) {
308 18         28 $to++;
309 18 100       52 last unless $to < $num_vertices;
310 12         43 $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         4 while ($from < $num_vertices) {
319 5 50       8 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         14 for ($mask = 1 << 5; $mask != 0; $mask >>= 1) {
324 25         44 $output_edge->();
325 25         32 $to++;
326 25 100       58 if ($to >= $num_vertices) {
327 5         6 $from++;
328 5 100       12 last unless $from < $num_vertices;
329 4         7 $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         15 my $str;
338 10         37 my $len = read($fh, $str, 1);
339 10 50       136 if (! defined $len) {
340 0         0 $error = "Error reading: $!";
341 0         0 last;
342             }
343 10 100       41 if ($str eq "\r") {
344 1         2 next; # skip CR in case reading MS-DOS file as bytes
345             }
346 9 50 66     42 if ($str eq '' || $str eq "\n") {
347 9         57 last; # EOF or EOL, good
348             }
349             }
350             }
351              
352 21         56 return 1;
353 43         248 };
354              
355              
356 43 100       97 if ($read->()) {
357 30         851 return 1; # successful read
358             }
359 13 100       27 if (defined $error) {
360             ### $error
361 11   100     31 my $error_func = $options{'error_func'} || \&Carp::croak;
362 11         277 $error_func->($error);
363 10         281 return undef;
364             }
365 2         54 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 77     77   51364 my ($n) = @_;
375 77         118 my $str;
376             my $bitpos;
377 77 100       202 if ($n > 258047) { # binary 0b_111110_111111_111111 octal 0767777
    100          
378 7         554 $str = '~~';
379 7         12 $bitpos = (6-1)*6;
380             } elsif ($n > 62) {
381 1         2 $str = '~';
382 1         1 $bitpos = (3-1)*6;
383             } else {
384 69         121 $str = '';
385 69         90 $bitpos = 0;
386             }
387 77         152 do { # big endian, high to low
388 114         13624 $str .= chr( (($n >> $bitpos) & 0x3F) + 63 );
389             } while (($bitpos-=6) >= 0);
390 77         2976 return $str;
391             }
392              
393             sub _edges_iterator_none {
394 2     2   5 return;
395             }
396             sub _edge_predicate_none {
397 1     1   4 return 0;
398             }
399              
400             sub write_graph {
401 68     68 1 4491 my %options = @_;
402             ### %options
403              
404 68         125 my $fh = $options{'fh'};
405 68 100 100     286 if (! $fh
406             && defined(my $str_ref = $options{'str_ref'})) {
407             ### str_ref ...
408 44         1250 require IO::String;
409 44         8148 $fh = IO::String->new($$str_ref);
410             }
411 68 100 66     1907 if (! $fh
412             && defined(my $filename = $options{'filename'})) {
413             ### $filename
414 2 50       145 open $fh, '>', $filename
415             or return 0;
416             }
417              
418 68         120 my $format = $options{'format'};
419 68 100       156 if (! defined $format) { $format = 'graph6'; }
  8         11  
420              
421 68         119 my $num_vertices = $options{'num_vertices'};
422 68 100 66     167 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         7 foreach my $edge (@$edge_aref) {
427 2         16 $num_vertices = max($num_vertices, @$edge);
428             }
429 2         3 $num_vertices += 1;
430             }
431 68 50       131 if (! defined $num_vertices) {
432 0         0 croak 'Missing num_vertices';
433             }
434             ### $num_vertices
435              
436             print $fh
437 68 100       255 ($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 68         1149 my $bitpos = 5;
445 68         98 my $word = 0;
446             my $put_bit = sub {
447 654     654   5846 my ($bit) = @_;
448 654         878 $word |= $bit << $bitpos;
449 654 100       978 if ($bitpos > 0) {
450 545         646 $bitpos--;
451             } else {
452 109 50       363 print $fh chr($word + 63) or return 0;
453 109         1779 $bitpos = 5;
454 109         149 $word = 0;
455             }
456 654         1568 return 1;
457 68         301 };
458              
459 68 100       163 if ($format eq 'sparse6') {
460 30         41 my $edge_iterator;
461              
462 30 100       76 if (my $edge_aref = $options{'edge_aref'}) {
463             ### edge_aref ...
464             # swap to [from <= to]
465 27 100       57 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       79 @edges = sort { ($a->[1] <=> $b->[1]) || ($a->[0] <=> $b->[0]) } @edges;
  41         104  
471             $edge_iterator = sub {
472 82 100   82   99 return @{(shift @edges) || []};
  82         331  
473 27         87 };
474             }
475              
476 30 100 100     90 if (! $edge_iterator
477             && (my $edge_predicate = $options{'edge_predicate'})) {
478             ### edge_predicate ...
479 1         2 my $from = 0;
480 1         2 my $to = -1;
481             $edge_iterator = sub {
482 2     2   4 for (;;) {
483 497         2275 $from++;
484 497 100       748 if ($from > $to) {
485 32         35 $to++;
486 32 100       51 if ($to >= $num_vertices) {
487 1         3 return;
488             }
489 31         38 $from = 0;
490             }
491 496 100       693 if ($edge_predicate->($from,$to)) {
492 1         11 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         38 my $width = 0;
502 30 100       71 if ($num_vertices > 0) {
503 29         67 while (($num_vertices-1) >> $width) { $width++; }
  59         115  
504             }
505             ### $width
506              
507             my $put_n = sub {
508 63     63   102 my ($n) = @_;
509 63         133 for (my $i = $width-1; $i >= 0; $i--) {
510 145 50       251 $put_bit->(($n >> $i) & 1) or return 0;
511             }
512 63         136 return 1;
513 30         103 };
514              
515             # When doing a "set v" for a new to >= v+2, the b[i] bit can be either 0
516             # or 1. When 1 it means v++ increment, and the x[i]=to is still >v so
517             # set v. The code here follows the nauty tools ntos6() and emits b[i]=1.
518              
519 30         50 my $v = 0;
520 30         69 while (my ($from, $to) = $edge_iterator->()) {
521             ### edge: "$from $to"
522              
523 56 100       116 if ($to == $v + 1) {
524             ### increment v ...
525 30 50       53 $put_bit->(1) or return 0;
526              
527             } else {
528 26 100       60 if ($to != $v) { # $to >= $v+2
529             ### set v ...
530 7 50 33     17 ($put_bit->(1) # set v done with b[i]=1
531             && $put_n->($to))
532             or return 0;
533             }
534 26 50       43 $put_bit->(0) or return 0; # v unchanged
535             }
536             ### write: $from
537 56 50       108 $put_n->($from) or return 0; # edge ($from, $v)
538              
539 56         101 $v = $to;
540             }
541              
542 30 100       97 if ($bitpos != 5) {
543             ### pad: $bitpos+1
544             ### $v
545              
546             # Rule for padding so not to look like self-loop n-1 to n-1.
547             # There are $bitpos+1 many bits to pad.
548             # b[i]=0 bit if num_vertices = 2,4,8,16 so width=1,2,3,4
549             # and pad >= width+1
550             # and edge involving n-2 so final v=n-2
551             # 0 111 is set v=n-1 provided prev <= n-2
552             # 1 111 is a v+1 and edge n-1,v which is n-1,n out of range
553 22 50 33     171 if (($width >= 1 && $width <= 4)
      66        
      66        
      66        
554             && $num_vertices == (1 << $width) # 1,2,4,8
555             && $bitpos >= $width # room for final b[i] and x[i]
556             && $v == $num_vertices - 2) {
557             ### pad 0 ...
558 0 0       0 $put_bit->(0) or return 0;
559             }
560              
561             ### pad with 1s: $bitpos
562 22         52 until ($bitpos == 5) {
563 74 50       139 $put_bit->(1) or return 0;
564             }
565             }
566              
567             } else {
568 38         81 my $edge_predicate = $options{'edge_predicate'};
569              
570 38 100 100     125 if (! $edge_predicate
571             && (my $edge_aref = $options{'edge_aref'})) {
572             ### edge_predicate from edge_aref ...
573 7         13 my %edge_hash;
574 7         16 foreach my $edge (@$edge_aref) {
575 17         30 my ($from, $to) = @$edge;
576 17 100 100     49 if ($from > $to && $format eq 'graph6') { ($from,$to) = ($to,$from); }
  2         6  
577 17         51 $edge_hash{$from}->{$to} = undef;
578             }
579             $edge_predicate = sub {
580 65     65   107 my ($from, $to) = @_;
581 65         155 return exists $edge_hash{$from}->{$to};
582 7         28 };
583             }
584              
585 38   100     88 $edge_predicate ||= \&_edge_predicate_none;
586              
587 38 100       89 if ($format eq 'graph6') {
    50          
588 26         69 foreach my $to (1 .. $num_vertices-1) {
589 47         86 foreach my $from (0 .. $to-1) {
590 92 100       174 $put_bit->($edge_predicate->($from,$to) ? 1 : 0) or return 0;
    50          
591             }
592             }
593             } elsif ($format eq 'digraph6') {
594 12         29 foreach my $from (0 .. $num_vertices-1) {
595 44         85 foreach my $to (0 .. $num_vertices-1) {
596 172 100       297 $put_bit->($edge_predicate->($from,$to) ? 1 : 0) or return 0;
    50          
597             }
598             }
599             } else {
600 0         0 croak 'Unrecognised format: ',$format;
601             }
602              
603 38         89 until ($bitpos == 5) {
604 108 50       161 $put_bit->(0) or return 0;
605             }
606             }
607              
608 68 50       164 print $fh "\n" or return 0;
609 68         1357 return 1;
610             }
611              
612             # if (! $edge_predicate
613             # && (my $edge_matrix = $options{'edge_matrix'})) {
614             # $edge_predicate = sub {
615             # my ($from, $to) = @_;
616             # return $edge_matrix->[$from]->[$to];
617             # };
618             # }
619              
620             1;
621             __END__