diff --git a/lib/dotcom_web/live/daily_schedules_live.ex b/lib/dotcom_web/live/daily_schedules_live.ex
new file mode 100644
index 0000000000..29b68e0c9b
--- /dev/null
+++ b/lib/dotcom_web/live/daily_schedules_live.ex
@@ -0,0 +1,241 @@
+defmodule DotcomWeb.DailySchedulesLive do
+ @moduledoc """
+ A simple basic-auth gated page to explore what kinds of daily schedules are actually available for the routes we serve.
+ """
+
+ use DotcomWeb, :live_view
+
+ alias Phoenix.LiveView
+
+ @impl LiveView
+ def mount(_params, _session, socket) do
+ {
+ :ok,
+ socket
+ }
+ end
+
+ @impl true
+ def handle_params(params, _uri, socket) do
+ {:noreply, socket |> assign(:route_id, params |> Map.get("route_id", "1"))}
+ end
+
+ @impl true
+ def handle_event("change-route", %{"route_id" => route_id}, socket) do
+ new_path = socket |> live_path(__MODULE__, %{"route_id" => route_id})
+ {:noreply, socket |> push_patch(to: new_path)}
+ end
+
+ @impl true
+ def render(%{route_id: route_id} = assigns) do
+ services = Services.Repo.by_route_id(route_id)
+
+ daily_schedules =
+ services
+ |> Enum.flat_map(fn svc ->
+ svc
+ |> Services.Service.all_valid_dates_for_service()
+ |> Enum.map(&{&1, svc})
+ end)
+ |> Enum.group_by(fn {date, _svc} -> date end)
+ |> Enum.map(fn {date, list} ->
+ {date, list |> Enum.map(fn {_date, svc} -> svc end) |> Enum.sort_by(& &1.id)}
+ end)
+ |> Enum.sort_by(fn {date, _svc_list} -> date end, &Date.before?/2)
+ |> Enum.group_by(fn {_date, svc_list} -> svc_list end)
+ |> Enum.map(fn {svc_list, date_tuples} ->
+ %{
+ services: svc_list,
+ dates: date_tuples |> Enum.map(fn {date, _svc_list} -> date end)
+ }
+ end)
+ |> Enum.map(&add_holiday_status/1)
+ |> Enum.map(&add_title/1)
+ |> Enum.sort_by(& &1.title)
+
+ {holiday_schedules, non_holiday_schedules} = daily_schedules |> Enum.split_with(& &1.holiday?)
+
+ assigns =
+ assigns
+ |> assign(:route_id, route_id)
+ |> assign(:daily_schedules, daily_schedules)
+ |> assign(:holiday_schedules, holiday_schedules)
+ |> assign(:non_holiday_schedules, non_holiday_schedules)
+
+ ~H"""
+
Daily Schedules
+
+ <.route_changer selected_route_id={@route_id} />
+
+ Regular Schedules
+ <.daily_schedule :for={schedule <- @non_holiday_schedules} schedule={schedule} />
+
+ Holiday Schedules
+ <.daily_schedule :for={schedule <- @holiday_schedules} schedule={schedule} />
+ """
+ end
+
+ defp add_holiday_status(schedule) do
+ schedule
+ |> Map.put(:holiday?, schedule.services |> Enum.any?(&(&1.typicality == :holiday_service)))
+ end
+
+ defp add_title(schedule) do
+ schedule
+ |> Map.put(:title, title(schedule))
+ end
+
+ defp title(schedule) do
+ days_of_week = schedule.dates |> Enum.map(&Date.day_of_week/1) |> MapSet.new()
+ date_set = schedule.dates |> MapSet.new()
+
+ [
+ {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-11-27]])) end, "Thanksgiving"},
+ {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-11-28]])) end,
+ "Day After Thanksgiving"},
+ {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-11-11]])) end, "Veterans Day"},
+ {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-12-25]])) end, "Christmas Day"},
+ {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-12-31]])) end, "New Year's Eve"},
+ {fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-01-01]])) end, "New Year's Day"},
+ {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-12-25], ~D[2026-01-01]])) end,
+ "Christmas and New Year's"},
+ {fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-01-19]])) end,
+ "Martin Luther King Jr. Day"},
+ {fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-02-16]])) end, "Presidents Day"},
+ {fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-01-19], ~D[2026-02-16]])) end,
+ "Martin Luther King Jr. and Presidents' Day"},
+ {fn -> MapSet.subset?(days_of_week, MapSet.new([5])) end, "Friday"},
+ {fn -> MapSet.subset?(days_of_week, MapSet.new([1, 2, 3, 4])) end, "Monday - Thursday"},
+ {fn -> MapSet.subset?(days_of_week, MapSet.new([1, 2, 3, 4, 5])) end, "Weekday"},
+ {fn -> MapSet.subset?(days_of_week, MapSet.new([6])) end, "Saturday"},
+ {fn -> MapSet.subset?(days_of_week, MapSet.new([7])) end, "Sunday"},
+ {fn -> true end, "???"}
+ ]
+ |> Stream.filter(fn {condition_fun, _title} -> condition_fun.() end)
+ |> Stream.map(fn {_condition_fun, title} -> title end)
+ |> Enum.take(1)
+ |> List.first()
+ end
+
+ defp daily_schedule(assigns) do
+ %{services: services} = assigns.schedule
+ title = services |> Enum.map(& &1.id) |> Enum.sort() |> Enum.join(", ")
+ assigns = assigns |> assign(:title, title)
+
+ ~H"""
+
+
+ {@schedule.title}
+
+ <.icon
+ name="chevron-down"
+ class="ml-auto h-3 w-3 group-open/schedule:rotate-180 transition-all"
+ />
+
+
+
+
Services
+
+
+
{svc.id}
+
+
Name
+
{svc.name}
+
+
Start Date
+
{svc.start_date}
+
+
End Date
+
{svc.end_date}
+
+
Valid Days
+
+
+ {pretty_day_of_week(day)}
+
+
+
+
Typicality
+
{svc.typicality}
+
+
+
Added Dates
+
+
+
Removed Dates
+
+
+
+
+
+
+
Active Dates
+
+
+ {date} ({day_of_week(date)})
+
+
+
+
+
+
Unique days of the week
+
+
Enum.map(&Date.day_of_week/1) |> Enum.uniq() |> Enum.sort()
+ }
+ class="p-1 border-xs rounded text-xs"
+ >
+ {pretty_day_of_week(day_of_week)}
+
+
+
+
+
+
+ Full Schedule Data Blob
+ <.icon
+ name="chevron-down"
+ class="ml-auto h-3 w-3 group-open/code:rotate-180 transition-all"
+ />
+
+ {inspect @schedule, pretty: true}
+
+
+ """
+ end
+
+ defp day_of_week(date), do: date |> Date.day_of_week() |> pretty_day_of_week()
+
+ defp pretty_day_of_week(1), do: "Mon"
+ defp pretty_day_of_week(2), do: "Tue"
+ defp pretty_day_of_week(3), do: "Wed"
+ defp pretty_day_of_week(4), do: "Thu"
+ defp pretty_day_of_week(5), do: "Fri"
+ defp pretty_day_of_week(6), do: "Sat"
+ defp pretty_day_of_week(7), do: "Sun"
+ defp pretty_day_of_week(_), do: "?"
+
+ defp route_changer(assigns) do
+ assigns =
+ assigns
+ |> assign(:routes, Routes.Repo.all())
+
+ ~H"""
+
+ """
+ end
+end
diff --git a/lib/dotcom_web/live/preview_live.ex b/lib/dotcom_web/live/preview_live.ex
index 8c4ebcdef5..e31156c20a 100644
--- a/lib/dotcom_web/live/preview_live.ex
+++ b/lib/dotcom_web/live/preview_live.ex
@@ -5,11 +5,19 @@ defmodule DotcomWeb.PreviewLive do
use DotcomWeb, :live_view
+ alias DotcomWeb.DailySchedulesLive
alias DotcomWeb.Router.Helpers
alias DotcomWeb.ScheduleFinderLive
alias Phoenix.LiveView
@pages [
+ %{
+ arguments: [],
+ icon_name: "calendar-days",
+ icon_type: "solid",
+ module: DailySchedulesLive,
+ title: "Daily Schedules Experiment"
+ },
%{
arguments: ["Red", "0"],
icon_name: "icon-realtime-tracking",
diff --git a/lib/dotcom_web/router.ex b/lib/dotcom_web/router.ex
index d589b6b660..1b28b5bf89 100644
--- a/lib/dotcom_web/router.ex
+++ b/lib/dotcom_web/router.ex
@@ -304,6 +304,7 @@ defmodule DotcomWeb.Router do
live_session :default, layout: {DotcomWeb.LayoutView, :preview} do
live "/", PreviewLive
live "/schedules/:route_id/:direction_id", ScheduleFinderLive
+ live "/daily-schedules", DailySchedulesLive
end
end
diff --git a/lib/services/service.ex b/lib/services/service.ex
index cc93983143..a63ceed19e 100644
--- a/lib/services/service.ex
+++ b/lib/services/service.ex
@@ -164,13 +164,13 @@ defmodule Services.Service do
end
@spec all_valid_dates_for_service(t()) :: [Date.t()]
- defp all_valid_dates_for_service(%__MODULE__{
- start_date: from,
- end_date: until,
- added_dates: added_dates,
- removed_dates: removed_dates,
- valid_days: valid_days
- }) do
+ def all_valid_dates_for_service(%__MODULE__{
+ start_date: from,
+ end_date: until,
+ added_dates: added_dates,
+ removed_dates: removed_dates,
+ valid_days: valid_days
+ }) do
# fallback to today if either start or end date are nil
from = from || Timex.today()
until = until || Timex.today()
diff --git a/livebooks/service_daily_schedules.livemd b/livebooks/service_daily_schedules.livemd
new file mode 100644
index 0000000000..755c519d6f
--- /dev/null
+++ b/livebooks/service_daily_schedules.livemd
@@ -0,0 +1,34 @@
+# Service --> Daily Schedules
+
+## Services to Daily Schedules
+
+```elixir
+services = Services.Repo.by_route_id("39")
+
+services
+|> Enum.flat_map(fn svc ->
+ svc
+ |> Services.Service.all_valid_dates_for_service()
+ |> Enum.map(&{&1, svc})
+end)
+|> Enum.group_by(fn {date, _svc} -> date end)
+|> Enum.map(fn {date, list} ->
+ {date, list |> Enum.map(fn {_date, svc} -> svc end) |> Enum.sort_by(& &1.id)}
+end)
+|> Enum.sort_by(fn {date, _svc_list} -> date end, &Date.before?/2)
+|> Enum.group_by(fn {_date, svc_list} -> svc_list end)
+|> Enum.each(fn {svc_list, date_tuples} ->
+ dates =
+ date_tuples
+ |> Enum.map(fn {date, _svc_list} -> date end)
+ |> Enum.map(&"#{&1}")
+ |> Enum.join(", ")
+
+ IO.puts(svc_list |> Enum.map(& &1.id) |> Enum.join(", "))
+ IO.puts(" #{svc_list |> Enum.map(& &1.name) |> Enum.join(", ")}")
+ IO.puts(" #{svc_list |> Enum.map(& &1.type) |> Enum.join(", ")}")
+ IO.puts(" #{svc_list |> Enum.map(& &1.typicality) |> Enum.join(", ")}")
+ IO.puts(" #{dates}")
+ IO.puts("")
+end)
+```