bahn 1.0.2
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/History.txt +12 -0
- data/Manifest.txt +6 -0
- data/README.txt +64 -0
- data/Rakefile +11 -0
- data/lib/bahn.rb +478 -0
- data/test/test_bahn.rb +8 -0
- metadata +93 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
= bahn
|
2
|
+
|
3
|
+
== DESCRIPTION:
|
4
|
+
|
5
|
+
A library for accessing train information from Deutsche Bahn in an
|
6
|
+
object-oriented way
|
7
|
+
|
8
|
+
== FEATURES
|
9
|
+
|
10
|
+
* Exposes Station, Service and Stop objects
|
11
|
+
* Does hairy screen-scraping
|
12
|
+
* Works around bahn.de's crazy transient URLs (provided you don't
|
13
|
+
leave objects in scope for 24 hours or so)
|
14
|
+
|
15
|
+
== BUGS
|
16
|
+
|
17
|
+
* This is not an official Deutsche Bahn service, nor does it use an official API;
|
18
|
+
it does screen-scraping on their HTML, and is therefore liable to break if Deutsche Bahn
|
19
|
+
update their website code.
|
20
|
+
* Train travel times do not take timezones into account, and may end up 24 hours out
|
21
|
+
because whenever it sees a time going 'backwards' (e.g. depart 08:40, arrive 08:20) it
|
22
|
+
assumes that just under 24 hours has elapsed. Also, summary timetables for some very long
|
23
|
+
distance trains (such as the Trans-Siberian Express) may only show stations that are more
|
24
|
+
than 24 hours apart, so there's no way to know that we should be adding on 24 hours to
|
25
|
+
the time.
|
26
|
+
|
27
|
+
== SYNOPSIS:
|
28
|
+
|
29
|
+
require 'bahn'
|
30
|
+
ulm = Bahn::Station.find(:first, :name => 'Ulm')
|
31
|
+
#=> #<Bahn::Station:0x55fb4 @id=8000170, @y_coord="48399019", @name="Ulm Hbf", @x_coord="9982161">
|
32
|
+
my_train = ulm.departures(:include => :ice).find{|departure| departure.service.name == 'ICE 699'}
|
33
|
+
#=> #<Bahn::Stop @time=07:55 @station="Ulm Hbf" @destination="M\303\274nchen Hbf">
|
34
|
+
my_train.destination.arrival_time
|
35
|
+
#=> 09:17
|
36
|
+
|
37
|
+
== INSTALL:
|
38
|
+
|
39
|
+
* sudo gem install bahn
|
40
|
+
|
41
|
+
== LICENSE:
|
42
|
+
|
43
|
+
(The MIT License)
|
44
|
+
|
45
|
+
Copyright (c) 2009 Matthew Westcott
|
46
|
+
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
48
|
+
a copy of this software and associated documentation files (the
|
49
|
+
'Software'), to deal in the Software without restriction, including
|
50
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
51
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
52
|
+
permit persons to whom the Software is furnished to do so, subject to
|
53
|
+
the following conditions:
|
54
|
+
|
55
|
+
The above copyright notice and this permission notice shall be
|
56
|
+
included in all copies or substantial portions of the Software.
|
57
|
+
|
58
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
59
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
60
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
61
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
62
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
63
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
64
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/lib/bahn.rb
ADDED
@@ -0,0 +1,478 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'json'
|
4
|
+
require 'iconv'
|
5
|
+
require 'hpricot'
|
6
|
+
require 'cgi'
|
7
|
+
require 'digest/sha1'
|
8
|
+
|
9
|
+
module Bahn
|
10
|
+
VERSION = '1.0.2'
|
11
|
+
|
12
|
+
# Represents a time of day on a 24-hour clock, without implying any particular date.
|
13
|
+
class ClockTime < Time
|
14
|
+
# Create a new ClockTime object for the time specified by hours and mins
|
15
|
+
def self.clock(hours, mins)
|
16
|
+
self.utc(1970, 1, 1, hours, mins, 0)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parses a string of the form "hh:mm" and converts it to a ClockTime object
|
20
|
+
def self.parse(str)
|
21
|
+
if str =~ /(\d+):(\d+)/ then self.utc(1970, 1, 1, $1, $2, 0) end
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect # :nodoc:
|
25
|
+
to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s # :nodoc:
|
29
|
+
sprintf("%02d:%02d", hour, min)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Station
|
34
|
+
# Finds and returns one or many Station objects. Invoked through one of the following forms:
|
35
|
+
#
|
36
|
+
# * <tt>Bahn::Station.find(id)</tt> - returns the Station with the given ID
|
37
|
+
# * <tt>Bahn::Station.find(:first, :name => name)</tt> - returns the first station matching name
|
38
|
+
# * <tt>Bahn::Station.find(:all, :name => name)</tt> - returns an array of all stations matching name
|
39
|
+
def self.find(id_or_type, opts = {})
|
40
|
+
case id_or_type
|
41
|
+
when :first
|
42
|
+
query = autocomplete_query(opts[:name])
|
43
|
+
query.size ? self.new(:autocomplete_result => query.first) : nil
|
44
|
+
when :all
|
45
|
+
query = autocomplete_query(opts[:name])
|
46
|
+
query.collect {|result| self.new(:autocomplete_result => result)}
|
47
|
+
else # assume a numeric ID
|
48
|
+
self.new(:id => id_or_type)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(opts) # :nodoc:
|
53
|
+
if opts[:autocomplete_result]
|
54
|
+
populate_from_autocomplete_result(opts[:autocomplete_result])
|
55
|
+
else
|
56
|
+
@id = opts[:id].to_i
|
57
|
+
@name = opts[:name]
|
58
|
+
@x_coord = opts[:x_coord]
|
59
|
+
@y_coord = opts[:y_coord]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# The numeric ID of this station.
|
64
|
+
attr_reader :id
|
65
|
+
|
66
|
+
# Returns the station name.
|
67
|
+
def name
|
68
|
+
fetch_autocomplete_result if @name.nil?
|
69
|
+
@name
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the X coordinate of the station; appears to be a WGS84 longitude in units of 10^-6 degrees.
|
73
|
+
def x_coord
|
74
|
+
fetch_autocomplete_result if @x_coord.nil?
|
75
|
+
@x_coord
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the Y coordinate of the station; appears to be a WGS84 latitude in units of 10^-6 degrees.
|
79
|
+
def y_coord
|
80
|
+
fetch_autocomplete_result if @y_coord.nil?
|
81
|
+
@y_coord
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns an Enumerable listing of Stop objects for all services departing from this station.
|
85
|
+
# The list can be filtered by transport type through the following options:
|
86
|
+
# * <tt>:include</tt> - One of the symbols :ice, :ic_ec, :ir, :regional, :urban, :bus, :boat, :subway, :tram, or an array of them, to determine which transport types should be included in the list
|
87
|
+
# * <tt>:exclude</tt> - Specifies one or more of the above transport types to be excluded from the results.
|
88
|
+
def departures(opts = {})
|
89
|
+
DepartureBoard.new(self, opts)
|
90
|
+
end
|
91
|
+
|
92
|
+
# =====
|
93
|
+
private
|
94
|
+
# =====
|
95
|
+
def self.autocomplete_query(term)
|
96
|
+
uri = "http://reiseauskunft.bahn.de/bin/ajax-getstop.exe/en?REQ0JourneyStopsS0A=1&REQ0JourneyStopsS0G=#{CGI.escape(term.to_s)}"
|
97
|
+
io = open(uri)
|
98
|
+
response_text = Iconv.iconv('utf-8', io.charset, io.read).first
|
99
|
+
response_json = JSON.parse(response_text.scan(/\{.*\}/).first)
|
100
|
+
response_json['suggestions']
|
101
|
+
end
|
102
|
+
|
103
|
+
def fetch_autocomplete_result
|
104
|
+
populate_from_autocomplete_result(Bahn::Station.autocomplete_query(@id).first)
|
105
|
+
end
|
106
|
+
|
107
|
+
def populate_from_autocomplete_result(autocomplete_data)
|
108
|
+
@id = autocomplete_data['id'].scan(/\@L=(\d+)\@/).first.first.to_i
|
109
|
+
@name = autocomplete_data['id'].scan(/\@O=([^\@]*)\@/).first.first
|
110
|
+
@x_coord = autocomplete_data['xcoord']
|
111
|
+
@y_coord = autocomplete_data['ycoord']
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class DepartureBoard
|
116
|
+
TRANSPORT_TYPES = {
|
117
|
+
:ice =>0x100,
|
118
|
+
:ic_ec => 0x80,
|
119
|
+
:ir => 0x40,
|
120
|
+
:regional => 0x20,
|
121
|
+
:urban => 0x10,
|
122
|
+
:bus => 0x08,
|
123
|
+
:boat => 0x04,
|
124
|
+
:subway => 0x02,
|
125
|
+
:tram => 0x01
|
126
|
+
}
|
127
|
+
include Enumerable
|
128
|
+
def initialize(station, opts)
|
129
|
+
@station = station
|
130
|
+
|
131
|
+
transport_types = Array(opts[:include] || TRANSPORT_TYPES.keys) - Array(opts[:exclude])
|
132
|
+
filter_num = transport_types.inject(0) {|sum, type| sum + (TRANSPORT_TYPES[type] || 0) }
|
133
|
+
filter = sprintf("%09b", filter_num)
|
134
|
+
@url_prefix = "http://reiseauskunft.bahn.de/bin/bhftafel.exe/en?input=#{station.id}&productsFilter=#{filter}&start=1&boardType=dep&dateBegin=12.12.10&dateEnd=10.12.11&time=00:00"
|
135
|
+
@departure_pages = []
|
136
|
+
end
|
137
|
+
|
138
|
+
def each
|
139
|
+
0.upto(23) do |hour|
|
140
|
+
doc = departure_page(hour)
|
141
|
+
# find all tr children of table.result which contain a td.train
|
142
|
+
# and do not have class 'browse'
|
143
|
+
departure_docs = doc / 'table.result tr[td.train]:not(.browse)'
|
144
|
+
departure_docs.each do |departure_doc|
|
145
|
+
service_link = departure_doc % 'td.train:nth(2) a'
|
146
|
+
destination_link = departure_doc % 'td.route span.bold a'
|
147
|
+
(destination_id, destination_time_string), = destination_link['onclick'].scan(/^sHC\(this, '', '(\d+)','([^']*)'\)/)
|
148
|
+
destination_time = ClockTime.parse(destination_time_string)
|
149
|
+
|
150
|
+
intermediate_stops = (departure_doc % 'td.route').children.find_all{|node| node.text?}.join(' ')
|
151
|
+
departure_time = ClockTime.parse((departure_doc % 'td.time').inner_text)
|
152
|
+
last_time = departure_time
|
153
|
+
days_passed = 0
|
154
|
+
|
155
|
+
intermediate_stops.scan(/\d+:\d+/) do |time_string|
|
156
|
+
time = ClockTime.parse(time_string)
|
157
|
+
days_passed += 1 if time < last_time
|
158
|
+
last_time = time
|
159
|
+
end
|
160
|
+
inferred_time_to_destination = days_passed * 24*60*60 + (destination_time - departure_time)
|
161
|
+
|
162
|
+
platform_td = departure_doc % 'td.platform'
|
163
|
+
if platform_td.nil?
|
164
|
+
platform = :none
|
165
|
+
else
|
166
|
+
platform = platform_td.inner_text.strip
|
167
|
+
end
|
168
|
+
|
169
|
+
yield Stop.new(
|
170
|
+
:station => @station,
|
171
|
+
:service => Service.new(
|
172
|
+
:path => service_link['href'].sub(/\?.*/, ''),
|
173
|
+
:name => service_link.inner_text.strip.gsub(/\s+/, ' '),
|
174
|
+
:destination_info => {
|
175
|
+
:station => Station.new(
|
176
|
+
:id => destination_id,
|
177
|
+
:name => destination_link.inner_text.strip
|
178
|
+
),
|
179
|
+
:arrival_time => destination_time
|
180
|
+
}
|
181
|
+
),
|
182
|
+
:departure_time => departure_time,
|
183
|
+
:platform => (platform == '' ? :none : platform),
|
184
|
+
:inferred_time_to_destination => inferred_time_to_destination
|
185
|
+
)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# =====
|
191
|
+
private
|
192
|
+
# =====
|
193
|
+
def departure_page(hour)
|
194
|
+
@departure_pages[hour] ||= Hpricot(open("#{@url_prefix}%2B#{hour * 60}"))
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Represents a scheduled train (or other transportation) service. This is not specific to
|
199
|
+
# a particular day's train, but encompasses all trains taking this route at this
|
200
|
+
# time of day. When services are subject to daily or seasonal variations, each such variation
|
201
|
+
# is considered a Service of its own, even if they share the same service name.
|
202
|
+
class Service
|
203
|
+
def initialize(opts) # :nodoc:
|
204
|
+
@path = opts[:path]
|
205
|
+
@name = opts[:name]
|
206
|
+
if opts[:origin_info]
|
207
|
+
@origin = Stop.new(opts[:origin_info].merge(
|
208
|
+
:service => self,
|
209
|
+
:arrival_time => :none,
|
210
|
+
:arrival_time_from_origin => :none,
|
211
|
+
:arrival_time_to_destination => :none,
|
212
|
+
:departure_time_from_origin => 0
|
213
|
+
))
|
214
|
+
end
|
215
|
+
if opts[:destination_info]
|
216
|
+
@destination = Stop.new(opts[:destination_info].merge(
|
217
|
+
:service => self,
|
218
|
+
:arrival_time_to_destination => 0,
|
219
|
+
:departure_time => :none,
|
220
|
+
:departure_time_from_origin => :none,
|
221
|
+
:departure_time_to_destination => :none
|
222
|
+
))
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# The designated name for this service, if any - e.g. "ICE 692".
|
227
|
+
attr_reader :name
|
228
|
+
|
229
|
+
# Returns an array of Stop objects for all the stops that this service makes.
|
230
|
+
def stops
|
231
|
+
if !@stops
|
232
|
+
stop_docs = traininfo_response_doc / "table.result tr[td.station]"
|
233
|
+
last_time = nil
|
234
|
+
days_passed = 0
|
235
|
+
|
236
|
+
origin_departure_time = nil
|
237
|
+
|
238
|
+
@stops = stop_docs.collect {|stop_doc|
|
239
|
+
station_link = stop_doc % 'td.station a'
|
240
|
+
|
241
|
+
arrival_time = ClockTime.parse((stop_doc % 'td.arrival').inner_text)
|
242
|
+
departure_time = ClockTime.parse((stop_doc % 'td.departure').inner_text)
|
243
|
+
origin_departure_time ||= departure_time
|
244
|
+
|
245
|
+
if arrival_time.nil?
|
246
|
+
arrival_time_from_origin = :none
|
247
|
+
else
|
248
|
+
days_passed += 1 if (!last_time.nil? && arrival_time < last_time)
|
249
|
+
arrival_time_from_origin = days_passed * 24*60*60 + (arrival_time - origin_departure_time)
|
250
|
+
last_time = arrival_time
|
251
|
+
end
|
252
|
+
|
253
|
+
if departure_time.nil?
|
254
|
+
departure_time_from_origin = :none
|
255
|
+
else
|
256
|
+
days_passed += 1 if (!last_time.nil? && departure_time < last_time)
|
257
|
+
departure_time_from_origin = days_passed * 24*60*60 + (departure_time - origin_departure_time)
|
258
|
+
last_time = departure_time
|
259
|
+
end
|
260
|
+
|
261
|
+
platform = (stop_doc % 'td.platform').inner_text.strip
|
262
|
+
|
263
|
+
Stop.new(
|
264
|
+
:station => Station.new(
|
265
|
+
:id => station_link['href'].scan(/&input=[^&]*%23(\d+)&/).first.first,
|
266
|
+
:name => station_link.inner_text
|
267
|
+
),
|
268
|
+
:service => self,
|
269
|
+
:arrival_time => arrival_time || :none,
|
270
|
+
:departure_time => departure_time || :none,
|
271
|
+
:platform => (platform == '' ? :none : platform),
|
272
|
+
:arrival_time_from_origin => arrival_time_from_origin,
|
273
|
+
:departure_time_from_origin => departure_time_from_origin
|
274
|
+
)
|
275
|
+
}
|
276
|
+
end
|
277
|
+
@stops
|
278
|
+
end
|
279
|
+
|
280
|
+
# Returns the Stop object for the departure station of this service.
|
281
|
+
def origin
|
282
|
+
@origin ||= stops.first
|
283
|
+
end
|
284
|
+
|
285
|
+
# Returns the Stop object for the destination station of this service.
|
286
|
+
def destination
|
287
|
+
@destination ||= stops.last
|
288
|
+
end
|
289
|
+
|
290
|
+
def inspect # :nodoc:
|
291
|
+
"#<#{self.class} @name=#{@name.inspect} @origin=#{@origin.inspect} @destination=#{@destination.inspect}>"
|
292
|
+
end
|
293
|
+
|
294
|
+
# Returns a hash code which will match for any two Service objects that stop at the same list of
|
295
|
+
# stations at the same times.
|
296
|
+
def hash
|
297
|
+
stops.collect{|stop| stop.subhash}.hash
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns a hash code which will match for any two Service objects that stop at the same list of
|
301
|
+
# stations at the same times; like hash, but using SHA1 to make it more collision-resistant.
|
302
|
+
# (This is the nearest thing we have to a unique ID, as Deutsche Bahn do not expose
|
303
|
+
# unique IDs for services)
|
304
|
+
def long_hash
|
305
|
+
Digest::SHA1.hexdigest(stops.collect{|stop| stop.long_hash_element}.join)
|
306
|
+
end
|
307
|
+
|
308
|
+
# Returns an array of strings indicating the features of this train, as listed as 'Comments:' on
|
309
|
+
# the Deutsche Bahn website; e.g. "Subject to compulsory reservation", "Sleeping-car". There
|
310
|
+
# doesn't seem to be a consistent format for these.
|
311
|
+
def features
|
312
|
+
if @features.nil?
|
313
|
+
tr = (traininfo_response_doc / 'table.remarks tr').find {|tr| (tr % 'th').inner_text == 'Comments:'}
|
314
|
+
if tr
|
315
|
+
@features = (tr / 'td strong').collect {|elem| elem.inner_text}.uniq
|
316
|
+
else
|
317
|
+
@features = []
|
318
|
+
end
|
319
|
+
end
|
320
|
+
@features
|
321
|
+
end
|
322
|
+
|
323
|
+
# =====
|
324
|
+
private
|
325
|
+
# =====
|
326
|
+
def traininfo_response_doc
|
327
|
+
@traininfo_response_doc ||= Hpricot(open("http://reiseauskunft.bahn.de#{@path}"))
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Represents a stop made at a Station by a Service.
|
332
|
+
class Stop
|
333
|
+
def initialize(opts) # :nodoc:
|
334
|
+
# for the following fields, use :none to mean none supplied (as opposed to not fetched yet):
|
335
|
+
# @arrival_time, @departure_time, @platform, @arrival_time_from_origin, @departure_time_from_origin
|
336
|
+
|
337
|
+
@station = opts[:station] # required
|
338
|
+
@service = opts[:service] # required
|
339
|
+
@arrival_time = opts[:arrival_time]
|
340
|
+
@departure_time = opts[:departure_time] # arrival_time or departure_time is required
|
341
|
+
@platform = opts[:platform]
|
342
|
+
@arrival_time_from_origin = opts[:arrival_time_from_origin]
|
343
|
+
@departure_time_from_origin = opts[:departure_time_from_origin]
|
344
|
+
|
345
|
+
@inferred_time_to_destination = opts[:inferred_time_to_destination]
|
346
|
+
|
347
|
+
# these can be calculated from arrival_time_from_origin and departure_time_from_origin
|
348
|
+
# (but require service.origin, and therefore service.stops, to have been looked up)
|
349
|
+
@arrival_time_to_destination = opts[:arrival_time_to_destination]
|
350
|
+
@departure_time_to_destination = opts[:departure_time_to_destination]
|
351
|
+
end
|
352
|
+
|
353
|
+
# The Station where this stop occurs.
|
354
|
+
attr_reader :station
|
355
|
+
# The Service making this stop.
|
356
|
+
attr_reader :service
|
357
|
+
|
358
|
+
# The name of the station for this stop (provided as a shortcut for stop.station.name).
|
359
|
+
def name
|
360
|
+
@station.name
|
361
|
+
end
|
362
|
+
|
363
|
+
# The Stop object for the departure station (provided as a shortcut for stop.service.origin).
|
364
|
+
def origin
|
365
|
+
@service.origin
|
366
|
+
end
|
367
|
+
|
368
|
+
# The Stop object for the destination station (provided as a shortcut for stop.service.destination).
|
369
|
+
def destination
|
370
|
+
@service.destination
|
371
|
+
end
|
372
|
+
|
373
|
+
# Returns the platform number or name for this stop, or nil if not specified.
|
374
|
+
def platform
|
375
|
+
get_full_details if @platform.nil?
|
376
|
+
@platform == :none ? nil : @platform
|
377
|
+
end
|
378
|
+
|
379
|
+
# Returns the ClockTime at which the service arrives at this station, or nil if not specified
|
380
|
+
# (e.g. because this is the origin station for the service, or is for boarding only).
|
381
|
+
def arrival_time
|
382
|
+
get_full_details if @arrival_time.nil?
|
383
|
+
@arrival_time == :none ? nil : @arrival_time
|
384
|
+
end
|
385
|
+
|
386
|
+
# Returns the ClockTime at which the service leaves at this station, or nil if not specified
|
387
|
+
# (e.g. because this is the destination station for the service, or is for alighting only).
|
388
|
+
def departure_time
|
389
|
+
get_full_details if @departure_time.nil?
|
390
|
+
@departure_time == :none ? nil : @departure_time
|
391
|
+
end
|
392
|
+
|
393
|
+
# Returns the time between departing the origin station and arriving at this station,
|
394
|
+
# as a number of seconds, or nil if arrival time is not specified.
|
395
|
+
def arrival_time_from_origin
|
396
|
+
get_full_details if @arrival_time_from_origin.nil?
|
397
|
+
@arrival_time_from_origin == :none ? nil : @arrival_time_from_origin
|
398
|
+
end
|
399
|
+
|
400
|
+
# Returns the time between departing the origin station and departing from this station,
|
401
|
+
# as a number of seconds, or nil if departure time is not specified.
|
402
|
+
def departure_time_from_origin
|
403
|
+
get_full_details if @departure_time_from_origin.nil?
|
404
|
+
@departure_time_from_origin == :none ? nil : @departure_time_from_origin
|
405
|
+
end
|
406
|
+
|
407
|
+
# Returns the time between arriving at this station and arriving at the destination station,
|
408
|
+
# as a number of seconds, or nil if arrival time is not specified.
|
409
|
+
def arrival_time_to_destination
|
410
|
+
if @arrival_time_to_destination.nil?
|
411
|
+
if arrival_time_from_origin == :none
|
412
|
+
@arrival_time_to_destination == :none
|
413
|
+
else
|
414
|
+
@arrival_time_to_destination = @service.destination.arrival_time_from_origin - arrival_time_from_origin
|
415
|
+
end
|
416
|
+
end
|
417
|
+
@arrival_time_to_destination
|
418
|
+
end
|
419
|
+
|
420
|
+
# Returns the time between departing from this station and arriving at the destination station,
|
421
|
+
# as a number of seconds, or nil if departure time is not specified.
|
422
|
+
def departure_time_to_destination
|
423
|
+
if @departure_time_to_destination.nil?
|
424
|
+
if departure_time_from_origin == :none
|
425
|
+
@departure_time_to_destination == :none
|
426
|
+
else
|
427
|
+
@departure_time_to_destination = @service.destination.arrival_time_from_origin - departure_time_from_origin
|
428
|
+
end
|
429
|
+
end
|
430
|
+
@departure_time_to_destination
|
431
|
+
end
|
432
|
+
|
433
|
+
# Returns calculated time to destination while minimising additional hits to the Deutsche Bahn website:
|
434
|
+
# * if the service timetable has already been fetched, use that;
|
435
|
+
# * if nothing relevant has been fetched yet, fetch the service timetable and use that;
|
436
|
+
# * if only the station timetable has been fetched, use that. This may result in an inaccurate count -
|
437
|
+
# see BUGS in README.txt.
|
438
|
+
def inferred_time_to_destination
|
439
|
+
@inferred_time_to_destination ||= departure_time_to_destination || arrival_time_to_destination
|
440
|
+
end
|
441
|
+
|
442
|
+
def inspect # :nodoc:
|
443
|
+
"#<#{self.class} @time=#{(@departure_time.nil? || @departure_time == :none ? @arrival_time : @departure_time).inspect} @station=#{@station.name.inspect} @destination=#{service.destination.station.name.inspect}>"
|
444
|
+
end
|
445
|
+
|
446
|
+
# Code identifying this stop, which can form part of the hash for the Service.
|
447
|
+
# Not quite suitable as a hash for this stop in its own right, as different trains
|
448
|
+
# at the same station at the same time will have the same hash...
|
449
|
+
def subhash # :nodoc:
|
450
|
+
[@station.id, departure_time, arrival_time].hash
|
451
|
+
end
|
452
|
+
|
453
|
+
# alternative implementation, because standard Ruby hashes are probably not collision-resistant enough
|
454
|
+
def long_hash_element # :nodoc:
|
455
|
+
"[#{@station.id},#{departure_time},#{arrival_time}]"
|
456
|
+
end
|
457
|
+
|
458
|
+
# =====
|
459
|
+
private
|
460
|
+
# =====
|
461
|
+
def get_full_details
|
462
|
+
if @arrival_time and @arrival_time != :none
|
463
|
+
stop = @service.stops.find {|stop|
|
464
|
+
stop.station.id == @station.id && stop.arrival_time == @arrival_time
|
465
|
+
}
|
466
|
+
@departure_time = stop.departure_time || :none
|
467
|
+
else
|
468
|
+
stop = @service.stops.find {|stop|
|
469
|
+
stop.station.id == @station.id && stop.departure_time == @departure_time
|
470
|
+
}
|
471
|
+
@arrival_time = stop.arrival_time || :none
|
472
|
+
end
|
473
|
+
@platform = stop.platform || :none
|
474
|
+
@arrival_time_from_origin = stop.arrival_time_from_origin || :none
|
475
|
+
@departure_time_from_origin = stop.departure_time_from_origin || :none
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
data/test/test_bahn.rb
ADDED
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bahn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 1.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Matt Westcott
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-23 00:00:00 +00:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: hoe
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 47
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 8
|
33
|
+
- 0
|
34
|
+
version: 2.8.0
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
37
|
+
description: |-
|
38
|
+
A library for accessing train information from Deutsche Bahn in an
|
39
|
+
object-oriented way
|
40
|
+
email:
|
41
|
+
- matt@west.co.tt
|
42
|
+
executables: []
|
43
|
+
|
44
|
+
extensions: []
|
45
|
+
|
46
|
+
extra_rdoc_files:
|
47
|
+
- History.txt
|
48
|
+
- Manifest.txt
|
49
|
+
- README.txt
|
50
|
+
files:
|
51
|
+
- History.txt
|
52
|
+
- Manifest.txt
|
53
|
+
- README.txt
|
54
|
+
- Rakefile
|
55
|
+
- lib/bahn.rb
|
56
|
+
- test/test_bahn.rb
|
57
|
+
has_rdoc: true
|
58
|
+
homepage:
|
59
|
+
licenses: []
|
60
|
+
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options:
|
63
|
+
- --main
|
64
|
+
- README.txt
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
hash: 3
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
version: "0"
|
85
|
+
requirements: []
|
86
|
+
|
87
|
+
rubyforge_project: bahn
|
88
|
+
rubygems_version: 1.3.7
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: A library for accessing train information from Deutsche Bahn in an object-oriented way
|
92
|
+
test_files:
|
93
|
+
- test/test_bahn.rb
|