tangle 0.8.2 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +37 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +15 -4
- data/CHANGELOG.md +45 -0
- data/Gemfile +4 -10
- data/README.md +8 -3
- data/Rakefile +3 -1
- data/bin/console +5 -4
- data/bin/setup +0 -1
- data/lib/tangle.rb +7 -4
- data/lib/tangle/base_graph.rb +154 -0
- data/lib/tangle/{graph_vertices.rb → base_graph_private.rb} +12 -35
- data/lib/tangle/base_graph_protected.rb +50 -0
- data/lib/tangle/currify.rb +58 -0
- data/lib/tangle/directed/acyclic/graph.rb +3 -0
- data/lib/tangle/directed/acyclic/partial_order.rb +3 -0
- data/lib/tangle/directed/edge.rb +4 -2
- data/lib/tangle/directed/graph.rb +28 -6
- data/lib/tangle/edge.rb +5 -14
- data/lib/tangle/errors.rb +2 -0
- data/lib/tangle/mixin.rb +2 -1
- data/lib/tangle/mixin/directory.rb +48 -13
- data/lib/tangle/undirected/edge.rb +30 -0
- data/lib/tangle/undirected/graph.rb +73 -0
- data/lib/tangle/undirected/simple/graph.rb +22 -0
- data/lib/tangle/version.rb +8 -4
- data/ruby-tangle.sublime-project +8 -0
- data/tangle.gemspec +19 -7
- metadata +103 -27
- data/.travis.yml +0 -5
- data/lib/tangle/graph.rb +0 -81
- data/lib/tangle/graph_edges.rb +0 -49
- data/lib/tangle/mixin/connectedness.rb +0 -68
- data/lib/tangle/simple/graph.rb +0 -17
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tangle
|
4
|
+
#
|
5
|
+
# Protected methods of BaseGraph
|
6
|
+
#
|
7
|
+
module BaseGraphProtected
|
8
|
+
protected
|
9
|
+
|
10
|
+
def copy_vertices_and_edges(from)
|
11
|
+
@vertices = from.instance_variable_get(:@vertices).dup
|
12
|
+
@vertices_by_name = from.instance_variable_get(:@vertices_by_name).dup
|
13
|
+
@edges = from.instance_variable_get(:@edges).dup
|
14
|
+
end
|
15
|
+
|
16
|
+
def select_vertices!(selected = nil)
|
17
|
+
vertices.each do |vertex|
|
18
|
+
delete_vertex(vertex) if block_given? && !yield(vertex)
|
19
|
+
next if selected.nil?
|
20
|
+
|
21
|
+
delete_vertex(vertex) unless selected.any? { |vtx| vtx.eql?(vertex) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def insert_vertex(vertex, name = nil)
|
26
|
+
@vertices[vertex] = Set[]
|
27
|
+
@vertices_by_name[name] = vertex unless name.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete_vertex(vertex)
|
31
|
+
@vertices[vertex].each do |edge|
|
32
|
+
delete_edge(edge) if edge.include?(vertex)
|
33
|
+
end
|
34
|
+
@vertices.delete(vertex)
|
35
|
+
@vertices_by_name.delete_if { |_, vtx| vtx.eql?(vertex) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Insert a prepared edge into the graph
|
39
|
+
#
|
40
|
+
def insert_edge(edge)
|
41
|
+
@edges << edge
|
42
|
+
edge.each_vertex { |vertex| @vertices.fetch(vertex) << edge }
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete_edge(edge)
|
46
|
+
edge.each_vertex { |vertex| @vertices.fetch(vertex).delete(edge) }
|
47
|
+
@edges.delete(edge)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tangle
|
4
|
+
# Raised for zero arity of a currified method, subclass of ArgumentError
|
5
|
+
class CurrifyError < ArgumentError
|
6
|
+
def initialize(msg = 'method accepts no arguments', *)
|
7
|
+
super
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Currification of instance methods, for adding callbacks to other objects
|
12
|
+
module Currify
|
13
|
+
def self.included(base)
|
14
|
+
base.extend(ClassMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Class method extensions for currification of instance methods
|
18
|
+
module ClassMethods
|
19
|
+
# Return a list of currified methods for a given tag.
|
20
|
+
#
|
21
|
+
# :call-seq:
|
22
|
+
# self.class.currified_methods(tag) => Array of Symbol
|
23
|
+
def currified_methods(tag)
|
24
|
+
mine = @currified_methods&.[](tag) || []
|
25
|
+
return mine unless superclass.respond_to?(:currified_methods)
|
26
|
+
|
27
|
+
superclass.currified_methods(tag) + mine
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Add a symbol to the list of currified methods for a tag.
|
33
|
+
#
|
34
|
+
# :call-seq:
|
35
|
+
# class X
|
36
|
+
# currify :tag, :method
|
37
|
+
def currify(tag, method)
|
38
|
+
raise CurrifyError if instance_method(method).arity.zero?
|
39
|
+
|
40
|
+
@currified_methods ||= {}
|
41
|
+
@currified_methods[tag] ||= []
|
42
|
+
@currified_methods[tag] << method
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def define_currified_methods(obj, tag)
|
49
|
+
self.class.currified_methods(tag)&.each do |name|
|
50
|
+
obj.instance_exec(name, method(name).curry) do |method_name, method|
|
51
|
+
define_singleton_method(method_name) do |*args|
|
52
|
+
method.call(self, *args)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
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
|
|
@@ -17,6 +19,7 @@ module Tangle
|
|
17
19
|
def insert_edge(edge)
|
18
20
|
raise CyclicError if successor?(edge.head, edge.tail) ||
|
19
21
|
predecessor?(edge.tail, edge.head)
|
22
|
+
|
20
23
|
super
|
21
24
|
end
|
22
25
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Tangle
|
2
4
|
module Directed
|
3
5
|
module Acyclic
|
@@ -29,6 +31,7 @@ module Tangle
|
|
29
31
|
raise GraphError unless graph == other.graph
|
30
32
|
return 0 if vertex == other.vertex
|
31
33
|
return -1 if graph.successor?(vertex, other.vertex)
|
34
|
+
|
32
35
|
1
|
33
36
|
end
|
34
37
|
end
|
data/lib/tangle/directed/edge.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
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
|
-
|
2
|
-
|
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::
|
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, **kwargs)
|
121
|
+
Edge.new(*args, **kwargs)
|
122
|
+
end
|
101
123
|
end
|
102
124
|
end
|
103
125
|
end
|
data/lib/tangle/edge.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
|
-
|
4
|
+
require_relative 'mixin'
|
3
5
|
|
4
6
|
module Tangle
|
5
7
|
#
|
6
|
-
# An edge in
|
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
|
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
|
data/lib/tangle/errors.rb
CHANGED
data/lib/tangle/mixin.rb
CHANGED
@@ -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
|
@@ -7,17 +9,25 @@ module Tangle
|
|
7
9
|
# options are:
|
8
10
|
# root: root directory for the structure (mandatory)
|
9
11
|
# loaders: list of object loader lambdas (mandatory)
|
12
|
+
# ->(graph, **) { ... } => finished?
|
10
13
|
# follow_links: bool for following symlinks to directories
|
11
|
-
#
|
14
|
+
# exclude_root: bool for excluding the root directory
|
15
|
+
#
|
16
|
+
# All bool options default to false.
|
17
|
+
#
|
18
|
+
# A loader lambda is called with the graph as only positional
|
19
|
+
# argument, and a number of keyword arguments:
|
12
20
|
#
|
13
|
-
#
|
14
|
-
#
|
21
|
+
# path: Path of current filesystem object
|
22
|
+
# parent: Path of filesystem parent object
|
23
|
+
# lstat: File.lstat for path
|
24
|
+
# stat: File.stat for path, if lstat.symlink?
|
15
25
|
#
|
16
|
-
# The lambdas are called in order until one returns true
|
26
|
+
# The lambdas are called in order until one returns true.
|
17
27
|
#
|
18
28
|
# Example:
|
19
|
-
# loader = lambda do |g, path
|
20
|
-
# vertex =
|
29
|
+
# loader = lambda do |g, path:, parent:, lstat:, **|
|
30
|
+
# vertex = kwargs[:lstat]
|
21
31
|
# g.add_vertex(vertex, name: path)
|
22
32
|
# g.add_edge(g[parent], vertex) unless parent.nil?
|
23
33
|
# end
|
@@ -30,25 +40,50 @@ module Tangle
|
|
30
40
|
|
31
41
|
private
|
32
42
|
|
33
|
-
def initialize_kwarg_directory(
|
43
|
+
def initialize_kwarg_directory(options)
|
34
44
|
@root_directory = options.fetch(:root)
|
35
45
|
@directory_loaders = options.fetch(:loaders)
|
36
46
|
@follow_directory_links = options[:follow_links]
|
47
|
+
@exclude_root = options[:exclude_root]
|
37
48
|
load_directory_graph(@root_directory)
|
38
49
|
end
|
39
50
|
|
40
51
|
def load_directory_graph(path, parent = nil)
|
41
|
-
|
42
|
-
loader.to_proc.call(self, path, parent)
|
43
|
-
end
|
44
|
-
|
45
|
-
return if File.symlink?(path) && !@follow_directory_links
|
46
|
-
return unless File.directory?(path)
|
52
|
+
return unless load_directory_object(path, parent)
|
47
53
|
|
48
54
|
Dir.each_child(path) do |file|
|
49
55
|
load_directory_graph(File.join(path, file), path)
|
50
56
|
end
|
51
57
|
end
|
58
|
+
|
59
|
+
# Load a filesystem object into the graph, returning
|
60
|
+
# +true+ if the object was a directory (or link to one,
|
61
|
+
# and we're following links).
|
62
|
+
def load_directory_object(path, parent = nil)
|
63
|
+
if @exclude_root
|
64
|
+
return true if path == @root_directory
|
65
|
+
|
66
|
+
parent = nil if parent == @root_directory
|
67
|
+
end
|
68
|
+
|
69
|
+
try_directory_loaders(path, parent)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Try each directory loader, returning true if the object has
|
73
|
+
# children to follow
|
74
|
+
def try_directory_loaders(path, parent)
|
75
|
+
stat = lstat = File.lstat(path)
|
76
|
+
stat = File.stat(path) if lstat.symlink?
|
77
|
+
|
78
|
+
@directory_loaders.any? do |loader|
|
79
|
+
loader.to_proc.call(self, path: path, parent: parent,
|
80
|
+
lstat: lstat, stat: stat)
|
81
|
+
end
|
82
|
+
|
83
|
+
return if lstat.symlink? && !@follow_directory_links
|
84
|
+
|
85
|
+
stat.directory?
|
86
|
+
end
|
52
87
|
end
|
53
88
|
end
|
54
89
|
end
|