| 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 |