tangle 0.9.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49ee5268eb1851a1087ba45efac9f00794a248dfce75aadab846f6617d91620e
4
- data.tar.gz: cc4d0d76f99b86c5494758311637bcb4610555afb58118e059833f62992ef492
3
+ metadata.gz: dc791cdb0b95ae60507e9052e96b8c0c44550a1cde4ac88a5ab9be6661102044
4
+ data.tar.gz: bf20ee14175b42f5ae9e4675ca50d2c76937e1cc09493e7420a78eaf8885aa79
5
5
  SHA512:
6
- metadata.gz: c785bb5d7b83a53db61c03ffff2b6a26c17659eb9f79868e4882c884d0e97339b8748d32778bd2e73bb5293b5dd3a9a9adfef22ff38ab9e0f2c1c660dc0dd09c
7
- data.tar.gz: f042503d2a4d65f6f9ed4cce56e6ba94623ef6e57e73b306a690edce5d2276aa01a7e211d83a2d2aec096a85def0923e4c8da350eb417a37101217764c84c18e
6
+ metadata.gz: 3fba37c004a711c3ed33f6966853724023b5c1dbbe61d596342fa91259a46b490153147f16b12e01f8f9b5cc66156f34197fd91ff357c50e765afb8c8571f564
7
+ data.tar.gz: 0c18db46c410650ba81c4e90c891b1bedb19d0eb3e92079d68c709055883ac12b231162cd8235cbdb81fa4b7ab1bc7f4ad3d2281c0944a5c35387b2a8dc609c4
data/.gitignore CHANGED
@@ -11,3 +11,4 @@
11
11
  .rspec_status
12
12
  /Gemfile.lock
13
13
  /vendor
14
+ coverage
@@ -1,3 +1,6 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+
1
4
  Metrics/BlockLength:
2
5
  Exclude:
3
6
  - 'spec/*_spec.rb'
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/setup'
2
4
  require 'bundler/gem_tasks'
3
5
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'tangle'
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tangle/version'
2
4
  require 'tangle/errors'
3
- require 'tangle/graph'
4
- require 'tangle/simple/graph'
5
+ require 'tangle/undirected/graph'
6
+ require 'tangle/undirected/simple/graph'
5
7
  require 'tangle/directed/graph'
6
8
  require 'tangle/directed/acyclic/graph'
7
9
 
@@ -20,8 +22,9 @@ require 'tangle/directed/acyclic/graph'
20
22
  # => Directed graph with no edge cycles
21
23
  #
22
24
  module Tangle
23
- MultiGraph = Tangle::Graph
24
- SimpleGraph = Tangle::Simple::Graph
25
+ Graph = Tangle::Undirected::Graph
26
+ MultiGraph = Tangle::Undirected::Graph
27
+ SimpleGraph = Tangle::Undirected::Simple::Graph
25
28
  DiGraph = Tangle::Directed::Graph
26
29
  DAG = Tangle::Directed::Acyclic::Graph
27
30
  end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'currify'
4
+ require_relative 'mixin'
5
+ require_relative 'base_graph_protected'
6
+ require_relative 'base_graph_private'
7
+
8
+ module Tangle
9
+ #
10
+ # Abstract base class for (un)directed graphs
11
+ #
12
+ class BaseGraph
13
+ include Tangle::Currify
14
+ include Tangle::Mixin::Initialize
15
+ include Tangle::BaseGraphProtected
16
+ include Tangle::BaseGraphPrivate
17
+
18
+ # Initialize a new graph, preloading it with vertices and edges
19
+ #
20
+ # Graph[+vertices+] => Graph
21
+ # Graph[+vertices+, +edges+) => Graph
22
+ #
23
+ # When +vertices+ is a hash, it contains initialization kwargs as
24
+ # values and vertex names as keys. When +vertices+ is an array of
25
+ # initialization kwargs, the vertices will be be anonymous.
26
+ #
27
+ # +edges+ can contain an array of exactly two, either names of vertices
28
+ # or vertices.
29
+ #
30
+ # Any kwarg supported by Graph.new is also allowed.
31
+ #
32
+ def self.[](vertices, edges = {}, **kwargs)
33
+ graph = new(**kwargs)
34
+ vertices.each { |vertex| graph.add_vertex(vertex) }
35
+ edges.each { |from, to| graph.add_edge(from, to) }
36
+ graph
37
+ end
38
+
39
+ # Initialize a new graph, optionally preloading it with vertices and edges
40
+ #
41
+ # Graph.new() => Graph
42
+ # Graph.new(mixins: [MixinModule, ...], ...) => Graph
43
+ #
44
+ # +mixins+ is an array of modules that can be mixed into the various
45
+ # classes that makes up a graph. Initialization of a Graph, Vertex or Edge
46
+ # looks for submodules in each mixin, with the same name and extends
47
+ # any created object. Defaults to [Tangle::Mixin::Connectedness].
48
+ #
49
+ # Any subclass of Graph should also subclass Edge to manage its unique
50
+ # constraints.
51
+ #
52
+ def initialize(currify: false, mixins: [], **kwargs)
53
+ @currify = currify
54
+ initialize_vertices
55
+ initialize_edges
56
+ initialize_mixins(mixins: mixins, **kwargs)
57
+ end
58
+
59
+ # Return a subgraph, optionally filtered by a vertex selector block
60
+ #
61
+ # subgraph => Graph
62
+ # subgraph { |vertex| ... } => Graph
63
+ #
64
+ # Unless a selector is provided, the subgraph contains the entire graph.
65
+ #
66
+ def subgraph(included = nil, &selector)
67
+ result = clone
68
+ result.select_vertices!(included) unless included.nil?
69
+ result.select_vertices!(&selector) if block_given?
70
+ result
71
+ end
72
+
73
+ def to_s
74
+ "#<#{self.class}: #{vertices.count} vertices, #{edges.count} edges>"
75
+ end
76
+ alias inspect to_s
77
+
78
+ # Fetch a vertex by its name
79
+ def fetch(name)
80
+ @vertices_by_name.fetch(name)
81
+ end
82
+
83
+ # Return a named vertex
84
+ def [](name)
85
+ @vertices_by_name[name]
86
+ end
87
+
88
+ # Return all vertices in the graph
89
+ def vertices
90
+ @vertices.keys
91
+ end
92
+
93
+ # Select vertices in the graph
94
+ def select(&selector)
95
+ @vertices.each_key.select(&selector)
96
+ end
97
+
98
+ # Add a vertex into the graph
99
+ #
100
+ # If a name: is given, or the vertex responds to :name,
101
+ # it will be registered by name in the graph
102
+ def add_vertex(vertex, name: nil)
103
+ name ||= callback(vertex, :name)
104
+ insert_vertex(vertex, name)
105
+ define_currified_methods(vertex, :vertex) if @currify
106
+ callback(vertex, :added_to_graph, self)
107
+ self
108
+ end
109
+ alias << add_vertex
110
+
111
+ # Remove a vertex from the graph
112
+ def remove_vertex(vertex)
113
+ @vertices[vertex].each do |edge|
114
+ remove_edge(edge) if edge.include?(vertex)
115
+ end
116
+ delete_vertex(vertex)
117
+ callback(vertex, :removed_from_graph, self)
118
+ end
119
+
120
+ # Get all edges.
121
+ #
122
+ # edges => Array
123
+ #
124
+ def edges(vertex = nil)
125
+ return @edges if vertex.nil?
126
+ @vertices.fetch(vertex)
127
+ end
128
+ currify :vertex, :edges
129
+
130
+ # Add a new edge to the graph
131
+ #
132
+ # add_edge(vtx1, vtx2, ...) => Edge
133
+ #
134
+ def add_edge(*vertices, **kvargs)
135
+ edge = new_edge(*vertices, mixins: @mixins, **kvargs)
136
+ insert_edge(edge)
137
+ vertices.each { |vertex| callback(vertex, :edge_added, edge) }
138
+ edge
139
+ end
140
+
141
+ # Remove an edge from the graph
142
+ def remove_edge(edge)
143
+ delete_edge(edge)
144
+ edge.each_vertex { |vertex| callback(vertex, :edge_removed, edge) }
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tangle
4
+ #
5
+ # Private methods of BaseGraph
6
+ #
7
+ module BaseGraphPrivate
8
+ private
9
+
10
+ def callback(receiver, method, *args)
11
+ receiver.send(method, *args) if receiver.respond_to?(method)
12
+ end
13
+
14
+ # Initialize vertex related attributes
15
+ def initialize_vertices
16
+ @vertices = {}
17
+ @vertices_by_name = {}
18
+ end
19
+
20
+ def initialize_edges
21
+ @edges = Set[]
22
+ end
23
+
24
+ # Yield each reachable vertex to a block, breadth first
25
+ def each_vertex_breadth_first(start_vertex, walk_method)
26
+ remaining = [start_vertex]
27
+ remaining.each_with_object([]) do |vertex, history|
28
+ history << vertex
29
+ yield vertex
30
+ send(walk_method, vertex).each do |other|
31
+ remaining << other unless history.include?(other)
32
+ end
33
+ end
34
+ end
35
+
36
+ def vertex_enumerator(start_vertex, walk_method)
37
+ Enumerator.new do |yielder|
38
+ each_vertex_breadth_first(start_vertex, walk_method) do |vertex|
39
+ yielder << vertex
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tangle
4
+ #
5
+ # Protected methods of BaseGraph
6
+ #
7
+ module BaseGraphProtected
8
+ protected
9
+
10
+ def select_vertices!(selected = nil)
11
+ vertices.each do |vertex|
12
+ delete_vertex(vertex) if block_given? && !yield(vertex)
13
+ next if selected.nil?
14
+ delete_vertex(vertex) unless selected.any? { |vtx| vtx.eql?(vertex) }
15
+ end
16
+ end
17
+
18
+ def insert_vertex(vertex, name = nil)
19
+ @vertices[vertex] = Set[]
20
+ @vertices_by_name[name] = vertex unless name.nil?
21
+ end
22
+
23
+ def delete_vertex(vertex)
24
+ @vertices[vertex].each do |edge|
25
+ delete_edge(edge) if edge.include?(vertex)
26
+ end
27
+ @vertices.delete(vertex)
28
+ @vertices_by_name.delete_if { |_, vtx| vtx.eql?(vertex) }
29
+ end
30
+
31
+ # Insert a prepared edge into the graph
32
+ #
33
+ def insert_edge(edge)
34
+ @edges << edge
35
+ edge.each_vertex { |vertex| @vertices.fetch(vertex) << edge }
36
+ end
37
+
38
+ def delete_edge(edge)
39
+ edge.each_vertex { |vertex| @vertices.fetch(vertex).delete(edge) }
40
+ @edges.delete(edge)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tangle
4
+ # Currification of instance methods, for adding callbacks to other objects
5
+ module Currify
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ # Class method extensions for currification of instance methods
11
+ module ClassMethods
12
+ # Return a list of currified methods for a given tag.
13
+ #
14
+ # :call-seq:
15
+ # self.class.currified_methods(tag) => Array of Symbol
16
+ def currified_methods(tag)
17
+ mine = @currified_methods&.[](tag) || []
18
+ return mine unless superclass.respond_to?(:currified_methods)
19
+ superclass.currified_methods(tag) + mine
20
+ end
21
+
22
+ private
23
+
24
+ # Add a symbol to the list of currified methods for a tag.
25
+ #
26
+ # :call-seq:
27
+ # class X
28
+ # currify :tag, :method
29
+ def currify(tag, method)
30
+ @currified_methods ||= {}
31
+ @currified_methods[tag] ||= []
32
+ @currified_methods[tag] << method
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def define_currified_methods(obj, tag)
39
+ self.class.currified_methods(tag)&.each do |name|
40
+ obj.instance_exec(name, method(name).curry) do |method_name, method|
41
+ define_singleton_method(method_name) do |*args|
42
+ method.call(self, *args)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tangle/directed/graph'
2
4
  require 'tangle/directed/acyclic/partial_order'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Tangle
2
4
  module Directed
3
5
  module Acyclic
@@ -1,4 +1,6 @@
1
- require 'tangle/edge'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../edge'
2
4
 
3
5
  module Tangle
4
6
  module Directed
@@ -31,7 +33,7 @@ module Tangle
31
33
  super
32
34
  @tail = tail
33
35
  @head = head
34
- @vertices = { tail => head }
36
+ @vertices = { tail => head }.freeze
35
37
  end
36
38
  end
37
39
  end
@@ -1,103 +1,125 @@
1
- require 'tangle/graph'
2
- require 'tangle/directed/edge'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_graph'
4
+ require_relative 'edge'
3
5
 
4
6
  module Tangle
5
7
  module Directed
6
8
  #
7
9
  # A directed graph
8
- class Graph < Tangle::Graph
9
- Edge = Tangle::Directed::Edge
10
- DEFAULT_MIXINS = [].freeze
11
-
10
+ class Graph < Tangle::BaseGraph
12
11
  # Return the incoming edges for +vertex+
13
12
  def in_edges(vertex)
14
13
  edges(vertex).select { |edge| edge.head?(vertex) }
15
14
  end
15
+ currify :vertex, :in_edges
16
16
 
17
17
  # Return the direct predecessors of +vertex+
18
18
  def direct_predecessors(vertex)
19
19
  in_edges(vertex).map(&:tail).to_set
20
20
  end
21
+ currify :vertex, :direct_predecessors
21
22
 
22
23
  # Is +other+ a direct predecessor of +vertex+?
23
24
  def direct_predecessor?(vertex, other)
24
25
  direct_predecessors(vertex).include?(other)
25
26
  end
27
+ currify :vertex, :direct_predecessor?
26
28
 
27
29
  # Return a breadth first enumerator for all predecessors
28
30
  def predecessors(vertex)
29
31
  vertex_enumerator(vertex, :direct_predecessors)
30
32
  end
33
+ currify :vertex, :predecessors
31
34
 
32
35
  # Is +other+ a predecessor of +vertex+?
33
36
  def predecessor?(vertex, other)
34
37
  predecessors(vertex).any? { |vtx| other.eql?(vtx) }
35
38
  end
39
+ currify :vertex, :predecessor?
36
40
 
37
41
  # Return a subgraph with all predecessors of a +vertex+
38
42
  def predecessor_subgraph(vertex, &selector)
39
43
  subgraph(predecessors(vertex), &selector)
40
44
  end
45
+ currify :vertex, :predecessor_subgraph
41
46
 
42
47
  # Return the outgoing edges for +vertex+
43
48
  def out_edges(vertex)
44
49
  edges(vertex).select { |edge| edge.tail?(vertex) }
45
50
  end
51
+ currify :vertex, :out_edges
46
52
 
47
53
  # Return the direct successors of +vertex+
48
54
  def direct_successors(vertex)
49
55
  out_edges(vertex).map(&:head).to_set
50
56
  end
57
+ currify :vertex, :direct_successors
51
58
 
52
59
  # Is +other+ a direct successor of +vertex+?
53
60
  def direct_successor?(vertex, other)
54
61
  direct_successors(vertex).include?(other)
55
62
  end
63
+ currify :vertex, :direct_successor?
56
64
 
57
65
  # Return a breadth first enumerator for all successors
58
66
  def successors(vertex)
59
67
  vertex_enumerator(vertex, :direct_successors)
60
68
  end
69
+ currify :vertex, :successors
61
70
 
62
71
  # Is +other+ a successor of +vertex+?
63
72
  def successor?(vertex, other)
64
73
  successors(vertex).any? { |vtx| other.eql?(vtx) }
65
74
  end
75
+ currify :vertex, :successor?
66
76
 
67
77
  # Return a subgraph with all successors of a +vertex+
68
78
  def successor_subgraph(vertex, &selector)
69
79
  subgraph(successors(vertex), &selector)
70
80
  end
81
+ currify :vertex, :successor_subgraph
71
82
 
72
83
  # Return the in degree for +vertex+
73
84
  def in_degree(vertex)
74
85
  in_edges(vertex).count
75
86
  end
87
+ currify :vertex, :in_degree
76
88
 
77
89
  # Return the out degree for +vertex+
78
90
  def out_degree(vertex)
79
91
  out_edges(vertex).count
80
92
  end
93
+ currify :vertex, :out_degree
81
94
 
82
95
  # Is +vertex+ a sink in the graph?
83
96
  def sink?(vertex)
84
97
  out_degree(vertex).zero?
85
98
  end
99
+ currify :vertex, :sink?
86
100
 
87
101
  # Is +vertex+ a source in the graph?
88
102
  def source?(vertex)
89
103
  in_degree(vertex).zero?
90
104
  end
105
+ currify :vertex, :source?
91
106
 
92
107
  # Is +vertex+ internal in the graph?
93
108
  def internal?(vertex)
94
109
  !(sink?(vertex) || source?(vertex))
95
110
  end
111
+ currify :vertex, :internal?
96
112
 
97
113
  # Is the graph balanced?
98
114
  def balanced?
99
115
  vertices.all? { |vertex| in_degree(vertex) == out_degree(vertex) }
100
116
  end
117
+
118
+ private
119
+
120
+ def new_edge(*args)
121
+ Edge.new(*args)
122
+ end
101
123
  end
102
124
  end
103
125
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
- require 'tangle/errors'
4
+ require_relative 'mixin'
3
5
 
4
6
  module Tangle
5
7
  #
6
- # An edge in a graph, connecting two vertices
8
+ # An edge in an undirected graph, connecting two vertices
7
9
  #
8
10
  class Edge
9
11
  include Tangle::Mixin::Initialize
@@ -31,16 +33,6 @@ module Tangle
31
33
  @vertices.fetch(from_vertex)
32
34
  end
33
35
 
34
- def to_s
35
- vertex1, vertex2 = @vertices.keys
36
- "{#{vertex1}<->#{vertex2}}"
37
- end
38
- alias inspect to_s
39
-
40
- def each_vertex(&block)
41
- @vertices.each_key(&block)
42
- end
43
-
44
36
  def include?(vertex)
45
37
  each_vertex.include?(vertex)
46
38
  end
@@ -51,9 +43,8 @@ module Tangle
51
43
 
52
44
  private
53
45
 
54
- def initialize_vertices(vertex1, vertex2 = vertex1)
46
+ def initialize_vertices(vertex1, vertex2)
55
47
  @loop = vertex1 == vertex2
56
- @vertices = { vertex1 => vertex2, vertex2 => vertex1 }.freeze
57
48
  end
58
49
  end
59
50
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Tangle
2
4
  #
3
5
  # LoopError is raised when a looped edge is disallowed.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Tangle
2
4
  module Mixin
3
5
  #
@@ -10,7 +12,6 @@ module Tangle
10
12
 
11
13
  def initialize_mixins(mixins: nil, **kwargs)
12
14
  @mixins = mixins
13
-
14
15
  extend_with_mixins unless @mixins.nil?
15
16
  initialize_kwargs(**kwargs) unless kwargs.empty?
16
17
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Tangle
2
4
  module Mixin
3
5
  # Tangle mixin for loading a directory structure as a graph
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative '../edge'
5
+
6
+ module Tangle
7
+ module Undirected
8
+ #
9
+ # An edge in an undirected graph, connecting two vertices
10
+ #
11
+ class Edge < Tangle::Edge
12
+ def each_vertex(&block)
13
+ @vertices.each_key(&block)
14
+ end
15
+
16
+ def to_s
17
+ vertex1, vertex2 = @vertices.keys
18
+ "{#{vertex1}<->#{vertex2}}"
19
+ end
20
+ alias inspect to_s
21
+
22
+ private
23
+
24
+ def initialize_vertices(vertex1, vertex2 = vertex1)
25
+ super
26
+ @vertices = { vertex1 => vertex2, vertex2 => vertex1 }.freeze
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_graph'
4
+ require_relative 'edge'
5
+
6
+ module Tangle
7
+ module Undirected
8
+ #
9
+ # Undirected graph
10
+ #
11
+ class Graph < Tangle::BaseGraph
12
+ # Two vertices are adjacent if there is an edge between them
13
+ def adjacent?(vertex, other)
14
+ edges(vertex).any? { |edge| edge[vertex] == other }
15
+ end
16
+
17
+ # Return the set of adjacent vertices
18
+ def adjacent(vertex)
19
+ Set.new(edges(vertex).map { |edge| edge.walk(vertex) })
20
+ end
21
+
22
+ # Get the largest connected subgraph for a vertex.
23
+ # Also aliased as :component and :connected_component
24
+ #
25
+ # connected_subgraph(vertex) => Graph
26
+ #
27
+ def connected_subgraph(vertex)
28
+ subgraph { |other| connected_vertices?(vertex, other) }
29
+ end
30
+ alias component connected_subgraph
31
+ alias connected_component connected_subgraph
32
+
33
+ # Get the largest subgraph that is not connected to a vertex, or what's
34
+ # left after removing the connected subgraph.
35
+ #
36
+ def disconnected_subgraph(vertex)
37
+ subgraph { |other| !connected_vertices?(vertex, other) }
38
+ end
39
+
40
+ # A graph is connected if all vertices are connected to all vertices
41
+ # An empty graph is disconnected.
42
+ #
43
+ def connected?(*tested_vertices)
44
+ tested_vertices = vertices if tested_vertices.empty?
45
+ return false if tested_vertices.empty?
46
+
47
+ tested_vertices.combination(2).all? do |pair|
48
+ this, that = pair.to_a
49
+ reachable(this).any? { |other| other == that }
50
+ end
51
+ end
52
+
53
+ # A graph is disconnected if any vertex is not connected to all other.
54
+ # An empty graph is disconnected.
55
+ #
56
+ def disconnected?(*tested_vertices)
57
+ !connected?(*tested_vertices)
58
+ end
59
+
60
+ # Return a breadth-first Enumerator for all reachable vertices,
61
+ # by transitive adjacency.
62
+ def reachable(start_vertex)
63
+ vertex_enumerator(start_vertex, :adjacent)
64
+ end
65
+
66
+ private
67
+
68
+ def new_edge(*args)
69
+ Edge.new(*args)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../graph'
4
+
5
+ module Tangle
6
+ module Undirected
7
+ module Simple
8
+ #
9
+ # A simple graph, without loops and multiple edges
10
+ class Graph < Tangle::Undirected::Graph
11
+ protected
12
+
13
+ def insert_edge(edge)
14
+ raise LoopError if edge.loop?
15
+ raise MultiEdgeError if adjacent?(*edge.each_vertex)
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'git-version-bump'
2
4
 
3
5
  module Tangle
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
@@ -21,6 +22,9 @@ Gem::Specification.new do |spec|
21
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
23
  spec.require_paths = ['lib']
23
24
 
25
+ spec.platform = Gem::Platform::RUBY
26
+ spec.required_ruby_version = '~> 2.3'
27
+
24
28
  spec.add_dependency 'git-version-bump', '~> 0.15'
25
29
 
26
30
  spec.add_development_dependency 'bundler', '~> 1.16'
@@ -28,4 +32,5 @@ Gem::Specification.new do |spec|
28
32
  spec.add_development_dependency 'rake', '~> 10.0'
29
33
  spec.add_development_dependency 'rspec', '~> 3.0'
30
34
  spec.add_development_dependency 'rubocop', '~> 0.52'
35
+ spec.add_development_dependency 'simplecov', '~> 0.16'
31
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tangle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Calle Englund
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-05-25 00:00:00.000000000 Z
11
+ date: 2018-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: git-version-bump
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0.52'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.16'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.16'
97
111
  description:
98
112
  email:
99
113
  - calle@discord.bofh.se
@@ -113,19 +127,21 @@ files:
113
127
  - bin/console
114
128
  - bin/setup
115
129
  - lib/tangle.rb
130
+ - lib/tangle/base_graph.rb
131
+ - lib/tangle/base_graph_private.rb
132
+ - lib/tangle/base_graph_protected.rb
133
+ - lib/tangle/currify.rb
116
134
  - lib/tangle/directed/acyclic/graph.rb
117
135
  - lib/tangle/directed/acyclic/partial_order.rb
118
136
  - lib/tangle/directed/edge.rb
119
137
  - lib/tangle/directed/graph.rb
120
138
  - lib/tangle/edge.rb
121
139
  - lib/tangle/errors.rb
122
- - lib/tangle/graph.rb
123
- - lib/tangle/graph_edges.rb
124
- - lib/tangle/graph_vertices.rb
125
140
  - lib/tangle/mixin.rb
126
- - lib/tangle/mixin/connectedness.rb
127
141
  - lib/tangle/mixin/directory.rb
128
- - lib/tangle/simple/graph.rb
142
+ - lib/tangle/undirected/edge.rb
143
+ - lib/tangle/undirected/graph.rb
144
+ - lib/tangle/undirected/simple/graph.rb
129
145
  - lib/tangle/version.rb
130
146
  - tangle.gemspec
131
147
  homepage: https://github.com/notcalle/ruby-tangle
@@ -138,9 +154,9 @@ require_paths:
138
154
  - lib
139
155
  required_ruby_version: !ruby/object:Gem::Requirement
140
156
  requirements:
141
- - - ">="
157
+ - - "~>"
142
158
  - !ruby/object:Gem::Version
143
- version: '0'
159
+ version: '2.3'
144
160
  required_rubygems_version: !ruby/object:Gem::Requirement
145
161
  requirements:
146
162
  - - ">="
@@ -1,83 +0,0 @@
1
- require 'tangle/mixin'
2
- require 'tangle/mixin/connectedness'
3
- require 'tangle/edge'
4
- require 'tangle/graph_vertices'
5
- require 'tangle/graph_edges'
6
-
7
- module Tangle
8
- #
9
- # Base class for all kinds of graphs
10
- #
11
- class Graph
12
- include Tangle::GraphVertices
13
- include Tangle::GraphEdges
14
- include Tangle::Mixin::Initialize
15
- Edge = Tangle::Edge
16
- DEFAULT_MIXINS = Tangle::Mixin::Connectedness::MIXINS
17
-
18
- # Initialize a new graph, preloading it with vertices and edges
19
- #
20
- # Graph[+vertices+] => Graph
21
- # Graph[+vertices+, +edges+) => Graph
22
- #
23
- # When +vertices+ is a hash, it contains initialization kwargs as
24
- # values and vertex names as keys. When +vertices+ is an array of
25
- # initialization kwargs, the vertices will be be anonymous.
26
- #
27
- # +edges+ can contain an array of exactly two, either names of vertices
28
- # or vertices.
29
- #
30
- # Any kwarg supported by Graph.new is also allowed.
31
- #
32
- def self.[](vertices, edges = {}, **kwargs)
33
- graph = new(**kwargs)
34
- vertices.each { |vertex| graph.add_vertex(vertex) }
35
- edges.each { |from, to| graph.add_edge(from, to) }
36
- graph
37
- end
38
-
39
- # Initialize a new graph, optionally preloading it with vertices and edges
40
- #
41
- # Graph.new() => Graph
42
- # Graph.new(mixins: [MixinModule, ...], ...) => Graph
43
- #
44
- # +mixins+ is an array of modules that can be mixed into the various
45
- # classes that makes up a graph. Initialization of a Graph, Vertex or Edge
46
- # looks for submodules in each mixin, with the same name and extends
47
- # any created object. Defaults to [Tangle::Mixin::Connectedness].
48
- #
49
- # Any subclass of Graph should also subclass Edge to manage its unique
50
- # constraints.
51
- #
52
- def initialize(mixins: self.class::DEFAULT_MIXINS, **kwargs)
53
- initialize_vertices
54
- initialize_edges
55
- initialize_mixins(mixins: mixins, **kwargs)
56
- end
57
-
58
- # Return a subgraph, optionally filtered by a vertex selector block
59
- #
60
- # subgraph => Graph
61
- # subgraph { |vertex| ... } => Graph
62
- #
63
- # Unless a selector is provided, the subgraph contains the entire graph.
64
- #
65
- def subgraph(included = nil, &selector)
66
- result = clone
67
- result.select_vertices!(included) unless included.nil?
68
- result.select_vertices!(&selector) if block_given?
69
- result
70
- end
71
-
72
- def to_s
73
- "#<#{self.class}: #{vertices.count} vertices, #{edges.count} edges>"
74
- end
75
- alias inspect to_s
76
-
77
- private
78
-
79
- def callback(receiver, method, *args)
80
- receiver.send(method, *args) if receiver.respond_to?(method)
81
- end
82
- end
83
- end
@@ -1,52 +0,0 @@
1
- require 'set'
2
-
3
- module Tangle
4
- # Edge related methods in a graph
5
- module GraphEdges
6
- # Get all edges.
7
- #
8
- # edges => Array
9
- #
10
- def edges(vertex = nil)
11
- return @edges if vertex.nil?
12
- @vertices.fetch(vertex)
13
- end
14
-
15
- # Add a new edge to the graph
16
- #
17
- # add_edge(vtx1, vtx2, ...) => Edge
18
- #
19
- def add_edge(*vertices, **kvargs)
20
- edge = self.class::Edge.new(*vertices, mixins: @mixins, **kvargs)
21
- insert_edge(edge)
22
- vertices.each { |vertex| callback(vertex, :edge_added, edge) }
23
- edge
24
- end
25
-
26
- # Remove an edge from the graph
27
- def remove_edge(edge)
28
- delete_edge(edge)
29
- edge.each_vertex { |vertex| callback(vertex, :edge_removed, edge) }
30
- end
31
-
32
- protected
33
-
34
- # Insert a prepared edge into the graph
35
- #
36
- def insert_edge(edge)
37
- @edges << edge
38
- edge.each_vertex { |vertex| @vertices.fetch(vertex) << edge }
39
- end
40
-
41
- def delete_edge(edge)
42
- edge.each_vertex { |vertex| @vertices.fetch(vertex).delete(edge) }
43
- @edges.delete(edge)
44
- end
45
-
46
- private
47
-
48
- def initialize_edges
49
- @edges = Set[]
50
- end
51
- end
52
- end
@@ -1,98 +0,0 @@
1
- require 'set'
2
-
3
- module Tangle
4
- # Vertex related methods in a graph
5
- module GraphVertices
6
- # Fetch a vertex by its name
7
- def fetch(name)
8
- @vertices_by_name.fetch(name)
9
- end
10
-
11
- # Return a named vertex
12
- def [](name)
13
- @vertices_by_name[name]
14
- end
15
-
16
- # Return all vertices in the graph
17
- def vertices
18
- @vertices.keys
19
- end
20
-
21
- # Select vertices in the graph
22
- def select(&selector)
23
- @vertices.each_key.select(&selector)
24
- end
25
-
26
- # Add a vertex into the graph
27
- #
28
- # If a name: is given, or the vertex responds to :name,
29
- # it will be registered by name in the graph
30
- def add_vertex(vertex, name: nil)
31
- name ||= callback(vertex, :name)
32
- insert_vertex(vertex, name)
33
- callback(vertex, :added_to_graph, self)
34
- self
35
- end
36
- alias << add_vertex
37
-
38
- # Remove a vertex from the graph
39
- def remove_vertex(vertex)
40
- @vertices[vertex].each do |edge|
41
- remove_edge(edge) if edge.include?(vertex)
42
- end
43
- delete_vertex(vertex)
44
- callback(vertex, :removed_from_graph, self)
45
- end
46
-
47
- protected
48
-
49
- def select_vertices!(selected = nil)
50
- vertices.each do |vertex|
51
- delete_vertex(vertex) if block_given? && !yield(vertex)
52
- next if selected.nil?
53
- delete_vertex(vertex) unless selected.any? { |vtx| vtx.eql?(vertex) }
54
- end
55
- end
56
-
57
- def insert_vertex(vertex, name = nil)
58
- @vertices[vertex] = Set[]
59
- @vertices_by_name[name] = vertex unless name.nil?
60
- end
61
-
62
- def delete_vertex(vertex)
63
- @vertices[vertex].each do |edge|
64
- delete_edge(edge) if edge.include?(vertex)
65
- end
66
- @vertices.delete(vertex)
67
- @vertices_by_name.delete_if { |_, vtx| vtx.eql?(vertex) }
68
- end
69
-
70
- private
71
-
72
- # Initialize vertex related attributes
73
- def initialize_vertices
74
- @vertices = {}
75
- @vertices_by_name = {}
76
- end
77
-
78
- # Yield each reachable vertex to a block, breadth first
79
- def each_vertex_breadth_first(start_vertex, walk_method)
80
- remaining = [start_vertex]
81
- remaining.each_with_object([]) do |vertex, history|
82
- history << vertex
83
- yield vertex
84
- send(walk_method, vertex).each do |other|
85
- remaining << other unless history.include?(other)
86
- end
87
- end
88
- end
89
-
90
- def vertex_enumerator(start_vertex, walk_method)
91
- Enumerator.new do |yielder|
92
- each_vertex_breadth_first(start_vertex, walk_method) do |vertex|
93
- yielder << vertex
94
- end
95
- end
96
- end
97
- end
98
- end
@@ -1,68 +0,0 @@
1
- module Tangle
2
- module Mixin
3
- #
4
- # Mixins for adding connectedness features
5
- #
6
- module Connectedness
7
- MIXINS = [Tangle::Mixin::Connectedness].freeze
8
- #
9
- # Mixin for adding connectedness to a graph
10
- #
11
- module Graph
12
- # Two vertices are adjacent if there is an edge between them
13
- def adjacent?(vertex, other)
14
- edges(vertex).any? { |edge| edge[vertex] == other }
15
- end
16
-
17
- # Return the set of adjacent vertices
18
- def adjacent(vertex)
19
- Set.new(edges(vertex).map { |edge| edge.walk(vertex) })
20
- end
21
-
22
- # Get the largest connected subgraph for a vertex.
23
- # Also aliased as :component and :connected_component
24
- #
25
- # connected_subgraph(vertex) => Graph
26
- #
27
- def connected_subgraph(vertex)
28
- subgraph { |other| connected_vertices?(vertex, other) }
29
- end
30
- alias component connected_subgraph
31
- alias connected_component connected_subgraph
32
-
33
- # Get the largest subgraph that is not connected to a vertex, or what's
34
- # left after removing the connected subgraph.
35
- #
36
- def disconnected_subgraph(vertex)
37
- subgraph { |other| !connected_vertices?(vertex, other) }
38
- end
39
-
40
- # A graph is connected if all vertices are connected to all vertices
41
- # An empty graph is disconnected.
42
- #
43
- def connected?(*tested_vertices)
44
- tested_vertices = vertices if tested_vertices.empty?
45
- return false if tested_vertices.empty?
46
-
47
- tested_vertices.combination(2).all? do |pair|
48
- this, that = pair.to_a
49
- reachable(this).any? { |other| other == that }
50
- end
51
- end
52
-
53
- # A graph is disconnected if any vertex is not connected to all other.
54
- # An empty graph is disconnected.
55
- #
56
- def disconnected?(*tested_vertices)
57
- !connected?(*tested_vertices)
58
- end
59
-
60
- # Return a breadth-first Enumerator for all reachable vertices,
61
- # by transitive adjacency.
62
- def reachable(start_vertex)
63
- vertex_enumerator(start_vertex, :adjacent)
64
- end
65
- end
66
- end
67
- end
68
- end
@@ -1,17 +0,0 @@
1
- require 'tangle/graph'
2
-
3
- module Tangle
4
- module Simple
5
- #
6
- # A simple graph, without loops and multiple edges
7
- class Graph < Tangle::Graph
8
- protected
9
-
10
- def insert_edge(edge)
11
- raise LoopError if edge.loop?
12
- raise MultiEdgeError if adjacent?(*edge.each_vertex)
13
- super
14
- end
15
- end
16
- end
17
- end