tangle 0.8.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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, **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
- 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
data/lib/tangle/errors.rb CHANGED
@@ -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.
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
- # (default false)
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
- # A loader lambda will be called like
14
- # ->(graph, path, parent_path) { ... }
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, parent|
20
- # vertex = File::Stat.new(path)
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(**options)
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
- @directory_loaders.any? do |loader|
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