From 60674fe663bad689f5f7fb5445fdc06cf1e5ed86 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 4 Apr 2026 12:58:19 +0100 Subject: [PATCH] Add Circle Line timetable info. --- templates/results.html | 1 + trip_planner.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/templates/results.html b/templates/results.html index 7a7259f..5426995 100644 --- a/templates/results.html +++ b/templates/results.html @@ -150,6 +150,7 @@ {{ row.connection_duration }}{% if row.connection_minutes < 80 %} ⚠️{% endif %} +
Circle {{ row.circle_line_depart }} → STP {{ row.circle_arrive_checkin }} {{ row.depart_st_pancras }} diff --git a/trip_planner.py b/trip_planner.py index c7e3286..e732b93 100644 --- a/trip_planner.py +++ b/trip_planner.py @@ -9,6 +9,12 @@ MAX_GWR_MINUTES = 110 DATE_FMT = '%Y-%m-%d' TIME_FMT = '%H:%M' +# Circle Line: Paddington (H&C) → Kings Cross St Pancras +CIRCLE_LINE_MINUTES_PAST = [8, 18, 28, 38, 48, 58] # departures past each hour +CIRCLE_LINE_JOURNEY_MINUTES = 11 +PAD_WALK_TO_UNDERGROUND_MINUTES = 8 # GWR platform → Paddington Underground +KX_WALK_TO_CHECKIN_MINUTES = 10 # Kings Cross St Pancras platform → St Pancras check-in + # Bristol Temple Meads → London Paddington walk-on single fares. # Weekday restrictions (Mon–Fri only): @@ -45,6 +51,28 @@ def _parse_dt(date: str, time: str) -> datetime: return datetime.strptime(f"{date} {time}", f"{DATE_FMT} {TIME_FMT}") +def _next_circle_line(arrive_paddington: datetime) -> tuple[datetime, datetime]: + """ + Given GWR arrival at Paddington, return (circle_line_depart, arrive_checkin). + + Walk 10 min to Paddington Underground, catch next Circle Line at :08/:18/:28/:38/:48/:58, + 11 min journey to Kings Cross St Pancras, 10 min walk to St Pancras check-in. + """ + earliest_board = arrive_paddington + timedelta(minutes=PAD_WALK_TO_UNDERGROUND_MINUTES) + minute = earliest_board.minute + depart_minute = next((m for m in CIRCLE_LINE_MINUTES_PAST if m >= minute), None) + if depart_minute is None: + circle_depart = (earliest_board + timedelta(hours=1)).replace( + minute=CIRCLE_LINE_MINUTES_PAST[0], second=0, microsecond=0 + ) + else: + circle_depart = earliest_board.replace(minute=depart_minute, second=0, microsecond=0) + arrive_checkin = circle_depart + timedelta( + minutes=CIRCLE_LINE_JOURNEY_MINUTES + KX_WALK_TO_CHECKIN_MINUTES + ) + return circle_depart, arrive_checkin + + def _fmt_duration(minutes: int) -> str: h, m = divmod(minutes, 60) if h and m: @@ -122,6 +150,7 @@ def combine_trips( total_mins = int((arr_dest - dep_bri).total_seconds() / 60) ticket = cheapest_gwr_ticket(gwr['depart_bristol'], travel_date) + circle_depart, arrive_checkin = _next_circle_line(arr_pad) trips.append({ 'depart_bristol': gwr['depart_bristol'], 'arrive_paddington': gwr['arrive_paddington'], @@ -129,6 +158,8 @@ def combine_trips( 'gwr_duration': _fmt_duration(int((arr_pad - dep_bri).total_seconds() / 60)), 'connection_minutes': int((dep_stp - arr_pad).total_seconds() / 60), 'connection_duration': _fmt_duration(int((dep_stp - arr_pad).total_seconds() / 60)), + 'circle_line_depart': circle_depart.strftime(TIME_FMT), + 'circle_arrive_checkin': arrive_checkin.strftime(TIME_FMT), 'depart_st_pancras': es['depart_st_pancras'], 'arrive_destination': es['arrive_destination'], 'train_number': es.get('train_number', ''),