Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: unify local time and service date logic #2420

Merged
merged 1 commit into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions lib/screens/departures/departure.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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} ->
Expand Down Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions lib/screens/headways.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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}
Expand Down
18 changes: 7 additions & 11 deletions lib/screens/last_trip/poller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion lib/screens/schedules/schedule.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
34 changes: 12 additions & 22 deletions lib/screens/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
2 changes: 1 addition & 1 deletion lib/screens/v2/candidate_generator/dup/departures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
33 changes: 11 additions & 22 deletions lib/screens/v2/candidate_generator/gl_eink/headways.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
10 changes: 3 additions & 7 deletions lib/screens/v2/candidate_generator/widgets/cr_departures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand Down
5 changes: 3 additions & 2 deletions lib/screens/v2/screen_data/parameters.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 7 additions & 14 deletions lib/screens/v2/widget_instance/cr_departures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions lib/screens/v2/widget_instance/departures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 3 additions & 6 deletions lib/screens/v2/widget_instance/line_map.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 3 additions & 7 deletions lib/screens/v2/widget_instance/overnight_cr_departures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}
Expand Down
6 changes: 3 additions & 3 deletions lib/screens/v2/widget_instance/reconstructed_alert.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading