#!/usr/bin/perl
use strict;
use warnings;
-use 5.10.0; # pull in say
+use 5.010; # pull in say
=head1 NAME
my $debug = 0;
my ( $help, $man );
-GetOptions( "debug" => \$debug,
- "help" => \$help,
- "man" => \$man,
- );
+GetOptions(
+ "debug" => \$debug,
+ "help" => \$help,
+ "man" => \$man,
+);
-pod2usage(1) if( $help );
-pod2usage( -verbose => 2 ) if( $man );
+pod2usage(1) if ($help);
+pod2usage( -verbose => 2 ) if ($man);
-unless( scalar( @ARGV ) == 4 ){
- pod2usage( "Invalid number of arguments passed (use --help for more details)" );
+unless ( scalar(@ARGV) == 4 ) {
+ pod2usage("Invalid number of arguments passed (use --help for more details)");
}
# Time everything
# },
my %journeys;
-
# This is the overall route
my $route = get_stops( $url, 1 );
# Add the overall route to the journeys list, otherwise if it's cheapest the
# summary doesn't have its details.
-$journeys{$route->{stops}[0]->{abbr}}->{$route->{stops}[-1]->{abbr}} = $route;
+$journeys{ $route->{stops}[0]->{abbr} }->{ $route->{stops}[-1]->{abbr} } = $route;
-debug( "Route:", Dumper( $route ) );
+debug( "Route:", Dumper($route) );
# Give us some more useful variables. Note that we need the array because we
# need the order.
# all_stops is simply a list of stops, in order
-my @all_stops = map { $_->{abbr} } @{$route->{stops}};
+my @all_stops = map { $_->{abbr} } @{ $route->{stops} };
+
# stop_details is a hash of:
# SHORTCODE => { point => 'station name',
-# arrives => 'arrival_time',
+# arrives => 'arrival_time',
# departs => 'departure_time',
# },
# to make looking things up by shortcode so much easier.
# Note that this is based on the original route.
-my %stop_details = map { $_->{abbr} => { departs => $_->{departs}, arrives => $_->{arrives}, point => $_->{point} } } @{$route->{stops}};
+my %stop_details = map {
+ $_->{abbr} => {
+ departs => $_->{departs},
+ arrives => $_->{arrives},
+ point => $_->{point}
+ }
+} @{ $route->{stops} };
debug( "Stop details:", Dumper( \%stop_details ) );
say "Cheapest option:";
my $sum = 0;
-if ( 2 == @best_route ){
+if ( 2 == @best_route ) {
say "[direct price]";
}
-for(my $i = 0; $i < $#best_route; $i++ ){
- my $journey = $journeys{$best_route[$i]}->{$best_route[$i+1]};
- my $source_station = $stop_details{$best_route[$i]}->{point};
- my $dest_station = $stop_details{$best_route[$i+1]}->{point};
- my $departs = $journey->{stops}->[0]->{departs};
- my $arrives = $journey->{stops}->[-1]->{arrives};
- printf "%s (%s) -> %s (%s): %s -> %s: £%s (%s)\n", $source_station, $best_route[$i], $dest_station, $best_route[$i+1], $departs, $arrives, $journey->{price}, $journey->{type};
+for ( my $i = 0; $i < $#best_route; $i++ ) {
+ my $journey = $journeys{ $best_route[$i] }->{ $best_route[ $i + 1 ] };
+ my $source_station = $stop_details{ $best_route[$i] }->{point};
+ my $dest_station = $stop_details{ $best_route[ $i + 1 ] }->{point};
+ my $departs = $journey->{stops}->[0]->{departs};
+ my $arrives = $journey->{stops}->[-1]->{arrives};
+ printf "%s (%s) -> %s (%s): %s -> %s: £%s (%s)\n", $source_station, $best_route[$i],
+ $dest_station, $best_route[ $i + 1 ], $departs, $arrives, $journey->{price},
+ $journey->{type};
$sum += $journey->{price};
}
-printf "Total price: £%.2f\n", $sum;
+printf "Total price: £%.2f\n", $sum;
printf "Direct price: £%.2f\n", $route->{price};
-my $saving = $route->{price} - $sum;
+my $saving = $route->{price} - $sum;
my $percent = 100 * $saving / $route->{price};
printf "Saving: £%d (%.f%%)\n", $saving, $percent;
printf "[calculated in %ds]\n", ( time - $timer );
# Get ticket price, type and stops from a URL
sub get_stops {
my ( $url, $full_journey ) = @_;
- debug( "get_stops( $url )" );
+ debug("get_stops( $url )");
- $mech->get( $url );
+ $mech->get($url);
my $tree = HTML::TreeBuilder->new_from_content( $mech->content );
my $stop_li;
foreach my $entry ( $tree->look_down('_tag', 'ul', 'class', 'results')->look_down('_tag', 'li') ){
- my ( $dep ) = $entry->look_down('_tag', 'strong')->as_text =~ m{^\s*(\d+:\d+)\s};
+ my ($dep) = $entry->look_down( '_tag', 'strong' )->as_text =~ m{^\s*(\d+:\d+)\s};
+
# If it's the full journey (ie finding the route based on a
# user-supplied time) be a little more flexible, take the first result.
- if( $full_journey or ( $dep eq ( split( m{/}, $url ) )[5] ) ){
+ if ( $full_journey or ( $dep eq ( split( m{/}, $url ) )[5] ) ) {
$stop_li = $entry;
last;
}
}
- unless( $stop_li ){
+ unless ($stop_li) {
die "Cannot find any train leaving at the requested time from $url";
}
# check if it's direct or not:
- my $change_link = $stop_li->look_down('_tag', 'a', 'class', 'change_link');
- if( $change_link ){
+ my $change_link = $stop_li->look_down( '_tag', 'a', 'class', 'change_link' );
+ if ($change_link) {
die "Error: first train returned by $url is not direct!\n";
}
my $details_url = $stop_li->look_down( '_tag', 'a', 'class', 'calling_link' )->attr('href');
# tidy up
$ticket_price =~ s{[^0-9.]}{}g;
- $ticket_type =~ s{^\s+}{};
+ $ticket_type =~ s{^\s+}{};
- debug( "Ticket type: $ticket_type" );
- debug( "Cost: $ticket_price" );
+ debug("Ticket type: $ticket_type");
+ debug("Cost: $ticket_price");
- my $stops = _parse_stopping_points( "$query_site/$details_url" );
+ my $stops = _parse_stopping_points("$query_site/$details_url");
my %return = (
- stops => $stops,
- type => $ticket_type,
- price => $ticket_price,
+ stops => $stops,
+ type => $ticket_type,
+ price => $ticket_price,
);
return \%return;
}
# Get stops from a given URL
sub _parse_stopping_points {
- my ( $url ) = @_;
+ my ($url) = @_;
my @stops;
my $mech = WWW::Mechanize->new();
- $mech->get( $url );
+ $mech->get($url);
my $tree = HTML::TreeBuilder->new_from_content( $mech->content );
# intermediate stops
my $calling_points_div = $tree->look_down( '_tag', 'div', 'class', 'calling' );
my @rows = $calling_points_div->look_down( '_tag', 'tr' );
- foreach my $row ( @rows ){
- my $point_ent = $row->look_down('_tag', 'td', 'class', 'calling-points');
- next unless( $point_ent );
+ foreach my $row (@rows) {
+ my $point_ent = $row->look_down( '_tag', 'td', 'class', 'calling-points' );
+ next unless ($point_ent);
my ( $point, $abbr ) = $point_ent->as_text() =~ m{(.*)\s\[([A-Z]+)\]};
my $arrives = $row->look_down( '_tag', 'td', 'class', 'arrives' )->as_text();
my $departs = $row->look_down( '_tag', 'td', 'class', 'departs' )->as_text();
- debug( "$point ($abbr): Arrives: $arrives; departs $departs" );
+ debug("$point ($abbr): Arrives: $arrives; departs $departs");
my %res = (
arrives => $arrives,
departs => $departs,
my $srcdest = $tree->look_down( '_tag', 'div', 'id', 'content' )->look_down('_tag', 'h2')->as_text;
my ( $departs, $arrives ) = $tree->look_down( '_tag', 'div', 'id', 'content' )->look_down('_tag', 'p')->as_text() =~ m{Leaving\sat\s(\d+:\d+),\sarriving\sat\s(\d+:\d+)}x;
- my ( $src_station, $src_abbr, $dest_station, $dest_abbr ) =
- $srcdest =~ m{\s*(.+)\s+\[([A-Z]+)\]\s*to\s*(.+)\s+\[([A-Z]+)\]};
- debug( "Source: $src_station, $src_abbr at $departs" );
- debug( "Dest: $dest_station, $dest_abbr at $arrives" );
- push @stops, { arrives => $arrives, point => $dest_station, abbr => $dest_abbr };
- unshift @stops, { departs => $departs, point => $src_station, abbr => $src_abbr };
+ my ( $src_station, $src_abbr, $dest_station, $dest_abbr )
+ = $srcdest =~ m{\s*(.+)\s+\[([A-Z]+)\]\s*to\s*(.+)\s+\[([A-Z]+)\]};
+ debug("Source: $src_station, $src_abbr at $departs");
+ debug("Dest: $dest_station, $dest_abbr at $arrives");
+ push @stops, { arrives => $arrives, point => $dest_station, abbr => $dest_abbr };
+ unshift @stops, { departs => $departs, point => $src_station, abbr => $src_abbr };
$tree = $tree->delete();
return \@stops;
# Walk a list of stops and work out their cost, then populate the graph
sub get_substops {
- my ( $stop_list ) = @_;
+ my ($stop_list) = @_;
+
+ for ( my $i = 0; $i <= $#{$stop_list}; $i++ ) {
+ for ( my $j = $i + 1; $j <= $#{$stop_list}; $j++ ) {
- for( my $i = 0; $i <= $#{$stop_list}; $i++ ){
- for( my $j = $i + 1; $j <= $#{$stop_list}; $j++ ){
# skip src-dest as we've already calculated that
- next if( 0 == $i and $#{$stop_list} == $j );
+ next if ( 0 == $i and $#{$stop_list} == $j );
my $src = $stop_list->[$i];
my $dst = $stop_list->[$j];
- debug( "checking $src -> $dst ($i -> $j)" );
+ debug("checking $src -> $dst ($i -> $j)");
+
+ my $depart = $stop_details{$src}->{departs};
+ my $url = "${query_site}/${src}/${dst}/${depart}/${date}";
+ my $journey = get_stops($url);
- my $depart = $stop_details{$src}->{departs};
- my $url = "${query_site}/${src}/${dst}/${depart}/${date}";
- my $journey = get_stops( $url );
# Sanity check departure and arrival times match up with the source journey
my $expected_depart = $stop_details{$src}->{departs};
- my $actual_depart = $journey->{stops}[0]{departs};
- unless( $expected_depart eq $actual_depart ){
+ my $actual_depart = $journey->{stops}[0]{departs};
+ unless ( $expected_depart eq $actual_depart ) {
die "Error: train $src -> $dst does not depart at the expected time! (Expected $expected_depart, actual $actual_depart)";
}
my $expected_arrive = $stop_details{$dst}->{arrives};
- my $actual_arrive = $journey->{stops}[-1]{arrives};
- unless( $expected_arrive eq $actual_arrive ){
+ my $actual_arrive = $journey->{stops}[-1]{arrives};
+ unless ( $expected_arrive eq $actual_arrive ) {
die "Error: train $src -> $dst does not arrive at the expected time! (Expected $expected_arrive, actual $actual_arrive)";
}
- $graph->add_weighted_edge($src, $dst, $journey->{price} );
+ $graph->add_weighted_edge( $src, $dst, $journey->{price} );
$journeys{$src}->{$dst} = $journey;
}
}
# Simple debugging function
sub debug {
- if( $debug ){
+ if ($debug) {
say join( "\n", @_ );
}
}