matsadler-tube 0.1.0

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.
@@ -0,0 +1,72 @@
1
+ module Tube # :nodoc:
2
+
3
+ # Models the data gathered on a tube line from the tfl.gov.uk "Live travel
4
+ # news" page.
5
+ #
6
+ class Line
7
+ attr_reader :id
8
+ attr_accessor :status, :problem, :message, :name
9
+ alias_method :problem?, :problem
10
+
11
+ # :call-seq: Line.new(id, status, problem=false, message=nil, name=nil)
12
+ #
13
+ # Create a new Line. If name is ommited it will be set to id.
14
+ #
15
+ def initialize( id, status, problem=false, message=nil, name=nil )
16
+ @id = id
17
+ @status = status
18
+ @problem = problem
19
+ @message = message
20
+ @name = name || @id
21
+ end
22
+
23
+ # :call-seq: line.to_hash -> hash
24
+ #
25
+ # Returns a hash representation of the object.
26
+ # {"id" => "central", "status" => "Good service", "problem" => false,
27
+ # "message" => nil, "name" => "Central"}
28
+ #
29
+ #
30
+ def to_hash
31
+ instance_variables.inject( {} ) do |memo, var|
32
+ memo.merge( {var[1..-1] => instance_variable_get( var )} )
33
+ end
34
+ end
35
+
36
+ # :call-seq: line.to_json -> string
37
+ #
38
+ # Returns a string of JSON representing the object.
39
+ # '{"id":"central", "status":"Good service", "problem":false,
40
+ # "message":null, "name":"Central"}'
41
+ #
42
+ def to_json( *args )
43
+ to_hash.to_json( *args )
44
+ end
45
+
46
+ # :call-seq: line.to_xml -> string
47
+ # line.to_xml(false) -> rexml_document.
48
+ #
49
+ # Returns a string of XML representing the object.
50
+ # <line>
51
+ # <id>central</id>
52
+ # <status>Good service</status>
53
+ # <problem>false</problem>
54
+ # <message/>
55
+ # <name>Central</name>
56
+ # </line>
57
+ #
58
+ # Alternately pass false as the only argument to get an instance of
59
+ # REXML::Document.
60
+ #
61
+ def to_xml( as_string=true )
62
+ doc = REXML::Document.new
63
+ root = doc.add_element( "line" )
64
+ instance_variables.each do |var|
65
+ el = root.add_element( var[1..-1] )
66
+ el.add_text( instance_variable_get( var ).to_s )
67
+ end
68
+ if as_string then doc.to_s else doc end
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,28 @@
1
+ module Tube # :nodoc:
2
+
3
+ # Really just an array with an appropriate #to_xml method.
4
+ #
5
+ class LineGroup < Array
6
+
7
+ # :call-seq: line_group.to_xml -> string
8
+ # line_group.to_xml(false) -> rexml_document.
9
+ #
10
+ # Returns a string of XML representing the object.
11
+ # <lines>
12
+ # contents of the array as xml...
13
+ # </lines>
14
+ #
15
+ # Alternately pass false as the only argument to get an instance of
16
+ # REXML::Document.
17
+ #
18
+ def to_xml( as_string=true )
19
+ doc = REXML::Document.new
20
+ root = doc.add_element( "lines" )
21
+ each do |e|
22
+ root.add_element( e.to_xml( false ) )
23
+ end
24
+ if as_string then doc.to_s else doc end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,61 @@
1
+ module Tube # :nodoc:
2
+
3
+ # Models the data gathered on a tube station from the tfl.gov.uk "Live travel
4
+ # news" page.
5
+ #
6
+ class Station
7
+ attr_reader :name
8
+ attr_accessor :message
9
+
10
+ # :call-seq: Station.new(name, message=nil)
11
+ #
12
+ # Create a new Station.
13
+ #
14
+ def initialize( name, message=nil )
15
+ @name = name
16
+ @message = message
17
+ end
18
+
19
+ # :call-seq: station.to_hash -> hash
20
+ #
21
+ # Returns a hash representation of the object.
22
+ # {"name" => "Bank", "message" => "Undergoing escalator refurbishment."}
23
+ #
24
+ def to_hash
25
+ instance_variables.inject( {} ) do |memo, var|
26
+ memo.merge( {var[1..-1] => instance_variable_get( var )} )
27
+ end
28
+ end
29
+
30
+ # :call-seq: station.to_json -> string
31
+ #
32
+ # Returns a string of JSON representing the object.
33
+ # '{"name":"Bank", "message":"Undergoing escalator refurbishment."}'
34
+ #
35
+ def to_json( *args )
36
+ to_hash.to_json( *args )
37
+ end
38
+
39
+ # :call-seq: station.to_xml -> string
40
+ # station.to_xml(false) -> rexml_document.
41
+ #
42
+ # Returns a string of XML representing the object.
43
+ # <station>
44
+ # <name>Bank</name>
45
+ # <message>Undergoing escalator refurbishment.</message>
46
+ # </station>
47
+ #
48
+ # Alternately pass false as the only argument to get an instance of
49
+ # REXML::Document.
50
+ #
51
+ def to_xml( as_string=true )
52
+ doc = REXML::Document.new
53
+ root = doc.add_element( "station" )
54
+ instance_variables.each do |var|
55
+ root.add_element( var[1..-1] ).add_text( instance_variable_get( var ) )
56
+ end
57
+ if as_string then doc.to_s else doc end
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,65 @@
1
+ module Tube # :nodoc:
2
+
3
+ # Really just an array that can have a name, plus appropriate #to_json and
4
+ # #to_xml methods.
5
+ #
6
+ class StationGroup < Array
7
+ attr_reader :name
8
+
9
+ # :call-seq: StationGroup.new(name, size=0, obj=nil)
10
+ # StationGroup.new(name, array)
11
+ # StationGroup.new(name, size) {|index| block }
12
+ #
13
+ # See Array.new.
14
+ #
15
+ def initialize( name, *args )
16
+ @name = name
17
+ super( *args )
18
+ end
19
+
20
+ # :call-seq: station_group.to_hash -> hash
21
+ #
22
+ # Returns a hash representation of the object. Also calls #to_hash on its
23
+ # contents.
24
+ # {"Closed stations" => [array contents...]}
25
+ #
26
+ def to_hash
27
+ {@name => to_a.map {|e| e.to_hash}}
28
+ end
29
+
30
+ # :call-seq: station_group.to_json -> string
31
+ #
32
+ # Returns a string of JSON representing the object.
33
+ # '{"Closed stations":[array contents...]}'
34
+ #
35
+ def to_json( *args )
36
+ to_hash.to_json( *args )
37
+ end
38
+
39
+ # :call-seq: station_group.to_xml -> string
40
+ # station_group.to_xml(false) -> rexml_document.
41
+ #
42
+ # Returns a string of XML representing the object.
43
+ # <station_group>
44
+ # <name>Closed stations</name>
45
+ # <stations>
46
+ # contents of the array as xml...
47
+ # </stations>
48
+ # </station_group>
49
+ #
50
+ # Alternately pass false as the only argument to get an instance of
51
+ # REXML::Document.
52
+ #
53
+ def to_xml( as_string=true )
54
+ doc = REXML::Document.new
55
+ root = doc.add_element( "station_group" )
56
+ root.add_element( "name" ).add_text( name )
57
+ stations = root.add_element( "stations" )
58
+ each do |e|
59
+ stations.add_element( e.to_xml( false ) )
60
+ end
61
+ if as_string then doc.to_s else doc end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,62 @@
1
+ module Tube # :nodoc:
2
+
3
+ # Really just an array with appropriate #to_json and #to_xml methods.
4
+ #
5
+ class StationGroupGroup < Array
6
+
7
+ # :call-seq: station_group_group.to_hash -> hash
8
+ #
9
+ # Returns a hash representation of the object.
10
+ # Calls #to_hash on its contents (which should be station groups) and then
11
+ # merges those hashes. The end results should look something like:
12
+ # {"Closed stations" => [station group contents...],
13
+ # "Station maintenance" => [station group contents...]}
14
+ #
15
+ def to_hash
16
+ inject( {} ) do |memo, e|
17
+ memo.merge( e.to_hash )
18
+ end
19
+ end
20
+
21
+ # :call-seq: station_group_group.to_json -> string
22
+ #
23
+ # Returns a string of JSON representing the object.
24
+ # '{"Closed stations": [station group contents...],
25
+ # "Station maintenance": [station group contents...]}'
26
+ #
27
+ def to_json( *args )
28
+ to_hash.to_json( *args )
29
+ end
30
+
31
+ # :call-seq: station_group_group.to_xml -> string
32
+ # station_group_group.to_xml(false) -> rexml_document.
33
+ #
34
+ # Returns a string of XML representing the object.
35
+ # <station_groups>
36
+ # <station_group>
37
+ # <name>Closed stations</name>
38
+ # <stations>
39
+ # contents of the station group as xml...
40
+ # </stations>
41
+ # </station_group>
42
+ # <station_group>
43
+ # <name>Station maintenance</name>
44
+ # <stations>
45
+ # contents of the station group as xml...
46
+ # </stations>
47
+ # </station_group>
48
+ # </station_groups>
49
+ #
50
+ # Alternately pass false as the only argument to get an instance of
51
+ # REXML::Document.
52
+ #
53
+ def to_xml( as_string=true )
54
+ doc = REXML::Document.new
55
+ root = doc.add_element( "station_groups" )
56
+ each do |e|
57
+ root.add_element( e.to_xml( false ) )
58
+ end
59
+ if as_string then doc.to_s else doc end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,317 @@
1
+ module Tube # :nodoc:
2
+
3
+ # Models the status of the London Underground network as displayed on
4
+ # http://www.tfl.gov.uk/tfl/livetravelnews/realtime/tube/default.html.
5
+ #
6
+ # It is a very thin abstraction over the tfl website, as a result the access
7
+ # to data on stations is somewhat different to lines due to the differing
8
+ # presentation.
9
+ # However it is very dynamic, for example should the East London line return
10
+ # it will show up in the lines array automatically.
11
+ #
12
+ # ==Example Usage
13
+ # require 'tube/status'
14
+ #
15
+ # status = Tube::Status.get
16
+ #
17
+ # broken_lines = status.lines.select {|line| line.problem?}
18
+ # broken_lines.collect {|line| line.name}
19
+ # #=> ["Circle", "District", "Jubilee", "Metropolitan", "Northern"]
20
+ #
21
+ # status.line(:circle).message
22
+ # #=> "Saturday 7 and Sunday 8 March, suspended."
23
+ #
24
+ # closed_stations = status.station_group(:closed)
25
+ # closed_stations.collect {|station| station.name}
26
+ # #=> ["Blackfriars", "Hatton Cross"]
27
+ #
28
+ # status.find_station("hatton").message
29
+ # #=> "Saturday 7 and Sunday 8 March, closed."
30
+ #
31
+ # status.updated.strftime("%I:%M%p")
32
+ # #=> "04:56PM"
33
+ #
34
+ # status.reload
35
+ # status.updated.strftime("%I:%M%p")
36
+ # #=> "05:00PM"
37
+ #
38
+ # ==Converting to JSON and XML
39
+ # All objects come complete with #to_json and #to_xml methods. These depend
40
+ # upon the ruby json and REXML libraries.
41
+ #
42
+ # ===XML
43
+ # status.line(:central).to_xml
44
+ # #=> "<line><id>central</id><status>Good service</status>
45
+ # <problem>false</problem><message/><name>Central</name></line>"
46
+ #
47
+ # ===JSON
48
+ # status.line(:central).to_json
49
+ # #=> '{"id":"central", "status":"Good service", "problem":false,
50
+ # "message":null, "name":"Central"}'
51
+ #
52
+ class Status
53
+ attr_reader :updated, :lines, :station_groups
54
+
55
+ # :call-seq: Status.get -> status
56
+ # Status.new -> status
57
+ #
58
+ # Request and parse the status of the London Underground network from the
59
+ # tfl.gov.uk "Live travel news" page.
60
+ #
61
+ def initialize( url=
62
+ "http://www.tfl.gov.uk/tfl/livetravelnews/realtime/tube/default.html" )
63
+
64
+ @url = url
65
+ reload
66
+ end
67
+
68
+ class << self
69
+ alias get new
70
+ end
71
+
72
+ # :call-seq: status.reload -> status
73
+ #
74
+ # Re-request the latest status and reload all data.
75
+ #
76
+ def reload
77
+ doc = Hpricot( open( @url ) )
78
+
79
+ time_el = doc.at( "div#service-board" ).previous_sibling.children.first
80
+ time_text = time_el.inner_text.match( /(\d?\d:\d\d(a|p)m)/ )[0]
81
+ time_zone = if is_bst? then "+0100" else "+0000" end
82
+ @updated = Time.parse( "#{time_text} #{time_zone}" )
83
+
84
+ lines = doc.search( "dl#lines dt" ).map do |el|
85
+ id = el.attributes["class"]
86
+ name = el.inner_text.strip
87
+
88
+ status = el.next_sibling
89
+ if status_el = status.at( "h3" )
90
+ status_text = status_el.inner_text.strip
91
+ message = format_messages( status.search( "div.message p" ) )
92
+ else
93
+ status_text = status.inner_text.strip
94
+ end
95
+ problem = status.attributes["class"] == "problem"
96
+
97
+ Line.new( id, status_text, problem, message, name )
98
+ end
99
+ @lines = LineGroup.new( lines )
100
+
101
+ @station_groups = StationGroupGroup.new
102
+ doc.search( "dl#stations dt" ).each do |el|
103
+ station_group = StationGroup.new( el.inner_text.strip )
104
+ while el = el.next_sibling
105
+ if el.to_html =~ /^<dd/ && name_el = el.at( "h3" )
106
+ name = name_el.inner_text.strip
107
+ message = format_messages( el.search( "div.message p" ) )
108
+ station = Station.new( name, message )
109
+ station_group.push( station )
110
+ elsif el.to_html =~ /^<dt/
111
+ break
112
+ end
113
+ end
114
+ @station_groups.push( station_group )
115
+ end
116
+
117
+ self
118
+ end
119
+
120
+ # :call-seq: status.line(string) -> line or nil
121
+ # status.line(symbol) -> line or nil
122
+ # status.line(regexp) -> line or nil
123
+ #
124
+ # Get a single Line object. Passing a string will do a fuzzy match on the
125
+ # line id, a symbol will have to match the line id exactly.
126
+ #
127
+ # status.line(:hammersmithandcity) #=>\
128
+ #<Tube::Line:0x1163964 @id="hammersmithandcity", @name="H'smith & City"...
129
+ # status.line("waterloo") #=>\
130
+ #<Tube::Line:0x113d700 @id="waterlooandcity", @name="Waterloo & City"...
131
+ # status.line("east london") #=> nil # no longer exists
132
+ #
133
+ def line( name )
134
+ if name.is_a?( String )
135
+ name = name.gsub( /\s/, "" )
136
+ name.gsub!( /&/, "and" )
137
+ name.gsub!( /'/, ".+" )
138
+ name = Regexp.new( name, true )
139
+ elsif name.is_a?( Symbol )
140
+ name = /^#{name}$/
141
+ end
142
+ @lines.detect {|line| line.id =~ name}
143
+ end
144
+
145
+ # :call-seq: status.find_station(string) -> station or nil
146
+ # status.find_station(symbol) -> station or nil
147
+ # status.find_station(regexp) -> station or nil
148
+ #
149
+ # Get a single Station object. See #find_stations for more details. If more
150
+ # than one station is found the one with the shortest name will be returned.
151
+ #
152
+ def find_station( name )
153
+ results = find_stations( name )
154
+
155
+ if results.length == 1
156
+ results.first
157
+ else
158
+ results.sort do |a,b|
159
+ a.name.gsub(/\(.+\)/, "").length <=> b.name.gsub(/\(.+\)/, "").length
160
+ end.first
161
+ end
162
+ end
163
+
164
+ # :call-seq: status.find_stations(string) -> array
165
+ # status.find_stations(symbol) -> array
166
+ # status.find_stations(regexp) -> array
167
+ # status.find_stations(array) -> array
168
+ # status.find_stations -> array
169
+ #
170
+ # Get all Station objects matching the argument. Passing a string will do a
171
+ # fuzzy match on the station name, a symbol will have to match the station
172
+ # name exactly, an array will return all matches (without duplicates) for
173
+ # the contents of the array.
174
+ #
175
+ # With no arguments simply returns all stations.
176
+ #
177
+ # status.find_stations("acton") \
178
+ #=> [#<Tube::Station:0x110d7e4 @message="Closed.", @name="East Acton">]
179
+ # status.find_stations(:Bank) #=> [] # No problems at Bank
180
+ # status.find_stations("tower bridge") #=> [] # No station by that name
181
+ #
182
+ def find_stations( name=nil )
183
+ stations = @station_groups.flatten
184
+
185
+ case name
186
+ when String
187
+ # this could be simplified once I'm more confident about the format of
188
+ # station names
189
+ name = name.gsub( /\s/, "\\s*" )
190
+ name.gsub!( /(and|&)/, "(and|&)" )
191
+ name = Regexp.new( name, true )
192
+ stations = stations.select {|station| station.name =~ name}
193
+ when Regexp
194
+ stations = stations.select {|station| station.name =~ name}
195
+ when Symbol
196
+ stations = stations.select {|station| station.name == name.to_s}
197
+ when Array
198
+ stations = name.map {|n| search_stations( n )}.flatten.uniq
199
+ when nil
200
+ stations
201
+ end
202
+ end
203
+
204
+ # :call-seq: status.station_group(string) -> station_group or nil
205
+ # status.station_group(symbol) -> station_group or nil
206
+ # status.station_group(regexp) -> station_group or nil
207
+ #
208
+ # Get a single StationGroup object. Both string and symbol are fuzzy
209
+ # matches.
210
+ #
211
+ # It appears the two groups are "Closed stations" and "Station maintenance"
212
+ #
213
+ # status.station_group(:closed) #=> [stations...]
214
+ # status.station_group(:maintenance) #=> [stations...]
215
+ #
216
+ def station_group( name )
217
+ if name.is_a?( String ) || name.is_a?( Symbol )
218
+ name = Regexp.new( name.to_s, true )
219
+ end
220
+ @station_groups.detect {|group| group.name =~ name}
221
+ end
222
+
223
+ # :call-seq: status.to_hash -> hash
224
+ #
225
+ # Returns a hash representation of the object.
226
+ # {"stations" => {station groups...},
227
+ # "lines" => [lines...],
228
+ # "updated" => Fri Feb 13 23:31:30 +0000 2009}
229
+ #
230
+ def to_hash
231
+ {"updated" => updated,
232
+ "lines" => lines.map {|e| e.to_hash},
233
+ "stations" => station_groups.to_hash}
234
+ end
235
+
236
+ # :call-seq: status.to_json -> string
237
+ #
238
+ # Returns a string of JSON representing the object.
239
+ # '{"stations":{stations...},
240
+ # "lines":[lines...],
241
+ # "updated":"Fri Feb 13 23:31:30 +0000 2009"}'
242
+ #
243
+ def to_json( *args )
244
+ to_hash.to_json( *args )
245
+ end
246
+
247
+ # :call-seq: status.to_xml -> string
248
+ # status.to_xml(false) -> rexml_document
249
+ #
250
+ # Returns a string of XML representing the object.
251
+ # <status>
252
+ # <updated>Fri Feb 13 23:31:30 +0000 2009</updated>
253
+ # lines as xml...
254
+ # station_groups as xml...
255
+ # </status>
256
+ #
257
+ # Alternately pass false as the only argument to get an instance of
258
+ # REXML::Document.
259
+ #
260
+ def to_xml( as_string=true )
261
+ doc = REXML::Document.new
262
+ root = doc.add_element( "status" )
263
+ root.add_element( "updated" ).add_text( updated.to_s )
264
+ root.add_element( lines.to_xml( false ) )
265
+ root.add_element( station_groups.to_xml( false ) )
266
+ if as_string then doc.to_s else doc end
267
+ end
268
+
269
+ private
270
+
271
+ # :call-seq: format_messages(messages) -> string
272
+ #
273
+ # Takes an array of elements, strips those elements of elements they contain
274
+ # and returns only the text within the root elements.
275
+ # <p>Suspended. Rail replacement bus services operate.</p>
276
+ # <p>Service A: ...</p>
277
+ # <p><a href="/transform">See how we are transforming the Tube</a></p>
278
+ # becomes
279
+ # Suspended. Rail replacement bus services operate.
280
+ # Service A: ...
281
+ #
282
+ def format_messages( messages )
283
+ text_messages = messages.map do |message|
284
+ if message.children
285
+ message.children.select {|child| child.text?}.join( " " )
286
+ end
287
+ end.compact
288
+ text_messages.reject! {|m| m.empty?}
289
+ text_messages.map {|m| m.gsub( /\s+/, " " ).strip}.join( "\n" )
290
+ end
291
+
292
+ # :call-seq: is_bst? -> bool
293
+ #
294
+ # Is British Summer Time currently in effect.
295
+ #
296
+ def is_bst?
297
+ bst_start = last_sunday_of_preceding_month( "april" )
298
+ bst_end = last_sunday_of_preceding_month( "november" )
299
+
300
+ one_hour = 3600
301
+ bst_start = Time.gm(bst_start.year, bst_start.month) + one_hour
302
+ bst_end = Time.gm(bst_end.year, bst_end.month) + one_hour
303
+
304
+ (bst_start..bst_end).include?(Time.now.getgm)
305
+ end
306
+
307
+ # :call-seq: last_sunday_of_preceding_month(month_name) -> date
308
+ #
309
+ def last_sunday_of_preceding_month( month_name )
310
+ start_of_preceding_month = Date.parse( month_name )
311
+ week_day = start_of_preceding_month.wday
312
+ distance_from_sunday = if week_day == 0 then 7 else week_day end
313
+ start_of_preceding_month - distance_from_sunday
314
+ end
315
+
316
+ end
317
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+ require 'open-uri'
4
+ require 'time'
5
+ require 'json'
6
+ require 'rexml/document'
7
+
8
+ require 'line'
9
+ require 'station'
10
+ require 'line_group'
11
+ require 'station_group'
12
+ require 'station_group_group'
13
+ require 'status'
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: matsadler-tube
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mat Sadler
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-24 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hpricot
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.8.1
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: json
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: A simple Ruby library to access the status of the London Underground network.
36
+ email: mat@sourcetagsandcodes.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - lib/line.rb
45
+ - lib/line_group.rb
46
+ - lib/station.rb
47
+ - lib/station_group.rb
48
+ - lib/station_group_group.rb
49
+ - lib/status.rb
50
+ - lib/tube.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/matsadler/tube
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --line-numbers
56
+ - --inline-source
57
+ - --title
58
+ - Tube
59
+ - --main
60
+ - Tube::Status
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.2.0
79
+ signing_key:
80
+ specification_version: 2
81
+ summary: A simple Ruby library to access the status of the London Underground network.
82
+ test_files: []
83
+