From 5d20adaa246c731a4bcedaf26bdfda706c56f915 Mon Sep 17 00:00:00 2001 From: Cora Grant Date: Mon, 27 Jan 2025 17:05:31 -0500 Subject: [PATCH] refactor: unify local time and service date logic * Replaces a multitude of slightly different one-off routines for translating a datetime into a "service date" with calls to a shared utility function. * Replaces a bunch of duplicated `America/New_York` strings with calls to a new utility function. --- lib/screens/departures/departure.ex | 9 ++--- lib/screens/headways.ex | 7 ++-- lib/screens/last_trip/poller.ex | 18 ++++------ lib/screens/schedules/schedule.ex | 4 ++- lib/screens/util.ex | 34 +++++++------------ .../v2/candidate_generator/dup/departures.ex | 2 +- .../candidate_generator/gl_eink/headways.ex | 33 ++++++------------ .../widgets/cr_departures.ex | 10 ++---- lib/screens/v2/screen_data/parameters.ex | 5 +-- .../v2/widget_instance/cr_departures.ex | 21 ++++-------- lib/screens/v2/widget_instance/departures.ex | 5 +-- lib/screens/v2/widget_instance/line_map.ex | 9 ++--- .../overnight_cr_departures.ex | 10 ++---- .../v2/widget_instance/reconstructed_alert.ex | 6 ++-- .../v2/widget_instance/shuttle_bus_info.ex | 11 +++--- test/screens/util_test.exs | 6 ++-- 16 files changed, 71 insertions(+), 119 deletions(-) diff --git a/lib/screens/departures/departure.ex b/lib/screens/departures/departure.ex index be7fa20a2..30496b3f2 100644 --- a/lib/screens/departures/departure.ex +++ b/lib/screens/departures/departure.ex @@ -62,12 +62,7 @@ defmodule Screens.Departures.Departure do @spec fetch_schedules_by_datetime(query_params(), DateTime.t()) :: {:ok, t()} | :error def fetch_schedules_by_datetime(%{} = query_params, dt) do - # Find the current service date by shifting the given datetime to Pacific Time. - # This splits the service day at 3am, as midnight at Pacific Time is always 3am here. - {:ok, pacific_time} = DateTime.shift_zone(dt, "America/Los_Angeles") - service_date = DateTime.to_date(pacific_time) - - schedules = Schedule.fetch(query_params, Date.to_string(service_date)) + schedules = Schedule.fetch(query_params, dt |> Util.service_date() |> Date.to_string()) case schedules do {:ok, data} -> @@ -378,7 +373,7 @@ defmodule Screens.Departures.Departure do end defp format_query_param({:date, %DateTime{} = date}) do - {"filter[date]", Util.get_service_date_today(date)} + {"filter[date]", Util.service_date(date)} end defp format_query_param({:date, %Date{} = date}) do diff --git a/lib/screens/headways.ex b/lib/screens/headways.ex index a20fdeaf7..64a438116 100644 --- a/lib/screens/headways.ex +++ b/lib/screens/headways.ex @@ -7,6 +7,7 @@ defmodule Screens.Headways do alias Screens.Routes.Route alias Screens.SignsUiConfig.Cache, as: SignsUi alias Screens.Stops.Stop + alias Screens.Util # Compact mapping of stop IDs to headway keys, leaning on the fact that subway stop IDs happen # to be numeric and often contiguous as we "traverse" the line in a given direction. @@ -187,10 +188,8 @@ defmodule Screens.Headways do @spec period(DateTime.t()) :: :peak | :off_peak | :saturday | :sunday defp period(datetime) do - local_dt = DateTime.shift_zone!(datetime, "America/New_York") - - # Subtract 3 hours, since the service day starts/ends at 3:00 AM - day_of_week = local_dt |> DateTime.add(-3, :hour) |> Date.day_of_week() + local_dt = Util.to_eastern(datetime) + day_of_week = local_dt |> Util.service_date() |> Date.day_of_week() time = {local_dt.hour, local_dt.minute} am_peak? = time >= {7, 0} and time < {9, 0} diff --git a/lib/screens/last_trip/poller.ex b/lib/screens/last_trip/poller.ex index 55a6abe96..fe94e4cac 100644 --- a/lib/screens/last_trip/poller.ex +++ b/lib/screens/last_trip/poller.ex @@ -2,10 +2,10 @@ defmodule Screens.LastTrip.Poller do @moduledoc """ GenServer that polls predictions to calculate the last trip of the day """ - alias Screens.LastTrip.Cache - alias Screens.LastTrip.Parser - alias Screens.LastTrip.TripUpdates - alias Screens.LastTrip.VehiclePositions + + alias Screens.LastTrip.{Cache, Parser, TripUpdates, VehiclePositions} + alias Screens.Util + use GenServer defstruct [:next_reset] @@ -73,14 +73,10 @@ defmodule Screens.LastTrip.Poller do |> Cache.update_recent_departures() end - defp now(now_fn \\ &DateTime.utc_now/0) do - now_fn.() |> DateTime.shift_zone!("America/New_York") - end + defp now(now_fn \\ &DateTime.utc_now/0), do: now_fn.() |> Util.to_eastern() defp next_reset do - now() - |> DateTime.add(1, :day) - |> DateTime.to_date() - |> DateTime.new!(~T[03:30:00], "America/New_York") + now = now() + DateTime.new!(Date.add(now, 1), ~T[03:30:00], now.time_zone) end end diff --git a/lib/screens/schedules/schedule.ex b/lib/screens/schedules/schedule.ex index 442dbc707..03dbc0e54 100644 --- a/lib/screens/schedules/schedule.ex +++ b/lib/screens/schedules/schedule.ex @@ -27,7 +27,9 @@ defmodule Screens.Schedules.Schedule do direction_id: Trip.direction() } - @spec fetch(Departure.query_params(), String.t() | nil) :: {:ok, list(t())} | :error + @spec fetch(Departure.query_params()) :: {:ok, list(t())} | :error + @spec fetch(Departure.query_params(), DateTime.t() | Date.t() | String.t() | nil) :: + {:ok, list(t())} | :error def fetch(%{} = query_params, date \\ nil) do extra_params = if is_nil(date), do: %{}, else: %{date: date} diff --git a/lib/screens/util.ex b/lib/screens/util.ex index d9e428f89..ba3e3df7d 100644 --- a/lib/screens/util.ex +++ b/lib/screens/util.ex @@ -159,30 +159,20 @@ defmodule Screens.Util do def to_set(ids) when is_list(ids), do: MapSet.new(ids) def to_set(%MapSet{} = already_a_set), do: already_a_set + @doc "Shifts a datetime into Eastern time." + @spec to_eastern(DateTime.t()) :: DateTime.t() + def to_eastern(datetime), do: DateTime.shift_zone!(datetime, "America/New_York") + @doc """ - Calculates the service day for the given DateTime. - For context, MBTA service days end at 3am, not at midnight. - So getting the service day means subtracting 3 hours from the current time. - To avoid duplicate DateTime calculations existing throughout the code, - this function will handle the actual calculations needed to get the service day. - """ - @spec get_service_date_today(DateTime.t()) :: Date.t() - def get_service_date_today(now) do - {:ok, now_eastern} = DateTime.shift_zone(now, "America/New_York") - - # If it is at least 3am, the current date matches the service date. - # If current time is between 12am and 3am, the date has changed but we are still in service for the previous day. - # That means we need to subtract 1 day to get the current service date. - if now_eastern.hour >= 3 do - DateTime.to_date(now_eastern) - else - Date.add(now_eastern, -1) - end - end + Determines the MBTA service date at a given moment in time. - @spec get_service_date_tomorrow(DateTime.t()) :: Date.t() - def get_service_date_tomorrow(now) do - Date.add(get_service_date_today(now), 1) + The boundary between service dates is 3:00am local time. In the period between midnight and + 3:00am, the calendar date is one day ahead of the service date. + """ + @spec service_date(DateTime.t()) :: Date.t() + def service_date(datetime) do + dt = to_eastern(datetime) + if dt.hour >= 3, do: DateTime.to_date(dt), else: Date.add(dt, -1) end @doc """ diff --git a/lib/screens/v2/candidate_generator/dup/departures.ex b/lib/screens/v2/candidate_generator/dup/departures.ex index 4e971a4b7..99cf3b0d6 100644 --- a/lib/screens/v2/candidate_generator/dup/departures.ex +++ b/lib/screens/v2/candidate_generator/dup/departures.ex @@ -630,7 +630,7 @@ defmodule Screens.V2.CandidateGenerator.Dup.Departures do end tomorrow = - case fetch_schedules_fn.(fetch_params, Util.get_service_date_tomorrow(now)) do + case fetch_schedules_fn.(fetch_params, now |> Util.service_date() |> Date.add(1)) do {:ok, schedules} when schedules != [] -> Enum.filter(schedules, &(&1.route.id in route_ids_serving_section)) diff --git a/lib/screens/v2/candidate_generator/gl_eink/headways.ex b/lib/screens/v2/candidate_generator/gl_eink/headways.ex index a1e9a5a9e..252966d10 100644 --- a/lib/screens/v2/candidate_generator/gl_eink/headways.ex +++ b/lib/screens/v2/candidate_generator/gl_eink/headways.ex @@ -2,6 +2,7 @@ defmodule Screens.V2.CandidateGenerator.GlEink.Headways do @moduledoc false alias Screens.Schedules.Schedule + alias Screens.Util @dayparts [ {:late_night, ~T[00:00:00], :close}, @@ -13,9 +14,9 @@ defmodule Screens.V2.CandidateGenerator.GlEink.Headways do {:late_night, ~T[20:00:00], :midnight} ] - def by_route_id(route_id, stop_id, direction_id, service_level, time \\ DateTime.utc_now()) do - current_schedule = schedule_with_override(time, service_level) - current_daypart = daypart(time, stop_id, direction_id) + def by_route_id(route_id, stop_id, direction_id, service_level, now \\ DateTime.utc_now()) do + current_schedule = schedule_with_override(now, service_level) + current_daypart = daypart(now, stop_id, direction_id) headway(route_id, current_schedule, current_daypart) end @@ -79,34 +80,27 @@ defmodule Screens.V2.CandidateGenerator.GlEink.Headways do defp headway("Green-E", :sunday, :early_morning), do: 15 defp headway("Green-E", :sunday, _), do: 12 - defp schedule_with_override(time, service_level) do + defp schedule_with_override(datetime, service_level) do # Level 3 turns weekday into Saturday schedule # Level 4 is always Sunday schedule # Otherwise, use normal schedule - case {service_level, schedule(time)} do + case {service_level, schedule(datetime)} do {3, :weekday} -> :saturday {4, _} -> :sunday {_, schedule} -> schedule end end - defp schedule(utc_time) do - # Note: This is a hack. - # Split the service day at 3am by shifting to Pacific Time. - # Midnight at Pacific Time is always 3am here. - {:ok, pacific_time} = DateTime.shift_zone(utc_time, "America/Los_Angeles") - service_date = DateTime.to_date(pacific_time) - - case Date.day_of_week(service_date) do + defp schedule(datetime) do + case datetime |> Util.service_date() |> Date.day_of_week() do 7 -> :sunday 6 -> :saturday _ -> :weekday end end - defp daypart(utc_time, stop_id, direction_id) do - {:ok, local_time} = DateTime.shift_zone(utc_time, "America/New_York") - local_time = DateTime.to_time(local_time) + defp daypart(datetime, stop_id, direction_id) do + local_time = datetime |> Util.to_eastern() |> DateTime.to_time() {daypart, _, _} = Enum.find( @@ -148,12 +142,7 @@ defmodule Screens.V2.CandidateGenerator.GlEink.Headways do defp service_start_or_end(stop_id, direction_id, min_or_max_fn) do with {:ok, schedules} <- Schedule.fetch(%{stop_ids: [stop_id], direction_id: direction_id}), [_ | _] = arrival_times <- get_arrival_times(schedules) do - {:ok, local_dt} = - arrival_times - |> min_or_max_fn.() - |> DateTime.shift_zone("America/New_York") - - DateTime.to_time(local_dt) + arrival_times |> min_or_max_fn.() |> Util.to_eastern() |> DateTime.to_time() else _ -> nil end diff --git a/lib/screens/v2/candidate_generator/widgets/cr_departures.ex b/lib/screens/v2/candidate_generator/widgets/cr_departures.ex index 026066153..32a3ff664 100644 --- a/lib/screens/v2/candidate_generator/widgets/cr_departures.ex +++ b/lib/screens/v2/candidate_generator/widgets/cr_departures.ex @@ -2,6 +2,7 @@ defmodule Screens.V2.CandidateGenerator.Widgets.CRDepartures do @moduledoc false alias Screens.Schedules.Schedule + alias Screens.Util alias Screens.V2.Departure alias Screens.V2.WidgetInstance.CRDepartures, as: CRDeparturesWidget alias Screens.V2.WidgetInstance.{DeparturesNoData, OvernightCRDepartures} @@ -107,12 +108,7 @@ defmodule Screens.V2.CandidateGenerator.Widgets.CRDepartures do end defp fetch_last_schedule_tomorrow(direction_to_destination, station, now) do - # Any time between midnight and 3AM should be considered part of yesterday's service day. - service_datetime = - now |> DateTime.shift_zone!("America/New_York") |> DateTime.add(-3 * 60 * 60, :second) - - next_service_day = - service_datetime |> DateTime.add(60 * 60 * 24, :second) |> Timex.format!("{YYYY}-{0M}-{0D}") + service_date_tomorrow = now |> Util.service_date() |> Date.add(1) params = %{ direction_id: direction_to_destination, @@ -127,7 +123,7 @@ defmodule Screens.V2.CandidateGenerator.Widgets.CRDepartures do sort: "-departure_time" } - {:ok, schedules} = Schedule.fetch(params, next_service_day) + {:ok, schedules} = Schedule.fetch(params, service_date_tomorrow) List.first(schedules) end diff --git a/lib/screens/v2/screen_data/parameters.ex b/lib/screens/v2/screen_data/parameters.ex index d4fe5967e..fecf1d06b 100644 --- a/lib/screens/v2/screen_data/parameters.ex +++ b/lib/screens/v2/screen_data/parameters.ex @@ -115,8 +115,9 @@ defmodule Screens.V2.ScreenData.Parameters do night_volume: night_volume } } -> - {:ok, now} = DateTime.shift_zone(now, "America/New_York") - if Util.time_in_range?(now, night_start, night_end), do: night_volume, else: day_volume + if now |> Util.to_eastern() |> Util.time_in_range?(night_start, night_end), + do: night_volume, + else: day_volume end end diff --git a/lib/screens/v2/widget_instance/cr_departures.ex b/lib/screens/v2/widget_instance/cr_departures.ex index 1742adbdb..3b6da6838 100644 --- a/lib/screens/v2/widget_instance/cr_departures.ex +++ b/lib/screens/v2/widget_instance/cr_departures.ex @@ -4,6 +4,7 @@ defmodule Screens.V2.WidgetInstance.CRDepartures do alias Screens.Log alias Screens.Predictions.Prediction alias Screens.Stops.Stop + alias Screens.Util alias Screens.V2.Departure alias Screens.V2.WidgetInstance.Serializer.RoutePill alias ScreensConfig.V2.CRDepartures @@ -133,14 +134,12 @@ defmodule Screens.V2.WidgetInstance.CRDepartures do } = departure, now ) do - {:ok, scheduled_departure_time} = + scheduled_departure_time = if is_nil(schedule) do Log.error("cr_departures_no_scheduled_time", departure: departure) - {:ok, nil} + nil else - %Departure{schedule: schedule} - |> Departure.time() - |> DateTime.shift_zone("America/New_York") + %Departure{schedule: schedule} |> Departure.time() |> Util.to_eastern() end cond do @@ -166,10 +165,8 @@ defmodule Screens.V2.WidgetInstance.CRDepartures do # Prediction is missing a vehicle so is not valuable to us. Show schedule but flag as delayed if departure time for prediction is after schedule. defp serialize_prediction_missing_vehicle(scheduled_departure_time, prediction) do - {:ok, predicted_departure_time} = - %Departure{prediction: prediction} - |> Departure.time() - |> DateTime.shift_zone("America/New_York") + predicted_departure_time = + %Departure{prediction: prediction} |> Departure.time() |> Util.to_eastern() is_delayed = delayed?(scheduled_departure_time, predicted_departure_time) @@ -184,11 +181,7 @@ defmodule Screens.V2.WidgetInstance.CRDepartures do ) do %Prediction{stop: %Stop{id: stop_id}, vehicle: vehicle} = prediction - {:ok, predicted_departure_time} = - departure - |> Departure.time() - |> DateTime.shift_zone("America/New_York") - + predicted_departure_time = departure |> Departure.time() |> Util.to_eastern() stop_type = Departure.stop_type(departure) second_diff = DateTime.diff(predicted_departure_time, now) minute_diff = round(second_diff / 60) diff --git a/lib/screens/v2/widget_instance/departures.ex b/lib/screens/v2/widget_instance/departures.ex index ce416d80c..ece1314bc 100644 --- a/lib/screens/v2/widget_instance/departures.ex +++ b/lib/screens/v2/widget_instance/departures.ex @@ -483,11 +483,12 @@ defmodule Screens.V2.WidgetInstance.Departures do end defp serialize_timestamp(departure_time, now) do - {:ok, local_time} = DateTime.shift_zone(departure_time, "America/New_York") + local_time = Util.to_eastern(departure_time) hour = 1 + Integer.mod(local_time.hour - 1, 12) minute = local_time.minute am_pm = if local_time.hour >= 12, do: :pm, else: :am - show_am_pm = Util.get_service_date_tomorrow(now).day == local_time.day + service_date_tomorrow = now |> Util.service_date() |> Date.add(1) + show_am_pm = local_time.day == service_date_tomorrow.day %{type: :timestamp, hour: hour, minute: minute, am_pm: am_pm, show_am_pm: show_am_pm} end end diff --git a/lib/screens/v2/widget_instance/line_map.ex b/lib/screens/v2/widget_instance/line_map.ex index 24805bacf..6ccf5a76f 100644 --- a/lib/screens/v2/widget_instance/line_map.ex +++ b/lib/screens/v2/widget_instance/line_map.ex @@ -3,6 +3,7 @@ defmodule Screens.V2.WidgetInstance.LineMap do alias Screens.Predictions.Prediction alias Screens.Trips.Trip + alias Screens.Util alias Screens.V2.Departure alias Screens.V2.WidgetInstance.LineMap alias Screens.Vehicles.Vehicle @@ -280,12 +281,8 @@ defmodule Screens.V2.WidgetInstance.LineMap do if prediction_count < 2 and not is_nil(departure) do %{name: origin_stop_name} = Enum.at(stops, 0) - {:ok, local_time} = - departure - |> Departure.time() - |> DateTime.shift_zone("America/New_York") - - {:ok, timestamp} = Timex.format(local_time, "{h12}:{m}") + {:ok, timestamp} = + departure |> Departure.time() |> Util.to_eastern() |> Timex.format("{h12}:{m}") %{timestamp: timestamp, station_name: origin_stop_name} end diff --git a/lib/screens/v2/widget_instance/overnight_cr_departures.ex b/lib/screens/v2/widget_instance/overnight_cr_departures.ex index 5b007606a..690a76e99 100644 --- a/lib/screens/v2/widget_instance/overnight_cr_departures.ex +++ b/lib/screens/v2/widget_instance/overnight_cr_departures.ex @@ -7,6 +7,7 @@ defmodule Screens.V2.WidgetInstance.OvernightCRDepartures do alias Screens.Schedules.Schedule alias Screens.Trips.Trip + alias Screens.Util alias Screens.V2.Departure alias Screens.V2.WidgetInstance @@ -30,19 +31,14 @@ defmodule Screens.V2.WidgetInstance.OvernightCRDepartures do def serialize(%__MODULE__{ destination: destination, direction_to_destination: direction_to_destination, - last_tomorrow_schedule: - %Schedule{ - departure_time: departure_time - } = schedule + last_tomorrow_schedule: %Schedule{departure_time: departure_time} = schedule }) do - {:ok, local_departure_time} = DateTime.shift_zone(departure_time, "America/New_York") - {headsign_stop, headsign_via} = format_headsign(Departure.headsign(%Departure{schedule: schedule})) %{ direction: direction_to_destination, - last_schedule_departure_time: local_departure_time, + last_schedule_departure_time: Util.to_eastern(departure_time), last_schedule_headsign_stop: headsign_stop, last_schedule_headsign_via: serialize_via_string(destination, headsign_via) } diff --git a/lib/screens/v2/widget_instance/reconstructed_alert.ex b/lib/screens/v2/widget_instance/reconstructed_alert.ex index 15047b98c..9c4591789 100644 --- a/lib/screens/v2/widget_instance/reconstructed_alert.ex +++ b/lib/screens/v2/widget_instance/reconstructed_alert.ex @@ -1107,12 +1107,12 @@ defmodule Screens.V2.WidgetInstance.ReconstructedAlert do end defp format_updated_at(updated_at, now) do - shifted_updated_at = DateTime.shift_zone!(updated_at, "America/New_York") + local_updated_at = Util.to_eastern(updated_at) if Date.compare(updated_at, now) == :lt do - Timex.format!(shifted_updated_at, "{M}/{D}/{YY}") + Timex.format!(local_updated_at, "{M}/{D}/{YY}") else - Timex.format!(shifted_updated_at, "{WDfull}, {h12}:{m} {am}") + Timex.format!(local_updated_at, "{WDfull}, {h12}:{m} {am}") end end diff --git a/lib/screens/v2/widget_instance/shuttle_bus_info.ex b/lib/screens/v2/widget_instance/shuttle_bus_info.ex index be2d2acf0..adce9db3b 100644 --- a/lib/screens/v2/widget_instance/shuttle_bus_info.ex +++ b/lib/screens/v2/widget_instance/shuttle_bus_info.ex @@ -75,11 +75,8 @@ defmodule Screens.V2.WidgetInstance.ShuttleBusInfo do def audio_view(_instance), do: ScreensWeb.V2.Audio.ShuttleBusInfoView defp get_minute_range(schedule, now) do - # Shift the time to local time. - {:ok, local_time_now} = DateTime.shift_zone(now, "America/New_York") - - # Shift to find current service day. i.e. 1AM Monday is actually still in the Sunday service day. - service_day_now = DateTime.add(local_time_now, -3 * 60 * 60, :second) + local_now = Util.to_eastern(now) + service_day_of_week = local_now |> Util.service_date() |> Date.day_of_week() %ShuttleBusSchedule{minute_range: minutes_range_to_destination} = Enum.find(schedule, fn %ShuttleBusSchedule{ @@ -94,8 +91,8 @@ defmodule Screens.V2.WidgetInstance.ShuttleBusInfo do :sunday -> [7] end - Date.day_of_week(service_day_now) in day_range and - Util.time_in_range?(DateTime.to_time(local_time_now), start_time, end_time) + service_day_of_week in day_range and + Util.time_in_range?(DateTime.to_time(local_now), start_time, end_time) end) minutes_range_to_destination diff --git a/test/screens/util_test.exs b/test/screens/util_test.exs index 00ec2be4e..fe0be9a90 100644 --- a/test/screens/util_test.exs +++ b/test/screens/util_test.exs @@ -102,19 +102,19 @@ defmodule Screens.UtilTest do end end - describe "get_service_date_today/1" do + describe "service_date/1" do test "returns the current date if after 3am" do now_eastern = DateTime.new!(~D[2022-01-01], ~T[09:00:00], "America/New_York") now = DateTime.shift_zone!(now_eastern, "Etc/UTC") expected = ~D[2022-01-01] - assert(expected == get_service_date_today(now)) + assert(expected == service_date(now)) end test "returns the yesterday's date if between 12am and 3am" do now_eastern = DateTime.new!(~D[2022-01-01], ~T[00:00:00], "America/New_York") now = DateTime.shift_zone!(now_eastern, "Etc/UTC") expected = ~D[2021-12-31] - assert expected == get_service_date_today(now) + assert expected == service_date(now) end end end