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.03 {
2              
3 2     2   307605 use warnings;
  2         13  
  2         85  
4 2     2   9 use strict;
  2         3  
  2         43  
5              
6 2     2   784 use parent 'Graph';
  2         507  
  2         10  
7 2     2   89 use Carp;
  2         3  
  2         1138  
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 35694 my ($self, $vertex, $opts) = @_;
34 17 50       42 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     54 my $return_weights = (defined $opts && $opts->{weights});
39 17   66     43 my $return_hash = (defined $opts && $opts->{hash});
40 17 100       33 my $save_paths = ($return_hash) ? [] : undef;
41             # Save all nodes (default), or only the callback-flagged terminal nodes?
42 17   100     61 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     54 my $attr = (defined $opts) ? ($opts->{attribute} // 'weight') : 'weight';
49 17 100       41 my $max_weight = $opts->{max} if defined $opts;
50 17 100       33 my $add_vertex = $opts->{vertex} if defined $opts;
51 17 100 100     51 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         19 my (%todo, %path, %weight);
58 17 100       19 foreach my $s (@{ref $vertex ? $vertex : [$vertex]}) {
  17         49  
59 19         33 $todo{$s} = $s;
60 19         37 $path{$s} = [$s];
61 19         31 $weight{$s} = 0;
62             }
63 17         30 my %terminal;
64             my %seen;
65 17         51 my %init = %todo;
66 17         33 while (keys %todo) {
67 87         605 my @todo = values %todo;
68 87         112 for my $t (@todo) {
69 164         685 $seen{$t} = delete $todo{$t};
70 164         412 for my $s ($self->$next($t)) {
71 174 50       10082 next unless $self->has_vertex($s);
72 174         1729 my $newvalue;
73 174 50       265 if (defined $attr) {
74 174 100       219 if ($add_vertex) { # Add vertex attribute value
75 20   66     39 $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 154   100     302 $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 174 100       28923 if (defined $cb_check) {
89 64 100       174 if ($terminal{$s} = &$cb_check($self, $s, $newvalue, $opts)) {
90 6         56 $seen{$s} = $s;
91             }
92             }
93             # Do not save vertices beyond a defined maximum weight
94 174 100 100     924 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 171 100       258 if (defined $save_paths) {
99             my $this_node = { vertex => $s,
100 168         184 path => [@{$path{$t}}, $s],
  168         559  
101             weight => $newvalue };
102 168 100       312 $this_node->{terminal} = $terminal{$s} if exists $terminal{$s};
103 168 100 100     457 push @{$save_paths}, $this_node if ($save_all || $terminal{$s});
  110         166  
104             }
105             # Only save new paths, and shorter-than-previously-found paths.
106 171 100 100     386 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 153 100 66     303 delete $seen{$s} if (defined $weight{$s} && $newvalue < $weight{$s});
111 153         213 $weight{$s} = $newvalue;
112 153         154 $path{$s} = [@{$path{$t}}, $s];
  153         318  
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 171 100       274 next if ($terminal{$s});
121 165 100       442 $todo{$s} = $s unless exists $seen{$s};
122             }
123             }
124             }
125 17         472 for my $v (keys %init) {
126 19         35 delete $seen{$v};
127 19         23 delete $weight{$v};
128             }
129             # return $save_paths if defined $return_all;
130 17 100       37 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         20 my $ret = {};
135 16         20 foreach my $v (@{$save_paths}) {
  16         22  
136 110 100 100     292 $ret->{$v->{vertex}} = $v if (!defined $ret->{$v->{vertex}} || ($ret->{$v->{vertex}}->{weight} > $v->{weight}));
137             }
138 16         138 return $ret;
139             }
140 1 50       7 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   14 no warnings 'redefine', 'once'; ## no critic
  2         2  
  2         178  
147             *Graph::traverse = \&traverse;
148             }
149              
150             };
151              
152             1;
153              
154             __END__