simple_dag 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fa2d29444d0ba1fc5d58a978b2155b6ae935bbab
4
+ data.tar.gz: 169ecd6b74d0dbcb692be5d7d31451a962acefbf
5
+ SHA512:
6
+ metadata.gz: bfec6a34670faf8dfc41cb179e8a77c2aa5185f7810d380072bce9fd2295b457a4dd7c56e9cb8577b5e8fc03a56992d03f6ea842b5e6f163e7db70932e5eddd8
7
+ data.tar.gz: e4efddca431809b0e28e04cfa80b7eb30e170242e33f12c4fc3653e329c727d6fddb615d1f4a88deaa1771839929c2efb2c0431a023373519d0820fac90f1a39
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ -Ilib
3
+
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,6 @@
1
+ # Style
2
+
3
+ Please use Rubocop with default settings as a beautifier. In the atom ide, for
4
+ instance, it can be used in combination with the package atom-beautify.
5
+
6
+ Also use a maximal line length of 80 characters.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,33 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ simple_dag (0.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.2.5)
10
+ rake (10.4.2)
11
+ rspec (3.1.0)
12
+ rspec-core (~> 3.1.0)
13
+ rspec-expectations (~> 3.1.0)
14
+ rspec-mocks (~> 3.1.0)
15
+ rspec-core (3.1.7)
16
+ rspec-support (~> 3.1.0)
17
+ rspec-expectations (3.1.2)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.1.0)
20
+ rspec-mocks (3.1.3)
21
+ rspec-support (~> 3.1.0)
22
+ rspec-support (3.1.2)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ rake (~> 10)
29
+ rspec (~> 3)
30
+ simple_dag!
31
+
32
+ BUNDLED WITH
33
+ 1.16.1
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2013 Kevin Rutherford
4
+
5
+ Modified work Copyright 2018 Fabian Sobanski
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # DAG
2
+
3
+ Simple directed acyclic graphs for Ruby.
4
+
5
+ ## History
6
+
7
+ This ruby gem started out as a fork of [kevinrutherford's dag implementation](https://github.com/kevinrutherford/dag). If you want to migrate
8
+ from his implementation to this one, have a look at the
9
+ [breaking changes](#breaking-changes). Have a look at
10
+ [performance improvements](#performance-improvements) to see why you might
11
+ want to migrate.
12
+
13
+ ## Installation
14
+
15
+ Install the gem
16
+
17
+ ```
18
+ gem install simple_dag
19
+ ```
20
+
21
+ Or add it to your Gemfile and run `bundle`.
22
+
23
+ ``` ruby
24
+ gem 'simple_dag'
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```ruby
30
+ require 'simple_dag'
31
+
32
+ dag = DAG.new
33
+
34
+ v1 = dag.add_vertex
35
+ v2 = dag.add_vertex
36
+ v3 = dag.add_vertex
37
+
38
+ dag.add_edge from: v1, to: v2
39
+ dag.add_edge from: v2, to: v3
40
+
41
+ v1.path_to?(v3) # => true
42
+ v3.path_to?(v1) # => false
43
+
44
+ dag.add_edge from: v3, to: v1 # => ArgumentError: A DAG must not have cycles
45
+
46
+ dag.add_edge from: v1, to: v2 # => ArgumentError: Edge already exists
47
+ dag.add_edge from: v1, to: v3
48
+ v1.successors # => [v2, v3]
49
+ ```
50
+
51
+ See the specs for more detailed usage scenarios.
52
+
53
+ ## Compatibility
54
+
55
+ Tested with Ruby 2.2, 2.3, 2.4, 2.5, JRuby, Rubinius.
56
+ Builds with Ruby 2.5 and JRuby are currently failing. See
57
+ [this issue](https://github.com/fsobanski/dag/issues/1) for details.
58
+
59
+ ## Differences to [dag](https://github.com/kevinrutherford/dag)
60
+
61
+ ### Breaking changes
62
+
63
+ - The function `DAG::Vertex#has_path_to?` aliased as
64
+ `DAG::Vertex#has_descendant?` and `DAG::Vertex#has_descendent?` has been renamed
65
+ to `DAG::Vertex#path_to?`. The aliases have been removed.
66
+
67
+ - The function `DAG::Vertex#has_ancestor?` aliased as
68
+ `DAG::Vertex#is_reachable_from?` has been renamed to
69
+ `DAG::Vertex#reachable_from?`. The aliases have been removed.
70
+
71
+ - The array of edges returned by `DAG#edges` is no longer sorted by insertion
72
+ order of the edges.
73
+
74
+ - `DAG::Vertex#path_to?` and `DAG::Vertex#reachable_from?` no longer raise
75
+ errors if the vertex passed as an argument is not a vertex in the same `DAG`.
76
+ Instead, they just return `false`.
77
+
78
+ - [Parallel edges](https://en.wikipedia.org/wiki/Multiple_edges) are no longer
79
+ allowed in the dag. Instead, `DAG#add_edge` raises an `ArgumentError` if you
80
+ try to add an edge between two adjacent vertices. If you want to model a
81
+ multigraph, you can add a weight payload to the edges that contains a natural
82
+ number.
83
+
84
+ ### New functions
85
+
86
+ - `DAG#topological_sort` returns a topological sort of the vertices in the dag
87
+ in a theoretically optimal computational time complexity.
88
+
89
+ - `DAG#enumerated_edges` returns an `Enumerator` of the edges in the dag.
90
+
91
+ ### Performance improvements
92
+
93
+ - The computational complexity of `DAG::Vertex#outgoing_edges` has
94
+ improved to a constant because the edges are no longer stored in one array in
95
+ the `DAG`. Instead, the edges are now stored in their respective source
96
+ `Vertex`.
97
+
98
+ - The performance of `DAG::Vertex#successors` has improved because firstly,
99
+ it depends on `DAG::Vertex#outgoing_edges` and secondly the call to
100
+ `Array#uniq` is no longer necessary since parallel edges are prohibited.
101
+
102
+ - The computational complexities of `DAG::Vertex#descendants`,
103
+ `DAG::Vertex#path_to?` and `DAG::Vertex#reachable_from?` have improved because
104
+ the functions depend on `DAG::Vertex#successors`.
105
+
106
+ - The computational complexity of `DAG::Vertex#incoming_edges` is
107
+ unchanged: Linear in the number of all edges in the `DAG`.
108
+
109
+ - The performance of `DAG::Vertex#predecessors` has improved because the call
110
+ to `Array#uniq` is no longer necessary since parallel edges are prohibited.
111
+
112
+ - The performance of `DAG::Vertex#ancestors` has improved because the function
113
+ depends on `DAG::Vertex#predecessors`.
114
+
115
+ - The computational complexity of `DAG::add_edge` has improved because the
116
+ cycle check in the function depends on `DAG::Vertex#path_to?`.
117
+
118
+ - The performance of `DAG#subgraph` has improved because the function depends
119
+ on `DAG::Vertex#descendants`, `DAG::Vertex#ancestors` and `DAG::add_edge`.
120
+
121
+ - The computational complexity of `DAG::edges` has worsened from a constant
122
+ complexity to a linear complexity. This is irrelevant if you want to iterate
123
+ over all the edges in the graph. You should consider using
124
+ `DAG#enumerated_edges` for a better space utilization.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new('spec')
4
+
5
+ task :default => :spec
6
+
@@ -0,0 +1,102 @@
1
+ require 'set'
2
+
3
+ class DAG
4
+ class Vertex
5
+ attr_reader :dag, :payload, :outgoing_edges
6
+
7
+ def initialize(dag, payload)
8
+ @dag = dag
9
+ @payload = payload
10
+ @outgoing_edges = []
11
+ end
12
+
13
+ private :initialize
14
+
15
+ def incoming_edges
16
+ @dag.enumerated_edges.select { |e| e.destination == self }
17
+ end
18
+
19
+ def predecessors
20
+ incoming_edges.map(&:origin)
21
+ end
22
+
23
+ def successors
24
+ @outgoing_edges.map(&:destination)
25
+ end
26
+
27
+ def inspect
28
+ "DAG::Vertex:#{@payload.inspect}"
29
+ end
30
+
31
+ #
32
+ # Is there a path from here to +other+ following edges in the DAG?
33
+ #
34
+ # @param [DAG::Vertex] another Vertex is the same DAG
35
+ # @raise [ArgumentError] if +other+ is not a Vertex
36
+ # @return true iff there is a path following edges within this DAG
37
+ #
38
+ def path_to?(other)
39
+ raise ArgumentError, 'You must supply a vertex' unless other.is_a? Vertex
40
+ visited = Set.new
41
+
42
+ visit = lambda { |v|
43
+ return false if visited.include? v
44
+ return true if v.successors.lazy.include? other
45
+ return true if v.successors.lazy.any? { |succ| visit.call succ }
46
+ visited.add v
47
+ false
48
+ }
49
+
50
+ visit.call self
51
+ end
52
+
53
+ #
54
+ # Is there a path from +other+ to here following edges in the DAG?
55
+ #
56
+ # @param [DAG::Vertex] another Vertex is the same DAG
57
+ # @raise [ArgumentError] if +other+ is not a Vertex
58
+ # @return true iff there is a path following edges within this DAG
59
+ #
60
+ def reachable_from?(other)
61
+ raise ArgumentError, 'You must supply a vertex' unless other.is_a? Vertex
62
+ other.path_to? self
63
+ end
64
+
65
+ #
66
+ # Retrieve a value from the vertex's payload.
67
+ # This is a shortcut for vertex.payload[key].
68
+ #
69
+ # @param key [Object] the payload key
70
+ # @return the corresponding value from the payload Hash, or nil if not found
71
+ #
72
+ def [](key)
73
+ @payload[key]
74
+ end
75
+
76
+ def ancestors(result_set = Set.new)
77
+ predecessors.each do |v|
78
+ unless result_set.include? v
79
+ result_set.add(v)
80
+ v.ancestors(result_set)
81
+ end
82
+ end
83
+ result_set
84
+ end
85
+
86
+ def descendants(result_set = Set.new)
87
+ successors.each do |v|
88
+ unless result_set.include? v
89
+ result_set.add(v)
90
+ v.descendants(result_set)
91
+ end
92
+ end
93
+ result_set
94
+ end
95
+
96
+ private
97
+
98
+ def add_edge(destination, properties)
99
+ Edge.new(self, destination, properties).tap { |e| @outgoing_edges << e }
100
+ end
101
+ end
102
+ end
data/lib/simple_dag.rb ADDED
@@ -0,0 +1,139 @@
1
+ require 'set'
2
+
3
+ require_relative 'simple_dag/vertex'
4
+
5
+ class DAG
6
+ Edge = Struct.new(:origin, :destination, :properties)
7
+
8
+ attr_reader :vertices
9
+
10
+ #
11
+ # Create a new Directed Acyclic Graph
12
+ #
13
+ # @param [Hash] options configuration options
14
+ # @option options [Module] mix this module into any created +Vertex+
15
+ #
16
+ def initialize(options = {})
17
+ @vertices = []
18
+ @mixin = options[:mixin]
19
+ @n_of_edges = 0
20
+ end
21
+
22
+ def add_vertex(payload = {})
23
+ Vertex.new(self, payload).tap do |v|
24
+ v.extend(@mixin) if @mixin
25
+ @vertices << v
26
+ end
27
+ end
28
+
29
+ def add_edge(attrs)
30
+ origin = attrs[:origin] || attrs[:source] || attrs[:from] || attrs[:start]
31
+ destination = attrs[:destination] || attrs[:sink] || attrs[:to] ||
32
+ attrs[:end]
33
+ properties = attrs[:properties] || {}
34
+ raise ArgumentError, 'Origin must be a vertex in this DAG' unless
35
+ my_vertex?(origin)
36
+ raise ArgumentError, 'Destination must be a vertex in this DAG' unless
37
+ my_vertex?(destination)
38
+ raise ArgumentError, 'Edge already exists' if
39
+ origin.successors.include? destination
40
+ raise ArgumentError, 'A DAG must not have cycles' if origin == destination
41
+ raise ArgumentError, 'A DAG must not have cycles' if
42
+ destination.path_to?(origin)
43
+ @n_of_edges += 1
44
+ origin.send :add_edge, destination, properties
45
+ end
46
+
47
+ # @return Enumerator over all edges in the dag
48
+ def enumerated_edges
49
+ Enumerator.new(@n_of_edges) do |e|
50
+ @vertices.each { |v| v.outgoing_edges.each { |out| e << out } }
51
+ end
52
+ end
53
+
54
+ def edges
55
+ enumerated_edges.to_a
56
+ end
57
+
58
+ def subgraph(predecessors_of = [], successors_of = [])
59
+ (predecessors_of + successors_of).each do |v|
60
+ raise ArgumentError, 'You must supply a vertex in this DAG' unless
61
+ my_vertex?(v)
62
+ end
63
+
64
+ result = self.class.new(mixin: @mixin)
65
+ vertex_mapping = {}
66
+
67
+ # Get the set of predecessors verticies and add a copy to the result
68
+ predecessors_set = Set.new(predecessors_of)
69
+ predecessors_of.each { |v| v.ancestors(predecessors_set) }
70
+
71
+ predecessors_set.each do |v|
72
+ vertex_mapping[v] = result.add_vertex(v.payload)
73
+ end
74
+
75
+ # Get the set of successor vertices and add a copy to the result
76
+ successors_set = Set.new(successors_of)
77
+ successors_of.each { |v| v.descendants(successors_set) }
78
+
79
+ successors_set.each do |v|
80
+ vertex_mapping[v] = result.add_vertex(v.payload) unless
81
+ vertex_mapping.include? v
82
+ end
83
+
84
+ # get the unique edges
85
+ edge_set = (
86
+ predecessors_set.flat_map(&:incoming_edges) +
87
+ successors_set.flat_map(&:outgoing_edges)
88
+ ).uniq
89
+
90
+ # Add them to the result via the vertex mapping
91
+ edge_set.each do |e|
92
+ result.add_edge(
93
+ from: vertex_mapping[e.origin],
94
+ to: vertex_mapping[e.destination],
95
+ properties: e.properties
96
+ )
97
+ end
98
+
99
+ result
100
+ end
101
+
102
+ # Returns an array of the vertices in the graph in a topological order, i.e.
103
+ # for every path in the dag from a vertex v to a vertex u, v comes before u
104
+ # in the array.
105
+ #
106
+ # Uses a depth first search.
107
+ #
108
+ # Assuming that the method include? of class Set runs in linear time, which
109
+ # can be assumed in all practical cases, this method runs in O(n+m) where
110
+ # m is the number of edges and n is the number of vertices.
111
+ def topological_sort
112
+ result_size = 0
113
+ result = Array.new(@vertices.length)
114
+ visited = Set.new
115
+
116
+ visit = lambda { |v|
117
+ return if visited.include? v
118
+ v.successors.each do |u|
119
+ visit.call u
120
+ end
121
+ visited.add v
122
+ result_size += 1
123
+ result[-result_size] = v
124
+ }
125
+
126
+ @vertices.each do |v|
127
+ next if visited.include? v
128
+ visit.call v
129
+ end
130
+
131
+ result
132
+ end
133
+
134
+ private
135
+
136
+ def my_vertex?(v)
137
+ v.is_a?(Vertex) && (v.dag == self)
138
+ end
139
+ end
@@ -0,0 +1,137 @@
1
+ require 'spec_helper'
2
+
3
+ describe DAG::Vertex do
4
+ let(:dag) { DAG.new }
5
+ subject { dag.add_vertex }
6
+ let(:v1) { dag.add_vertex(name: :v1) }
7
+ let(:v2) { dag.add_vertex(name: :v2) }
8
+ let(:v3) { dag.add_vertex(name: 'v3') }
9
+
10
+ describe '#path_to?' do
11
+ it 'cannot have a path to a non-vertex' do
12
+ expect { subject.path_to?(23) }.to raise_error(ArgumentError)
13
+ end
14
+
15
+ it 'cannot have a path to a vertex in a different DAG' do
16
+ expect(subject.path_to?(DAG.new.add_vertex)).to be_falsey
17
+ end
18
+ end
19
+
20
+ describe '#reachable_from?' do
21
+ it 'ancestors must be a vertex' do
22
+ expect { subject.reachable_from?(23) }.to raise_error(ArgumentError)
23
+ end
24
+
25
+ it 'ancestors must be in the same DAG' do
26
+ expect(subject.reachable_from?(DAG.new.add_vertex)).to be_falsey
27
+ end
28
+ end
29
+
30
+ describe 'with a payload' do
31
+ subject { dag.add_vertex(name: 'Fred', size: 34) }
32
+
33
+ it 'allows the payload to be accessed' do
34
+ expect(subject[:name]).to eq('Fred')
35
+ expect(subject[:size]).to eq(34)
36
+ expect(subject.payload).to eq(name: 'Fred', size: 34)
37
+ end
38
+
39
+ it 'returns nil for missing payload key' do
40
+ expect(subject[56]).to be_nil
41
+ end
42
+
43
+ it 'allows the payload to be changed' do
44
+ subject.payload[:another] = 'ha'
45
+ expect(subject[:another]).to eq('ha')
46
+ end
47
+ end
48
+
49
+ context 'with predecessors' do
50
+ before do
51
+ dag.add_edge from: v1, to: subject
52
+ dag.add_edge from: v2, to: subject
53
+ end
54
+
55
+ it 'has the correct predecessors' do
56
+ expect(subject.predecessors).to eq([v1, v2])
57
+ end
58
+
59
+ it 'has no successors' do
60
+ expect(subject.successors).to be_empty
61
+ end
62
+
63
+ it 'has no paths to its predecessors' do
64
+ expect(subject.path_to?(v1)).to be_falsey
65
+ expect(subject.path_to?(v2)).to be_falsey
66
+ end
67
+
68
+ context 'with multiple paths' do
69
+ it 'throws an exception' do
70
+ expect { dag.add_edge from: v1, to: subject }
71
+ .to raise_error(ArgumentError)
72
+ end
73
+ end
74
+
75
+ it 'has the correct ancestors' do
76
+ expect(subject.reachable_from?(v1)).to be_truthy
77
+ expect(subject.reachable_from?(v2)).to be_truthy
78
+ expect(subject.reachable_from?(v3)).to be_falsey
79
+ end
80
+ end
81
+
82
+ context 'with successors' do
83
+ before do
84
+ dag.add_edge from: subject, to: v1
85
+ dag.add_edge from: subject, to: v2
86
+ end
87
+
88
+ it 'has no predecessors' do
89
+ expect(subject.predecessors).to be_empty
90
+ end
91
+
92
+ it 'has the correct successors' do
93
+ expect(subject.successors).to eq([v1, v2])
94
+ end
95
+
96
+ it 'has paths to its successors' do
97
+ expect(subject.path_to?(v1)).to be_truthy
98
+ expect(subject.path_to?(v2)).to be_truthy
99
+ end
100
+
101
+ it 'has no ancestors' do
102
+ expect(subject.reachable_from?(v1)).to be_falsey
103
+ expect(subject.reachable_from?(v2)).to be_falsey
104
+ end
105
+ end
106
+
107
+ context 'in a deep DAG' do
108
+ before do
109
+ dag.add_edge from: subject, to: v1
110
+ dag.add_edge from: v1, to: v2
111
+ end
112
+
113
+ it 'has a deep path to v2' do
114
+ expect(subject.path_to?(v2)).to be_truthy
115
+ end
116
+
117
+ it 'has no path to v3' do
118
+ expect(subject.path_to?(v3)).to be_falsey
119
+ end
120
+
121
+ it 'recognises that it is an ancestor of v2' do
122
+ expect(v2.reachable_from?(subject)).to be_truthy
123
+ end
124
+
125
+ it 'is known to all descendants' do
126
+ expect(v2.ancestors).to eq(Set.new([v1, subject]))
127
+ end
128
+
129
+ it 'knows has no ancestors' do
130
+ expect(subject.ancestors).to eq(Set.new)
131
+ end
132
+
133
+ it 'knows has all descendants' do
134
+ expect(subject.descendants).to eq(Set.new([v1, v2]))
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,231 @@
1
+ require 'spec_helper'
2
+
3
+ describe DAG do
4
+ it 'when new' do
5
+ expect(subject.vertices).to be_empty
6
+ expect(subject.enumerated_edges.size).to eq(0)
7
+ end
8
+
9
+ context 'with one vertex' do
10
+ it 'has only that vertex' do
11
+ v = subject.add_vertex
12
+ expect(subject.vertices).to eq([v])
13
+ end
14
+
15
+ it 'has no edges' do
16
+ v = subject.add_vertex
17
+ expect(v.outgoing_edges).to be_empty
18
+ expect(v.incoming_edges).to be_empty
19
+ end
20
+ end
21
+
22
+ context 'using a mix-in module' do
23
+ subject { DAG.new(mixin: Thing) }
24
+ let(:v) { subject.add_vertex(name: 'Fred') }
25
+
26
+ module Thing
27
+ def my_name
28
+ payload[:name]
29
+ end
30
+ end
31
+
32
+ it 'mixes the module into evey vertex' do
33
+ expect(v.is_a?(Thing)).to be_truthy
34
+ end
35
+
36
+ it 'allows the module to access the payload' do
37
+ expect(v.my_name).to eq('Fred')
38
+ end
39
+ end
40
+
41
+ context 'creating an edge' do
42
+ let(:v1) { subject.add_vertex }
43
+
44
+ context 'when valid' do
45
+ let(:v2) { subject.add_vertex }
46
+ let!(:e1) do
47
+ subject.add_edge(from: v1, to: v2, properties: { foo: 'bar' })
48
+ end
49
+
50
+ it 'leaves the origin vertex' do
51
+ expect(v1.outgoing_edges).to eq([e1])
52
+ end
53
+
54
+ it 'arrives at the destination vertex' do
55
+ expect(v2.incoming_edges).to eq([e1])
56
+ end
57
+
58
+ it 'adds no other edges' do
59
+ expect(v1.incoming_edges).to be_empty
60
+ expect(v2.outgoing_edges).to be_empty
61
+ end
62
+
63
+ it 'can specify properties' do
64
+ expect(e1.properties[:foo]).to eq('bar')
65
+ end
66
+ end
67
+
68
+ context 'when invalid' do
69
+ it 'requires an origin vertex' do
70
+ expect { subject.add_edge(to: v1) }.to raise_error(ArgumentError)
71
+ end
72
+
73
+ it 'requires a destination vertex' do
74
+ expect { subject.add_edge(from: v1) }.to raise_error(ArgumentError)
75
+ end
76
+
77
+ it 'requires the endpoints to be vertices' do
78
+ expect { subject.add_edge(from: v1, to: 23) }
79
+ .to raise_error(ArgumentError)
80
+ expect { subject.add_edge(from: 45, to: v1) }
81
+ .to raise_error(ArgumentError)
82
+ end
83
+
84
+ it 'requires the endpoints to be in the same DAG' do
85
+ v2 = DAG.new.add_vertex
86
+ expect { subject.add_edge(from: v1, to: v2) }
87
+ .to raise_error(ArgumentError)
88
+ expect { subject.add_edge(from: v2, to: v1) }
89
+ .to raise_error(ArgumentError)
90
+ end
91
+
92
+ it 'rejects an edge that would create a loop' do
93
+ v2 = subject.add_vertex
94
+ v3 = subject.add_vertex
95
+ v4 = subject.add_vertex
96
+ subject.add_edge from: v1, to: v2
97
+ subject.add_edge from: v2, to: v3
98
+ subject.add_edge from: v3, to: v4
99
+ expect { subject.add_edge from: v4, to: v1 }
100
+ .to raise_error(ArgumentError)
101
+ end
102
+
103
+ it 'rejects an edge from a vertex to itself' do
104
+ expect { subject.add_edge from: v1, to: v1 }
105
+ .to raise_error(ArgumentError)
106
+ end
107
+ end
108
+
109
+ context 'with different keywords' do
110
+ let(:v1) { subject.add_vertex }
111
+ let(:v2) { subject.add_vertex }
112
+
113
+ it 'allows :origin and :destination' do
114
+ expect { subject.add_edge(origin: v1, destination: v2) }
115
+ .to_not raise_error
116
+ end
117
+ end
118
+
119
+ context 'with different keywords' do
120
+ let(:v1) { subject.add_vertex }
121
+ let(:v2) { subject.add_vertex }
122
+
123
+ it 'allows :source and :sink' do
124
+ expect { subject.add_edge(source: v1, sink: v2) }.to_not raise_error
125
+ end
126
+ end
127
+
128
+ context 'with different keywords' do
129
+ let(:v1) { subject.add_vertex }
130
+ let(:v2) { subject.add_vertex }
131
+
132
+ it 'allows :from and :to' do
133
+ expect { subject.add_edge(from: v1, to: v2) }
134
+ .to_not raise_error
135
+ end
136
+ end
137
+
138
+ context 'with different keywords' do
139
+ let(:v1) { subject.add_vertex }
140
+ let(:v2) { subject.add_vertex }
141
+
142
+ it 'allows :start and :end' do
143
+ expect { subject.add_edge(start: v1, end: v2) }.to_not raise_error
144
+ end
145
+ end
146
+ end
147
+
148
+ context 'given a dag' do
149
+ subject { DAG.new(mixin: Thing) }
150
+ module Thing
151
+ def my_name
152
+ payload[:name]
153
+ end
154
+ end
155
+
156
+ let(:joe) { subject.add_vertex(name: 'joe') }
157
+ let(:bob) { subject.add_vertex(name: 'bob') }
158
+ let(:jane) { subject.add_vertex(name: 'jane') }
159
+ let(:ada) { subject.add_vertex(name: 'ada') }
160
+ let(:chris) { subject.add_vertex(name: 'chris') }
161
+ let!(:e1) do
162
+ subject.add_edge(origin: joe, destination: bob,
163
+ properties: { name: 'father of' })
164
+ end
165
+ let!(:e2) { subject.add_edge(origin: joe, destination: jane) }
166
+ let!(:e3) { subject.add_edge(origin: bob, destination: jane) }
167
+ let!(:e4) { subject.add_edge(origin: ada, destination: joe) }
168
+ let!(:e5) { subject.add_edge(origin: jane, destination: chris) }
169
+
170
+ describe '.subgraph' do
171
+ it 'returns a graph' do
172
+ expect(subject.subgraph).to be_an_instance_of(DAG)
173
+ end
174
+
175
+ it 'of ada and her ancestors' do
176
+ subgraph = subject.subgraph([ada], [])
177
+ expect(subgraph.vertices.size).to eq(1)
178
+ expect(subgraph.vertices[0].my_name).to eq('ada')
179
+ expect(subgraph.enumerated_edges.size).to eq(0)
180
+ end
181
+
182
+ it 'of joe and his descendants' do
183
+ subgraph = subject.subgraph([], [joe])
184
+ expect(Set.new(subgraph.vertices.map(&:my_name)))
185
+ .to eq(Set.new(%w[joe bob jane chris]))
186
+ expect(subgraph.enumerated_edges.size).to eq(4)
187
+ end
188
+
189
+ it 'of Jane and her ancestors' do
190
+ subgraph = subject.subgraph([jane], [])
191
+ expect(Set.new(subgraph.vertices.map(&:my_name)))
192
+ .to eq(Set.new(%w[joe bob jane ada]))
193
+ expect(subgraph.enumerated_edges.size).to eq(4)
194
+ end
195
+
196
+ it 'of chris and his descendants' do
197
+ subgraph = subject.subgraph([], [chris])
198
+ expect(Set.new(subgraph.vertices.map(&:my_name)))
199
+ .to eq(Set.new(['chris']))
200
+ expect(subgraph.enumerated_edges.size).to eq(0)
201
+ end
202
+
203
+ it 'of bob and his descendants' do
204
+ subgraph = subject.subgraph([], [bob])
205
+ expect(Set.new(subgraph.vertices.map(&:my_name)))
206
+ .to eq(Set.new(%w[bob jane chris]))
207
+ expect(subgraph.enumerated_edges.size).to eq(2)
208
+ end
209
+
210
+ it 'there is something incestuous going on here' do
211
+ subgraph = subject.subgraph([bob], [bob])
212
+ expect(Set.new(subgraph.vertices.map(&:my_name)))
213
+ .to eq(Set.new(%w[bob jane joe ada chris]))
214
+ expect(subgraph.enumerated_edges.size).to eq(4)
215
+ end
216
+ end
217
+
218
+ describe '.topological_sort' do
219
+ it 'returns a correct topological sort' do
220
+ sort = subject.topological_sort
221
+ expect(sort).to be_an_instance_of(Array)
222
+ expect(sort.size).to eq(5)
223
+ expect(sort[0].my_name).to eq('ada')
224
+ expect(sort[1].my_name).to eq('joe')
225
+ expect(sort[2].my_name).to eq('bob')
226
+ expect(sort[3].my_name).to eq('jane')
227
+ expect(sort[4].my_name).to eq('chris')
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'simple_dag'
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_dag
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Kevin Rutherford
8
+ - Fabian Sobanski
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2018-01-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '10'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '10'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rspec
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '3'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '3'
42
+ description: A simple library for working with directed acyclic graphs
43
+ email:
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".rspec"
49
+ - ".yardopts"
50
+ - CONTRIBUTING.md
51
+ - Gemfile
52
+ - Gemfile.lock
53
+ - LICENSE
54
+ - README.md
55
+ - Rakefile
56
+ - lib/simple_dag.rb
57
+ - lib/simple_dag/vertex.rb
58
+ - spec/lib/simple_dag/vertex_spec.rb
59
+ - spec/lib/simple_dag_spec.rb
60
+ - spec/spec_helper.rb
61
+ homepage: https://github.com/fsobanski/simple_dag
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 2.6.13
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Simple directed acyclic graphs
85
+ test_files:
86
+ - spec/lib/simple_dag/vertex_spec.rb
87
+ - spec/lib/simple_dag_spec.rb
88
+ - spec/spec_helper.rb