connected 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3207847fee29e8a23ed1a8456571a8e88ab3e73c3d2df63d8cadad3b34b6382f
4
- data.tar.gz: 6c9944da0f906bb91ad8add7ae63dfed303452ac8f4f6b228b8e769cdb69de96
3
+ metadata.gz: b64513edd7cde21b6e0bc6f9de81c212fd221fd6e5bbfa288ff740c4c77d1c91
4
+ data.tar.gz: ea3e881fbe51977b4c9af37ce16ef1b37c401b7496ee1d816a4c393e1531678d
5
5
  SHA512:
6
- metadata.gz: 12ba3ef422355e9b9cc608a29c90579af5e4adee228e94bc4348de44e595c8e673c9ec12cc319143a28b6e39d26807ede22232edfe12756feed08a21a4768f4c
7
- data.tar.gz: 4ab7b7e541f3171dbd841980f0d63d0ad83a0cf18504d6d11b2a8e499ba70f4738f0ad093737b10e38af485f055f10268f9152c32a2ba111c23abbc2adacdb84
6
+ metadata.gz: fa98e68ddf6456ab303edc691af58e0b33d149562d7b0cd50ab52289e512bda2dc4fc62bf5320929c5ca1f4b393af9acb9a9b7eede3a16999f32ecf4ec089330
7
+ data.tar.gz: 8a4a5311a6b928d845859943986a71de53e4b904b4bd83a0585d4f72ac889d4c265c95225019c3955f5a7daccf02300e3e0cc3cbbd49765ea1272a2ee12475a9
data/.gitignore CHANGED
@@ -9,3 +9,5 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+
13
+ *.gem
data/.rubocop.yml CHANGED
@@ -1,44 +1,55 @@
1
- AllCops:
2
- TargetRubyVersion: 2.6
3
-
4
- Metrics/MethodLength:
5
- Max: 30
1
+ require:
2
+ - rubocop-rake
3
+ - rubocop-rspec
6
4
 
7
5
  Layout/LineLength:
8
6
  Max: 100
9
7
 
10
- Metrics/ClassLength:
11
- Max: 150
8
+ AllCops:
9
+ Exclude:
10
+ - 'db/schema.rb'
11
+ - 'vendor/**/*'
12
+ TargetRubyVersion: 3.1
13
+ NewCops: enable
12
14
 
13
- Metrics/ModuleLength:
14
- Max: 150
15
+ Metrics/AbcSize:
16
+ Max: 21
15
17
 
16
- Metrics/CyclomaticComplexity:
17
- Max: 7
18
+ Metrics/BlockLength:
19
+ Max: 35
20
+ Exclude:
21
+ - 'spec/**/*_spec.rb'
22
+ - 'Rakefile'
23
+ - '*.gemspec'
18
24
 
19
- Metrics/AbcSize:
25
+ Metrics/MethodLength:
20
26
  Max: 25
21
27
 
22
- Metrics/PerceivedComplexity:
23
- Max: 8
28
+ Metrics/ModuleLength:
29
+ Max: 160
24
30
  Exclude:
25
- - 'spec/**/*_spec.rb'
31
+ - 'spec/**/*_spec.rb'
26
32
 
27
- Metrics/BlockLength:
28
- Max: 35
33
+ Metrics/ClassLength:
34
+ Max: 300
29
35
  Exclude:
30
- - '*.gemspec'
31
- - Rakefile
32
- - 'spec/**/*_spec.rb'
33
- - 'spec/spec_helper.rb'
36
+ - 'spec/**/*_spec.rb'
34
37
 
35
- Metrics/ParameterLists:
36
- Max: 6
38
+ Gemspec/RequireMFA:
39
+ Enabled: false
37
40
 
38
- Naming/MethodParameterName:
41
+ Style/MixinUsage:
39
42
  Exclude:
40
- - 'spec/**/*.rb'
43
+ - "bin/console"
44
+
45
+ Style/StringLiterals:
46
+ Enabled: true
47
+ EnforcedStyle: double_quotes
48
+
49
+ Style/StringLiteralsInInterpolation:
50
+ Enabled: true
51
+ EnforcedStyle: double_quotes
41
52
 
42
- Security/Eval:
53
+ Style/StringConcatenation:
43
54
  Exclude:
44
- - Gemfile
55
+ - 'Rakefile'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.2
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source 'https://rubygems.org'
3
+ source "https://rubygems.org"
4
4
 
5
5
  # Specify your gem's dependencies in spf.gemspec
6
6
  gemspec
data/Gemfile.lock CHANGED
@@ -1,54 +1,99 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- connected (0.1.0)
4
+ connected (0.1.1)
5
+ matrix
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
8
9
  specs:
9
- ast (2.4.0)
10
- diff-lcs (1.3)
11
- docile (1.3.2)
12
- parallel (1.19.1)
13
- parser (2.7.1.3)
14
- ast (~> 2.4.0)
15
- rainbow (3.0.0)
16
- rake (13.0.1)
17
- regexp_parser (1.7.0)
18
- rexml (3.2.4)
19
- rspec (3.9.0)
20
- rspec-core (~> 3.9.0)
21
- rspec-expectations (~> 3.9.0)
22
- rspec-mocks (~> 3.9.0)
23
- rspec-core (3.9.2)
24
- rspec-support (~> 3.9.3)
25
- rspec-expectations (3.9.2)
10
+ ast (2.4.2)
11
+ backport (1.2.0)
12
+ benchmark (0.2.1)
13
+ diff-lcs (1.5.0)
14
+ docile (1.4.0)
15
+ e2mmap (0.1.0)
16
+ jaro_winkler (1.5.4)
17
+ json (2.6.3)
18
+ kramdown (2.4.0)
19
+ rexml
20
+ kramdown-parser-gfm (1.1.0)
21
+ kramdown (~> 2.0)
22
+ matrix (0.4.2)
23
+ mini_portile2 (2.8.0)
24
+ nokogiri (1.13.10)
25
+ mini_portile2 (~> 2.8.0)
26
+ racc (~> 1.4)
27
+ parallel (1.22.1)
28
+ parser (3.1.3.0)
29
+ ast (~> 2.4.1)
30
+ racc (1.6.1)
31
+ rainbow (3.1.1)
32
+ rake (13.0.6)
33
+ regexp_parser (2.6.1)
34
+ reverse_markdown (2.1.1)
35
+ nokogiri
36
+ rexml (3.2.5)
37
+ rspec (3.12.0)
38
+ rspec-core (~> 3.12.0)
39
+ rspec-expectations (~> 3.12.0)
40
+ rspec-mocks (~> 3.12.0)
41
+ rspec-core (3.12.0)
42
+ rspec-support (~> 3.12.0)
43
+ rspec-expectations (3.12.0)
26
44
  diff-lcs (>= 1.2.0, < 2.0)
27
- rspec-support (~> 3.9.0)
28
- rspec-mocks (3.9.1)
45
+ rspec-support (~> 3.12.0)
46
+ rspec-mocks (3.12.1)
29
47
  diff-lcs (>= 1.2.0, < 2.0)
30
- rspec-support (~> 3.9.0)
31
- rspec-support (3.9.3)
32
- rubocop (0.85.0)
48
+ rspec-support (~> 3.12.0)
49
+ rspec-support (3.12.0)
50
+ rubocop (1.40.0)
51
+ json (~> 2.3)
33
52
  parallel (~> 1.10)
34
- parser (>= 2.7.0.1)
53
+ parser (>= 3.1.2.1)
35
54
  rainbow (>= 2.2.2, < 4.0)
36
- regexp_parser (>= 1.7)
37
- rexml
38
- rubocop-ast (>= 0.0.3)
55
+ regexp_parser (>= 1.8, < 3.0)
56
+ rexml (>= 3.2.5, < 4.0)
57
+ rubocop-ast (>= 1.23.0, < 2.0)
39
58
  ruby-progressbar (~> 1.7)
40
- unicode-display_width (>= 1.4.0, < 2.0)
41
- rubocop-ast (0.0.3)
42
- parser (>= 2.7.0.1)
43
- ruby-progressbar (1.10.1)
44
- simplecov (0.18.5)
59
+ unicode-display_width (>= 1.4.0, < 3.0)
60
+ rubocop-ast (1.24.0)
61
+ parser (>= 3.1.1.0)
62
+ rubocop-rake (0.6.0)
63
+ rubocop (~> 1.0)
64
+ rubocop-rspec (2.15.0)
65
+ rubocop (~> 1.33)
66
+ ruby-progressbar (1.11.0)
67
+ simplecov (0.21.2)
45
68
  docile (~> 1.1)
46
69
  simplecov-html (~> 0.11)
47
- simplecov-cobertura (1.3.1)
48
- simplecov (~> 0.8)
49
- simplecov-html (0.12.2)
50
- unicode-display_width (1.7.0)
51
- yard (0.9.25)
70
+ simplecov_json_formatter (~> 0.1)
71
+ simplecov-cobertura (2.1.0)
72
+ rexml
73
+ simplecov (~> 0.19)
74
+ simplecov-html (0.12.3)
75
+ simplecov_json_formatter (0.1.4)
76
+ solargraph (0.47.2)
77
+ backport (~> 1.2)
78
+ benchmark
79
+ bundler (>= 1.17.2)
80
+ diff-lcs (~> 1.4)
81
+ e2mmap
82
+ jaro_winkler (~> 1.5)
83
+ kramdown (~> 2.3)
84
+ kramdown-parser-gfm (~> 1.1)
85
+ parser (~> 3.0)
86
+ reverse_markdown (>= 1.0.5, < 3)
87
+ rubocop (>= 0.52)
88
+ thor (~> 1.0)
89
+ tilt (~> 2.0)
90
+ yard (~> 0.9, >= 0.9.24)
91
+ thor (1.2.1)
92
+ tilt (2.0.11)
93
+ unicode-display_width (2.3.0)
94
+ webrick (1.7.0)
95
+ yard (0.9.28)
96
+ webrick (~> 1.7.0)
52
97
 
53
98
  PLATFORMS
54
99
  ruby
@@ -57,11 +102,14 @@ DEPENDENCIES
57
102
  bundler (~> 2)
58
103
  connected!
59
104
  rake (~> 13)
60
- rspec (~> 3.0)
61
- rubocop (~> 0.50)
62
- simplecov (~> 0.15)
63
- simplecov-cobertura (~> 1.3)
105
+ rspec (~> 3.10)
106
+ rubocop (~> 1.40)
107
+ rubocop-rake (~> 0.6)
108
+ rubocop-rspec (~> 2.11)
109
+ simplecov (~> 0.21)
110
+ simplecov-cobertura (~> 2.1)
111
+ solargraph (~> 0.45)
64
112
  yard (~> 0.9)
65
113
 
66
114
  BUNDLED WITH
67
- 2.1.4
115
+ 2.3.26
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Connected
4
4
 
5
- _Connected_ aims to be useful for overlaying and solving both directed and undirected graphs. The goal is to provide generic mixins to add a solver to real code, rather than an academic demonstration of how Dijkstra's algorithm works.
5
+ _Connected_ aims to be useful for overlaying and searching both directed and undirected graphs. The goal is to provide generic mixins that add graph search capabilities to real code, rather than an academic demonstration of how Dijkstra's algorithm works.
6
6
 
7
7
  ## Installation
8
8
 
@@ -24,7 +24,7 @@ Or install it yourself as:
24
24
 
25
25
  ### Simple Uses
26
26
 
27
- For a simpler use-cases, the classes `Connected::GenericNode`, `Connected::GenericConnection`, and `Connected::Path` will suffice. These can either be subclassed or extended to wrap any real work. First, I'll just demonstrate how they work with an undirected graph (meaning a set of bidirectional connections):
27
+ For simpler use-cases, the classes `Connected::GenericNode`, `Connected::GenericConnection`, and `Connected::Path` will suffice. These can either be subclassed or extended to wrap any real work. First, I'll just demonstrate how they work with an undirected graph (meaning a set of bidirectional connections):
28
28
 
29
29
  ```ruby
30
30
  require 'connected'
@@ -115,7 +115,33 @@ Path.find(from: node_a, to: node_d).to_s
115
115
  # => "a -> b -> d"
116
116
  ```
117
117
 
118
- All the above examples are based on connections created using `node.connects_to other_node`, which creates a bi-directional connection. For representing _directed_ graph, you can add `directed: true` to the method which causes it to only create the connection you explicitly described (meaning it won't create the connection back for you).
118
+ All the above examples are based on connections created using `node.connects_to other_node`, which creates a bi-directional connection. For representing a _directed_ graph, you can add `directed: true` to the method which causes it to only create the connection you explicitly described (meaning it won't create the connection back for you). Under the hood, however, the graph itself is _always_ directed; this is just a convenient way to not automatically make both directions of a connection.
119
+
120
+ Speaking of graphs, there is also a `Connected::GenericGraph` class that can be used either explicitly for slightly more complicated scenarios or it can be created dynamically as needed:
121
+
122
+ ```ruby
123
+ node_a.subtree # returns a GenericGraph based on all nodes reachable from `node_a`
124
+ # => #<Connected::GenericGraph:0x00007f...
125
+ node_a.subtree.radius
126
+ # => 4
127
+ node_a.subtree.diameter
128
+ # => 7
129
+ node_a.subtree.adjacency_matrix
130
+ # => Matrix[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 0], [1, 1, 0, 1]]
131
+ node_a.subtree.complete?
132
+ # => false
133
+ ```
134
+
135
+ This dynamic graph is also used on some methods directly on Edges:
136
+
137
+ ```ruby
138
+ node_a.eccentricity
139
+ # => 7
140
+ node_a.neighborhood
141
+ # => #<Connected::GenericGraph:0x00007fd35...
142
+ node_a.neighborhood.vertices.map(&:name)
143
+ # => ["b", "c"]
144
+ ```
119
145
 
120
146
  ### Overlaying on Other Objects
121
147
 
data/Rakefile CHANGED
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
- require 'rubocop/rake_task'
6
- require 'yard'
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+ require "yard"
7
7
 
8
8
  RSpec::Core::RakeTask.new(:spec)
9
9
  RuboCop::RakeTask.new(:rubocop)
10
10
  YARD::Rake::YardocTask.new do |y|
11
11
  y.options = [
12
- '--markup', 'markdown'
12
+ "--markup", "markdown"
13
13
  ]
14
14
  end
15
15
 
data/bin/console CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'bundler/setup'
5
- require 'connected'
4
+ require "bundler/setup"
5
+ require "connected"
6
6
 
7
7
  # You can add fixtures and/or initialization code here to make experimenting
8
8
  # with your gem easier. You can also use a different console, if you like.
@@ -11,5 +11,5 @@ require 'connected'
11
11
  # require "pry"
12
12
  # Pry.start
13
13
 
14
- require 'irb'
14
+ require "irb"
15
15
  IRB.start(__FILE__)
data/connected.gemspec CHANGED
@@ -1,35 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lib/connected/version'
3
+ require_relative "lib/connected/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = 'connected'
6
+ spec.name = "connected"
7
7
  spec.version = Connected::VERSION
8
- spec.authors = ['Jonathan Gnagy']
9
- spec.email = ['jonathan.gnagy@gmail.com']
8
+ spec.authors = ["Jonathan Gnagy"]
9
+ spec.email = ["jonathan.gnagy@gmail.com"]
10
10
 
11
- spec.summary = 'A shortest path first gem'
12
- spec.description = 'A Ruby object-oriented solver for directed and undirected ' \
13
- 'graphs based loosely on Dijkstra\'s algorithm'
14
- spec.homepage = 'https://github.com/jgnagy/connected'
11
+ spec.summary = "A shortest path first gem"
12
+ spec.description = "A Ruby object-oriented search library for directed and undirected " \
13
+ "graphs based loosely on Dijkstra's algorithm"
14
+ spec.homepage = "https://github.com/jgnagy/connected"
15
15
 
16
- spec.license = 'MIT'
17
- spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = "~> 3.1"
18
18
 
19
19
  # Specify which files should be added to the gem when it is released.
20
20
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
21
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
22
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
23
  end
24
- spec.bindir = 'exe'
24
+ spec.bindir = "exe"
25
25
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
- spec.require_paths = ['lib']
26
+ spec.require_paths = ["lib"]
27
27
 
28
- spec.add_development_dependency 'bundler', '~> 2'
29
- spec.add_development_dependency 'rake', '~> 13'
30
- spec.add_development_dependency 'rspec', '~> 3.0'
31
- spec.add_development_dependency 'rubocop', '~> 0.50'
32
- spec.add_development_dependency 'simplecov', '~> 0.15'
33
- spec.add_development_dependency 'simplecov-cobertura', '~> 1.3'
34
- spec.add_development_dependency 'yard', '~> 0.9'
28
+ spec.add_dependency "matrix"
29
+
30
+ spec.add_development_dependency "bundler", "~> 2"
31
+ spec.add_development_dependency "rake", "~> 13"
32
+ spec.add_development_dependency "rspec", "~> 3.10"
33
+ spec.add_development_dependency "rubocop", "~> 1.40"
34
+ spec.add_development_dependency "rubocop-rake", "~> 0.6"
35
+ spec.add_development_dependency "rubocop-rspec", "~> 2.11"
36
+ spec.add_development_dependency "simplecov", "~> 0.21"
37
+ spec.add_development_dependency "simplecov-cobertura", "~> 2.1"
38
+ spec.add_development_dependency "solargraph", "~> 0.45"
39
+ spec.add_development_dependency "yard", "~> 0.9"
35
40
  end
@@ -13,7 +13,7 @@ module Connected
13
13
  validate_metric(metric)
14
14
  validate_state(state)
15
15
 
16
- raise 'Invalid Connection from and to same Vertex' if from == to
16
+ raise "Invalid Connection from and to same Vertex" if from == to
17
17
 
18
18
  @from = from
19
19
  @to = to
@@ -34,19 +34,19 @@ module Connected
34
34
  private
35
35
 
36
36
  def validate_from(node)
37
- raise 'Invalid from Node' unless node.is_a?(Vertex)
37
+ raise "Invalid from Node" unless node.is_a?(Vertex)
38
38
  end
39
39
 
40
40
  def validate_to(node)
41
- raise 'Invalid to Node' unless node.is_a?(Vertex)
41
+ raise "Invalid to Node" unless node.is_a?(Vertex)
42
42
  end
43
43
 
44
44
  def validate_metric(num)
45
- raise 'Invalid metric' unless num.is_a?(Numeric) && num.positive?
45
+ raise "Invalid metric" unless num.is_a?(Numeric) && num.positive?
46
46
  end
47
47
 
48
48
  def validate_state(value)
49
- raise 'Invalid state' unless %i[open closed].include?(value)
49
+ raise "Invalid state" unless %i[open closed].include?(value)
50
50
  end
51
51
  end
52
52
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Connected
4
+ # A generic implementation of a Graph
5
+ class GenericGraph
6
+ include Graph
7
+ end
8
+ end
@@ -18,10 +18,10 @@ module Connected
18
18
  return true if neighbors.include?(other)
19
19
 
20
20
  connections << GenericConnection.new(
21
- from: self, to: other, metric: metric, state: state.to_sym
21
+ from: self, to: other, metric:, state: state.to_sym
22
22
  )
23
23
 
24
- other.connects_to(self, metric: metric, state: state) unless directed
24
+ other.connects_to(self, metric:, state:) unless directed
25
25
  end
26
26
 
27
27
  def disconnect_from(other, directed: false)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Connected
4
+ # A collection of related vertices and edges
5
+ module Graph
6
+ include Comparable
7
+
8
+ def initialize(vertices:, edges: nil)
9
+ @vertices = vertices
10
+ @edges = edges
11
+ end
12
+
13
+ def adjacency_matrix
14
+ rows = @vertices.map do |i|
15
+ @vertices.map do |j|
16
+ i == j || i.neighbors.include?(j) ? 1 : 0
17
+ end.to_a
18
+ end.to_a
19
+ Matrix.rows(rows)
20
+ end
21
+
22
+ # A "complete graph" is one where all vertices connect to all others
23
+ def complete?
24
+ density == 1 || vertices.size == 1
25
+ end
26
+
27
+ # The density is the ratio to a complete graph of the same number of vertices
28
+ def density
29
+ return 0 if edges.size.zero? # otherwise we'd divide by 0!
30
+
31
+ # Assumes an directed graph for now, therefore e / n(n -1)
32
+ # is the ratio of edges (e) to edges in a complete graph
33
+ edges.size.to_f / ((vertices.size * (vertices.size - 1)))
34
+ end
35
+
36
+ # The diameter of a graph is the longest of all shortest paths between all vertices on a graph
37
+ def diameter
38
+ longest = nil
39
+ vertices.each do |v|
40
+ dist = v.eccentricity
41
+ longest = dist if longest.nil? || dist > longest
42
+ end
43
+
44
+ longest
45
+ end
46
+
47
+ def edges
48
+ @edges || vertices.map(&:connections).flatten.uniq
49
+ end
50
+
51
+ def length
52
+ edges.size
53
+ end
54
+
55
+ def radius
56
+ shortest = nil
57
+ vertices.each do |v|
58
+ ecc = v.eccentricity
59
+ shortest = ecc if shortest.nil? || ecc < shortest
60
+ end
61
+
62
+ shortest
63
+ end
64
+
65
+ def vertices
66
+ @vertices
67
+ end
68
+
69
+ def <=>(other)
70
+ vertices == other.vertices && edges == other.edges
71
+ end
72
+ end
73
+ end
@@ -46,14 +46,22 @@ module Connected
46
46
  nodes.last
47
47
  end
48
48
 
49
- def to_s(separator = ' -> ')
49
+ def to_s(separator = " -> ")
50
50
  nodes.map(&:name).join(separator)
51
51
  end
52
52
 
53
53
  # rubocop:disable Metrics/AbcSize
54
54
  # rubocop:disable Metrics/CyclomaticComplexity
55
55
  # rubocop:disable Metrics/PerceivedComplexity
56
- def self.all(from:, to:, include_closed: false, debug: false, suboptimal: false)
56
+ def self.all(
57
+ from:,
58
+ to:,
59
+ include_closed: false,
60
+ debug: false,
61
+ suboptimal: false,
62
+ min_by: :cost,
63
+ cache: {}
64
+ )
57
65
  paths = []
58
66
 
59
67
  path_queue = from.neighbors.map { |n| new([from, n]) }
@@ -62,50 +70,55 @@ module Connected
62
70
  this_path = path_queue.pop
63
71
  next unless this_path.open? || include_closed
64
72
 
65
- puts "Walking from #{this_path.nodes.map(&:name).join(' to ')}" if debug
73
+ unless suboptimal
74
+ next if cache[this_path.to.name]&.<= this_path.send(min_by.to_sym)
75
+
76
+ cache[this_path.to.name] = this_path.send(min_by.to_sym)
77
+ end
78
+
79
+ puts "Walking from #{this_path.nodes.map(&:name).join(" to ")}" if debug
66
80
 
67
81
  if this_path.to == to
68
- puts "Found destination with #{this_path.nodes.map(&:name).join(' to ')}" if debug
82
+ puts "Found destination with #{this_path.nodes.map(&:name).join(" to ")}" if debug
69
83
  paths << this_path
70
84
  else
71
- highmetric = paths.max_by(&:cost)&.cost
72
- highops = paths.max_by(&:hops)&.hops
85
+ maxmeasure = paths.map(&min_by.to_sym).max
73
86
 
74
87
  this_path.to.neighbors.each do |n|
75
88
  new_path = this_path.branch(n)
76
89
  next unless new_path
77
90
 
78
- if paths.empty? || new_path.cost <= highmetric || new_path.hops <= highops || suboptimal
91
+ if paths.empty? || new_path.send(min_by.to_sym) <= maxmeasure || suboptimal
79
92
  path_queue.unshift(new_path)
80
93
  elsif debug
81
- puts "Skipping #{new_path.nodes.map(&:name).join(' to ')}"
94
+ puts "Skipping #{new_path.nodes.map(&:name).join(" to ")}"
82
95
  end
83
96
  end
84
97
  end
85
98
  end
86
99
 
87
100
  # Return the list of paths, sorted first by cost then by hops
88
- paths.sort_by { |p| [p.cost, p.hops] }
101
+ paths.sort_by { |p| min_by == :cost ? p.cost : p.hops }
89
102
  end
90
103
  # rubocop:enable Metrics/AbcSize
91
104
  # rubocop:enable Metrics/CyclomaticComplexity
92
105
  # rubocop:enable Metrics/PerceivedComplexity
93
106
 
94
- def self.find(from:, to:, include_closed: false)
95
- all(from: from, to: to, include_closed: include_closed).first
107
+ def self.find(from:, to:, include_closed: false, min_by: :cost, cache: {})
108
+ all(from:, to:, include_closed:, min_by:, cache:).first
96
109
  end
97
110
 
98
111
  private
99
112
 
100
113
  def validate_nodes(list)
101
114
  # Want to throw an exception if there are loops
102
- raise 'Invalid Nodes list, duplicates found' unless list.size == list.uniq.size
115
+ raise "Invalid Nodes list, duplicates found" unless list.size == list.uniq.size
103
116
 
104
117
  list.each_with_index do |item, index|
105
118
  break if index == list.size - 1
106
119
 
107
120
  # Each node should connect to the next (no leaves except the end of the list)
108
- raise 'Invalid Nodes list, broken chain' unless item.neighbors.include?(list[index + 1])
121
+ raise "Invalid Nodes list, broken chain" unless item.neighbors.include?(list[index + 1])
109
122
  end
110
123
 
111
124
  true
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Connected
4
- VERSION = '0.1.0'
4
+ VERSION = "0.1.1"
5
5
  end
@@ -8,14 +8,69 @@ module Connected
8
8
  raise "#connections() MUST be implemented on #{self.class.name}"
9
9
  end
10
10
 
11
+ def edges
12
+ connections
13
+ end
14
+
15
+ def graph
16
+ @graph || subtree
17
+ end
18
+
11
19
  # A shortcut for retrieving this node's neighbors
12
20
  def neighbors
13
21
  connections.map(&:to).uniq
14
22
  end
15
23
 
24
+ # The neighborhood of a Vertex is a Graph of all neighbors and any connections
25
+ # they have to each other.
26
+ # @see https://en.wikipedia.org/wiki/Neighbourhood_(graph_theory)
27
+ def neighborhood(closed: false)
28
+ vertices = neighbors
29
+ vertices << self if closed
30
+ n_edges = vertices.map(&:connections).flatten.uniq.select { |c| vertices.include?(c.to) }
31
+ GenericGraph.new(vertices:, edges: n_edges)
32
+ end
33
+
16
34
  # Retrieves the Connection object responsible for connecting to a Node
17
35
  def connection_to(other)
18
36
  connections.select { |c| c.to == other }.min_by(&:metric)
19
37
  end
38
+
39
+ def eccentricity
40
+ reachable_vertices.map do |r|
41
+ self == r ? nil : Path.find(from: self, to: r).cost
42
+ end.compact.max
43
+ end
44
+
45
+ def reaches?(other)
46
+ reachable_vertices.include?(other)
47
+ end
48
+
49
+ def reachable_from?(other)
50
+ other.reachable_vertices.include?(self)
51
+ end
52
+
53
+ def reachable_vertices
54
+ rverts = neighbors + [self]
55
+
56
+ queue = (neighbors.map(&:neighbors).flatten.uniq - neighbors)
57
+
58
+ until queue.empty?
59
+ this_vert = queue.pop
60
+ next if this_vert == self
61
+
62
+ rverts << this_vert
63
+ this_vert.neighbors.each do |v|
64
+ queue << v unless rverts.include?(v)
65
+ end
66
+ end
67
+
68
+ rverts.uniq
69
+ end
70
+
71
+ # A subtree is a subgraph including all edges and vertices reachable from a vertex
72
+ def subtree
73
+ GenericGraph.new(vertices: reachable_vertices)
74
+ end
20
75
  end
21
76
  end
data/lib/connected.rb CHANGED
@@ -1,8 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'connected/version'
4
- require 'connected/edge'
5
- require 'connected/vertex'
6
- require 'connected/generic_node'
7
- require 'connected/generic_connection'
8
- require 'connected/path'
3
+ # Standard Library
4
+ require "matrix"
5
+
6
+ # Internal
7
+ require "connected/version"
8
+ require "connected/edge"
9
+ require "connected/vertex"
10
+ require "connected/graph"
11
+ require "connected/generic_graph"
12
+ require "connected/generic_node"
13
+ require "connected/generic_connection"
14
+ require "connected/path"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: connected
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Gnagy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-04 00:00:00.000000000 Z
11
+ date: 2022-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: matrix
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,56 +58,98 @@ dependencies:
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: '3.0'
61
+ version: '3.10'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: '3.0'
68
+ version: '3.10'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rubocop
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '0.50'
75
+ version: '1.40'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.40'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.11'
62
104
  type: :development
63
105
  prerelease: false
64
106
  version_requirements: !ruby/object:Gem::Requirement
65
107
  requirements:
66
108
  - - "~>"
67
109
  - !ruby/object:Gem::Version
68
- version: '0.50'
110
+ version: '2.11'
69
111
  - !ruby/object:Gem::Dependency
70
112
  name: simplecov
71
113
  requirement: !ruby/object:Gem::Requirement
72
114
  requirements:
73
115
  - - "~>"
74
116
  - !ruby/object:Gem::Version
75
- version: '0.15'
117
+ version: '0.21'
76
118
  type: :development
77
119
  prerelease: false
78
120
  version_requirements: !ruby/object:Gem::Requirement
79
121
  requirements:
80
122
  - - "~>"
81
123
  - !ruby/object:Gem::Version
82
- version: '0.15'
124
+ version: '0.21'
83
125
  - !ruby/object:Gem::Dependency
84
126
  name: simplecov-cobertura
85
127
  requirement: !ruby/object:Gem::Requirement
86
128
  requirements:
87
129
  - - "~>"
88
130
  - !ruby/object:Gem::Version
89
- version: '1.3'
131
+ version: '2.1'
90
132
  type: :development
91
133
  prerelease: false
92
134
  version_requirements: !ruby/object:Gem::Requirement
93
135
  requirements:
94
136
  - - "~>"
95
137
  - !ruby/object:Gem::Version
96
- version: '1.3'
138
+ version: '2.1'
139
+ - !ruby/object:Gem::Dependency
140
+ name: solargraph
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.45'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.45'
97
153
  - !ruby/object:Gem::Dependency
98
154
  name: yard
99
155
  requirement: !ruby/object:Gem::Requirement
@@ -108,8 +164,8 @@ dependencies:
108
164
  - - "~>"
109
165
  - !ruby/object:Gem::Version
110
166
  version: '0.9'
111
- description: A Ruby object-oriented solver for directed and undirected graphs based
112
- loosely on Dijkstra's algorithm
167
+ description: A Ruby object-oriented search library for directed and undirected graphs
168
+ based loosely on Dijkstra's algorithm
113
169
  email:
114
170
  - jonathan.gnagy@gmail.com
115
171
  executables: []
@@ -120,6 +176,7 @@ files:
120
176
  - ".images/logo.png"
121
177
  - ".rspec"
122
178
  - ".rubocop.yml"
179
+ - ".ruby-version"
123
180
  - ".travis.yml"
124
181
  - Gemfile
125
182
  - Gemfile.lock
@@ -132,7 +189,9 @@ files:
132
189
  - lib/connected.rb
133
190
  - lib/connected/edge.rb
134
191
  - lib/connected/generic_connection.rb
192
+ - lib/connected/generic_graph.rb
135
193
  - lib/connected/generic_node.rb
194
+ - lib/connected/graph.rb
136
195
  - lib/connected/path.rb
137
196
  - lib/connected/version.rb
138
197
  - lib/connected/vertex.rb
@@ -146,16 +205,16 @@ require_paths:
146
205
  - lib
147
206
  required_ruby_version: !ruby/object:Gem::Requirement
148
207
  requirements:
149
- - - ">="
208
+ - - "~>"
150
209
  - !ruby/object:Gem::Version
151
- version: 2.6.0
210
+ version: '3.1'
152
211
  required_rubygems_version: !ruby/object:Gem::Requirement
153
212
  requirements:
154
213
  - - ">="
155
214
  - !ruby/object:Gem::Version
156
215
  version: '0'
157
216
  requirements: []
158
- rubygems_version: 3.0.8
217
+ rubygems_version: 3.3.7
159
218
  signing_key:
160
219
  specification_version: 4
161
220
  summary: A shortest path first gem