Refine conference exploration day labels
This commit is contained in:
parent
6a601f5c14
commit
8866b78202
2 changed files with 216 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue