From 5ec7d5752f26f7dbf69617616e7b83574df40cec Mon Sep 17 00:00:00 2001 From: Eddie Maldonado Date: Thu, 9 Jul 2026 10:32:38 -0400 Subject: [PATCH 1/7] refactor: add direction_id argument to stop_shuttling_on_route/4 --- lib/concentrate/filter/alert/shuttles.ex | 3 ++- lib/concentrate/group_filter/shuttle.ex | 14 ++++++------- .../filter/alert/shuttles_test.exs | 16 +++++++------- .../concentrate/group_filter/shuttle_test.exs | 21 ++++++++++++++++--- test/support/filter/fakes.ex | 12 +++++------ 5 files changed, 41 insertions(+), 25 deletions(-) diff --git a/lib/concentrate/filter/alert/shuttles.ex b/lib/concentrate/filter/alert/shuttles.ex index db19acee..0d20a795 100644 --- a/lib/concentrate/filter/alert/shuttles.ex +++ b/lib/concentrate/filter/alert/shuttles.ex @@ -36,9 +36,10 @@ defmodule Concentrate.Filter.Alert.Shuttles do @spec stop_shuttling_on_route( route_id :: String.t(), stop_id :: String.t(), + direction_id :: 0 | 1, :calendar.date() | integer ) :: :start | :stop | :through | nil - def stop_shuttling_on_route(route_id, stop_id, date_or_timestamp) + def stop_shuttling_on_route(route_id, stop_id, _direction_id, date_or_timestamp) when is_binary(route_id) and is_binary(stop_id) do case TimeTable.date_overlaps(@table, {:route_stop, route_id, stop_id}, date_or_timestamp) do [atom | _] -> atom diff --git a/lib/concentrate/group_filter/shuttle.ex b/lib/concentrate/group_filter/shuttle.ex index a436a12c..2fa05f00 100644 --- a/lib/concentrate/group_filter/shuttle.ex +++ b/lib/concentrate/group_filter/shuttle.ex @@ -21,7 +21,7 @@ defmodule Concentrate.GroupFilter.Shuttle do stus = if is_tuple(date) and is_binary(route_id) and module.trip_shuttling?(trip_id, route_id, direction_id, date) do - shuttle_updates(route_id, stus, module) + shuttle_updates(route_id, direction_id, stus, module) else stus end @@ -31,27 +31,27 @@ defmodule Concentrate.GroupFilter.Shuttle do def filter(%TripGroup{} = other, _module), do: other - defp shuttle_updates(route_id, stus, module) do + defp shuttle_updates(route_id, direction_id, stus, module) do initial_state = {false, false} stus - |> Enum.flat_map_reduce(initial_state, &shuttle_stop(route_id, module, &1, &2)) + |> Enum.flat_map_reduce(initial_state, &shuttle_stop(route_id, direction_id, module, &1, &2)) |> elem(0) end - defp shuttle_stop(route_id, shuttle_module, stop_time_updates, state) + defp shuttle_stop(route_id, direction_id, shuttle_module, stop_time_updates, state) - defp shuttle_stop(_route_id, _module, stu, {true, true} = state) do + defp shuttle_stop(_route_id, _direction_id, _module, stu, {true, true} = state) do {[StopTimeUpdate.skip(stu)], state} end - defp shuttle_stop(route_id, module, stu, {has_started?, has_shuttled?} = state) do + defp shuttle_stop(route_id, direction_id, module, stu, {has_started?, has_shuttled?} = state) do time = StopTimeUpdate.time(stu) if is_integer(time) do stop_id = StopTimeUpdate.stop_id(stu) - case module.stop_shuttling_on_route(route_id, stop_id, time) do + case module.stop_shuttling_on_route(route_id, stop_id, direction_id, time) do nil -> drop_arrival_time_if_after_shuttle(stu, has_shuttled?) diff --git a/test/concentrate/filter/alert/shuttles_test.exs b/test/concentrate/filter/alert/shuttles_test.exs index b4ad0d9b..ff198241 100644 --- a/test/concentrate/filter/alert/shuttles_test.exs +++ b/test/concentrate/filter/alert/shuttles_test.exs @@ -69,7 +69,7 @@ defmodule Concentrate.Filter.Alert.ShuttlesTest do end end - describe "stop_shuttling_on_route?/1" do + describe "stop_shuttling_on_route/4" do setup :supervised test "returns a atom indicating whether the route is shuttling a particular stop" do @@ -99,11 +99,11 @@ defmodule Concentrate.Filter.Alert.ShuttlesTest do handle_events([[alert]], :from, :state) - assert stop_shuttling_on_route("route", "through", 5) == :through - assert stop_shuttling_on_route("route", "through", 21) == nil - assert stop_shuttling_on_route("route", "through", {1970, 1, 1}) == :through - assert stop_shuttling_on_route("route", "start", 5) == :start - assert stop_shuttling_on_route("route", "stop", 5) == :stop + assert stop_shuttling_on_route("route", "through", 0, 5) == :through + assert stop_shuttling_on_route("route", "through", 0, 21) == nil + assert stop_shuttling_on_route("route", "through", 0, {1970, 1, 1}) == :through + assert stop_shuttling_on_route("route", "start", 0, 5) == :start + assert stop_shuttling_on_route("route", "stop", 0, 5) == :stop end end @@ -112,8 +112,8 @@ defmodule Concentrate.Filter.Alert.ShuttlesTest do refute trip_shuttling?("trip", "route", 0, 0) end - test "stop_shuttling_on_route/3 returns nil" do - assert stop_shuttling_on_route("route", "stop", 0) == nil + test "stop_shuttling_on_route/4 returns nil" do + assert stop_shuttling_on_route("route", "stop", 0, 0) == nil end end diff --git a/test/concentrate/group_filter/shuttle_test.exs b/test/concentrate/group_filter/shuttle_test.exs index 629f71bb..906b1d24 100644 --- a/test/concentrate/group_filter/shuttle_test.exs +++ b/test/concentrate/group_filter/shuttle_test.exs @@ -7,6 +7,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do @trip_id "trip" @route_id "route" + @direction_id 0 @valid_date {1970, 1, 1} @valid_date_time 8 @@ -28,6 +29,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: @trip_id, route_id: @route_id, + direction_id: @direction_id, start_date: @valid_date ) @@ -59,6 +61,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: @trip_id, route_id: @route_id, + direction_id: @direction_id, start_date: {1970, 1, 1} ), stus: [ @@ -112,6 +115,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: @trip_id, route_id: @route_id, + direction_id: @direction_id, start_date: {1970, 1, 1} ), stus: [ @@ -149,6 +153,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: @trip_id, route_id: @route_id, + direction_id: @direction_id, start_date: {1970, 1, 1} ), stus: [ @@ -187,6 +192,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: @trip_id, route_id: @route_id, + direction_id: @direction_id, start_date: {1970, 1, 1} ), stus: [ @@ -210,6 +216,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: @trip_id, route_id: @route_id, + direction_id: @direction_id, start_date: {1970, 1, 1} ), stus: [ @@ -230,7 +237,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: @trip_id, route_id: "single_direction", - direction_id: 0, + direction_id: @direction_id, start_date: {1970, 1, 1} ), stus: [ @@ -274,7 +281,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: "other_trip", route_id: @route_id, - direction_id: 0, + direction_id: @direction_id, start_date: {1970, 1, 1} ), stus: [ @@ -296,6 +303,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: @trip_id, route_id: @route_id, + direction_id: @direction_id, start_date: {1970, 1, 1} ), stus: [ @@ -349,6 +357,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: @trip_id, route_id: @route_id, + direction_id: @direction_id, start_date: {1970, 1, 1} ), stus: [ @@ -389,6 +398,7 @@ defmodule Concentrate.GroupFilter.ShuttleTest do TripDescriptor.new( trip_id: @trip_id, route_id: @route_id, + direction_id: @direction_id, start_date: {1970, 1, 1} ), stus: [ @@ -417,7 +427,12 @@ defmodule Concentrate.GroupFilter.ShuttleTest do test "updates on non-shuttle trips are not modified" do group = %TripGroup{ - td: TripDescriptor.new(trip_id: "other_trip", start_date: @valid_date), + td: + TripDescriptor.new( + trip_id: "other_trip", + direction_id: @direction_id, + start_date: @valid_date + ), stus: [ StopTimeUpdate.new( trip_id: "other_trip", diff --git a/test/support/filter/fakes.ex b/test/support/filter/fakes.ex index e690084f..44bda4db 100644 --- a/test/support/filter/fakes.ex +++ b/test/support/filter/fakes.ex @@ -108,10 +108,10 @@ defmodule Concentrate.Filter.FakeShuttles do def trip_shuttling?("trip", "single_direction", 0, {1970, 1, 1}), do: true def trip_shuttling?(_trip_id, _route_id, _direction_id, {_, _, _}), do: false - def stop_shuttling_on_route("route", "shuttle_1", 8), do: :through - def stop_shuttling_on_route("route", "shuttle_2", 8), do: :through - def stop_shuttling_on_route("route", "shuttle_start", 8), do: :start - def stop_shuttling_on_route("route", "shuttle_stop", 8), do: :stop - def stop_shuttling_on_route("single_direction", "shuttle_1", 8), do: :through - def stop_shuttling_on_route(_, _, dt) when is_integer(dt), do: nil + def stop_shuttling_on_route("route", "shuttle_1", _direction_id, 8), do: :through + def stop_shuttling_on_route("route", "shuttle_2", _direction_id, 8), do: :through + def stop_shuttling_on_route("route", "shuttle_start", _direction_id, 8), do: :start + def stop_shuttling_on_route("route", "shuttle_stop", _direction_id, 8), do: :stop + def stop_shuttling_on_route("single_direction", "shuttle_1", 0, 8), do: :through + def stop_shuttling_on_route(_, _, _, dt) when is_integer(dt), do: nil end From 153e54bd1f5ac89587171d698653618816410d4e Mon Sep 17 00:00:00 2001 From: Eddie Maldonado Date: Thu, 9 Jul 2026 11:07:58 -0400 Subject: [PATCH 2/7] test: test case analogous to Lowell Line shuttle --- .../concentrate/group_filter/shuttle_test.exs | 42 +++++++++++++++++++ test/support/filter/fakes.ex | 1 + 2 files changed, 43 insertions(+) diff --git a/test/concentrate/group_filter/shuttle_test.exs b/test/concentrate/group_filter/shuttle_test.exs index 906b1d24..db3046d6 100644 --- a/test/concentrate/group_filter/shuttle_test.exs +++ b/test/concentrate/group_filter/shuttle_test.exs @@ -424,6 +424,48 @@ defmodule Concentrate.GroupFilter.ShuttleTest do assert StopTimeUpdate.departure_time(stop) end + test "start of a shuttle in one direction doesn't affect STUs in the opposite direction" do + group = + %TripGroup{ + td: + TripDescriptor.new( + trip_id: @trip_id, + route_id: @route_id, + direction_id: @direction_id, + start_date: {1970, 1, 1} + ), + stus: [ + StopTimeUpdate.new( + trip_id: @trip_id, + stop_id: "shuttle_start_unidirectional", + departure_time: @valid_date_time + ), + StopTimeUpdate.new( + trip_id: @trip_id, + stop_id: "after_shuttle", + arrival_time: @valid_date_time, + departure_time: @valid_date_time + ), + StopTimeUpdate.new( + trip_id: @trip_id, + stop_id: "after_shuttle_2", + arrival_time: @valid_date_time + ) + ] + } + + %TripGroup{stus: reduced} = filter(group, @module) + + assert [start_unidirectional, after_shuttle, after_shuttle_2] = reduced + assert StopTimeUpdate.schedule_relationship(start_unidirectional) == :SCHEDULED + assert StopTimeUpdate.departure_time(start_unidirectional) + assert StopTimeUpdate.schedule_relationship(after_shuttle) == :SCHEDULED + assert StopTimeUpdate.arrival_time(after_shuttle) + assert StopTimeUpdate.departure_time(after_shuttle) + assert StopTimeUpdate.schedule_relationship(after_shuttle_2) == :SCHEDULED + assert StopTimeUpdate.arrival_time(after_shuttle_2) + end + test "updates on non-shuttle trips are not modified" do group = %TripGroup{ diff --git a/test/support/filter/fakes.ex b/test/support/filter/fakes.ex index 44bda4db..58882edb 100644 --- a/test/support/filter/fakes.ex +++ b/test/support/filter/fakes.ex @@ -111,6 +111,7 @@ defmodule Concentrate.Filter.FakeShuttles do def stop_shuttling_on_route("route", "shuttle_1", _direction_id, 8), do: :through def stop_shuttling_on_route("route", "shuttle_2", _direction_id, 8), do: :through def stop_shuttling_on_route("route", "shuttle_start", _direction_id, 8), do: :start + def stop_shuttling_on_route("route", "shuttle_start_unidirectional", 1, 8), do: :start def stop_shuttling_on_route("route", "shuttle_stop", _direction_id, 8), do: :stop def stop_shuttling_on_route("single_direction", "shuttle_1", 0, 8), do: :through def stop_shuttling_on_route(_, _, _, dt) when is_integer(dt), do: nil From 5c2c69e0f33ae3e6b4fcc06442b585ced64d4ef2 Mon Sep 17 00:00:00 2001 From: Eddie Maldonado Date: Thu, 9 Jul 2026 11:18:36 -0400 Subject: [PATCH 3/7] feat: parse and handle direction_id from shuttle alerts --- lib/concentrate/filter/alert/shuttles.ex | 15 ++++++++++----- test/concentrate/filter/alert/shuttles_test.exs | 10 ++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/concentrate/filter/alert/shuttles.ex b/lib/concentrate/filter/alert/shuttles.ex index 0d20a795..825fd4de 100644 --- a/lib/concentrate/filter/alert/shuttles.ex +++ b/lib/concentrate/filter/alert/shuttles.ex @@ -39,9 +39,13 @@ defmodule Concentrate.Filter.Alert.Shuttles do direction_id :: 0 | 1, :calendar.date() | integer ) :: :start | :stop | :through | nil - def stop_shuttling_on_route(route_id, stop_id, _direction_id, date_or_timestamp) + def stop_shuttling_on_route(route_id, stop_id, direction_id, date_or_timestamp) when is_binary(route_id) and is_binary(stop_id) do - case TimeTable.date_overlaps(@table, {:route_stop, route_id, stop_id}, date_or_timestamp) do + case TimeTable.date_overlaps( + @table, + {:route_stop, route_id, stop_id, direction_id}, + date_or_timestamp + ) do [atom | _] -> atom [] -> nil end @@ -81,9 +85,10 @@ defmodule Concentrate.Filter.Alert.Shuttles do route_stops = if is_binary(stop_id) and is_binary(route_id) do - [ - {{:route_stop, route_id, stop_id}, shuttle_type(InformedEntity.activities(entity))} - ] + for direction_id <- direction_ids(InformedEntity.direction_id(entity)) do + {{:route_stop, route_id, stop_id, direction_id}, + shuttle_type(InformedEntity.activities(entity))} + end else [] end diff --git a/test/concentrate/filter/alert/shuttles_test.exs b/test/concentrate/filter/alert/shuttles_test.exs index ff198241..ab09e8f3 100644 --- a/test/concentrate/filter/alert/shuttles_test.exs +++ b/test/concentrate/filter/alert/shuttles_test.exs @@ -88,6 +88,13 @@ defmodule Concentrate.Filter.Alert.ShuttlesTest do stop_id: "start", activities: ["BOARD", "RIDE"] ), + InformedEntity.new( + route_type: 2, + direction_id: 0, + route_id: "route", + stop_id: "start_unidirectional", + activities: ["BOARD", "RIDE"] + ), InformedEntity.new( route_type: 2, route_id: "route", @@ -103,6 +110,9 @@ defmodule Concentrate.Filter.Alert.ShuttlesTest do assert stop_shuttling_on_route("route", "through", 0, 21) == nil assert stop_shuttling_on_route("route", "through", 0, {1970, 1, 1}) == :through assert stop_shuttling_on_route("route", "start", 0, 5) == :start + assert stop_shuttling_on_route("route", "start", 1, 5) == :start + assert stop_shuttling_on_route("route", "start_unidirectional", 0, 5) == :start + assert stop_shuttling_on_route("route", "start_unidirectional", 1, 5) == nil assert stop_shuttling_on_route("route", "stop", 0, 5) == :stop end end From d5166cccf7ccc27966c6cad2035c6576b3469728 Mon Sep 17 00:00:00 2001 From: Eddie Maldonado Date: Thu, 9 Jul 2026 11:21:17 -0400 Subject: [PATCH 4/7] chore(ci): bump version of asdf-vm/actions/install --- .github/workflows/elixir.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 0a37ab97..74f16152 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -35,7 +35,7 @@ jobs: key: ${{ runner.os }}-asdf-v2-${{ hashFiles('.tool-versions') }} id: asdf-cache # only run `asdf install` if we didn't hit the cache - - uses: asdf-vm/actions/install@v1 + - uses: asdf-vm/actions/install@v4 if: steps.asdf-cache.outputs.cache-hit != 'true' # if we did hit the cache, set up the environment - name: Setup ASDF environment From 4d1b9bec4fec0a96fde80e4677a281842456c5c7 Mon Sep 17 00:00:00 2001 From: Eddie Maldonado Date: Fri, 10 Jul 2026 11:39:50 -0400 Subject: [PATCH 5/7] chore(ci): bump cache key to not use old version of ASDF deps --- .github/workflows/elixir.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 74f16152..fad41062 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -32,7 +32,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.asdf - key: ${{ runner.os }}-asdf-v2-${{ hashFiles('.tool-versions') }} + key: ${{ runner.os }}-asdf-v4-${{ hashFiles('.tool-versions') }} id: asdf-cache # only run `asdf install` if we didn't hit the cache - uses: asdf-vm/actions/install@v4 From c6e841fed881851a821aba7df23849317dfed4ec Mon Sep 17 00:00:00 2001 From: Eddie Maldonado Date: Fri, 10 Jul 2026 11:41:39 -0400 Subject: [PATCH 6/7] refactor: only evaluate shuttle_type/1 once --- lib/concentrate/filter/alert/shuttles.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/concentrate/filter/alert/shuttles.ex b/lib/concentrate/filter/alert/shuttles.ex index 825fd4de..e882a663 100644 --- a/lib/concentrate/filter/alert/shuttles.ex +++ b/lib/concentrate/filter/alert/shuttles.ex @@ -85,9 +85,10 @@ defmodule Concentrate.Filter.Alert.Shuttles do route_stops = if is_binary(stop_id) and is_binary(route_id) do + shuttle_type = shuttle_type(InformedEntity.activities(entity)) + for direction_id <- direction_ids(InformedEntity.direction_id(entity)) do - {{:route_stop, route_id, stop_id, direction_id}, - shuttle_type(InformedEntity.activities(entity))} + {{:route_stop, route_id, stop_id, direction_id}, shuttle_type} end else [] From c755676464de86f68d9e279b5e581c93e1042c89 Mon Sep 17 00:00:00 2001 From: Eddie Maldonado Date: Fri, 10 Jul 2026 11:44:01 -0400 Subject: [PATCH 7/7] refactor: better name boolean values Co-authored-by: Jon Zimbel <63608771+jzimbel-mbta@users.noreply.github.com> --- lib/concentrate/group_filter/shuttle.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/concentrate/group_filter/shuttle.ex b/lib/concentrate/group_filter/shuttle.ex index 2fa05f00..659877ba 100644 --- a/lib/concentrate/group_filter/shuttle.ex +++ b/lib/concentrate/group_filter/shuttle.ex @@ -32,7 +32,7 @@ defmodule Concentrate.GroupFilter.Shuttle do def filter(%TripGroup{} = other, _module), do: other defp shuttle_updates(route_id, direction_id, stus, module) do - initial_state = {false, false} + initial_state = {_has_started? = false, _has_shuttled? = false} stus |> Enum.flat_map_reduce(initial_state, &shuttle_stop(route_id, direction_id, module, &1, &2))