diff --git a/.travis.yml b/.travis.yml index 0ff5f7d..6a22a12 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: ruby rvm: - - '1.9.2' - - '1.9.3' - - jruby-19mode - - rbx-19mode - - '2.1.2' + - 2.2 + - 2.3 + - 2.4 + - 2.5 + - jruby + - rbx-3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5c54802 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Style + +Please use Rubocop with default settings as a beautifier. In the atom ide, for +instance, it can be used in combination with the package atom-beautify. + +Also use a maximal line length of 80 characters. diff --git a/README.md b/README.md index 70e4462..6a39b67 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # DAG -Very simple directed acyclic graphs for Ruby. +Simple directed acyclic graphs for Ruby. -[![Build Status](https://travis-ci.org/kevinrutherford/dag.png)](https://travis-ci.org/kevinrutherford/dag) -[![Dependency -Status](https://gemnasium.com/kevinrutherford/dag.png)](https://gemnasium.com/kevinrutherford/dag) -[![Code -Climate](https://codeclimate.com/github/kevinrutherford/dag.png)](https://codeclimate.com/github/kevinrutherford/dag) +## History + +This ruby gem started out as a fork of [kevinrutherford's dag implementation](https://github.com/kevinrutherford/dag). If you want to migrate +from his implementation to this one, have a look at the +[breaking changes](#breaking-changes). Have a look at +[performance improvements](#performance-improvements) to see why you might +want to migrate. ## Installation @@ -34,8 +36,8 @@ v3 = dag.add_vertex dag.add_edge from: v1, to: v2 dag.add_edge from: v2, to: v3 -v1.has_path_to?(v3) # => true -v3.has_path_to?(v1) # => false +v1.path_to?(v3) # => true +v3.path_to?(v1) # => false dag.add_edge from: v3, to: v1 # => ArgumentError: A DAG must not have cycles @@ -48,15 +50,83 @@ See the specs for more detailed usage scenarios. ## Compatibility -Tested with Ruby 1.9.x, JRuby, Rubinius. +Tested with Ruby 2.2, 2.3, 2.4, 2.5, JRuby, Rubinius. See the [build status](https://travis-ci.org/kevinrutherford/dag) for details. +## Differences to [dag](https://github.com/kevinrutherford/dag) + +### Breaking changes + +- The function `DAG::Vertex#has_path_to?` aliased as +`DAG::Vertex#has_descendant?` and `DAG::Vertex#has_descendent?` has been renamed +to `DAG::Vertex#path_to?`. The aliases have been removed. + +- The function `DAG::Vertex#has_ancestor?` aliased as +`DAG::Vertex#is_reachable_from?` has been renamed to +`DAG::Vertex#reachable_from?`. The aliases have been removed. + +- The array of edges returned by `DAG#edges` is no longer sorted by insertion +order of the edges. + +- `DAG::Vertex#path_to?` and `DAG::Vertex#reachable_from?` no longer raise +errors if the vertex passed as an argument is not a vertex in the same `DAG`. +Instead, they just return `false`. + +- [Parallel edges](https://en.wikipedia.org/wiki/Multiple_edges) are no longer +allowed in the dag. Instead, `DAG#add_edge` raises an `ArgumentError` if you +try to add an edge between two adjacent vertices. If you want to model a +multigraph, you can add a weight payload to the edges that contains a natural +number. + +### New functions + +- `DAG#topological_sort` returns a topological sort of the vertices in the dag +in a theoretically optimal computational time complexity. + +- `DAG#enumerated_edges` returns an `Enumerator` of the edges in the dag. + +### Performance improvements + +- The computational complexity of `DAG::Vertex#outgoing_edges` has +*improved* to a constant because the edges are no longer stored in one array in +the `DAG`. Instead, the edges are now stored in their respective source +`Vertex`. + +- The performance of `DAG::Vertex#successors` has *improved* because firstly, +it depends on `DAG::Vertex#outgoing_edges` and secondly the call to +`Array#uniq` is no longer necessary since parallel edges are prohibited. + +- The computational complexities of `DAG::Vertex#descendants`, +`DAG::Vertex#path_to?` and `DAG::Vertex#reachable_from?` have *improved* because +the functions depend on `DAG::Vertex#successors`. + +- The computational complexity of `DAG::Vertex#incoming_edges` is +*unchanged*: Linear in the number of all edges in the `DAG`. + +- The performance of `DAG::Vertex#predecessors` has *improved* because the call +to `Array#uniq` is no longer necessary since parallel edges are prohibited. + +- The performance of `DAG::Vertex#ancestors` has *improved* because the function +depends on `DAG::Vertex#predecessors`. + +- The computational complexity of `DAG::add_edge` has *improved* because the +cycle check in the function depends on `DAG::Vertex#path_to?`. + +- The performance of `DAG#subgraph` has *improved* because the function depends +on `DAG::Vertex#descendants`, `DAG::Vertex#ancestors` and `DAG::add_edge`. + +- The computational complexity of `DAG::edges` has worsened from a constant +complexity to a linear complexity. This is irrelevant if you want to iterate +over all the edges in the graph. You should then consider to use +`DAG#enumerated_edges` for a better space utilization. + ## License (The MIT License) Copyright (c) 2013 Kevin Rutherford +Modified work Copyright 2018 Fabian Sobanski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in @@ -75,4 +145,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/lib/dag.rb b/lib/dag.rb index cee9a17..0b366e6 100644 --- a/lib/dag.rb +++ b/lib/dag.rb @@ -3,10 +3,9 @@ require_relative 'dag/vertex' class DAG - Edge = Struct.new(:origin, :destination, :properties) - attr_reader :vertices, :edges + attr_reader :vertices # # Create a new Directed Acyclic Graph @@ -16,39 +15,53 @@ class DAG # def initialize(options = {}) @vertices = [] - @edges = [] @mixin = options[:mixin] + @n_of_edges = 0 end def add_vertex(payload = {}) - Vertex.new(self, payload).tap {|v| + Vertex.new(self, payload).tap do |v| v.extend(@mixin) if @mixin @vertices << v - } + end end def add_edge(attrs) origin = attrs[:origin] || attrs[:source] || attrs[:from] || attrs[:start] - destination = attrs[:destination] || attrs[:sink] || attrs[:to] || attrs[:end] + destination = attrs[:destination] || attrs[:sink] || attrs[:to] || + attrs[:end] properties = attrs[:properties] || {} - raise ArgumentError.new('Origin must be a vertex in this DAG') unless - is_my_vertex?(origin) - raise ArgumentError.new('Destination must be a vertex in this DAG') unless - is_my_vertex?(destination) - raise ArgumentError.new('A DAG must not have cycles') if origin == destination - raise ArgumentError.new('A DAG must not have cycles') if destination.has_path_to?(origin) - Edge.new(origin, destination, properties).tap {|e| @edges << e } + raise ArgumentError, 'Origin must be a vertex in this DAG' unless + my_vertex?(origin) + raise ArgumentError, 'Destination must be a vertex in this DAG' unless + my_vertex?(destination) + raise ArgumentError, 'Edge already exists' if + origin.successors.include? destination + raise ArgumentError, 'A DAG must not have cycles' if origin == destination + raise ArgumentError, 'A DAG must not have cycles' if + destination.path_to?(origin) + @n_of_edges += 1 + origin.send :add_edge, destination, properties end - def subgraph(predecessors_of = [], successors_of = []) + # @return Enumerator over all edges in the dag + def enumerated_edges + Enumerator.new(@n_of_edges) do |e| + @vertices.each { |v| v.outgoing_edges.each { |out| e << out } } + end + end + def edges + enumerated_edges.to_a + end - (predecessors_of + successors_of).each { |v| - raise ArgumentError.new('You must supply a vertex in this DAG') unless - is_my_vertex?(v) - } + def subgraph(predecessors_of = [], successors_of = []) + (predecessors_of + successors_of).each do |v| + raise ArgumentError, 'You must supply a vertex in this DAG' unless + my_vertex?(v) + end - result = self.class.new({mixin: @mixin}) + result = self.class.new(mixin: @mixin) vertex_mapping = {} # Get the set of predecessors verticies and add a copy to the result @@ -56,7 +69,7 @@ def subgraph(predecessors_of = [], successors_of = []) predecessors_of.each { |v| v.ancestors(predecessors_set) } predecessors_set.each do |v| - vertex_mapping[v] = result.add_vertex(payload=v.payload) + vertex_mapping[v] = result.add_vertex(v.payload) end # Get the set of successor vertices and add a copy to the result @@ -64,7 +77,8 @@ def subgraph(predecessors_of = [], successors_of = []) successors_of.each { |v| v.descendants(successors_set) } successors_set.each do |v| - vertex_mapping[v] = result.add_vertex(payload=v.payload) unless vertex_mapping.include? v + vertex_mapping[v] = result.add_vertex(v.payload) unless + vertex_mapping.include? v end # get the unique edges @@ -78,16 +92,48 @@ def subgraph(predecessors_of = [], successors_of = []) result.add_edge( from: vertex_mapping[e.origin], to: vertex_mapping[e.destination], - properties: e.properties) + properties: e.properties + ) end - return result + result end - private + # Returns an array of the vertices in the graph in a topological order, i.e. + # for every path in the dag from a vertex v to a vertex u, v comes before u + # in the array. + # + # Uses a depth first search. + # + # Assuming that the method include? of class Set runs in linear time, which + # can be assumed in all practical cases, this method runs in O(n+m) where + # m is the number of edges and n is the number of vertices. + def topological_sort + result_size = 0 + result = Array.new(@vertices.length) + visited = Set.new + + visit = lambda { |v| + return if visited.include? v + v.successors.each do |u| + visit.call u + end + visited.add v + result_size += 1 + result[-result_size] = v + } - def is_my_vertex?(v) - v.kind_of?(Vertex) and v.dag == self + @vertices.each do |v| + next if visited.include? v + visit.call v + end + + result end + private + + def my_vertex?(v) + v.is_a?(Vertex) && (v.dag == self) + end end diff --git a/lib/dag/vertex.rb b/lib/dag/vertex.rb index 3dba350..f62b6a7 100644 --- a/lib/dag/vertex.rb +++ b/lib/dag/vertex.rb @@ -1,31 +1,27 @@ require 'set' class DAG - class Vertex - attr_reader :dag, :payload + attr_reader :dag, :payload, :outgoing_edges def initialize(dag, payload) @dag = dag @payload = payload + @outgoing_edges = [] end private :initialize - def outgoing_edges - @dag.edges.select {|e| e.origin == self} - end - def incoming_edges - @dag.edges.select {|e| e.destination == self} + @dag.enumerated_edges.select { |e| e.destination == self } end def predecessors - incoming_edges.map(&:origin).uniq + incoming_edges.map(&:origin) end def successors - outgoing_edges.map(&:destination).uniq + @outgoing_edges.map(&:destination) end def inspect @@ -36,33 +32,26 @@ def inspect # Is there a path from here to +other+ following edges in the DAG? # # @param [DAG::Vertex] another Vertex is the same DAG - # @raise [ArgumentError] if +other+ is not a Vertex in the same DAG + # @raise [ArgumentError] if +other+ is not a Vertex # @return true iff there is a path following edges within this DAG # - def has_path_to?(other) - raise ArgumentError.new('You must supply a vertex in this DAG') unless - is_vertex_in_my_dag?(other) - successors.include?(other) || successors.any? {|v| v.has_path_to?(other) } + def path_to?(other) + raise ArgumentError, 'You must supply a vertex' unless other.is_a? Vertex + successors.include?(other) || successors.any? { |v| v.path_to? other } end - alias :has_descendant? :has_path_to? - alias :has_descendent? :has_path_to? # for backwards compat - # # Is there a path from +other+ to here following edges in the DAG? # # @param [DAG::Vertex] another Vertex is the same DAG - # @raise [ArgumentError] if +other+ is not a Vertex in the same DAG + # @raise [ArgumentError] if +other+ is not a Vertex # @return true iff there is a path following edges within this DAG # - def has_ancestor?(other) - raise ArgumentError.new('You must supply a vertex in this DAG') unless - is_vertex_in_my_dag?(other) - predecessors.include?(other) || predecessors.any? {|v| v.has_ancestor?(other) } + def reachable_from?(other) + raise ArgumentError, 'You must supply a vertex' unless other.is_a? Vertex + other.path_to? self end - alias :is_reachable_from? :has_ancestor? - # # Retrieve a value from the vertex's payload. # This is a shortcut for vertex.payload[key]. @@ -81,7 +70,7 @@ def ancestors(result_set = Set.new) v.ancestors(result_set) end end - return result_set + result_set end def descendants(result_set = Set.new) @@ -91,14 +80,13 @@ def descendants(result_set = Set.new) v.descendants(result_set) end end - return result_set + result_set end private - def is_vertex_in_my_dag?(v) - v.kind_of?(Vertex) and v.dag == self.dag + def add_edge(destination, properties) + Edge.new(self, destination, properties).tap { |e| @outgoing_edges << e } end end - end diff --git a/spec/lib/dag/vertex_spec.rb b/spec/lib/dag/vertex_spec.rb index 31350ac..52cb06a 100644 --- a/spec/lib/dag/vertex_spec.rb +++ b/spec/lib/dag/vertex_spec.rb @@ -7,23 +7,23 @@ let(:v2) { dag.add_vertex(name: :v2) } let(:v3) { dag.add_vertex(name: 'v3') } - describe '#has_path_to?' do + describe '#path_to?' do it 'cannot have a path to a non-vertex' do - expect { subject.has_path_to?(23) }.to raise_error(ArgumentError) + expect { subject.path_to?(23) }.to raise_error(ArgumentError) end it 'cannot have a path to a vertex in a different DAG' do - expect { subject.has_path_to?(DAG.new.add_vertex) }.to raise_error(ArgumentError) + expect(subject.path_to?(DAG.new.add_vertex)).to be_falsey end end - describe '#has_ancestor?' do + describe '#reachable_from?' do it 'ancestors must be a vertex' do - expect { subject.has_ancestor?(23) }.to raise_error(ArgumentError) + expect { subject.reachable_from?(23) }.to raise_error(ArgumentError) end it 'ancestors must be in the same DAG' do - expect { subject.has_ancestor?(DAG.new.add_vertex) }.to raise_error(ArgumentError) + expect(subject.reachable_from?(DAG.new.add_vertex)).to be_falsey end end @@ -33,7 +33,7 @@ it 'allows the payload to be accessed' do expect(subject[:name]).to eq('Fred') expect(subject[:size]).to eq(34) - expect(subject.payload).to eq({name: 'Fred', size: 34}) + expect(subject.payload).to eq(name: 'Fred', size: 34) end it 'returns nil for missing payload key' do @@ -61,21 +61,21 @@ end it 'has no paths to its predecessors' do - expect(subject.has_path_to?(v1)).to be_falsey - expect(subject.has_path_to?(v2)).to be_falsey + expect(subject.path_to?(v1)).to be_falsey + expect(subject.path_to?(v2)).to be_falsey end context 'with multiple paths' do - it 'lists each predecessor only once' do - dag.add_edge from: v1, to: subject - expect(subject.predecessors).to eq([v1, v2]) + it 'throws an exception' do + expect { dag.add_edge from: v1, to: subject } + .to raise_error(ArgumentError) end end it 'has the correct ancestors' do - expect(subject.has_ancestor?(v1)).to be_truthy - expect(subject.has_ancestor?(v2)).to be_truthy - expect(subject.has_ancestor?(v3)).to be_falsey + expect(subject.reachable_from?(v1)).to be_truthy + expect(subject.reachable_from?(v2)).to be_truthy + expect(subject.reachable_from?(v3)).to be_falsey end end @@ -94,20 +94,13 @@ end it 'has paths to its successors' do - expect(subject.has_path_to?(v1)).to be_truthy - expect(subject.has_path_to?(v2)).to be_truthy - end - - context 'with multiple paths' do - it 'lists each successor only once' do - dag.add_edge from: subject, to: v1 - expect(subject.successors).to eq([v1, v2]) - end + expect(subject.path_to?(v1)).to be_truthy + expect(subject.path_to?(v2)).to be_truthy end it 'has no ancestors' do - expect(subject.has_ancestor?(v1)).to be_falsey - expect(subject.has_ancestor?(v2)).to be_falsey + expect(subject.reachable_from?(v1)).to be_falsey + expect(subject.reachable_from?(v2)).to be_falsey end end @@ -118,15 +111,15 @@ end it 'has a deep path to v2' do - expect(subject.has_path_to?(v2)).to be_truthy + expect(subject.path_to?(v2)).to be_truthy end it 'has no path to v3' do - expect(subject.has_path_to?(v3)).to be_falsey + expect(subject.path_to?(v3)).to be_falsey end it 'recognises that it is an ancestor of v2' do - expect(v2.has_ancestor?(subject)).to be_truthy + expect(v2.reachable_from?(subject)).to be_truthy end it 'is known to all descendants' do @@ -134,13 +127,11 @@ end it 'knows has no ancestors' do - expect(subject.ancestors).to eq(Set.new()) + expect(subject.ancestors).to eq(Set.new) end it 'knows has all descendants' do expect(subject.descendants).to eq(Set.new([v1, v2])) end - end - end diff --git a/spec/lib/dag_spec.rb b/spec/lib/dag_spec.rb index 3f3b580..09493d1 100644 --- a/spec/lib/dag_spec.rb +++ b/spec/lib/dag_spec.rb @@ -1,10 +1,9 @@ require 'spec_helper' describe DAG do - it 'when new' do expect(subject.vertices).to be_empty - expect(subject.edges).to be_empty + expect(subject.enumerated_edges.size).to eq(0) end context 'with one vertex' do @@ -31,7 +30,7 @@ def my_name end it 'mixes the module into evey vertex' do - expect((Thing === v)).to be_truthy + expect(v.is_a?(Thing)).to be_truthy end it 'allows the module to access the payload' do @@ -44,7 +43,9 @@ def my_name context 'when valid' do let(:v2) { subject.add_vertex } - let!(:e1) { subject.add_edge(origin: v1, destination: v2) } + let!(:e1) do + subject.add_edge(from: v1, to: v2, properties: { foo: 'bar' }) + end it 'leaves the origin vertex' do expect(v1.outgoing_edges).to eq([e1]) @@ -59,17 +60,8 @@ def my_name expect(v2.outgoing_edges).to be_empty end - it 'it has no properties' do - expect(e1.properties).to be_empty - end - - it 'allows multiple edges between a pair of vertices' do - expect { subject.add_edge(origin: v1, destination: v2) }.to_not raise_error - end - it 'can specify properties' do - e2 = subject.add_edge(origin: v1, destination: v2, properties: {foo: 'bar'}) - expect(e2.properties[:foo]).to eq('bar') + expect(e1.properties[:foo]).to eq('bar') end end @@ -83,14 +75,18 @@ def my_name end it 'requires the endpoints to be vertices' do - expect { subject.add_edge(from: v1, to: 23) }.to raise_error(ArgumentError) - expect { subject.add_edge(from: 45, to: v1) }.to raise_error(ArgumentError) + expect { subject.add_edge(from: v1, to: 23) } + .to raise_error(ArgumentError) + expect { subject.add_edge(from: 45, to: v1) } + .to raise_error(ArgumentError) end it 'requires the endpoints to be in the same DAG' do v2 = DAG.new.add_vertex - expect { subject.add_edge(from: v1, to: v2) }.to raise_error(ArgumentError) - expect { subject.add_edge(from: v2, to: v1) }.to raise_error(ArgumentError) + expect { subject.add_edge(from: v1, to: v2) } + .to raise_error(ArgumentError) + expect { subject.add_edge(from: v2, to: v1) } + .to raise_error(ArgumentError) end it 'rejects an edge that would create a loop' do @@ -100,33 +96,53 @@ def my_name subject.add_edge from: v1, to: v2 subject.add_edge from: v2, to: v3 subject.add_edge from: v3, to: v4 - expect { subject.add_edge from: v4, to: v1 }.to raise_error(ArgumentError) + expect { subject.add_edge from: v4, to: v1 } + .to raise_error(ArgumentError) end it 'rejects an edge from a vertex to itself' do - expect { subject.add_edge from: v1, to: v1 }.to raise_error(ArgumentError) + expect { subject.add_edge from: v1, to: v1 } + .to raise_error(ArgumentError) + end + end + + context 'with different keywords' do + let(:v1) { subject.add_vertex } + let(:v2) { subject.add_vertex } + + it 'allows :origin and :destination' do + expect { subject.add_edge(origin: v1, destination: v2) } + .to_not raise_error end end context 'with different keywords' do let(:v1) { subject.add_vertex } let(:v2) { subject.add_vertex } - let!(:e1) { subject.add_edge(origin: v1, destination: v2) } it 'allows :source and :sink' do - expect(subject.add_edge(source: v1, sink: v2)).to eq(e1) + expect { subject.add_edge(source: v1, sink: v2) }.to_not raise_error end + end + + context 'with different keywords' do + let(:v1) { subject.add_vertex } + let(:v2) { subject.add_vertex } it 'allows :from and :to' do - expect(subject.add_edge(from: v1, to: v2)).to eq(e1) + expect { subject.add_edge(from: v1, to: v2) } + .to_not raise_error end + end + + context 'with different keywords' do + let(:v1) { subject.add_vertex } + let(:v2) { subject.add_vertex } it 'allows :start and :end' do - expect(subject.add_edge(start: v1, end: v2)).to eq(e1) + expect { subject.add_edge(start: v1, end: v2) }.to_not raise_error end - end - end context 'given a dag' do @@ -137,59 +153,79 @@ def my_name end end - let(:joe) { subject.add_vertex(name: "joe") } - let(:bob) { subject.add_vertex(name: "bob") } - let(:jane) { subject.add_vertex(name: "jane") } - let!(:e1) { subject.add_edge(origin: joe, destination: bob, properties: {name: "father of"} ) } + let(:joe) { subject.add_vertex(name: 'joe') } + let(:bob) { subject.add_vertex(name: 'bob') } + let(:jane) { subject.add_vertex(name: 'jane') } + let(:ada) { subject.add_vertex(name: 'ada') } + let(:chris) { subject.add_vertex(name: 'chris') } + let!(:e1) do + subject.add_edge(origin: joe, destination: bob, + properties: { name: 'father of' }) + end let!(:e2) { subject.add_edge(origin: joe, destination: jane) } let!(:e3) { subject.add_edge(origin: bob, destination: jane) } + let!(:e4) { subject.add_edge(origin: ada, destination: joe) } + let!(:e5) { subject.add_edge(origin: jane, destination: chris) } describe '.subgraph' do it 'returns a graph' do - expect(subject.subgraph()).to be_an_instance_of(DAG) + expect(subject.subgraph).to be_an_instance_of(DAG) end - it 'of joe and his ancestors' do - subgraph = subject.subgraph([joe,],[]) - expect(subgraph.vertices.length).to eq(1) - expect(subgraph.vertices[0].my_name).to eq("joe") - expect(subgraph.edges).to be_empty + it 'of ada and her ancestors' do + subgraph = subject.subgraph([ada], []) + expect(subgraph.vertices.size).to eq(1) + expect(subgraph.vertices[0].my_name).to eq('ada') + expect(subgraph.enumerated_edges.size).to eq(0) end it 'of joe and his descendants' do - subgraph = subject.subgraph([],[joe,]) - expect(Set.new(subgraph.vertices.map(&:my_name))).to eq(Set.new(["joe","bob","jane"])) - expect(subgraph.edges.length).to eq(3) + subgraph = subject.subgraph([], [joe]) + expect(Set.new(subgraph.vertices.map(&:my_name))) + .to eq(Set.new(%w[joe bob jane chris])) + expect(subgraph.enumerated_edges.size).to eq(4) end it 'of Jane and her ancestors' do - subgraph = subject.subgraph([jane,],[]) - expect(Set.new(subgraph.vertices.map(&:my_name))).to eq(Set.new(["joe","bob","jane"])) - expect(subgraph.edges.length).to eq(3) + subgraph = subject.subgraph([jane], []) + expect(Set.new(subgraph.vertices.map(&:my_name))) + .to eq(Set.new(%w[joe bob jane ada])) + expect(subgraph.enumerated_edges.size).to eq(4) end - it 'of jane and her descendants' do - subgraph = subject.subgraph([],[jane,]) - expect(Set.new(subgraph.vertices.map(&:my_name))).to eq(Set.new(["jane"])) - expect(subgraph.edges).to be_empty + it 'of chris and his descendants' do + subgraph = subject.subgraph([], [chris]) + expect(Set.new(subgraph.vertices.map(&:my_name))) + .to eq(Set.new(['chris'])) + expect(subgraph.enumerated_edges.size).to eq(0) end it 'of bob and his descendants' do - subgraph = subject.subgraph([],[bob,]) - expect(Set.new(subgraph.vertices.map(&:my_name))).to eq(Set.new(["bob","jane"])) - expect(subgraph.edges.length).to eq(1) + subgraph = subject.subgraph([], [bob]) + expect(Set.new(subgraph.vertices.map(&:my_name))) + .to eq(Set.new(%w[bob jane chris])) + expect(subgraph.enumerated_edges.size).to eq(2) end it 'there is something incestuous going on here' do - subgraph = subject.subgraph([bob,],[bob,]) - expect(Set.new(subgraph.vertices.map(&:my_name))).to eq(Set.new(["bob","jane","joe"])) - expect(subgraph.edges.length).to eq(2) - expect(subgraph.edges[0].properties).to eq({name: "father of"}) - expect(subgraph.edges[1].properties).to eq({}) + subgraph = subject.subgraph([bob], [bob]) + expect(Set.new(subgraph.vertices.map(&:my_name))) + .to eq(Set.new(%w[bob jane joe ada chris])) + expect(subgraph.enumerated_edges.size).to eq(4) end - end + describe '.topological_sort' do + it 'returns a correct topological sort' do + sort = subject.topological_sort + expect(sort).to be_an_instance_of(Array) + expect(sort.size).to eq(5) + expect(sort[0].my_name).to eq('ada') + expect(sort[1].my_name).to eq('joe') + expect(sort[2].my_name).to eq('bob') + expect(sort[3].my_name).to eq('jane') + expect(sort[4].my_name).to eq('chris') + end + end end - end