tangle 0.8.2 → 0.11.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 +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
|