has_shortest_path 0.0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +5 -0
- data/has_shortest_path.gemspec +23 -0
- data/lib/has_shortest_path.rb +173 -0
- data/lib/has_shortest_path/version.rb +3 -0
- metadata +88 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "has_shortest_path/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "has_shortest_path"
|
7
|
+
s.version = HasShortestPath::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Felix Jodoin"]
|
10
|
+
s.email = ["felix@fjstudios.net"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{Provides a simple way to find the shortest path in a graph of Rails records}
|
13
|
+
s.description = %q{Provides a simple way to find the shortest path in a graph of Rails records using Floyd's algorithm}
|
14
|
+
|
15
|
+
s.rubyforge_project = "has_shortest_path"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_dependency "activesupport", "3.0.5"
|
23
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
#require 'has_shortest_path/railtie' if defined?(Rails)
|
2
|
+
require 'active_support'
|
3
|
+
|
4
|
+
module HasShortestPath
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
Infinity = 1.0/0 unless defined?(Infinity)
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# +has_shortest_path+ defines a class as an edge in a graph
|
11
|
+
#
|
12
|
+
# * +via+: the class's accessor for edges that it is connected to
|
13
|
+
#
|
14
|
+
# * +weighted_with+: the class's accessor for the weight variable
|
15
|
+
#
|
16
|
+
# * +through+: the intermediate model that contains the weight (this is the vertex model)
|
17
|
+
#
|
18
|
+
def has_shortest_path options = {}
|
19
|
+
cattr_accessor :shortest_path_opts
|
20
|
+
cattr_accessor :weighted_array
|
21
|
+
|
22
|
+
self.shortest_path_opts = options
|
23
|
+
|
24
|
+
raise "has_shortest_path requires the arguments :via, :weighted_with, and :through." if shortest_path_opts[:via].nil? || shortest_path_opts[:through].nil? || shortest_path_opts[:weighted_with].nil?
|
25
|
+
|
26
|
+
# Weight of the vertex origination.destinations[1] => origination.connections[1].cost
|
27
|
+
# Set up a method like 'weighted_destinations' that will return an array of all of the edges we have a vertex to,
|
28
|
+
# with each edge having the weight from this edge to that edge.
|
29
|
+
attr_accessor shortest_path_opts[:weighted_with]
|
30
|
+
self.weighted_array = "weighted_#{self.shortest_path_opts[:via].to_s}".to_sym
|
31
|
+
define_method(weighted_array) do
|
32
|
+
self.send(shortest_path_opts[:via]).enum_for(:each_with_index).map { |dest, i| dest.cost = self.send(shortest_path_opts[:through])[i].cost; dest }
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# +shortest_path+ returns an array of intermediate edges in between this edge and the destination (exclusive).
|
39
|
+
# The array is empty if there are no intermediate edges.
|
40
|
+
# The array is nil if there is no available path.
|
41
|
+
# * +final_destination+: The edge record to attempt to find the shortest path to.
|
42
|
+
# * +options[:recompute]+: If set to true, all intermediate work will be discarded.
|
43
|
+
def shortest_path final_destination, options = {}
|
44
|
+
self.reload && @shortest_path_cache = nil if options[:recompute]
|
45
|
+
build_cache
|
46
|
+
|
47
|
+
# 1. Build an adjacency matrix out of this model's relevent records
|
48
|
+
@shortest_path_cache[:w_table] = gen_w_table unless @shortest_path_cache[:w_table].present?
|
49
|
+
|
50
|
+
# 2. Perform Floyd's shortest path algorithm
|
51
|
+
@shortest_path_cache[:p_table] = floyd(@shortest_path_cache[:w_table], @shortest_path_cache[:w_size]) unless @shortest_path_cache[:p_table].present?
|
52
|
+
|
53
|
+
# 3. Attempt to reconstruct a path to final_destination
|
54
|
+
# Only attempt reconstruction if final_destination is in the table!
|
55
|
+
# If final_destination is in the table, there must be a path, otherwise it would not have
|
56
|
+
# been included by traverse_all_edges
|
57
|
+
final_index = index_from_edge(final_destination)
|
58
|
+
if(final_index < @shortest_path_cache[:w_size] && final_index != nil) then
|
59
|
+
reconstruct_path(index_from_edge(self), final_index, @shortest_path_cache[:p_table])
|
60
|
+
else
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private #:nodoc
|
66
|
+
# Performs the Floyd-Warshall algorithm on the given W-Table, returning the P table used to reconstruct shortest paths
|
67
|
+
def floyd w_table, size #:nodoc
|
68
|
+
p_table = Array.new(size) { Array.new(size) { 0 } }
|
69
|
+
d_table = w_table
|
70
|
+
|
71
|
+
# 0...size is used since the w_table may be larger than needed
|
72
|
+
(0...size).each do |k|
|
73
|
+
(0...size).each do |i|
|
74
|
+
(0...size).each do |j|
|
75
|
+
if d_table[i][k] + d_table[k][j] < d_table[i][j] then
|
76
|
+
d_table[i][j] = d_table[i][k] + d_table[k][j]
|
77
|
+
p_table[i][j] = k
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
p_table
|
84
|
+
end
|
85
|
+
|
86
|
+
# Sets up the cache has, if it hasn't already been populated
|
87
|
+
def build_cache #:nodoc:
|
88
|
+
unless @shortest_path_cache.present? then
|
89
|
+
@shortest_path_cache = Hash.new
|
90
|
+
|
91
|
+
@shortest_path_cache[:visited_edges] = []
|
92
|
+
@shortest_path_cache[:next_edge_index] = 0
|
93
|
+
@shortest_path_cache[:edge_indices] = Hash.new
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Generates the w_table used as D0 for the Floyd-Warshall algorithm
|
98
|
+
def gen_w_table #:nodoc:
|
99
|
+
# Builds the w_table
|
100
|
+
# The w_table size cannot exceed the number of edges in the database
|
101
|
+
# If memory is a real constraint, could also loop through traverse_all_edges to get an accurate matrix size
|
102
|
+
|
103
|
+
# Set up the working variables
|
104
|
+
@shortest_path_cache[:w_table] = Array.new(self.class.count) { |j| Array.new(self.class.count) { |i| if i==j then 0 else Infinity end } }
|
105
|
+
|
106
|
+
traverse_all_edges(self)
|
107
|
+
@shortest_path_cache[:w_size] = @shortest_path_cache[:next_edge_index]
|
108
|
+
|
109
|
+
# This w_table matches the properties:
|
110
|
+
# w[i][j] = weight on edge if an edge between Vi and Vj exists
|
111
|
+
# w[i][j] = Infinity if no edge between Vi and Vj exists
|
112
|
+
# w[i][j] = 0 if i == j
|
113
|
+
@shortest_path_cache[:w_table]
|
114
|
+
end
|
115
|
+
|
116
|
+
# Reconstructs the path between start using the given p_table
|
117
|
+
# Returns an empty array if there are no intermediate vertices
|
118
|
+
# Vertices are in order of Start -> [Intermediate 1, Intermediate 2] -> Finish
|
119
|
+
def reconstruct_path start_index, finish_index, p_table #:nodoc:
|
120
|
+
ret = []
|
121
|
+
pqr = p_table[start_index][finish_index]
|
122
|
+
if pqr != 0 then
|
123
|
+
# recurse and concat the recursive results into our result array
|
124
|
+
reconstruct_path(start_index, pqr, p_table).each {|p| ret << p }
|
125
|
+
ret << edge_from_index(pqr)
|
126
|
+
reconstruct_path(pqr, finish_index, p_table).each {|p| ret << p }
|
127
|
+
end
|
128
|
+
ret
|
129
|
+
end
|
130
|
+
|
131
|
+
# Assigns a unique sequential index to each edge, or returns the existing index if it has already been set
|
132
|
+
def index_from_edge edge #:nodoc:
|
133
|
+
unless @shortest_path_cache[:edge_indices][edge].present? then
|
134
|
+
@shortest_path_cache[:edge_indices][edge] = @shortest_path_cache[:next_edge_index]
|
135
|
+
@shortest_path_cache[:next_edge_index] = @shortest_path_cache[:next_edge_index] + 1
|
136
|
+
end
|
137
|
+
@shortest_path_cache[:edge_indices][edge]
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns the edge's class from the given index (reverse of index_from_edge)
|
141
|
+
def edge_from_index index #:nodoc
|
142
|
+
if @shortest_path_cache[:edge_indices].has_value?(index) then
|
143
|
+
@shortest_path_cache[:edge_indices].index(index)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# adds the specified path to the w_table
|
148
|
+
def add_to_w_table start, finish #:nodoc
|
149
|
+
s_index = index_from_edge(start)
|
150
|
+
f_index = index_from_edge(finish)
|
151
|
+
|
152
|
+
@shortest_path_cache[:w_table][s_index][f_index] = finish.send(shortest_path_opts[:weighted_with])
|
153
|
+
end
|
154
|
+
|
155
|
+
def traverse_all_edges edge #:nodoc
|
156
|
+
# don't recurse infinitely..
|
157
|
+
unless @shortest_path_cache[:visited_edges].include?(edge) then
|
158
|
+
|
159
|
+
@shortest_path_cache[:visited_edges].insert(-1, edge) # mark this edge has having been visited
|
160
|
+
|
161
|
+
unless edge.send(weighted_array).empty? then
|
162
|
+
# don't recurse further if there aren't any other connections
|
163
|
+
edge.send(weighted_array).each do |path|
|
164
|
+
add_to_w_table(edge, path)
|
165
|
+
traverse_all_edges(path)
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
ActiveRecord::Base.send(:include, HasShortestPath) if defined?(ActiveRecord)
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: has_shortest_path
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Felix Jodoin
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-03-28 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: activesupport
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - "="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 13
|
30
|
+
segments:
|
31
|
+
- 3
|
32
|
+
- 0
|
33
|
+
- 5
|
34
|
+
version: 3.0.5
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
description: Provides a simple way to find the shortest path in a graph of Rails records using Floyd's algorithm
|
38
|
+
email:
|
39
|
+
- felix@fjstudios.net
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
files:
|
47
|
+
- .gitignore
|
48
|
+
- Gemfile
|
49
|
+
- Rakefile
|
50
|
+
- has_shortest_path.gemspec
|
51
|
+
- lib/has_shortest_path.rb
|
52
|
+
- lib/has_shortest_path/version.rb
|
53
|
+
has_rdoc: true
|
54
|
+
homepage: ""
|
55
|
+
licenses: []
|
56
|
+
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
hash: 3
|
68
|
+
segments:
|
69
|
+
- 0
|
70
|
+
version: "0"
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 3
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
requirements: []
|
81
|
+
|
82
|
+
rubyforge_project: has_shortest_path
|
83
|
+
rubygems_version: 1.5.2
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: Provides a simple way to find the shortest path in a graph of Rails records
|
87
|
+
test_files: []
|
88
|
+
|