Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.
89 changes: 79 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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.

98 changes: 72 additions & 26 deletions lib/dag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,55 +15,70 @@ 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
predecessors_set = Set.new(predecessors_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
successors_set = Set.new(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
Expand All @@ -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
Loading