tube 0.2.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.
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