File Coverage

blib/lib/Data/Graph/Util.pm
Criterion Covered Total %
statement 74 74 100.0
branch 16 16 100.0
condition 6 8 75.0
subroutine 10 10 100.0
pod 4 4 100.0
total 110 112 98.2


line stmt bran cond sub pod time code
1             package Data::Graph::Util;
2              
3 2     2   564335 use 5.010001;
  2         7  
4 2     2   11 use strict;
  2         4  
  2         64  
5 2     2   10 use warnings;
  2         3  
  2         144  
6              
7 2     2   11 use Exporter qw(import);
  2         6  
  2         630  
8              
9             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
10             our $DATE = '2023-12-20'; # DATE
11             our $DIST = 'Data-Graph-Util'; # DIST
12             our $VERSION = '0.007'; # VERSION
13              
14             our @EXPORT_OK = qw(
15             toposort
16             is_cyclic
17             is_acyclic
18             connected_components
19             );
20              
21             sub _toposort {
22 24     24   56 my $graph = shift;
23              
24             # this is the Kahn algorithm, ref:
25             # https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm
26              
27 24         44 my %in_degree;
28 24         94 for my $k (keys %$graph) {
29 47   100     275 $in_degree{$k} //= 0;
30 47         75 for (@{ $graph->{$k} }) { $in_degree{$_}++ }
  47         142  
  46         117  
31             }
32              
33             # collect nodes with no incoming edges (in_degree = 0)
34 24         48 my @S;
35 24 100       102 for (sort keys %in_degree) { unshift @S, $_ if $in_degree{$_} == 0 }
  57         192  
36              
37 24         49 my @L;
38 24         82 while (@S) {
39 46         78 my $n = pop @S;
40 46         102 push @L, $n;
41 46         72 for my $m (@{ $graph->{$n} }) {
  46         126  
42 35 100       88 if (--$in_degree{$m} == 0) {
43 27         86 unshift @S, $m;
44             }
45             }
46             }
47              
48 24 100       69 if (@L == keys(%$graph)) {
49 19 100       42 if (@_) {
50 2     2   14 no warnings 'uninitialized';
  2         5  
  2         1326  
51             # user specifies a list to be sorted according to @L. this is like
52             # Sort::ByExample but we implement it ourselves to avoid dependency.
53 4         34 my %pos;
54 4         17 for (0..$#L) { $pos{$L[$_]} = $_+1 }
  15         35  
55             return (0, [
56 4   66     8 sort { ($pos{$a} || @L+1) <=> ($pos{$b} || @L+1) } @{$_[0]}
  21   66     3964  
  4         20  
57             ]);
58             } else {
59 15         75 return (0, \@L);
60             }
61             } else {
62             # there is a cycle
63 5         25 return (1, \@L);
64             }
65             }
66              
67             sub toposort {
68 10     10 1 431238 my ($err, $res) = _toposort(@_);
69 10 100       44 die "Can't toposort(), graph is cyclic" if $err;
70 9         83 @$res;
71             }
72              
73             sub is_cyclic {
74 7     7 1 12702 my ($err, $res) = _toposort(@_);
75 7         48 $err;
76             }
77              
78             sub is_acyclic {
79 7     7 1 8698 my ($err, $res) = _toposort(@_);
80 7         59 !$err;
81             }
82              
83             sub connected_components {
84 5     5 1 8432 my $graph = shift;
85              
86             # create a map of bidirectional connections between nodes, to ease checking
87 5         39 my %connections;
88 5         22 for my $node1 (keys %$graph) {
89 13         23 for my $node2 (@{ $graph->{$node1} }) {
  13         29  
90 15         54 $connections{$node1}{$node2} = 1;
91 15         38 $connections{$node2}{$node1} = 1;
92             }
93             }
94              
95 5         12 my @subgraphs;
96 5         18 my %remaining_nodes = %$graph;
97              
98             # traverse a node to get a subgraph. remove the nodes from the original
99             # graph. repeat until there are no nodes left on the original graph.
100              
101 5         9 while (1) { # while there are still unlabeled nodes
102 11 100       48 my ($node1, $dependants1) = each %remaining_nodes or last;
103              
104 6         15 my $subgraph = {$node1 => $dependants1};
105 6         12 my %seen;
106 6         10 my @nodes_to_check = keys %{ $connections{$node1} };
  6         21  
107              
108 6         18 while (@nodes_to_check) { # while we can still find nodes connected to the subgraph
109 29         45 my $node2 = shift @nodes_to_check;
110 29 100       110 next if $seen{$node2}++;
111 15 100       47 if (my $dependants2 = delete $remaining_nodes{$node2}) {
112 10         21 $subgraph->{$node2} = $dependants2;
113 10         13 push @nodes_to_check, keys %{ $connections{$node2} };
  10         41  
114             }
115             }
116              
117 6         20 push @subgraphs, $subgraph;
118             }
119              
120 5         44 sort { scalar(keys %$b) <=> scalar(keys %$a) } @subgraphs;
  2         42  
121             }
122              
123             1;
124             # ABSTRACT: Utilities related to graph data structure
125              
126             __END__