File Coverage

blib/lib/Graph/Graph6.pm
Criterion Covered Total %
statement 297 331 89.7
branch 152 196 77.5
condition 46 68 67.6
subroutine 23 23 100.0
pod 2 2 100.0
total 520 620 83.8


line stmt bran cond sub pod time code
1             # Copyright 2015, 2016, 2017, 2018, 2021 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 3     3   7797 use 5.006; # for 3-arg open
  3         15  
20 3     3   16 use strict;
  3         4  
  3         74  
21 3     3   15 use warnings;
  3         4  
  3         158  
22 3     3   17 use List::Util 'max';
  3         12  
  3         328  
23 3     3   18 use Carp 'croak';
  3         6  
  3         164  
24              
25 3     3   18 use Exporter;
  3         4  
  3         257  
26             our @ISA = ('Exporter');
27             our @EXPORT_OK = ('read_graph','write_graph',
28             'HEADER_GRAPH6','HEADER_SPARSE6','HEADER_DIGRAPH6');
29              
30             our $VERSION = 9;
31              
32             # uncomment this to run the ### lines
33             # use Smart::Comments;
34              
35              
36 3     3   20 use constant HEADER_GRAPH6 => '>>graph6<<';
  3         3  
  3         295  
37 3     3   18 use constant HEADER_SPARSE6 => '>>sparse6<<';
  3         5  
  3         165  
38 3     3   18 use constant HEADER_DIGRAPH6 => '>>digraph6<<';
  3         5  
  3         7478  
39              
40             sub _read_header {
41 2     2   4 my ($fh, $str) = @_;
42 2         3 for (;;) {
43 5         14 my $s2 = getc $fh;
44 5 50       79 if (! defined $str) { return; }
  0         0  
45              
46 5         7 $str .= $s2;
47 5 100       15 if ($str eq substr(HEADER_GRAPH6, 0, length($str))) {
    50          
    50          
48 3 50       7 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         14 return $str;
69             }
70             }
71             }
72              
73             sub read_graph {
74 30     30 1 6077 my %options = @_;
75              
76 30         52 my $fh = $options{'fh'};
77 30 100       68 if (defined $options{'str'}) {
78 27         1328 require IO::String;
79 27         7983 $fh = IO::String->new($options{'str'});
80             }
81              
82 30         928 my $skip_newlines = 1;
83 30         46 my $allow_header = 1;
84 30         36 my $format = 'graph6';
85 30         33 my $initial = 1;
86 30         32 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 81     81   83 for (;;) {
93 108         137 my $str;
94 108         249 my $len = read($fh, $str, 1);
95 108 50       1334 if (! defined $len) {
96 0         0 $error = "Error reading: $!";
97 0         0 return -1;
98             }
99             ### read byte: $str
100              
101 108 100 100     219 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         10 next;
106             }
107 99         95 $skip_newlines = 0;
108              
109 99 100 100     214 if ($allow_header && $str eq '>') {
110 2         6 $str = _read_header($fh, $str);
111 2 50       7 if (defined $str) {
112 2         3 $error = "Incomplete header: $str";
113 2         4 return -1;
114             }
115 0         0 $allow_header = 0;
116 0         0 next;
117             }
118 97         89 $allow_header = 0;
119              
120 97         125 my $n = ord($str) - 63;
121 97 100 66     199 if ($n >= 0 && $n <= 63) {
122 65         114 return $n;
123             }
124              
125 32 100 100     83 if ($str eq '' || $str eq "\n") {
126             ### end of file or end of line ...
127 8         15 return -1;
128             }
129              
130 24 100 100     55 if ($initial && $str eq '&') {
131 3         4 $format = 'digraph6';
132             ### $format
133 3         4 $initial = 0;
134 3         6 next;
135             }
136 21 100 100     55 if ($initial && $str eq ':') {
137 15         18 $format = 'sparse6';
138             ### $format
139 15         29 $initial = 0;
140 15         23 next;
141             }
142 6 50       8 if ($str eq "\r") {
143             ### skip CR ...
144 0         0 next;
145             }
146              
147 6         8 $error = "Unrecognised character: $str";
148 6         63 return -1;
149             }
150 30         129 };
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 29     29   36 my $n = $read_byte->();
157 29         38 $initial = 0;
158 29 100       44 if ($n <= 62) {
159 28         53 return $n;
160             }
161 1         2 $n = $read_byte->();
162 1 50       3 if ($n < 0) {
163 1   50     5 $error ||= "Unexpected EOF";
164 1         1 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 30         79 };
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 30 100   30   82 if (! defined $fh) {
190 2 50       5 if (defined(my $filename = $options{'filename'})) {
191             open $fh, '<', $filename
192 2 100       104 or do {
193 1         23 $error = "Cannot open file $filename: $!";
194 1         4 return;
195             };
196             }
197             }
198              
199 29         58 my $num_vertices = $read_number->();
200             ### $num_vertices
201              
202 29 50       62 if (my $format_func = $options{'format_func'}) {
203 0         0 $format_func->($format);
204             }
205 29 50       43 if (my $format_ref = $options{'format_ref'}) {
206 0         0 $$format_ref = $format;
207             }
208              
209 29 100       54 if ($num_vertices < 0) {
210 9         14 return; # eof or possible error
211             }
212 20 100       35 if (my $num_vertices_func = $options{'num_vertices_func'}) {
213 7         18 $num_vertices_func->($num_vertices);
214             }
215 20 100       59 if (my $num_vertices_ref = $options{'num_vertices_ref'}) {
216 12         14 $$num_vertices_ref = $num_vertices;
217             }
218              
219 20         26 my $edge_func = $options{'edge_func'};
220 20         37 my $edge_aref = $options{'edge_aref'};
221 20 100       38 if ($edge_aref) { @$edge_aref = (); }
  11         13  
222              
223             ### $format
224 20 100       47 if ($format eq 'sparse6') {
225             ### sparse6 ...
226 14         15 my $v = 0;
227              
228             # number of bits required to represent $num_vertices - 1
229 14         14 my $width = 0;
230 14         25 while (($num_vertices-1) >> $width) { $width++; }
  31         44  
231              
232 14         16 my $bits = 0;
233 14         16 my $n = 0;
234 14         19 my $mask = (1 << $width) - 1;
235              
236 14         26 while ($v < $num_vertices) {
237 64 100       85 if ($bits < 1) {
238 28         36 $n = $read_byte->();
239 28 100       38 if ($n < 0) {
240             ### end n ...
241             ### $error
242 5         13 return ! defined $error;
243             }
244 23         24 $bits = 6;
245             }
246 59         58 $bits--;
247 59         69 my $b = ($n >> $bits) & 1; # first bit from $n
248 59         57 $v += $b; # propagate possible taintedness of $n,$b to $v
249             ### $b
250             ### to v: $v
251              
252 59         86 while ($bits < $width) { # fill $n,$bits to >= $width many bits
253 13         20 my $n2 = $read_byte->();
254 13 100       23 if ($n2 < 0) {
255             ### end n2 ...
256             ### $error
257 2         7 return ! defined $error;
258             }
259 11         13 $bits += 6;
260 11         11 $n <<= 6;
261 11         19 $n |= $n2;
262             }
263 57         74 $bits -= $width;
264 57         64 my $x = ($n >> $bits) & $mask;
265             ### $x
266              
267 57 100       93 if ($x > $v) {
    100          
268             ### set v: $x
269 16         22 $v = $x;
270             } elsif ($v < $num_vertices) { # padding can make v>n-1
271             ### edge: "$x - $v"
272 34 100       47 if ($edge_func) { $edge_func->($x, $v); }
  7         12  
273 34 100       76 if ($edge_aref) { push @$edge_aref, [$x, $v]; }
  26         64  
274             }
275             }
276             ### end ...
277              
278             } else {
279             ### graph6 or digraph6 ...
280 6         17 my $n;
281             my $mask;
282 6         0 my $from;
283 6         0 my $to;
284             my $output_edge = sub {
285 46 100       71 if ($n & $mask) {
286 13         15 my $taint0 = $n & 0;
287 13         15 my $from_taint = $from + $taint0;
288 13         17 my $to_taint = $to + $taint0;
289 13 100       23 if ($edge_func) { $edge_func->( $from_taint, $to_taint); }
  8         14  
290 13 100       50 if ($edge_aref) { push @$edge_aref, [$from_taint, $to_taint]; }
  1         3  
291             }
292 6         34 };
293              
294 6 100       22 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 5         8 $from = 0;
298 5         5 $to = 1;
299 5         12 while ($to < $num_vertices) {
300 5 50       8 if (($n = $read_byte->()) < 0) {
301 0   0     0 $error ||= "Unexpected EOF"; # end of file is not ok
302 0         0 return;
303             }
304 5         9 for ($mask = 1 << 5; $mask != 0; $mask >>= 1) {
305 21         40 $output_edge->();
306 21         24 $from++;
307 21 100       41 if ($from >= $to) {
308 9         10 $to++;
309 9 100       27 last unless $to < $num_vertices;
310 6         26 $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       6 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         8 for ($mask = 1 << 5; $mask != 0; $mask >>= 1) {
324 25         32 $output_edge->();
325 25         20 $to++;
326 25 100       43 if ($to >= $num_vertices) {
327 5         5 $from++;
328 5 100       10 last unless $from < $num_vertices;
329 4         5 $to = 0;
330             }
331             }
332             }
333             }
334              
335             # read \n or \r\n, so can take successive graphs from file handle
336 6         8 for (;;) {
337 7         8 my $str;
338 7         13 my $len = read($fh, $str, 1);
339 7 50       94 if (! defined $len) {
340 0         0 $error = "Error reading: $!";
341 0         0 last;
342             }
343 7 100       17 if ($str eq "\r") {
344 1         3 next; # skip CR in case reading MS-DOS file as bytes
345             }
346 6 50 66     25 if ($str eq '' || $str eq "\n") {
347 6         27 last; # EOF or EOL, good
348             }
349             }
350             }
351              
352 13         28 return 1;
353 30         160 };
354              
355              
356 30 100       51 if ($read->()) {
357 19         387 return 1; # successful read
358             }
359 11 100       18 if (defined $error) {
360             ### $error
361 10   100     20 my $error_func = $options{'error_func'} || \&Carp::croak;
362 10         253 $error_func->($error);
363 9         177 return undef;
364             }
365 1         15 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 24     24   44537 my ($n) = @_;
375 24         36 my $str;
376             my $bitpos;
377 24 100       69 if ($n > 258047) { # binary 0b_111110_111111_111111 octal 0767777
    100          
378 7         559 $str = '~~';
379 7         13 $bitpos = (6-1)*6;
380             } elsif ($n > 62) {
381 1         1 $str = '~';
382 1         2 $bitpos = (3-1)*6;
383             } else {
384 16         22 $str = '';
385 16         19 $bitpos = 0;
386             }
387 24         30 do { # big endian, high to low
388 61         11305 $str .= chr( (($n >> $bitpos) & 0x3F) + 63 );
389             } while (($bitpos-=6) >= 0);
390 24         2575 return $str;
391             }
392              
393             sub _edges_iterator_none {
394 2     2   5 return;
395             }
396             sub _edge_predicate_none {
397 1     1   2 return 0;
398             }
399              
400             sub write_graph {
401 15     15 1 3808 my %options = @_;
402             ### %options
403              
404 15         29 my $fh = $options{'fh'};
405 15 100 66     68 if (! $fh
406             && defined(my $str_ref = $options{'str_ref'})) {
407             ### str_ref ...
408 13         67 require IO::String;
409 13         67 $fh = IO::String->new($$str_ref);
410             }
411 15 100 66     491 if (! $fh
412             && defined(my $filename = $options{'filename'})) {
413             ### $filename
414 2 50       155 open $fh, '>', $filename
415             or return 0;
416             }
417              
418 15         31 my $format = $options{'format'};
419 15 100       30 if (! defined $format) { $format = 'graph6'; }
  8         17  
420              
421 15         24 my $num_vertices = $options{'num_vertices'};
422 15 100 66     37 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         4 foreach my $edge (@$edge_aref) {
427 2         12 $num_vertices = max($num_vertices, @$edge);
428             }
429 2         4 $num_vertices += 1;
430             }
431 15 50       24 if (! defined $num_vertices) {
432 0         0 croak 'Missing num_vertices';
433             }
434             ### $num_vertices
435              
436             print $fh
437 15 100       64 ($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 15         296 my $bitpos = 5;
445 15         20 my $word = 0;
446             my $put_bit = sub {
447 144     144   203 my ($bit) = @_;
448 144         152 $word |= $bit << $bitpos;
449 144 100       169 if ($bitpos > 0) {
450 120         119 $bitpos--;
451             } else {
452 24 50       69 print $fh chr($word + 63) or return 0;
453 24         442 $bitpos = 5;
454 24         27 $word = 0;
455             }
456 144         320 return 1;
457 15         66 };
458              
459 15 100       32 if ($format eq 'sparse6') {
460 4         7 my $edge_iterator;
461              
462 4 100       12 if (my $edge_aref = $options{'edge_aref'}) {
463             ### edge_aref ...
464             # swap to [from <= to]
465 1 50       3 my @edges = map { $_->[0] > $_->[1]
  4         13  
466             ? [ $_->[1], $_->[0] ]
467             : $_
468             } @$edge_aref;
469             # sort to ascending "to", and within those ascending "from"
470 1 50       8 @edges = sort { ($a->[1] <=> $b->[1]) || ($a->[0] <=> $b->[0]) } @edges;
  4         13  
471             $edge_iterator = sub {
472 5 100   5   5 return @{(shift @edges) || []};
  5         17  
473 1         5 };
474             }
475              
476 4 100 100     18 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   3 for (;;) {
483 497         1618 $from++;
484 497 100       607 if ($from > $to) {
485 32         30 $to++;
486 32 100       52 if ($to >= $num_vertices) {
487 1         8 return;
488             }
489 31         31 $from = 0;
490             }
491 496 100       583 if ($edge_predicate->($from,$to)) {
492 1         10 return ($from,$to);
493             }
494             }
495 1         29 };
496             }
497              
498 4   100     15 $edge_iterator ||= \&_edges_iterator_none;
499              
500             # $width = number of bits required to represent $num_vertices - 1
501 4         6 my $width = 0;
502 4 100       8 if ($num_vertices > 0) {
503 3         11 while (($num_vertices-1) >> $width) { $width++; }
  11         15  
504             }
505             ### $width
506              
507             my $put_n = sub {
508 6     6   8 my ($n) = @_;
509 6         12 for (my $i = $width-1; $i >= 0; $i--) {
510 20 50       30 $put_bit->(($n >> $i) & 1) or return 0;
511             }
512 6         12 return 1;
513 4         12 };
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 4         5 my $v = 0;
520 4         8 while (my ($from, $to) = $edge_iterator->()) {
521             ### edge: "$from $to"
522              
523 5 100       11 if ($to == $v + 1) {
524             ### increment v ...
525 2 50       5 $put_bit->(1) or return 0;
526              
527             } else {
528 3 100       7 if ($to != $v) { # $to >= $v+2
529             ### set v ...
530 1 50 33     3 ($put_bit->(1) # set v done with b[i]=1
531             && $put_n->($to))
532             or return 0;
533             }
534 3 50       4 $put_bit->(0) or return 0; # v unchanged
535             }
536             ### write: $from
537 5 50       10 $put_n->($from) or return 0; # edge ($from, $v)
538              
539 5         7 $v = $to;
540             }
541              
542 4 100       25 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 1 0 33     21 if (($width >= 1 && $width <= 4)
      33        
      33        
      33        
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 1         6 until ($bitpos == 5) {
563 4 50       5 $put_bit->(1) or return 0;
564             }
565             }
566              
567             } else {
568 11         18 my $edge_predicate = $options{'edge_predicate'};
569              
570 11 100 100     39 if (! $edge_predicate
571             && (my $edge_aref = $options{'edge_aref'})) {
572             ### edge_predicate from edge_aref ...
573 7         12 my %edge_hash;
574 7         29 foreach my $edge (@$edge_aref) {
575 17         35 my ($from, $to) = @$edge;
576 17 100 100     46 if ($from > $to && $format eq 'graph6') { ($from,$to) = ($to,$from); }
  2         6  
577 17         49 $edge_hash{$from}->{$to} = undef;
578             }
579             $edge_predicate = sub {
580 65     65   82 my ($from, $to) = @_;
581 65         135 return exists $edge_hash{$from}->{$to};
582 7         27 };
583             }
584              
585 11   100     28 $edge_predicate ||= \&_edge_predicate_none;
586              
587 11 100       45 if ($format eq 'graph6') {
    50          
588 8         22 foreach my $to (1 .. $num_vertices-1) {
589 16         23 foreach my $from (0 .. $to-1) {
590 38 100       66 $put_bit->($edge_predicate->($from,$to) ? 1 : 0) or return 0;
    50          
591             }
592             }
593             } elsif ($format eq 'digraph6') {
594 3         6 foreach my $from (0 .. $num_vertices-1) {
595 11         14 foreach my $to (0 .. $num_vertices-1) {
596 43 100       51 $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 11         30 until ($bitpos == 5) {
604 33 50       41 $put_bit->(0) or return 0;
605             }
606             }
607              
608 15 50       39 print $fh "\n" or return 0;
609 15         450 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__