Add return and inbound journey support
This commit is contained in:
parent
6ba71447ef
commit
9691632f65
12 changed files with 1687 additions and 486 deletions
145
trip_planner.py
145
trip_planner.py
|
|
@ -9,10 +9,13 @@ from tfl_fare import circle_line_fare
|
|||
|
||||
MIN_CONNECTION_MINUTES = 50
|
||||
MAX_CONNECTION_MINUTES = 110
|
||||
INBOUND_MIN_CONNECTION_MINUTES = 30
|
||||
INBOUND_MAX_CONNECTION_MINUTES = 120
|
||||
DATE_FMT = "%Y-%m-%d"
|
||||
TIME_FMT = "%H:%M"
|
||||
|
||||
PAD_WALK_TO_UNDERGROUND_MINUTES = 8 # GWR platform → Paddington (H&C Line) platform
|
||||
KX_WALK_TO_UNDERGROUND_MINUTES = 10 # St Pancras arrivals → King's Cross St Pancras Underground
|
||||
|
||||
|
||||
def _parse_dt(date: str, time: str) -> datetime:
|
||||
|
|
@ -30,7 +33,7 @@ def _circle_line_services(arrive_paddington: datetime) -> list[dict]:
|
|||
earliest_board = arrive_paddington + timedelta(
|
||||
minutes=PAD_WALK_TO_UNDERGROUND_MINUTES
|
||||
)
|
||||
services = circle_line.upcoming_services(earliest_board, count=2)
|
||||
services = circle_line.upcoming_services(earliest_board, count=2, direction='pad_to_kx')
|
||||
return [
|
||||
{
|
||||
"depart": dep.strftime(TIME_FMT),
|
||||
|
|
@ -41,6 +44,21 @@ def _circle_line_services(arrive_paddington: datetime) -> list[dict]:
|
|||
]
|
||||
|
||||
|
||||
def _circle_line_services_to_paddington(arrive_st_pancras: datetime) -> list[dict]:
|
||||
earliest_board = arrive_st_pancras + timedelta(
|
||||
minutes=KX_WALK_TO_UNDERGROUND_MINUTES
|
||||
)
|
||||
services = circle_line.upcoming_services(earliest_board, count=2, direction='kx_to_pad')
|
||||
return [
|
||||
{
|
||||
"depart": dep.strftime(TIME_FMT),
|
||||
"arrive_pad": arr.strftime(TIME_FMT),
|
||||
"fare": circle_line_fare(dep),
|
||||
}
|
||||
for dep, arr in services
|
||||
]
|
||||
|
||||
|
||||
def _fmt_duration(minutes: int) -> str:
|
||||
h, m = divmod(minutes, 60)
|
||||
if h and m:
|
||||
|
|
@ -80,6 +98,37 @@ def _is_viable_connection(
|
|||
return dep_bri, arr_pad, dep_stp, arr_dest
|
||||
|
||||
|
||||
def _is_viable_inbound_connection(
|
||||
eurostar: dict,
|
||||
gwr: dict,
|
||||
travel_date: str,
|
||||
min_connection_minutes: int,
|
||||
max_connection_minutes: int,
|
||||
) -> tuple[datetime, datetime, datetime, datetime] | None:
|
||||
try:
|
||||
dep_dest = _parse_dt(travel_date, eurostar["depart_destination"])
|
||||
arr_stp = _parse_dt(travel_date, eurostar["arrive_st_pancras"])
|
||||
dep_pad = _parse_dt(travel_date, gwr["depart_paddington"])
|
||||
arr_station = _parse_dt(travel_date, gwr["arrive_destination"])
|
||||
except (ValueError, KeyError):
|
||||
return None
|
||||
|
||||
if arr_stp < dep_dest:
|
||||
arr_stp += timedelta(days=1)
|
||||
if dep_pad < arr_stp:
|
||||
dep_pad += timedelta(days=1)
|
||||
if arr_station < dep_pad:
|
||||
arr_station += timedelta(days=1)
|
||||
|
||||
connection_minutes = (dep_pad - arr_stp).total_seconds() / 60
|
||||
if connection_minutes < min_connection_minutes:
|
||||
return None
|
||||
if connection_minutes > max_connection_minutes:
|
||||
return None
|
||||
|
||||
return dep_dest, arr_stp, dep_pad, arr_station
|
||||
|
||||
|
||||
def combine_trips(
|
||||
gwr_trains: list[dict],
|
||||
eurostar_trains: list[dict],
|
||||
|
|
@ -154,6 +203,68 @@ def combine_trips(
|
|||
return trips
|
||||
|
||||
|
||||
def combine_inbound_trips(
|
||||
eurostar_trains: list[dict],
|
||||
gwr_trains: list[dict],
|
||||
travel_date: str,
|
||||
min_connection_minutes: int = INBOUND_MIN_CONNECTION_MINUTES,
|
||||
max_connection_minutes: int = INBOUND_MAX_CONNECTION_MINUTES,
|
||||
gwr_fares: dict | None = None,
|
||||
) -> list[dict]:
|
||||
"""Return valid continent→UK combined trips."""
|
||||
trips = []
|
||||
|
||||
for es in eurostar_trains:
|
||||
for gwr in gwr_trains:
|
||||
connection = _is_viable_inbound_connection(
|
||||
es,
|
||||
gwr,
|
||||
travel_date,
|
||||
min_connection_minutes,
|
||||
max_connection_minutes,
|
||||
)
|
||||
if not connection:
|
||||
continue
|
||||
dep_dest, arr_stp, dep_pad, arr_station = connection
|
||||
total_mins = int((arr_station - dep_dest).total_seconds() / 60)
|
||||
# Destination time is CET/CEST, arrival at London is GMT/BST.
|
||||
eurostar_mins = int((arr_stp - dep_dest).total_seconds() / 60) + 60
|
||||
fare = (gwr_fares or {}).get(gwr["depart_paddington"])
|
||||
circle_svcs = _circle_line_services_to_paddington(arr_stp)
|
||||
trips.append(
|
||||
{
|
||||
"direction": "inbound",
|
||||
"depart_destination": es["depart_destination"],
|
||||
"check_in_by": (dep_dest - timedelta(minutes=30)).strftime(TIME_FMT),
|
||||
"arrive_st_pancras": es["arrive_st_pancras"],
|
||||
"depart_paddington": gwr["depart_paddington"],
|
||||
"arrive_uk_station": gwr["arrive_destination"],
|
||||
"arrive_platform": gwr.get("arrive_platform", ""),
|
||||
"headcode": gwr.get("headcode", ""),
|
||||
"gwr_duration": _fmt_duration(
|
||||
int((arr_station - dep_pad).total_seconds() / 60)
|
||||
),
|
||||
"connection_minutes": int((dep_pad - arr_stp).total_seconds() / 60),
|
||||
"connection_duration": _fmt_duration(
|
||||
int((dep_pad - arr_stp).total_seconds() / 60)
|
||||
),
|
||||
"circle_services": circle_svcs,
|
||||
"eurostar_duration": _fmt_duration(eurostar_mins),
|
||||
"train_number": es.get("train_number", ""),
|
||||
"total_duration": _fmt_duration(total_mins),
|
||||
"total_minutes": total_mins,
|
||||
"destination": es["destination"],
|
||||
"ticket_name": fare["ticket"] if fare else None,
|
||||
"ticket_price": fare["price"] if fare else None,
|
||||
"ticket_code": fare["code"] if fare else None,
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
trips.sort(key=lambda t: (t["depart_destination"], t["depart_paddington"]))
|
||||
return trips
|
||||
|
||||
|
||||
def find_unreachable_morning_eurostars(
|
||||
gwr_trains: list[dict],
|
||||
eurostar_trains: list[dict],
|
||||
|
|
@ -184,3 +295,35 @@ def find_unreachable_morning_eurostars(
|
|||
unreachable.append({**es, "eurostar_duration": _fmt_duration(eurostar_mins)})
|
||||
|
||||
return sorted(unreachable, key=lambda s: s["depart_st_pancras"])
|
||||
|
||||
|
||||
def find_unreachable_inbound_eurostars(
|
||||
eurostar_trains: list[dict],
|
||||
gwr_trains: list[dict],
|
||||
travel_date: str,
|
||||
min_connection_minutes: int = INBOUND_MIN_CONNECTION_MINUTES,
|
||||
max_connection_minutes: int = INBOUND_MAX_CONNECTION_MINUTES,
|
||||
) -> list[dict]:
|
||||
unreachable = []
|
||||
|
||||
for es in eurostar_trains:
|
||||
if any(
|
||||
_is_viable_inbound_connection(
|
||||
es,
|
||||
gwr,
|
||||
travel_date,
|
||||
min_connection_minutes,
|
||||
max_connection_minutes,
|
||||
)
|
||||
for gwr in gwr_trains
|
||||
):
|
||||
continue
|
||||
|
||||
dep_dest = _parse_dt(travel_date, es["depart_destination"])
|
||||
arr_stp = _parse_dt(travel_date, es["arrive_st_pancras"])
|
||||
if arr_stp < dep_dest:
|
||||
arr_stp += timedelta(days=1)
|
||||
eurostar_mins = int((arr_stp - dep_dest).total_seconds() / 60) + 60
|
||||
unreachable.append({**es, "eurostar_duration": _fmt_duration(eurostar_mins)})
|
||||
|
||||
return sorted(unreachable, key=lambda s: s["depart_destination"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue