simple_dag 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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