Refine conference exploration day labels

This commit is contained in:
Edward Betts 2026-08-03 14:57:06 +01:00
parent 6a601f5c14
commit 8866b78202
2 changed files with 216 additions and 1 deletions

View file

@ -6,7 +6,7 @@ import json
import os
import typing
import unicodedata
from datetime import date, datetime, timedelta, timezone
from datetime import date, datetime, time, timedelta, timezone
import flask
import pycountry
@ -22,6 +22,7 @@ COACH_CO2_KG_PER_KM = 0.027
FERRY_CO2_KG_PER_KM = 0.02254
BUS_CO2_KG_PER_KM = 0.1
CAR_CO2_KG_PER_KM = 0.218
NOON = time(12)
class Airline(typing.TypedDict, total=False):
@ -786,6 +787,58 @@ def add_coordinates_for_eurostar_home_feeder(
add_coordinate_if_missing(coordinates, coordinate_dict(from_station, "station"))
def timed_departures(item: StrDict) -> list[datetime]:
"""Return timed train/flight departure datetimes from a travel item."""
depart_values: list[typing.Any] = []
if item.get("type") in ("flight", "train"):
depart_values.append(item.get("depart"))
if item.get("type") == "train":
legs = item.get("legs", [])
if isinstance(legs, list):
depart_values.extend(
leg.get("depart") for leg in legs if isinstance(leg, dict)
)
return [value for value in depart_values if isinstance(value, datetime)]
def timed_arrivals(item: StrDict) -> list[datetime]:
"""Return timed train/flight arrival datetimes from a travel item."""
arrive_values: list[typing.Any] = []
if item.get("type") in ("flight", "train"):
arrive_values.append(item.get("arrive"))
if item.get("type") == "train":
legs = item.get("legs", [])
if isinstance(legs, list):
arrive_values.extend(
leg.get("arrive") for leg in legs if isinstance(leg, dict)
)
return [value for value in arrive_values if isinstance(value, datetime)]
def has_morning_train_or_flight_departure(trip: Trip, target_date: date) -> bool:
"""Return true if a train or flight leaves before noon on target_date."""
return any(
depart.date() == target_date and depart.time() < NOON
for item in trip.travel
for depart in timed_departures(item)
)
def has_afternoon_train_or_flight_arrival(trip: Trip, target_date: date) -> bool:
"""Return true if a train or flight arrives after noon on target_date."""
return any(
arrive.date() == target_date and arrive.time() > NOON
for item in trip.travel
for arrive in timed_arrivals(item)
)
def conference_free_days(trip: Trip) -> dict[str, tuple[int, int]]:
"""Return (days_before, days_after) exploration days for each conference.
@ -814,6 +867,18 @@ def conference_free_days(trip: Trip) -> dict[str, tuple[int, int]]:
)
days_before = (conf_attend_start(conf) - before_boundary).days
days_after = (after_boundary - conf_attend_end(conf)).days
if (
i == 0
and days_before > 0
and has_afternoon_train_or_flight_arrival(trip, before_boundary)
):
days_before -= 1
if (
i == len(sorted_confs) - 1
and days_after > 0
and has_morning_train_or_flight_departure(trip, after_boundary)
):
days_after -= 1
result[str(conf["start"])] = (days_before, days_after)
return result

View file

@ -467,3 +467,153 @@ def test_trip_title_ignores_generated_drive_stop_labels() -> None:
)
assert trip.title == "St Ives"
def test_conference_free_days_ignores_morning_train_departure_after() -> None:
"""A morning train on the day after a conference leaves no explore day."""
trip = Trip(
start=date(2027, 1, 29),
conferences=[
{
"name": "FOSDEM",
"start": date(2027, 1, 30),
"end": date(2027, 1, 31),
}
],
travel=[
{
"type": "train",
"depart": datetime(2027, 2, 1, 8, 57, tzinfo=timezone.utc),
"arrive": datetime(2027, 2, 1, 9, 57, tzinfo=timezone.utc),
"legs": [
{
"depart": datetime(2027, 2, 1, 8, 57, tzinfo=timezone.utc),
"arrive": datetime(2027, 2, 1, 9, 57, tzinfo=timezone.utc),
}
],
}
],
)
assert agenda.trip.conference_free_days(trip)["2027-01-30"] == (1, 0)
def test_conference_free_days_ignores_afternoon_train_arrival_before() -> None:
"""An afternoon train on the day before a conference leaves no explore day."""
trip = Trip(
start=date(2027, 1, 29),
conferences=[
{
"name": "FOSDEM",
"start": date(2027, 1, 30),
"end": date(2027, 1, 31),
}
],
travel=[
{
"type": "train",
"depart": datetime(2027, 1, 29, 13, 4, tzinfo=timezone.utc),
"arrive": datetime(2027, 1, 29, 16, 5, tzinfo=timezone.utc),
"legs": [
{
"depart": datetime(2027, 1, 29, 13, 4, tzinfo=timezone.utc),
"arrive": datetime(2027, 1, 29, 16, 5, tzinfo=timezone.utc),
}
],
}
],
)
assert agenda.trip.conference_free_days(trip)["2027-01-30"] == (0, 0)
def test_conference_free_days_keeps_morning_train_arrival_before() -> None:
"""A morning train arrival still leaves the afternoon to explore."""
trip = Trip(
start=date(2027, 1, 29),
conferences=[
{
"name": "FOSDEM",
"start": date(2027, 1, 30),
"end": date(2027, 1, 31),
}
],
travel=[
{
"type": "train",
"depart": datetime(2027, 1, 29, 8, 0, tzinfo=timezone.utc),
"arrive": datetime(2027, 1, 29, 11, 30, tzinfo=timezone.utc),
}
],
)
assert agenda.trip.conference_free_days(trip)["2027-01-30"] == (1, 0)
def test_conference_free_days_keeps_date_only_arrival_before() -> None:
"""Date-only travel should not be treated as a known afternoon arrival."""
trip = Trip(
start=date(2027, 1, 29),
conferences=[
{
"name": "FOSDEM",
"start": date(2027, 1, 30),
"end": date(2027, 1, 31),
}
],
travel=[
{
"type": "train",
"depart": date(2027, 1, 29),
"arrive": date(2027, 1, 29),
}
],
)
assert agenda.trip.conference_free_days(trip)["2027-01-30"] == (1, 0)
def test_conference_free_days_keeps_afternoon_train_departure_after() -> None:
"""An afternoon train still leaves the morning to explore."""
trip = Trip(
start=date(2027, 1, 29),
conferences=[
{
"name": "FOSDEM",
"start": date(2027, 1, 30),
"end": date(2027, 1, 31),
}
],
travel=[
{
"type": "train",
"depart": datetime(2027, 2, 1, 13, 4, tzinfo=timezone.utc),
"arrive": datetime(2027, 2, 1, 16, 5, tzinfo=timezone.utc),
}
],
)
assert agenda.trip.conference_free_days(trip)["2027-01-30"] == (1, 1)
def test_conference_free_days_keeps_date_only_departure_after() -> None:
"""Date-only travel should not be treated as a known morning departure."""
trip = Trip(
start=date(2027, 1, 29),
conferences=[
{
"name": "FOSDEM",
"start": date(2027, 1, 30),
"end": date(2027, 1, 31),
}
],
travel=[
{
"type": "train",
"depart": date(2027, 2, 1),
"arrive": date(2027, 2, 1),
}
],
)
assert agenda.trip.conference_free_days(trip)["2027-01-30"] == (1, 1)