File Coverage

blib/lib/WWW/NationalRail.pm
Criterion Covered Total %
statement 25 93 26.8
branch 0 40 0.0
condition 0 12 0.0
subroutine 8 10 80.0
pod 1 1 100.0
total 34 156 21.7


line stmt bran cond sub pod time code
1             package WWW::NationalRail;
2              
3 2     2   83549 use warnings;
  2         6  
  2         62  
4 2     2   9 use strict;
  2         4  
  2         64  
5 2     2   43 use 5.006_001;
  2         10  
  2         68  
6 2     2   10 use Carp;
  2         3  
  2         143  
7 2     2   2084 use HTML::TableExtract;
  2         41967  
  2         18  
8 2     2   4455 use WWW::Mechanize;
  2         564333  
  2         82  
9 2     2   26 use base qw(Class::Accessor);
  2         3  
  2         2259  
10              
11             our $VERSION = "0.1";
12              
13             __PACKAGE__->mk_accessors(qw(from to via out_date out_type out_hour out_minute
14             ret_date ret_type ret_hour ret_minute));
15             __PACKAGE__->mk_ro_accessors(qw(outward_summary return_summary
16             outward_detail return_detail error));
17              
18             my $url = "http://www.nationalrail.co.uk/planmyjourney/";
19              
20             sub search {
21 1     1 1 2897 my $self = shift;
22 1         14 my $mech = new WWW::Mechanize;
23 1         360913 delete $self->{error}; # reset error
24 1         7 $mech->get($url);
25              
26             # National Rail use odd field names
27 0           $mech->field( "0", $self->{from});
28 0           $mech->field( "1", $self->{to});
29 0           $mech->field( "11", $self->{out_date});
30 0 0         $mech->field( "14", $self->{ret_date}) if $self->{ret_date};
31 0           $mech->field( "3", $self->{via});
32 0 0         $mech->field( "9", $self->{out_type}) if $self->{out_type};
33 0 0         $mech->field( "outHourField", sprintf "%02d", $self->{out_hour})
34             if defined $self->{out_hour};
35 0 0         $mech->field( "outMinuteField", sprintf "%02d", $self->{out_minute})
36             if defined $self->{out_minute};
37 0 0         $mech->field( "retHourField", sprintf "%02d", $self->{ret_hour})
38             if defined $self->{ret_hour};
39 0 0         $mech->field( "retMinuteField", sprintf "%02d", $self->{ret_minute})
40             if defined $self->{ret_minute};
41              
42 0           $mech->submit_form();
43              
44 0 0         if ($mech->content =~ m!(.*)!) {
45 0           $self->{error} = "National Rail error: $1";
46 0           return;
47             }
48 0 0         if ($mech->content =~ m!
You searched for (.*)
!) {
49 0           $self->{error} = "Unknown station: $1";
50 0           return;
51             }
52              
53             # At this point we are looking at a page that says "We are getting the
54             # train times for the journey you have requested". Click the link. If we
55             # get the same page click it again.
56 0           while ($mech->find_link(url => "Display_Timetable.asp")) {
57 0           $mech->follow_link(url => "Display_Timetable.asp");
58             }
59              
60 0           $self->{_summary} = $mech->content;
61 0           $self->_parseSummary();
62 0           $mech->follow_link(url => "Matrix_Journey_Details.asp");
63 0           $self->{_detail} = $mech->content;
64 0           $self->_parseDetail();
65 0           return 1;
66             }
67              
68             sub _parseSummary {
69 0     0     my $self = shift;
70 0           my $te = new HTML::TableExtract( depth => 2 );
71 0           $te->parse($self->{_summary});
72 0           my @directions = qw(outward return);
73 0           my @fields = qw(depart arrive changes duration);
74              
75 0           foreach my $ts ($te->table_states) {
76             # check if we have already seen both directions
77 0 0         my $direction = shift @directions or carp "summary parse error";
78 0           my $summary;
79 0           my $row_num = 0;
80 0           foreach my $row ($ts->rows) { # one field per row
81 0 0 0       defined $row->[1] and $row->[1] ne "" or next; # careful of '0's
82 0           my $field = lc shift @$row;
83             # check field name is as expected
84 0 0         if ($fields[$row_num] ne $field) {
85             # don't worry if single journey and we are looking for a return
86             # that isn't there
87 0 0 0       return if $row_num == 0 and $direction eq "return";
88 0           carp "summary parse error";
89             }
90 0           $summary->[$_]{$field} = $row->[$_] for 0..$#$row; # transpose
91 0           $row_num++;
92             }
93 0 0         @$summary or carp "summary parse error";
94 0           $self->{$direction . "_summary"} = $summary;
95             }
96             }
97              
98             sub _parseDetail {
99 0     0     my $self = shift;
100 0           my $te = new HTML::TableExtract( );
101 0           $te->parse($self->{_detail});
102 0           my $direction;
103 0           foreach my $ts ($te->table_states) {
104 0 0         if ($ts->depth eq 3) {
105 0 0         (($ts->rows)[0]->[0]) =~ /^(.*) Journey: /
106             or carp "direction not found";
107 0           $direction = lc $1;
108             }
109 0 0 0       next if $ts->depth != 2 or $ts->rows == 1;
110 0 0         $direction or carp "direction not found";
111 0           my $journey = {};
112 0           my @legs;
113 0           foreach my $row ($ts->rows) {
114 0 0 0       next if not $row->[0] or $row->[0] eq "Station";
115 0 0         if ($row->[0] =~ /DURATION: ([0-9:]+)/) {
116 0           $journey->{duration} = $1;
117             } else {
118 0           my %leg;
119 0           @leg{qw(station arrive depart travelby operator)} = map {
120 0           s/[^a-zA-Z0-9: ]//g;
121 0 0         $_ ne "" ? $_ : undef; # careful of '0's
122             } @$row;
123 0           push @legs, \%leg;
124             }
125             }
126 0           $journey->{legs} = \@legs;
127 0           push @{$self->{$direction . "_detail"}}, $journey;
  0            
128             }
129             }
130              
131             1;
132              
133             =head1 NAME
134              
135             WWW::NationalRail - Perl interface to the UK rail timetable
136              
137             =head1 SYNOPSIS
138              
139             use WWW::NationalRail;
140              
141             my $rail = WWW::NationalRail->new({
142             from => "London",
143             to => "Cambridge",
144             out_date => "18/12/05",
145             out_type => "depart",
146             out_hour => 9,
147             out_minute => 0,
148             ret_date => "18/12/05",
149             ret_type => "depart",
150             ret_hour => 17,
151             ret_minute => 0,
152             });
153              
154             $rail->search or die $rail->error();
155              
156             my $os = $rail->outward_summary; # array reference
157             my $rs = $rail->return_summary;
158              
159             $os->[0]{depart} # "09:06"
160             $os->[0]{arrive} # "10:25"
161             $os->[0]{changes} # "0"
162             $os->[0]{duration} # "1:19"
163              
164             my $od = $rail->outward_detail;
165             my $rd = $rail->return_detail;
166              
167             $od->[0]->{duration}; # "1:19"
168              
169             my $legs = $od->[0]{legs} # array reference
170              
171             $legs->[0]{station} # "LONDON KINGS CROSS"
172             $legs->[0]{arrive} # undef
173             $legs->[0]{depart} # "09:06"
174             $legs->[0]{travelby} # "Train"
175             $legs->[0]{operator} # "WAGN RAIL"
176              
177             $rail->ret_hour(19); # change search parameters
178             $rail->search(); # and search again
179              
180             =head1 DESCRIPTION
181              
182             WWW::NationalRail is a Perl interface to the UK national rail timetable at
183             http://www.nationalrail.co.uk/planmyjourney/
184              
185             =over 4
186              
187             =item new()
188              
189             The constructor accepts the arguments for the search as a has
190             reference. The from and to fields are required, the rest are optional and will
191             use a National Rail supplied default.
192              
193             =over 11
194              
195             =item from
196              
197             Departure station.
198              
199             =item to
200              
201             Destination station.
202              
203             =item via
204              
205             Via station.
206              
207             =item out_date
208              
209             Outbound date in the format "DD/MM/YY". Defaults to today.
210              
211             =item out_type
212              
213             Possible values are "depart" to search by outbound departure time or
214             "arrive" to search by outbound arrival time. Defaults to "depart".
215              
216             =item out_hour
217              
218             Outbound hour, 0 to 23. Defaults to sometime in the near future.
219              
220             =item out_minute
221              
222             Outbound minute, 0 to 59. Defaults to sometime in the near future.
223              
224             =item ret_date
225              
226             Return date in the format "DD/MM/YY". Leave blank for one-way.
227              
228             =item ret_type
229              
230             Similar to out_type. Either "depart or "arrive". Defaults to "depart".
231              
232             =item ret_hour
233              
234             Return hour, 0 to 23. Leave blank for one-way.
235              
236             =item ret_minute
237              
238             Return minute, 0 to 59. Leave blank for one-way.
239              
240             =back
241              
242             =item search()
243              
244             Object method to run the search and parse the results.
245              
246             =item outbound_summary() and return_summary()
247              
248             Each returns a reference to an array of hashes.
249             For journeys in one direction return_summary() will be undef.
250             The hash representing a summary has four fields:
251              
252             =over 9
253              
254             =item depart
255              
256             Time of departure.
257              
258             =item arrive
259              
260             Time of arrival.
261              
262             =item changes
263              
264             Number of changes.
265              
266             =item duration
267              
268             Duration of the journey.
269              
270             =back
271              
272             =item outbound_details() and return_details()
273              
274             Each returns a reference to an array of hashes.
275             For journeys in one direction return_details() will be undef.
276             The hash representing a journey has four two fields:
277              
278             =over 9
279              
280             =item legs
281              
282             Reference to an array of hashes.
283              
284             =item duration
285              
286             Duration of the journey.
287              
288             =back
289              
290             The legs hash has four five fields:
291              
292             =over 9
293              
294             =item station
295              
296             Name of the station.
297              
298             =item arrive
299              
300             Time of arrival at this station, undef for the first leg.
301              
302             =item depart
303              
304             Time of departure from this station, undef for the last leg.
305              
306             =item travelby
307              
308             Means of transport, will usually be train, but could also be foot,
309             coach, or tube.
310              
311             =item operator
312              
313             The train operating company.
314              
315             =back
316              
317             =back
318              
319             =head1 AUTHOR
320              
321             Edward Betts, C<< >>
322              
323             =head1 COPYRIGHT AND LICENSE
324              
325             Copyright (C) 2005 by Edward Betts
326              
327             This library is free software; you can redistribute it and/or modify
328             it under the same terms as Perl itself, either Perl version 5.8.4 or,
329             at your option, any later version of Perl 5 you may have available.
330              
331             =cut