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 ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in has_shortest_path.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'bundler'
2
+ #require(File.join(File.dirname(__FILE__), 'test', 'dummy', 'config', 'application'))
3
+
4
+ Bundler::GemHelper.install_tasks
5
+ #Dummy::Application.load_tasks
@@ -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)
@@ -0,0 +1,3 @@
1
+ module HasShortestPath
2
+ VERSION = "0.0.1"
3
+ end
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
+