File Coverage

blib/lib/Graph/Traverse.pm
Criterion Covered Total %
statement 70 73 95.8
branch 39 48 81.2
condition 28 33 84.8
subroutine 6 6 100.0
pod 1 1 100.0
total 144 161 89.4


line stmt bran cond sub pod time code
1             package Graph::Traverse 0.02 {
2              
3 1     1   158977 use warnings;
  1         8  
  1         33  
4 1     1   5 use strict;
  1         2  
  1         21  
5              
6 1     1   417 use parent 'Graph';
  1         275  
  1         5  
7 1     1   47 use Carp;
  1         1  
  1         597  
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 13     13 1 14810 my ($self, $vertex, $opts) = @_;
34 13 50       31 carp "Must pass starting vertex" unless defined $vertex;
35              
36 13 100       27 my $cb_check = $opts->{cb} if defined $opts;
37              
38 13   66     39 my $return_weights = (defined $opts && $opts->{weights});
39 13   66     33 my $return_hash = (defined $opts && $opts->{hash});
40 13 100       22 my $save_paths = ($return_hash) ? [] : undef;
41              
42             # If option 'attribute' is defined, we accumulate weights from each edge.
43             # Define 'max' to terminate when a maximum weight is achieved.
44             # Define 'vertex' to accumulate vertex weights rather than edge weights.
45             # Define 'incr' to change the default weight value from 1.
46 13 100 100     39 my $attr = (defined $opts) ? ($opts->{attribute} // 'weight') : 'weight';
47 13 100       25 my $max_weight = $opts->{max} if defined $opts;
48 13 100       17 my $add_vertex = $opts->{vertex} if defined $opts;
49 13 100 100     34 my $incr = (defined $opts) ? ($opts->{default} // 1) : 1; # default weight for each edge
50              
51             # Use a method that will return a list of adjacent vertices.
52             # Other useful values are 'predecessors' and 'neighbors'.
53 13 100 100     45 my $next = (defined $opts ? ($opts->{next}) : undef) // 'successors';
54              
55 13         18 my (%todo, %path, %weight);
56 13 100       14 foreach my $s (@{ref $vertex ? $vertex : [$vertex]}) {
  13         29  
57 13         23 $todo{$s} = $s;
58 13         18 $path{$s} = [$s];
59 13         36 $weight{$s} = 0;
60             }
61 13         17 my %terminal;
62             my %seen;
63 13         39 my %init = %todo;
64 13         24 while (keys %todo) {
65 56         209 my @todo = values %todo;
66 56         73 for my $t (@todo) {
67 91         459 $seen{$t} = delete $todo{$t};
68 91         239 for my $s ($self->$next($t)) {
69 93 50       5104 next unless $self->has_vertex($s);
70 93         881 my $newvalue;
71 93 50       129 if (defined $attr) {
72 93 100       125 if ($add_vertex) { # Add vertex attribute value
73 20   66     45 $newvalue = $weight{$t} + ($self->get_vertex_attribute($s, $attr) // $incr);
74             } else { # Add edge attribute value (default 'weight', default value 1)
75             # Note, if our search function is 'predecessors' or 'neighbors' we
76             # may find nodes in reverse direction, but we want the edge attributes
77             # in either case
78 73   100     134 $newvalue = $weight{$t} + ($self->get_edge_attribute($t, $s, $attr) //
      66        
79             $self->get_edge_attribute($s, $t, $attr) //
80             $incr);
81             }
82             } else {
83 0         0 $newvalue = $weight{$t} + $incr;
84             }
85             # If callback function returns nonzero, do not traverse beyond this node.
86 93 50       9029 if (defined $cb_check) {
87 0 0       0 if ($terminal{$s} = &$cb_check($self, $s, $newvalue, $opts)) {
88 0         0 $seen{$s} = $s;
89             }
90             }
91             # Do not save vertices beyond a defined maximum weight
92 93 100 100     177 next if (defined $max_weight) && ($newvalue > $max_weight);
93             # Always save the found vertices. As we traverse,
94             # we may later encounter shortcuts which we must
95             # discard before the final return (see below).
96 90 100       125 if (defined $save_paths) {
97             my $this_node = { vertex => $s,
98 87         97 path => [@{$path{$t}}, $s],
  87         246  
99             weight => $newvalue };
100 87 50       147 $this_node->{terminal} = $terminal{$s} if exists $terminal{$s};
101 87         92 push @{$save_paths}, $this_node;
  87         130  
102             }
103             # Only save new paths, and shorter-than-previously-found paths.
104 90 100 100     196 if ((!defined $path{$s}) || ($newvalue < $weight{$s} )) {
105             # If new path is shorter than we previously
106             # found, then retrace all paths from this
107             # vertex onward.
108 79 100 66     154 delete $seen{$s} if (defined $weight{$s} && $newvalue < $weight{$s});
109 79         117 $weight{$s} = $newvalue;
110 79         86 $path{$s} = [@{$path{$t}}, $s];
  79         171  
111             }
112             # If callback function returns nonzero, do not
113             # traverse beyond this node. NOTE: In the case of
114             # multiple paths to the following node, the above
115             # does track the shortest path to the node, but
116             # the caller will not receive every combination of
117             # paths *through* the node.
118 90 50       132 next if ($terminal{$s});
119 90 100       257 $todo{$s} = $s unless exists $seen{$s};
120             }
121             }
122             }
123 13         399 for my $v (keys %init) {
124 13         19 delete $seen{$v};
125 13         20 delete $weight{$v};
126             }
127             # return $save_paths if defined $return_all;
128 13 100       23 if ($return_hash) {
129             # Scan list of found vertices, overwriting higher-valued
130             # (longer) paths with lower (shorter) ones which were
131             # found later.
132 12         17 my $ret = {};
133 12         13 foreach my $v (@{$save_paths}) {
  12         18  
134 87 100 100     199 $ret->{$v->{vertex}} = $v if (!defined $ret->{$v->{vertex}} || ($ret->{$v->{vertex}}->{weight} > $v->{weight}));
135             }
136 12         81 return $ret;
137             }
138 1 50       19 return $return_weights ? (%weight) : (values %seen);
139             }
140              
141             if (Graph->can('traverse')) {
142             carp ('Graph already has a traverse method.');
143             } else {
144 1     1   6 no warnings 'redefine', 'once'; ## no critic
  1         2  
  1         72  
145             *Graph::traverse = \&traverse;
146             }
147              
148             };
149              
150             1;
151              
152             __END__