lib/jinx/helpers/visitor.rb in jinx-2.1.3 vs lib/jinx/helpers/visitor.rb in jinx-2.1.4

- old
+ new

@@ -54,41 +54,29 @@ # # The to_enum method allows navigator iteration, e.g.: # Jinx::Visitor.new { |node| node.children }.to_enum(parent).detect { |node| node.value == 2 } class Visitor - attr_reader :options, :visited, :lineage, :cycles + attr_reader :options, :visited, :lineage # Creates a new Visitor which traverses the child objects returned by the navigator block. # The navigator block takes a parent node argument and returns an enumerator on the children - # to visit. + # to visit. The options argument is described in {Options.get}. # - # The options is a _symbol_, _symbol_ => _value_ hash or nil. A _symbol_ argument is the same - # as +{+_symbol_ +=> true}+. Supported options include the following: - # * +:depth_first+: +true+, +false+ or a Proc. If the value is a Proc, then - # value determines whether a child is visited depth-first. See the {#visit} method for more - # information. - # - # If the :operator option is set, then the visit operator block is called when a node is visited. - # The operator block argument is the visited node. - # - # @param [Symbol, {Symbol => Object}] opts the visit options. A symbol argument is the same - # as symbol => true - # @option opts [String] :depth_first depth-first traversal - # @option opts [Proc] :operator the operator applied to each visited node - # @option opts [String] :prune_cycle flag indicating whether to exclude cycles to the root in a visit + # @param [Symbol, {Symbol => Object}] opts the visit options + # @option opts [Boolean] :depth_first depth-first traversal + # @option opts [Boolean] :prune_cycle flag indicating whether to exclude cycles in a visit # @option opts [Boolean] :verbose print navigation log messages # @yield [parent] returns an enumerator on the children to visit # @yieldparam parent the current node def initialize(opts=nil, &navigator) raise ArgumentError.new('Visitor cannot be created without a navigator block') unless block_given? @navigator = navigator @options = Options.to_hash(opts) @depth_first_flag = @options[:depth_first] @prune_cycle_flag = @options[:prune_cycle] @lineage = [] - @cycles = [] @visited = {} @verbose = Options.get(:verbose, opts, false) @exclude = Set.new end @@ -195,23 +183,21 @@ # @yieldparam parent the currently visited node # @yieldparam [Array] children the nodes slated by this visitor to visit next # @raise [ArgumentError] if a block is not given to this method def filter raise ArgumentError.new("A filter block is not given to the visitor filter method") unless block_given? - Visitor.new(@options) { |node| yield(node, node_children(node)) } + self.class.new(@options) { |node| yield(node, node_children(node)) } end protected # Resets this visitor's state in preparation for a new visit. def clear # clear the lineage @lineage.clear # if the visited hash is not shared, then clear it @visited.clear unless @options.has_key?(:visited) - # clear the cycles - @cycles.clear end # Returns the children to visit for the given node. def node_children(node) children = @navigator.call(node) @@ -236,40 +222,45 @@ # Reset the exclusions if the prune cycles flag is set. @exclude.clear if @prune_cycle_flag result end + # Returns the nodes which occur within a cycle, excluding the cycle entry point. + # # @example - # visitor.visit(a) - # visit.to_enum #=> [a, b, c, u, v, x, y, z] - # visit.cycles #=> [[a, b, u, x, a], [c, z, c]] - # visit.cyclic_nodes #=> [b, u, x, c, z] - # @return [Array] the non-root nodes in visit cycles + # graph.paths #=> a -> b -> a, a -> c -> d -> c + # Visitor.new(graph, &navigator).cyclic_nodes(a) #=> [b, d] + # @param root the node to visit + # @return [Array] the nodes within visit cycles def cyclic_nodes(root) copts = @options.reject { |k, v| k == :prune_cycle } - cycler = Visitor.new(copts, &@navigator) + cyclic = Set.new + cycler = Visitor.new(copts) do |parent| + children = @navigator.call(parent) + # Look for a cycle back to the child. + children.each do |child| + index = cycler.lineage.index(child) + if index then + # The child is also a parent: add the nodes between + # the two occurrences of the child in the lineage. + cyclic.merge!(cycler.lineage[(index + 1)..-1]) + end + end + children + end cycler.visit(root) - cyclic = cycler.cycles.flatten.uniq - cyclic.delete(root) cyclic - end + end def visit_recursive(node, &operator) - # bail if no node or the node is specifically excluded + # Bail if no node or the node is specifically excluded. return if node.nil? or @exclude.include?(node) - # return the visited value if the node has already been visited - if @visited.has_key?(node) then - #capture a cycle - index = @lineage.index(node) - if index then - cycle = @lineage[index..-1] << node - @cycles << cycle - end - return @visited[node] - end - # return nil if the node has not been visited but has been navigated in a depth-first visit + # Return the visited value if the node has already been visited. + return @visited[node] if @visited.has_key?(node) + # Return nil if the node has not been visited but has been navigated in a + # depth-first visit. return if @lineage.include?(node) - # all systems go: visit the node graph + # All systems go: visit the node subgraph. visit_node_and_children(node, &operator) end # Visits the given node and its children. If this visitor is #{depth_first?}, then the # operator is applied to the children before the given node. Otherwise, the operator is