File Coverage

blib/lib/Graph/Traverse.pm
Criterion Covered Total %
statement 73 74 98.6
branch 46 50 92.0
condition 34 39 87.1
subroutine 6 6 100.0
pod 1 1 100.0
total 160 170 94.1


line stmt bran cond sub pod time code
1             package Graph::Traverse 0.04 {
2              
3 2     2   363474 use warnings;
  2         11  
  2         84  
4 2     2   11 use strict;
  2         5  
  2         48  
5              
6 2     2   959 use parent 'Graph';
  2         594  
  2         14  
7 2     2   110 use Carp;
  2         5  
  2         1313  
8              
9             sub traverse {
10             # Use as: $graph->search( START_VERTEX, [OPTS])
11             #
12             # Traverses edges from the start vertex (or verticess) [either
13             # a scalar with a single vertex's name, or an array of vertex
14             # names, may be passed], finding adjacent vertices using the
15             # 'next' function (by default, 'successors'), until either a
16             # maximum accumulated edge weight ('max' option, if given) is
17             # exceeded (by default using the 'weight' attribute, or
18             # specify an 'attribute'), or until a callback function ('cb')
19             # returns a nonzero value. Default is to return the list of
20             # vertices encountered in the search; use option 'weights' to
21             # return a list of vertex=>weight_value.
22             #
23             # Use option 'hash=>1' to return a hash where keys are vertex
24             # names, and values are a hash containing the 'path' to that
25             # vertex from the starting vertex, the 'weight' at that
26             # vertex, and 'terminal' the value of the callback function
27             # returned for that vertex (if nonzero, further nodes on that
28             # branch are not searched). Note that as we traverse the
29             # graph, we may encounter the same vertex several times, but
30             # only the shortest path (lowest weight) will be retained in
31             # the final hash.
32            
33 17     17 1 43405 my ($self, $vertex, $opts) = @_;
34 17 50       54 carp "Must pass starting vertex" unless defined $vertex;
35              
36 17 100       41 my $cb_check = $opts->{cb} if defined $opts;
37              
38 17   66     99 my $return_weights = (defined $opts && $opts->{weights});
39 17   66     68 my $return_hash = (defined $opts && $opts->{hash});
40 17 100       40 my $save_paths = ($return_hash) ? [] : undef;
41             # Save all nodes (default), or only the callback-flagged terminal nodes?
42 17   100     66 my $save_all = (defined $opts && ($opts->{all} // 1));
43              
44             # If option 'attribute' is defined, we accumulate weights from each edge.
45             # Define 'max' to terminate when a maximum weight is achieved.
46             # Define 'vertex' to accumulate vertex weights rather than edge weights.
47             # Define 'incr' to change the default weight value from 1.
48 17 100 100     126 my $attr = (defined $opts) ? ($opts->{attribute} // 'weight') : 'weight';
49 17 100       48 my $max_weight = $opts->{max} if defined $opts;
50 17 100       47 my $add_vertex = $opts->{vertex} if defined $opts;
51 17 100 100     66 my $incr = (defined $opts) ? ($opts->{default} // 1) : 1; # default weight for each edge
52              
53             # Use a method that will return a list of adjacent vertices.
54             # Other useful values are 'predecessors' and 'neighbors'.
55 17 100 100     62 my $next = (defined $opts ? ($opts->{next}) : undef) // 'successors';
56              
57 17         27 my (%todo, %path, %weight);
58 17 100       22 foreach my $s (@{ref $vertex ? $vertex : [$vertex]}) {
  17         53  
59 19         43 $todo{$s} = $s;
60 19         45 $path{$s} = [$s];
61 19         40 $weight{$s} = 0;
62             }
63 17         44 my %terminal;
64             my %seen;
65 17         57 my %init = %todo;
66 17         40 while (keys %todo) {
67 84         495 my @todo = values %todo;
68 84         158 for my $t (@todo) {
69 160         880 $seen{$t} = delete $todo{$t};
70 160         487 for my $s ($self->$next($t)) {
71 172 50       12070 next unless $self->has_vertex($s);
72 172         2104 my $newvalue;
73 172 50       298 if (defined $attr) {
74 172 100       273 if ($add_vertex) { # Add vertex attribute value
75 20   66     49 $newvalue = $weight{$t} + ($self->get_vertex_attribute($s, $attr) // $incr);
76             } else { # Add edge attribute value (default 'weight', default value 1)
77             # Note, if our search function is 'predecessors' or 'neighbors' we
78             # may find nodes in reverse direction, but we want the edge attributes
79             # in either case
80 152   100     345 $newvalue = $weight{$t} + ($self->get_edge_attribute($t, $s, $attr) //
      66        
81             $self->get_edge_attribute($s, $t, $attr) //
82             $incr);
83             }
84             } else {
85 0         0 $newvalue = $weight{$t} + $incr;
86             }
87             # If callback function returns nonzero, do not traverse beyond this node.
88 172 100       35439 if (defined $cb_check) {
89 64 100       138 if ($terminal{$s} = &$cb_check($self, $s, $newvalue, $opts)) {
90 6         82 $seen{$s} = $s;
91             }
92             }
93             # Do not save vertices beyond a defined maximum weight
94 172 100 100     1079 next if (defined $max_weight) && ($newvalue > $max_weight);
95             # Always save the found vertices. As we traverse,
96             # we may later encounter shortcuts which we must
97             # discard before the final return (see below).
98 169 100       319 if (defined $save_paths) {
99             my $this_node = { vertex => $s,
100 166         224 path => [@{$path{$t}}, $s],
  166         657  
101             weight => $newvalue };
102 166 100       376 $this_node->{terminal} = $terminal{$s} if exists $terminal{$s};
103 166 100 100     481 push @{$save_paths}, $this_node if ($save_all || $terminal{$s});
  108         200  
104             }
105             # Only save new paths, and shorter-than-previously-found paths.
106 169 100 100     475 if ((!defined $path{$s}) || ($newvalue < $weight{$s} )) {
107             # If new path is shorter than we previously
108             # found, then retrace all paths from this
109             # vertex onward.
110 150 100 66     367 delete $seen{$s} if (defined $weight{$s} && $newvalue < $weight{$s});
111 150         240 $weight{$s} = $newvalue;
112 150         209 $path{$s} = [@{$path{$t}}, $s];
  150         402  
113             }
114             # If callback function returns nonzero, do not
115             # traverse beyond this node. NOTE: In the case of
116             # multiple paths to the following node, the above
117             # does track the shortest path to the node, but
118             # the caller will not receive every combination of
119             # paths *through* the node.
120 169 100       348 next if ($terminal{$s});
121 163 100       540 $todo{$s} = $s unless exists $seen{$s};
122             }
123             }
124             }
125 17         573 for my $v (keys %init) {
126 19         32 delete $seen{$v};
127 19         37 delete $weight{$v};
128             }
129             # return $save_paths if defined $return_all;
130 17 100       43 if ($return_hash) {
131             # Scan list of found vertices, overwriting higher-valued
132             # (longer) paths with lower (shorter) ones which were
133             # found later.
134 16         31 my $ret = {};
135 16         21 foreach my $v (@{$save_paths}) {
  16         32  
136 108 100 100     350 $ret->{$v->{vertex}} = $v if (!defined $ret->{$v->{vertex}} || ($ret->{$v->{vertex}}->{weight} > $v->{weight}));
137             }
138 16         191 return $ret;
139             }
140 1 50       12 return $return_weights ? (%weight) : (values %seen);
141             }
142              
143             if (Graph->can('traverse')) {
144             carp ('Graph already has a traverse method.');
145             } else {
146 2     2   17 no warnings 'redefine', 'once'; ## no critic
  2         5  
  2         163  
147             *Graph::traverse = \&traverse;
148             }
149              
150             };
151              
152             1;
153              
154             __END__