routr 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/.rspec +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +9 -0
- data/Guardfile +8 -0
- data/LICENSE +19 -0
- data/README.md +76 -0
- data/Rakefile +13 -0
- data/cruise_config.rb +6 -0
- data/lib/routr.rb +11 -0
- data/lib/routr/calculator.rb +161 -0
- data/lib/routr/interface.rb +62 -0
- data/lib/routr/version.rb +3 -0
- data/routr.gemspec +20 -0
- data/spec/integration/routr_spec.rb +122 -0
- data/spec/unit/calculator_spec.rb +105 -0
- metadata +95 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
guard 'rspec', :all_on_start => true, :all_after_pass => true do
|
2
|
+
watch(%r{^spec/.+_spec\.rb$})
|
3
|
+
watch(%r{^lib/routr/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
|
4
|
+
watch(%r{^lib/routr\.rb$}) { |m| "spec/integration" }
|
5
|
+
watch(%r{^lib/routr/interface\.rb$}) { |m| "spec/integration" }
|
6
|
+
end
|
7
|
+
|
8
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2012 Levente Bagi
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
8
|
+
subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
19
|
+
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Routr
|
2
|
+
|
3
|
+
[](http://travis-ci.org/bagilevi/routr)
|
4
|
+
|
5
|
+
Implements Dijkstra's algorithm to find the shortest path in a graph.
|
6
|
+
|
7
|
+
The cost of an edge can be more general than just a numeric value.
|
8
|
+
You can inject a method for calculating the distance from source to the
|
9
|
+
next node given the distance from source to the current node and the
|
10
|
+
properties of an edge. A possible use case is the one used by PeerPost:
|
11
|
+
nodes are people, an edge is a connection between specifying when those
|
12
|
+
people regularly meet, you inject a method that calculates when people
|
13
|
+
meet next after a specified time.
|
14
|
+
|
15
|
+
# Usage
|
16
|
+
|
17
|
+
```
|
18
|
+
class MyInterface
|
19
|
+
NETWORK = {
|
20
|
+
'A' => { 'B' => 50, 'C' => 100 },
|
21
|
+
'B' => { 'D' => 50 },
|
22
|
+
'C' => { 'D' => 10 },
|
23
|
+
'D' => {}
|
24
|
+
}
|
25
|
+
|
26
|
+
# This will be used if :initial_distance is not supplied to calculate_route
|
27
|
+
def initial_distance
|
28
|
+
0
|
29
|
+
end
|
30
|
+
|
31
|
+
def neighbours_of(node_key)
|
32
|
+
NETWORK[node_key].keys
|
33
|
+
end
|
34
|
+
|
35
|
+
def calculate_new_distance(distance_so_far, a_key, b_key)
|
36
|
+
distance_so_far + NETWORK[a_key][b_key]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
route = Routr.new(MyInterface.new).calculate_route(
|
41
|
+
:from => 'A',
|
42
|
+
:to => 'D',
|
43
|
+
:initial_distance => 0
|
44
|
+
)
|
45
|
+
|
46
|
+
route.hops.count # => 2
|
47
|
+
route.members.count # => 3
|
48
|
+
|
49
|
+
route.hops.first.from # => 'A'
|
50
|
+
route.hops.first.to # => 'B'
|
51
|
+
route.hops.first.distance # => 50
|
52
|
+
route.hops.last.from # => 'B'
|
53
|
+
route.hops.last.to # => 'D'
|
54
|
+
route.hops.last.distance # => 100
|
55
|
+
|
56
|
+
route.members.first # => 'A'
|
57
|
+
route.members.last # => 'D'
|
58
|
+
|
59
|
+
route.distance # => 100
|
60
|
+
```
|
61
|
+
|
62
|
+
If you have normal addible edge costs, you can replace the
|
63
|
+
`calculate_new_distance` method with `edge_distance`:
|
64
|
+
|
65
|
+
```
|
66
|
+
def edge_cost(a_key, b_key)
|
67
|
+
NETWORK[a_key][b_key]
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
# License
|
72
|
+
|
73
|
+
Copyright 2012, Levente Bagi
|
74
|
+
|
75
|
+
Released under the MIT License, see the LICENSE file.
|
76
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc 'Default: run specs.'
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
desc "Run specs"
|
9
|
+
RSpec::Core::RakeTask.new do |t|
|
10
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
11
|
+
# Put spec opts in a file named .rspec in root
|
12
|
+
end
|
13
|
+
|
data/cruise_config.rb
ADDED
data/lib/routr.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
module Routr
|
2
|
+
class Calculator
|
3
|
+
|
4
|
+
def initialize(graph)
|
5
|
+
@graph = graph
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :graph
|
9
|
+
|
10
|
+
# based of wikipedia's pseudocode: http://en.wikipedia.org/wiki/Dijkstra's_algorithm
|
11
|
+
|
12
|
+
def dijkstra(initial_node, initial_distance = 0)
|
13
|
+
debug
|
14
|
+
debug "== routing from #{initial_node.inspect}"
|
15
|
+
initialize_temporary_variables
|
16
|
+
|
17
|
+
set_distance(initial_node, initial_distance)
|
18
|
+
add_unvisited_node(initial_node)
|
19
|
+
|
20
|
+
while any_unvisited_nodes?
|
21
|
+
u = node_with_minimum_distance
|
22
|
+
break if not reachable? u
|
23
|
+
mark_as_visited u
|
24
|
+
|
25
|
+
with_each_neighbour_of u do |v| # for each neighbor v of u: where v has not yet been removed from Q.
|
26
|
+
debug "neighbour: #{v}"
|
27
|
+
new_distance = calculate_new_distance(best_distance_to(u), u, v)
|
28
|
+
if (best_distance_to(v) == :infinity) || (new_distance < best_distance_to(v))
|
29
|
+
set_best_distance_to(v, new_distance)
|
30
|
+
add_unvisited_node(v)
|
31
|
+
set_previous_node_to(v, u)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
debug
|
35
|
+
end
|
36
|
+
debug "== end routing"
|
37
|
+
end
|
38
|
+
|
39
|
+
def shortest_path src, dest, initial_distance = 0, include_details = false
|
40
|
+
dijkstra src, initial_distance
|
41
|
+
path_to dest, include_details
|
42
|
+
end
|
43
|
+
|
44
|
+
def path_to(dest, include_details = false)
|
45
|
+
if reachable? dest
|
46
|
+
item = \
|
47
|
+
if include_details
|
48
|
+
{
|
49
|
+
:node => dest,
|
50
|
+
:distance => best_distance_to(dest)
|
51
|
+
}
|
52
|
+
else
|
53
|
+
dest
|
54
|
+
end
|
55
|
+
previous_path_to(dest, include_details) + [ item ]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def previous_path_to(dest, include_details = false)
|
60
|
+
if previous_node_to(dest).nil?
|
61
|
+
[]
|
62
|
+
else
|
63
|
+
path_to(previous_node_to(dest), include_details)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def edge_cost a, b
|
71
|
+
(graph.edge_cost(a, b) || :infinity).tap { |cost|
|
72
|
+
debug "Edge cost #{a} -> #{b} = #{cost.inspect}"
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def calculate_new_distance distance_so_far, a, b
|
77
|
+
if graph.respond_to?(:calculate_new_distance)
|
78
|
+
graph.calculate_new_distance distance_so_far, a, b
|
79
|
+
else
|
80
|
+
distance_so_far + edge_cost(a, b)
|
81
|
+
end.tap {|new_distance|
|
82
|
+
if new_distance < distance_so_far
|
83
|
+
raise "New distance < previous distance: #{new_distance.inspect} < #{distance_so_far.inspect}"
|
84
|
+
end
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
def initialize_temporary_variables
|
89
|
+
@d = {}
|
90
|
+
@prev = {}
|
91
|
+
@candidates = []
|
92
|
+
end
|
93
|
+
|
94
|
+
def best_distance_to(node)
|
95
|
+
debug "best_distance[#{node.inspect}] # -> #{@d[node].inspect}"
|
96
|
+
@d[node] || :infinity
|
97
|
+
end
|
98
|
+
|
99
|
+
def reachable?(node)
|
100
|
+
debug "reachable?(#{node.inspect})"
|
101
|
+
(best_distance_to(node) != :infinity)\
|
102
|
+
.tap {|r| debug "=> #{r.inspect}" }
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# OPTIMIZE: maintain a sorted list, so we don't have to search the whole set every time
|
107
|
+
def node_with_minimum_distance # unvisited node with minimum distance
|
108
|
+
debug "node_with_minimum_distance"
|
109
|
+
@candidates.min_by{ |x| best_distance_to(x) }\
|
110
|
+
.tap{|x| debug "=> #{x.inspect}"}
|
111
|
+
end
|
112
|
+
|
113
|
+
def mark_as_visited node
|
114
|
+
@candidates = @candidates - [node]
|
115
|
+
end
|
116
|
+
|
117
|
+
def any_unvisited_nodes?
|
118
|
+
debug "any_unvisited_nodes?"
|
119
|
+
@candidates.any?\
|
120
|
+
.tap{|r| debug "=> #{r.inspect}" }
|
121
|
+
end
|
122
|
+
|
123
|
+
def add_unvisited_node node
|
124
|
+
debug "add_unvisited_node #{node.inspect}"
|
125
|
+
@candidates << node
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
def set_distance node, value
|
130
|
+
@d[node] = value # Distance from source to source
|
131
|
+
end
|
132
|
+
|
133
|
+
def with_each_neighbour_of node, &block
|
134
|
+
graph.neighbours_of(node).tap{|neighbours| debug "Neighbours of: #{node} -> #{neighbours.inspect}"}.each(&block)
|
135
|
+
end
|
136
|
+
|
137
|
+
def set_best_distance_to node, new_distance
|
138
|
+
debug "best_distance[#{node.inspect}] = #{new_distance.inspect}"
|
139
|
+
@d[node] = new_distance
|
140
|
+
end
|
141
|
+
|
142
|
+
def set_previous_node_to node, previous_node
|
143
|
+
@prev[node] = previous_node
|
144
|
+
end
|
145
|
+
|
146
|
+
def previous_node_to(node)
|
147
|
+
@prev[node]
|
148
|
+
end
|
149
|
+
|
150
|
+
def debug *args
|
151
|
+
return
|
152
|
+
@i ||= 0
|
153
|
+
@i += 1
|
154
|
+
if @i < 1000
|
155
|
+
puts *args
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'routr/calculator'
|
2
|
+
module Routr
|
3
|
+
class Interface
|
4
|
+
def initialize interface
|
5
|
+
@interface = interface
|
6
|
+
end
|
7
|
+
|
8
|
+
def calculate_route attr
|
9
|
+
initial_distance = attr[:initial_distance] ||
|
10
|
+
(@interface.initial_distance if @interface.respond_to?(:initial_distance)) ||
|
11
|
+
0
|
12
|
+
raw_route = Routr::Calculator.new(@interface)\
|
13
|
+
.shortest_path(attr[:from], attr[:to], initial_distance, true)
|
14
|
+
route = Route.from_raw(raw_route)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
class Route
|
20
|
+
attr_reader :hops, :members, :distance, :raw
|
21
|
+
|
22
|
+
def initialize(raw)
|
23
|
+
@raw = raw
|
24
|
+
raw ||= []
|
25
|
+
@hops = raw[0..-2].to_enum.with_index.map{|item, index|
|
26
|
+
Hop.new(
|
27
|
+
:from => raw[index][:node],
|
28
|
+
:to => raw[index + 1][:node],
|
29
|
+
:distance => raw[index + 1][:distance]
|
30
|
+
)
|
31
|
+
}
|
32
|
+
@members = raw.map{|item| item[:node]}
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.from_raw(raw)
|
36
|
+
new(raw)
|
37
|
+
end
|
38
|
+
|
39
|
+
def distance
|
40
|
+
hops.last.distance
|
41
|
+
end
|
42
|
+
|
43
|
+
def found?
|
44
|
+
@hops.any?
|
45
|
+
end
|
46
|
+
|
47
|
+
def missing?
|
48
|
+
not found?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
class Hop
|
54
|
+
attr_reader :from, :to, :distance
|
55
|
+
def initialize(attributes)
|
56
|
+
@from = attributes[:from]
|
57
|
+
@to = attributes[:to]
|
58
|
+
@distance = attributes[:distance]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
data/routr.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "routr/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "routr"
|
7
|
+
s.version = Routr::VERSION
|
8
|
+
s.authors = ["Levente Bagi"]
|
9
|
+
s.email = ["levente@picklive.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Finds the shortest path in a graph, allowing unusual cost definitions}
|
12
|
+
s.description = %q{Implements Dijkstra's algorithm to find the shortest path in a graph. The cost of an edge can be more general than just a numeric value.}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_development_dependency "rspec"
|
20
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'routr'
|
2
|
+
|
3
|
+
describe Routr do
|
4
|
+
it "works for normal edge costs" do
|
5
|
+
class SimpleInterface
|
6
|
+
NETWORK = {
|
7
|
+
'A' => { 'B' => 50, 'C' => 100 },
|
8
|
+
'B' => { 'D' => 50 },
|
9
|
+
'C' => { 'D' => 10 },
|
10
|
+
'D' => {}
|
11
|
+
}
|
12
|
+
|
13
|
+
# This will be used if :initial_distance is not supplied to calculate_route
|
14
|
+
def initial_distance
|
15
|
+
0
|
16
|
+
end
|
17
|
+
|
18
|
+
def neighbours_of(node_key)
|
19
|
+
NETWORK[node_key].keys
|
20
|
+
end
|
21
|
+
|
22
|
+
def edge_cost(a_key, b_key)
|
23
|
+
NETWORK[a_key][b_key]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
route = Routr.new(SimpleInterface.new).calculate_route(
|
28
|
+
:from => 'A',
|
29
|
+
:to => 'D'
|
30
|
+
)
|
31
|
+
|
32
|
+
route.hops.count.should == 2
|
33
|
+
route.members.count.should == 3
|
34
|
+
|
35
|
+
route.hops.first.from.should == 'A'
|
36
|
+
route.hops.first.to.should == 'B'
|
37
|
+
route.hops.first.distance.should == 50
|
38
|
+
route.hops.last.from.should == 'B'
|
39
|
+
route.hops.last.to.should == 'D'
|
40
|
+
route.hops.last.distance.should == 100
|
41
|
+
|
42
|
+
route.members.first.should == 'A'
|
43
|
+
route.members.last.should == 'D'
|
44
|
+
|
45
|
+
route.distance.should == 100
|
46
|
+
end
|
47
|
+
|
48
|
+
it "works for custom edge costs" do
|
49
|
+
|
50
|
+
# This example uses a simplified version of time.
|
51
|
+
#
|
52
|
+
# The values denote a time in a day when the two nodes 'meet',
|
53
|
+
# where 1000 represents a whole day, i.e.
|
54
|
+
#
|
55
|
+
# A meets B at 500, 1500, 2500, etc.
|
56
|
+
# A meets C at 100, 1100, 2100, etc.
|
57
|
+
class CustomInterface
|
58
|
+
NETWORK = {
|
59
|
+
'A' => { 'B' => 300, 'C' => 600 },
|
60
|
+
'B' => { 'D' => 400 },
|
61
|
+
'C' => { 'D' => 700 },
|
62
|
+
'D' => {}
|
63
|
+
}
|
64
|
+
|
65
|
+
def neighbours_of(node_key)
|
66
|
+
NETWORK[node_key].keys
|
67
|
+
end
|
68
|
+
|
69
|
+
def calculate_new_distance(distance_so_far, a_key, b_key)
|
70
|
+
time_of_day = distance_so_far % 1000
|
71
|
+
beginning_of_day = distance_so_far - time_of_day
|
72
|
+
meeting_time = NETWORK[a_key][b_key]
|
73
|
+
if time_of_day <= meeting_time
|
74
|
+
beginning_of_day + meeting_time
|
75
|
+
else
|
76
|
+
beginning_of_day + 1000 + meeting_time
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
route = Routr.new(CustomInterface.new).calculate_route(
|
82
|
+
:from => 'A',
|
83
|
+
:to => 'D',
|
84
|
+
:initial_distance => 50_564
|
85
|
+
)
|
86
|
+
route.members.should == %w(A C D)
|
87
|
+
route.distance.should == 50_700
|
88
|
+
|
89
|
+
route = Routr.new(CustomInterface.new).calculate_route(
|
90
|
+
:from => 'A',
|
91
|
+
:to => 'D',
|
92
|
+
:initial_distance => 50_900
|
93
|
+
)
|
94
|
+
route.members.should == %w(A B D)
|
95
|
+
route.distance.should == 51_400
|
96
|
+
end
|
97
|
+
|
98
|
+
it "returns a missing route if not found" do
|
99
|
+
route = Routr.new(SimpleInterface.new).calculate_route(
|
100
|
+
:from => 'D',
|
101
|
+
:to => 'A',
|
102
|
+
:initial_distance => 0
|
103
|
+
)
|
104
|
+
route.hops.should == []
|
105
|
+
route.members.should == []
|
106
|
+
route.found?.should be_false
|
107
|
+
route.missing?.should be_true
|
108
|
+
end
|
109
|
+
|
110
|
+
it "exposes the route as raw ruby" do
|
111
|
+
route = Routr.new(SimpleInterface.new).calculate_route(
|
112
|
+
:from => 'A',
|
113
|
+
:to => 'D'
|
114
|
+
)
|
115
|
+
route.raw.should == [
|
116
|
+
{ :node => 'A', :distance => 0 },
|
117
|
+
{ :node => 'B', :distance => 50 },
|
118
|
+
{ :node => 'D', :distance => 100 },
|
119
|
+
]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "routr/calculator"
|
2
|
+
|
3
|
+
describe Routr::Calculator do
|
4
|
+
it "calculates shortest path" do
|
5
|
+
graph = Graph.new(
|
6
|
+
:edges => [
|
7
|
+
["a","b", 5],
|
8
|
+
["b","c", 3],
|
9
|
+
["c","d", 1],
|
10
|
+
["a","d",10],
|
11
|
+
["b","d", 2],
|
12
|
+
["f","g", 1],
|
13
|
+
]
|
14
|
+
)
|
15
|
+
|
16
|
+
r = Routr::Calculator.new(graph)
|
17
|
+
r.shortest_path("a", "a").should == %w(a)
|
18
|
+
r.shortest_path("a", "b").should == %w(a b)
|
19
|
+
r.shortest_path("a", "c").should == %w(a b c)
|
20
|
+
r.shortest_path("a", "d").should == %w(a b d)
|
21
|
+
r.shortest_path("a", "f").should == nil
|
22
|
+
r.shortest_path("a", "g").should == nil
|
23
|
+
end
|
24
|
+
|
25
|
+
it "calculates shortest path for graph with custom costs" do
|
26
|
+
graph = TimeGraph.new(
|
27
|
+
:edges => [
|
28
|
+
["a","b","1200"], # a and b meet every day at 12:00 noon
|
29
|
+
["b","c","1300"],
|
30
|
+
["a","d","1215"],
|
31
|
+
["d","c","1230"],
|
32
|
+
["c","e","1245"],
|
33
|
+
["f","g","1400"],
|
34
|
+
["a","h","1100"],
|
35
|
+
["h","i","1000"],
|
36
|
+
]
|
37
|
+
)
|
38
|
+
# h ---1000--- a ---1200--- b
|
39
|
+
# | | |
|
40
|
+
# 1000 1215 1300
|
41
|
+
# | | |
|
42
|
+
# i d ---1230--- c
|
43
|
+
# |
|
44
|
+
# 1245
|
45
|
+
# f ---1400--- g |
|
46
|
+
# e
|
47
|
+
#
|
48
|
+
# Meeting times: 4 digits: hhmm (every day)
|
49
|
+
# Actual times: 6 digits: ddhhmm
|
50
|
+
# 001200 = day 0, 12:00 noon
|
51
|
+
|
52
|
+
r = Routr::Calculator.new(graph)
|
53
|
+
r.shortest_path("a", "a", "001200").should == %w(a)
|
54
|
+
r.shortest_path("a", "b", "001200").should == %w(a b)
|
55
|
+
r.shortest_path("a", "c", "001200").should == %w(a d c)
|
56
|
+
r.shortest_path("a", "d", "001200").should == %w(a d)
|
57
|
+
r.shortest_path("a", "e", "001200").should == %w(a d c e)
|
58
|
+
r.shortest_path("a", "f", "001200").should == nil
|
59
|
+
r.shortest_path("a", "g", "001200").should == nil
|
60
|
+
r.shortest_path("a", "h", "001200").should == %w(a h)
|
61
|
+
r.shortest_path("a", "i", "001200").should == %w(a h i)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
class Graph
|
67
|
+
def initialize(attr)
|
68
|
+
@edges = {}
|
69
|
+
attr[:edges].each do |a, b, edge_info|
|
70
|
+
@edges[a] ||= {}
|
71
|
+
@edges[a][b] = edge_info
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def edge_cost a, b
|
76
|
+
(@edges[a] && @edges[a][b]) || (@edges[b] && @edges[b][a])
|
77
|
+
end
|
78
|
+
|
79
|
+
def neighbours_of(node)
|
80
|
+
(
|
81
|
+
( (@edges[node] && @edges[node].keys) || [] ) +
|
82
|
+
@edges.keys.select{|k| @edges[k].keys.include?(node)}
|
83
|
+
).uniq
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
class TimeGraph < Graph
|
90
|
+
private :edge_cost
|
91
|
+
|
92
|
+
def calculate_new_distance time_so_far, a, b
|
93
|
+
meeting_time_i = edge_cost(a, b).to_i # it's not actually a cost, but that's where we store the meeting value
|
94
|
+
time_of_day_so_far_i = time_so_far.to_i % 10000
|
95
|
+
|
96
|
+
# meeting time today
|
97
|
+
new_time_i = time_so_far.to_i + (meeting_time_i - time_of_day_so_far_i)
|
98
|
+
|
99
|
+
# meeting time tomorrow
|
100
|
+
new_time_i += 10000 if time_of_day_so_far_i > meeting_time_i
|
101
|
+
|
102
|
+
new_time_i.to_s.rjust(6,"0")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: routr
|
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
|
+
- Levente Bagi
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-03-29 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
description: Implements Dijkstra's algorithm to find the shortest path in a graph. The cost of an edge can be more general than just a numeric value.
|
35
|
+
email:
|
36
|
+
- levente@picklive.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- .gitignore
|
45
|
+
- .rspec
|
46
|
+
- .travis.yml
|
47
|
+
- Gemfile
|
48
|
+
- Guardfile
|
49
|
+
- LICENSE
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- cruise_config.rb
|
53
|
+
- lib/routr.rb
|
54
|
+
- lib/routr/calculator.rb
|
55
|
+
- lib/routr/interface.rb
|
56
|
+
- lib/routr/version.rb
|
57
|
+
- routr.gemspec
|
58
|
+
- spec/integration/routr_spec.rb
|
59
|
+
- spec/unit/calculator_spec.rb
|
60
|
+
homepage: ""
|
61
|
+
licenses: []
|
62
|
+
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.8.17
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Finds the shortest path in a graph, allowing unusual cost definitions
|
93
|
+
test_files:
|
94
|
+
- spec/integration/routr_spec.rb
|
95
|
+
- spec/unit/calculator_spec.rb
|