tube 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.txt ADDED
@@ -0,0 +1,36 @@
1
+ = Tube/Status
2
+
3
+ A simple MIT licensed Ruby library to access the status of the London Underground network as displayed on the Transport for London {Live travel news}[http://www.tfl.gov.uk/tfl/livetravelnews/realtime/tube/default.html] page.
4
+
5
+ == Installation
6
+
7
+ $ gem install tube --source http://gemcutter.org
8
+
9
+ == Examples
10
+ require 'tube/status'
11
+
12
+ status = Tube::Status.new
13
+
14
+ broken_lines = status.lines.select {|line| line.problem?}
15
+ broken_lines.collect {|line| line.name}
16
+ #=> ["Circle", "District", "Jubilee", "Metropolitan", "Northern"]
17
+
18
+ status.lines.detect {|line| line.id == :circle}.message
19
+ #=> "Saturday 7 and Sunday 8 March, suspended."
20
+
21
+ closed_stations = status.station_groups["Closed stations"]
22
+ closed_stations.collect {|station| station.name}
23
+ #=> ["Blackfriars", "Hatton Cross"]
24
+
25
+ stations = status.station_groups.values.flatten
26
+ stations.detect {|station| station.name =~ "hatton"}.message
27
+ #=> "Saturday 7 and Sunday 8 March, closed."
28
+
29
+ status.updated.strftime("%I:%M%p")
30
+ #=> "04:56PM"
31
+
32
+ status.reload
33
+ status.updated.strftime("%I:%M%p")
34
+ #=> "05:00PM"
35
+
36
+ See the {documentation}[http://sourcetagsandcodes.com/codes/tube_status/doc/] for more details.
data/lib/tube/line.rb ADDED
@@ -0,0 +1,24 @@
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 :name, :status, :problem, :message
9
+ alias problem? problem
10
+
11
+ # :call-seq: Line.new(id, name, status, problem, message=nil)
12
+ #
13
+ # Create a new Line.
14
+ #
15
+ def initialize( id, name, status, problem, message=nil )
16
+ @id = id
17
+ @name = name
18
+ @status = status
19
+ @problem = problem
20
+ @message = message
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,20 @@
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)
11
+ #
12
+ # Create a new Station.
13
+ #
14
+ def initialize( name, message )
15
+ @name = name
16
+ @message = message
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,84 @@
1
+ require 'open-uri'
2
+
3
+ require "#{File.dirname( __FILE__ )}/status_parser"
4
+ require "#{File.dirname( __FILE__ )}/line"
5
+ require "#{File.dirname( __FILE__ )}/station"
6
+
7
+ module Tube # :nodoc:
8
+
9
+ # Models the status of the London Underground network as displayed on
10
+ # http://www.tfl.gov.uk/tfl/livetravelnews/realtime/tube/default.html.
11
+ #
12
+ # It is a very thin abstraction over the tfl website, as a result the access
13
+ # to data on stations is somewhat different to lines due to the differing
14
+ # presentation.
15
+ # However it is very dynamic, for example should the East London line return
16
+ # it will show up in the lines array automatically.
17
+ #
18
+ # ==Example Usage
19
+ # require 'tube/status'
20
+ #
21
+ # status = Tube::Status.new
22
+ #
23
+ # broken_lines = status.lines.select {|line| line.problem?}
24
+ # broken_lines.collect {|line| line.name}
25
+ # #=> ["Circle", "District", "Jubilee", "Metropolitan", "Northern"]
26
+ #
27
+ # status.lines.detect {|line| line.id == :circle}.message
28
+ # #=> "Saturday 7 and Sunday 8 March, suspended."
29
+ #
30
+ # closed_stations = status.station_groups["Closed stations"]
31
+ # closed_stations.collect {|station| station.name}
32
+ # #=> ["Blackfriars", "Hatton Cross"]
33
+ #
34
+ # stations = status.station_groups.values.flatten
35
+ # stations.detect {|station| station.name =~ "hatton"}.message
36
+ # #=> "Saturday 7 and Sunday 8 March, closed."
37
+ #
38
+ # status.updated.strftime("%I:%M%p")
39
+ # #=> "04:56PM"
40
+ #
41
+ # status.reload
42
+ # status.updated.strftime("%I:%M%p")
43
+ # #=> "05:00PM"
44
+ #
45
+ class Status
46
+ attr_reader :updated, :lines, :station_groups
47
+
48
+ # :call-seq: Status.new -> status
49
+ #
50
+ # Request and parse the status of the London Underground network from the
51
+ # tfl.gov.uk "Live travel news" page.
52
+ #
53
+ def initialize( url=
54
+ "http://www.tfl.gov.uk/tfl/livetravelnews/realtime/tube/default.html" )
55
+ results = Tube::StatusParser.parse( open( url ) )
56
+ @updated = results[:updated]
57
+
58
+ @lines = results[:lines].map do |line|
59
+ id = line[:html_class].to_sym
60
+ name = line[:name]
61
+ status = line[:status][:headline]
62
+ problem = line[:status][:problem]
63
+ message = line[:status][:message]
64
+
65
+ Line.new( id, name, status, problem, message )
66
+ end
67
+
68
+ @station_groups = results[:station_groups].inject( {} ) do |memo, group|
69
+ stations = group[:stations].map do |station|
70
+ Station.new( station[:name], station[:message] )
71
+ end
72
+
73
+ memo[group[:name]] = stations
74
+ memo
75
+ end
76
+
77
+ self
78
+ end
79
+
80
+ alias reload initialize
81
+ public :reload
82
+
83
+ end
84
+ end
@@ -0,0 +1,135 @@
1
+ require 'time'
2
+ require 'date'
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+
6
+ module Tube # :nodoc:
7
+ module StatusParser # :nodoc:
8
+ extend self
9
+
10
+ def parse( html_doc )
11
+ service_board = Hpricot( html_doc ).at( "#service-board" )
12
+
13
+ updated_element = service_board.previous_sibling.children.first
14
+ updated = parse_updated( updated_element )
15
+
16
+ lines = service_board.search( "dl#lines dt" ).map do |line_element|
17
+ parse_line( line_element )
18
+ end
19
+
20
+ station_group_elements = service_board.search( "dl#stations dt" )
21
+ station_groups = station_group_elements.map do |station_group_element|
22
+ parse_station_group( station_group_element )
23
+ end
24
+
25
+ {:updated => updated, :lines => lines, :station_groups => station_groups}
26
+ end
27
+
28
+ def parse_updated( updated_element )
29
+ time_text = updated_element.inner_text.match( /(\d?\d:\d\d(a|p)m)/ )[0]
30
+ time_zone = if is_bst? then "+0100" else "+0000" end
31
+
32
+ Time.parse( "#{time_text} #{time_zone}" )
33
+ end
34
+
35
+ def parse_line( line_element )
36
+ name = line_element.inner_text.strip
37
+ html_class = line_element.attributes["class"]
38
+ status = parse_status( line_element.next_sibling )
39
+
40
+ {:name => name, :html_class => html_class, :status => status}
41
+ end
42
+
43
+ def parse_status( status_element )
44
+ header = status_element.at( "h3" )
45
+
46
+ if header
47
+ headline = header.inner_text.strip
48
+ message = parse_status_message( status_element.search( "div.message p" ) )
49
+ else
50
+ headline = status_element.inner_text.strip
51
+ end
52
+ problem = status_element.attributes["class"] == "problem"
53
+
54
+ {:headline => headline, :problem => problem, :message => message}
55
+ end
56
+
57
+ def parse_station_group( station_group_element )
58
+ name = station_group_element.inner_text.strip
59
+ stations = []
60
+
61
+ station_element = station_group_element
62
+ while station_element = station_element.next_sibling
63
+ if station_element.to_html =~ /^<dd/
64
+ stations.push( parse_station( station_element ) )
65
+ elsif station_element.to_html =~ /^<dt/
66
+ break
67
+ end
68
+ end
69
+
70
+ {:name => name, :stations => stations}
71
+ end
72
+
73
+ def parse_station( station_element )
74
+ name = station_element.at( "h3" ).inner_text.strip
75
+ message = parse_status_message( station_element.search("div.message p") )
76
+
77
+ {:name => name, :message => message}
78
+ end
79
+
80
+ def parse_status_message( messages )
81
+ text_messages = messages.map do |message|
82
+ if message.children
83
+ message.children.select {|child| child.text?}.join( " " )
84
+ end
85
+ end.compact
86
+ text_messages.reject! {|m| m.empty?}
87
+
88
+ text_messages.map {|m| m.gsub( /\s+/, " " ).strip}.join( "\n" )
89
+ end
90
+
91
+ private
92
+
93
+ # :call-seq: is_bst? -> bool
94
+ #
95
+ # Is British Summer Time currently in effect.
96
+ #
97
+ def is_bst?
98
+ bst_start = last_sunday_of_month( "march" )
99
+ bst_end = last_sunday_of_month( "october" )
100
+
101
+ one_hour = 3600
102
+ bst_start = Time.gm( bst_start.year, bst_start.month ) + one_hour
103
+ bst_end = Time.gm( bst_end.year, bst_end.month ) + one_hour
104
+
105
+ (bst_start..bst_end).include?( Time.now.getgm )
106
+ end
107
+
108
+ # :call-seq: last_sunday_of_month(month_name) -> date
109
+ #
110
+ def last_sunday_of_month( month )
111
+ start_of_next_month = Date.parse( next_month_name( month ) )
112
+
113
+ week_day = start_of_next_month.wday
114
+
115
+ distance_from_sunday = if week_day == 0 then 7 else week_day end
116
+ start_of_next_month - distance_from_sunday
117
+ end
118
+
119
+ # :call-seq: next_month_name(month_name) -> string
120
+ #
121
+ def next_month_name( month )
122
+ index = Date::MONTHNAMES.index( month.capitalize )
123
+ index ||= ABBR_MONTHNAMES.index( month.capitalize )
124
+
125
+ index += 1
126
+
127
+ if index >= 12
128
+ index = 1
129
+ end
130
+
131
+ Date::MONTHNAMES[index]
132
+ end
133
+
134
+ end
135
+ end
data/test/dummy.html ADDED
@@ -0,0 +1,71 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <title>Service update now &#124; Transport for London</title>
6
+ <!-- Transport for London website template version 1.7 -->
7
+
8
+ <meta http-equiv="content-type" content="text/html;charset=utf-8" />
9
+ </head>
10
+ <body class="template-6">
11
+ <!-- <lm>Sun, 15 Nov 2009 12:12:20 GMT</lm> -->
12
+
13
+ <div id="container" class="live-travel-news">
14
+ <div id="main-content">
15
+ <h1>Live travel news</h1>
16
+
17
+ <h2>Service update at 12:12pm <a href="later.html" id="works_later" class="alert" name="works_later">View engineering works planned for <strong>later today</strong></a></h2>
18
+ <div id="service-board">
19
+ <dl id="lines">
20
+ <dt class="central">Central</dt>
21
+ <dd>Good service</dd>
22
+ <dt class="district">District</dt>
23
+ <dd class="problem">
24
+ <h3>Part closure</h3>
25
+ <div class="message">
26
+ <p>Rail replacement bus</p>
27
+ <p>Service A: details...</p>
28
+ <p><a href="/transform">See how we are transforming the Tube</a></p>
29
+ </div>
30
+ </dd>
31
+ <dt class="waterlooandcity">Waterloo &amp; City</dt>
32
+ <dd class="problem">
33
+ <h3>Planned closure</h3>
34
+ <div class="message">
35
+ <p>Closed Sunday.</p>
36
+ <p><a href="/transform">See how we are transforming the Tube</a></p>
37
+ </div>
38
+ </dd>
39
+ </dl>
40
+ <dl id="stations">
41
+ <dt>Closed stations</dt>
42
+ <dd>
43
+ <h3>Bank</h3>
44
+ <div class="message">
45
+ <p>Closed due to excessive noise.</p>
46
+ <p><a href="/transform">See how we are transforming the Tube</a></p>
47
+ </div>
48
+ </dd>
49
+ <dd>
50
+ <h3>Holborn</h3>
51
+ <div class="message">
52
+ <p>Closed due to fire investigation.</p>
53
+ <p>Go to Chancery Lane or Tottenham Court Road instead.</p>
54
+ <p><a href="/transform">See how we are transforming the Tube</a></p>
55
+ </div>
56
+ </dd>
57
+
58
+ <dt>Station maintenance</dt>
59
+ <dd>
60
+ <h3>Elephant &amp; Castle</h3>
61
+ <div class="message">
62
+ <p>Reduced lift service.</p>
63
+ <p><a href="/transform">See how we are transforming the Tube</a></p>
64
+ </div>
65
+ </dd>
66
+ </dl>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </body>
71
+ </html>
@@ -0,0 +1,140 @@
1
+ require "test/unit"
2
+
3
+ require "#{File.dirname( __FILE__ )}/../lib/tube/status"
4
+
5
+ class TestStatusParser < Test::Unit::TestCase
6
+ def test_parse_updated
7
+ document = Hpricot("<h2>Service update at 6:26pm</h2>")
8
+ element = document.at("h2")
9
+ result = Tube::StatusParser.parse_updated(element)
10
+
11
+ assert_equal(Time.parse("6:26pm"), result)
12
+ end
13
+
14
+ def test_parse_updated_with_anchor_in_element
15
+ document = Hpricot(%Q{<h2>Service update at 12:12pm <a href="/later.html">View engineering works planned for later today</a></h2>})
16
+ element = document.at("h2")
17
+ result = Tube::StatusParser.parse_updated(element)
18
+
19
+ assert_equal(Time.parse("12:12pm"), result)
20
+ end
21
+
22
+ def test_parse_line
23
+ document = Hpricot(%Q{<dt class="central">Central</dt> <dd>Good service</dd>})
24
+ element = document.at("dt")
25
+ result = Tube::StatusParser.parse_line(element)
26
+
27
+ assert_equal("Central", result[:name])
28
+ assert_equal("central", result[:html_class])
29
+ assert_equal("Good service", result[:status][:headline])
30
+ end
31
+
32
+ def test_parse_line_with_complex_name
33
+ document = Hpricot(%Q{<dt class="waterlooandcity">Waterloo &amp; City</dt> <dd></dd>})
34
+ element = document.at("dt")
35
+ result = Tube::StatusParser.parse_line(element)
36
+
37
+ assert_equal("Waterloo & City", result[:name])
38
+ assert_equal("waterlooandcity", result[:html_class])
39
+ end
40
+
41
+ def test_parse_status
42
+ document = Hpricot("<dd>Good service</dd>")
43
+ element = document.at("dd")
44
+ result = Tube::StatusParser.parse_status(element)
45
+
46
+ assert_equal("Good service", result[:headline])
47
+ end
48
+
49
+ def test_parse_status_with_problem
50
+ document = Hpricot(%Q{<dd class="problem">Part suspended</dd>})
51
+ element = document.at("dd")
52
+ result = Tube::StatusParser.parse_status(element)
53
+
54
+ assert_equal(true, result[:problem])
55
+ end
56
+
57
+ def test_parse_status_with_header
58
+ document = Hpricot(%Q{<dd><h3>Part suspended</h3></dd>})
59
+ element = document.at("dd")
60
+ result = Tube::StatusParser.parse_status(element)
61
+
62
+ assert_equal("Part suspended", result[:headline])
63
+ end
64
+
65
+ def test_parse_status_with_message
66
+ document = Hpricot(%Q{<dd class="problem"><h3>Part closure</h3><div class="message"><p>engineering works, etc...</p></div></dd>})
67
+ element = document.at("dd")
68
+ result = Tube::StatusParser.parse_status(element)
69
+
70
+ assert_equal("Part closure", result[:headline])
71
+ assert_equal(true, result[:problem])
72
+ assert_equal("engineering works, etc...", result[:message])
73
+ end
74
+
75
+ def test_parse_status_message
76
+ document = Hpricot(%Q{<div><p>engineering works, etc...</p></div>})
77
+ elements = document.search("div p")
78
+ result = Tube::StatusParser.parse_status_message(elements)
79
+
80
+ assert_equal("engineering works, etc...", result)
81
+ end
82
+
83
+ def test_parse_status_message_with_multi_paragraph_message
84
+ document = Hpricot(%Q{<div><p>Rail replacement bus</p><p>Service A: details...</p></div>})
85
+ elements = document.search("div p")
86
+ result = Tube::StatusParser.parse_status_message(elements)
87
+
88
+ assert_equal("Rail replacement bus\nService A: details...", result)
89
+ end
90
+
91
+ def test_parse_status_message_removes_anchor_from_message
92
+ document = Hpricot(%Q{<div><p>Closed Sunday.</p><p><a href="/transform">See how we are transforming the Tube</a></p></div>})
93
+ elements = document.search("div p")
94
+ result = Tube::StatusParser.parse_status_message(elements)
95
+
96
+ assert_equal("Closed Sunday.", result)
97
+ end
98
+
99
+ def test_parse_station_group
100
+ document = Hpricot(%Q{<dt>Closed stations</dt>
101
+ <dd><h3>Bank</h3><div class="message"><p>Closed due to excessive noise.</p></div></dd>
102
+ <dd><h3>Holborn</h3><div class="message"><p>Closed due to fire investigation.</p></div></dd>})
103
+ element = document.at("dt")
104
+ result = Tube::StatusParser.parse_station_group(element)
105
+
106
+ assert_equal("Closed stations", result[:name])
107
+ assert_equal("Bank", result[:stations].first[:name])
108
+ assert_equal("Holborn", result[:stations].last[:name])
109
+ end
110
+
111
+ def test_parse_station
112
+ document = Hpricot(%Q{<dd><h3>Bank</h3><div class="message"><p>Closed due to excessive noise.</p></div></dd>})
113
+ element = document.at("dd")
114
+ result = Tube::StatusParser.parse_station(element)
115
+
116
+ assert_equal("Bank", result[:name])
117
+ # This seriously happend once.
118
+ assert_equal("Closed due to excessive noise.", result[:message])
119
+ end
120
+
121
+ def test_parse
122
+ # the file used here is an approximation of the most important bits of the
123
+ # Live travel news at http://www.tfl.gov.uk/tfl/livetravelnews/realtime/tube/default.html
124
+ document = open("#{File.dirname( __FILE__ )}/dummy.html")
125
+ result = Tube::StatusParser.parse(document)
126
+
127
+ assert(result)
128
+ assert_equal(Time.parse("12:12pm"), result[:updated])
129
+ assert_equal(3, result[:lines].length)
130
+ assert_equal("Central", result[:lines].first[:name])
131
+ assert_equal("Closed Sunday.", result[:lines].last[:status][:message])
132
+ assert_equal(2, result[:station_groups].length)
133
+ assert_equal(2, result[:station_groups].first[:stations].length)
134
+ assert_equal(1, result[:station_groups].last[:stations].length)
135
+ assert_equal("Closed stations", result[:station_groups].first[:name])
136
+ assert_equal("Elephant & Castle", result[:station_groups].last[:stations].first[:name])
137
+
138
+ assert_equal({:updated=>Time.parse("12:12pm"), :station_groups=>[{:stations=>[{:message=>"Closed due to excessive noise.", :name=>"Bank"}, {:message=>"Closed due to fire investigation.\nGo to Chancery Lane or Tottenham Court Road instead.", :name=>"Holborn"}], :name=>"Closed stations"}, {:stations=>[{:message=>"Reduced lift service.", :name=>"Elephant & Castle"}], :name=>"Station maintenance"}], :lines=>[{:status=>{:problem=>false, :message=>nil, :headline=>"Good service"}, :html_class=>"central", :name=>"Central"}, {:status=>{:problem=>true, :message=>"Rail replacement bus\nService A: details...", :headline=>"Part closure"}, :html_class=>"district", :name=>"District"}, {:status=>{:problem=>true, :message=>"Closed Sunday.", :headline=>"Planned closure"}, :html_class=>"waterlooandcity", :name=>"Waterloo & City"}]}, result)
139
+ end
140
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tube
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Sadler
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-15 00:00:00 +00: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
+ description: A simple Ruby library to access the status of the London Underground network.
26
+ email: mat@sourcetagsandcodes.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README.txt
33
+ files:
34
+ - lib/tube/line.rb
35
+ - lib/tube/station.rb
36
+ - lib/tube/status.rb
37
+ - lib/tube/status_parser.rb
38
+ - test/dummy.html
39
+ - test/status_parser_test.rb
40
+ - README.txt
41
+ has_rdoc: true
42
+ homepage: http://github.com/matsadler/tube
43
+ licenses: []
44
+
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --main
48
+ - README.txt
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 1.3.5
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: Access the status of the London Underground network.
70
+ test_files:
71
+ - test/status_parser_test.rb