diff --git a/agenda/trip.py b/agenda/trip.py index b0c50cf..6d34c6c 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -3,6 +3,7 @@ import decimal import os import typing +from collections import defaultdict from datetime import date, datetime, time from zoneinfo import ZoneInfo @@ -377,3 +378,34 @@ def get_coordinates_and_routes( route["geojson"] = read_geojson(data_dir, route.pop("geojson_filename")) return (coordinates, routes) + + +def calculate_yearly_stats(trips: list[Trip]) -> dict[int, StrDict]: + """Calculate total distance and distance by transport type grouped by year.""" + yearly_stats: defaultdict[int, StrDict] = defaultdict(dict) + for trip in trips: + year = trip.start.year + dist = trip.total_distance() + yearly_stats[year].setdefault("count", 0) + yearly_stats[year]["count"] += 1 + if dist: + yearly_stats[year]["total_distance"] = ( + yearly_stats[year].get("total_distance", 0) + trip.total_distance() + ) + for transport_type, distance in trip.distances_by_transport_type(): + yearly_stats[year].setdefault("distances_by_transport_type", {}) + yearly_stats[year]["distances_by_transport_type"][transport_type] = ( + yearly_stats[year]["distances_by_transport_type"].get(transport_type, 0) + + distance + ) + for country in trip.countries: + yearly_stats[year].setdefault("countries", set()) + yearly_stats[year]["countries"].add(country) + for leg in trip.travel: + if leg["type"] == "flight": + yearly_stats[year].setdefault("flight_count", 0) + yearly_stats[year]["flight_count"] += 1 + if leg["type"] == "train": + yearly_stats[year].setdefault("train_count", 0) + yearly_stats[year]["train_count"] += 1 + return dict(yearly_stats) diff --git a/agenda/utils.py b/agenda/utils.py index 0324df3..1b03fc3 100644 --- a/agenda/utils.py +++ b/agenda/utils.py @@ -1,23 +1,56 @@ """Utility functions.""" -import datetime +from datetime import date, datetime, timezone -DateOrDateTime = datetime.datetime | datetime.date +DateOrDateTime = datetime | date -def as_date(d: DateOrDateTime) -> datetime.date: +def as_date(d: DateOrDateTime) -> date: """Convert datetime to date.""" - if isinstance(d, datetime.datetime): + if isinstance(d, datetime): return d.date() - assert isinstance(d, datetime.date) + assert isinstance(d, date) return d -def as_datetime(d: DateOrDateTime) -> datetime.datetime: +def as_datetime(d: DateOrDateTime) -> datetime: """Date/time of event.""" - t0 = datetime.datetime.min.time() + t0 = datetime.min.time() return ( d - if isinstance(d, datetime.datetime) - else datetime.datetime.combine(d, t0).replace(tzinfo=datetime.timezone.utc) + if isinstance(d, datetime) + else datetime.combine(d, t0).replace(tzinfo=timezone.utc) ) + + +def human_readable_delta(future_date: date) -> str | None: + """ + Calculate the human-readable time delta for a given future date. + + Args: + future_date (date): The future date as a datetime.date object. + + Returns: + str: Human-readable time delta. + """ + # Ensure the input is a future date + if future_date <= date.today(): + return None + + # Calculate the delta + delta = future_date - date.today() + + # Convert delta to a more human-readable format + months, days = divmod(delta.days, 30) + weeks, days = divmod(days, 7) + + # Formatting the output + parts = [] + if months > 0: + parts.append(f"{months} months") + if weeks > 0: + parts.append(f"{weeks} weeks") + if days > 0: + parts.append(f"{days} days") + + return " ".join(parts) if parts else None diff --git a/web_view.py b/web_view.py index c3e663a..3fb1087 100755 --- a/web_view.py +++ b/web_view.py @@ -23,6 +23,7 @@ import agenda.fx import agenda.holidays import agenda.thespacedevs import agenda.trip +import agenda.utils from agenda import calendar, format_list_with_ampersand, travel, uk_tz from agenda.types import StrDict, Trip @@ -487,39 +488,6 @@ def trip_list_text() -> str: ) -def human_readable_delta(future_date: date) -> str | None: - """ - Calculate the human-readable time delta for a given future date. - - Args: - future_date (date): The future date as a datetime.date object. - - Returns: - str: Human-readable time delta. - """ - # Ensure the input is a future date - if future_date <= date.today(): - return None - - # Calculate the delta - delta = future_date - date.today() - - # Convert delta to a more human-readable format - months, days = divmod(delta.days, 30) - weeks, days = divmod(days, 7) - - # Formatting the output - parts = [] - if months > 0: - parts.append(f"{months} months") - if weeks > 0: - parts.append(f"{weeks} weeks") - if days > 0: - parts.append(f"{days} days") - - return " ".join(parts) if parts else None - - def get_prev_current_and_next_trip( start: str, trip_list: list[Trip] ) -> tuple[Trip | None, Trip | None, Trip | None]: @@ -582,7 +550,7 @@ def trip_page(start: str) -> str: get_country=agenda.get_country, format_list_with_ampersand=format_list_with_ampersand, holidays=holidays, - human_readable_delta=human_readable_delta, + euman_readable_delta=agenda.utils.human_readable_delta, ) @@ -614,37 +582,6 @@ def birthday_list() -> str: return flask.render_template("birthday_list.html", items=items, today=today) -def calculate_yearly_stats(trips: list[Trip]) -> dict[int, StrDict]: - """Calculate total distance and distance by transport type grouped by year.""" - yearly_stats: defaultdict[int, StrDict] = defaultdict(dict) - for trip in trips: - year = trip.start.year - dist = trip.total_distance() - yearly_stats[year].setdefault("count", 0) - yearly_stats[year]["count"] += 1 - if dist: - yearly_stats[year]["total_distance"] = ( - yearly_stats[year].get("total_distance", 0) + trip.total_distance() - ) - for transport_type, distance in trip.distances_by_transport_type(): - yearly_stats[year].setdefault("distances_by_transport_type", {}) - yearly_stats[year]["distances_by_transport_type"][transport_type] = ( - yearly_stats[year]["distances_by_transport_type"].get(transport_type, 0) - + distance - ) - for country in trip.countries: - yearly_stats[year].setdefault("countries", set()) - yearly_stats[year]["countries"].add(country) - for leg in trip.travel: - if leg["type"] == "flight": - yearly_stats[year].setdefault("flight_count", 0) - yearly_stats[year]["flight_count"] += 1 - if leg["type"] == "train": - yearly_stats[year].setdefault("train_count", 0) - yearly_stats[year]["train_count"] += 1 - return dict(yearly_stats) - - @app.route("/trip/stats") def trip_stats() -> str: """Travel stats: distance and price by year and travel type.""" @@ -654,7 +591,7 @@ def trip_stats() -> str: past = [item for item in trip_list if (item.end or item.start) < today] - yearly_stats = calculate_yearly_stats(past) + yearly_stats = agenda.trip.calculate_yearly_stats(past) return flask.render_template( "trip/stats.html",