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 +7 -0
- data/.rspec +3 -0
- data/.yardopts +1 -0
- data/CONTRIBUTING.md +6 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +33 -0
- data/LICENSE +23 -0
- data/README.md +124 -0
- data/Rakefile +6 -0
- data/lib/simple_dag/vertex.rb +102 -0
- data/lib/simple_dag.rb +139 -0
- data/spec/lib/simple_dag/vertex_spec.rb +137 -0
- data/spec/lib/simple_dag_spec.rb +231 -0
- data/spec/spec_helper.rb +3 -0
- metadata +88 -0
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
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private
|
data/CONTRIBUTING.md
ADDED
data/Gemfile
ADDED
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,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
|
data/spec/spec_helper.rb
ADDED
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
|