From e6499f3f3687adf8a5f4c4585ecd012ef39dca65 Mon Sep 17 00:00:00 2001 From: Dunya Kirkali Date: Wed, 25 Dec 2024 20:19:57 +0100 Subject: [PATCH 1/6] Add failing test --- test/day12.exs | 68 +++++++++++++++++++++++++++++++++++++++++ test/fixtures/day12.txt | 4 +++ 2 files changed, 72 insertions(+) create mode 100644 test/day12.exs create mode 100644 test/fixtures/day12.txt diff --git a/test/day12.exs b/test/day12.exs new file mode 100644 index 0000000..2d4b3fa --- /dev/null +++ b/test/day12.exs @@ -0,0 +1,68 @@ +defmodule UndirectedTest do + use ExUnit.Case, async: true + + def neighbors({x, y}) do + [ + {x + 1, y}, + {x, y + 1}, + {x - 1, y}, + {x, y - 1} + ] + end + + def to_graph(map) do + map + |> Enum.reduce(Graph.new(type: :undirected), fn {pos, plant}, g -> + pos + |> neighbors() + |> Enum.reduce(Graph.add_vertex(g, pos, plant), fn npos, gg -> + nchar = Map.get(map, npos, "#") + + if nchar == plant do + Graph.add_edge(gg, pos, npos) + else + gg + end + end) + end) + end + + def input(filename) do + filename + |> File.read!() + |> String.split("\n", trim: true) + |> Enum.with_index() + |> Enum.reduce(%{}, fn {line, row}, acc -> + line + |> String.split("", trim: true) + |> Enum.with_index() + |> Enum.reduce(acc, fn {char, col}, accc -> + Map.put(accc, {col, row}, char) + end) + end) + end + + test "failing case" do + graph = + Path.join([__DIR__, "fixtures", "day12.txt"]) + |> input() + |> to_graph() + + result = + graph + |> Graph.vertices() + |> Enum.map(fn vertex -> + Graph.reachable(graph, [vertex]) + |> Enum.sort() + end) + |> Enum.uniq() + + assert result == [ + [{2, 1}, {2, 2}, {3, 2}, {3, 3}], + [{0, 0}, {1, 0}, {2, 0}, {3, 0}], + [{0, 1}, {0, 2}, {1, 1}, {1, 2}], + [{0, 3}, {1, 3}, {2, 3}], + [{3, 1}] + ] + end +end diff --git a/test/fixtures/day12.txt b/test/fixtures/day12.txt new file mode 100644 index 0000000..b41163a --- /dev/null +++ b/test/fixtures/day12.txt @@ -0,0 +1,4 @@ +AAAA +BBCD +BBCC +EEEC From f691212b5023368d7b3f6a7baa621da450ed2f7e Mon Sep 17 00:00:00 2001 From: Dunya Kirkali Date: Sun, 29 Dec 2024 22:04:15 +0100 Subject: [PATCH 2/6] Simplify failing tests --- test/day12.exs | 68 ----------------------------------------- test/fixtures/day12.txt | 4 --- test/reachable_test.exs | 29 ++++++++++++++++++ 3 files changed, 29 insertions(+), 72 deletions(-) delete mode 100644 test/day12.exs delete mode 100644 test/fixtures/day12.txt create mode 100644 test/reachable_test.exs diff --git a/test/day12.exs b/test/day12.exs deleted file mode 100644 index 2d4b3fa..0000000 --- a/test/day12.exs +++ /dev/null @@ -1,68 +0,0 @@ -defmodule UndirectedTest do - use ExUnit.Case, async: true - - def neighbors({x, y}) do - [ - {x + 1, y}, - {x, y + 1}, - {x - 1, y}, - {x, y - 1} - ] - end - - def to_graph(map) do - map - |> Enum.reduce(Graph.new(type: :undirected), fn {pos, plant}, g -> - pos - |> neighbors() - |> Enum.reduce(Graph.add_vertex(g, pos, plant), fn npos, gg -> - nchar = Map.get(map, npos, "#") - - if nchar == plant do - Graph.add_edge(gg, pos, npos) - else - gg - end - end) - end) - end - - def input(filename) do - filename - |> File.read!() - |> String.split("\n", trim: true) - |> Enum.with_index() - |> Enum.reduce(%{}, fn {line, row}, acc -> - line - |> String.split("", trim: true) - |> Enum.with_index() - |> Enum.reduce(acc, fn {char, col}, accc -> - Map.put(accc, {col, row}, char) - end) - end) - end - - test "failing case" do - graph = - Path.join([__DIR__, "fixtures", "day12.txt"]) - |> input() - |> to_graph() - - result = - graph - |> Graph.vertices() - |> Enum.map(fn vertex -> - Graph.reachable(graph, [vertex]) - |> Enum.sort() - end) - |> Enum.uniq() - - assert result == [ - [{2, 1}, {2, 2}, {3, 2}, {3, 3}], - [{0, 0}, {1, 0}, {2, 0}, {3, 0}], - [{0, 1}, {0, 2}, {1, 1}, {1, 2}], - [{0, 3}, {1, 3}, {2, 3}], - [{3, 1}] - ] - end -end diff --git a/test/fixtures/day12.txt b/test/fixtures/day12.txt deleted file mode 100644 index b41163a..0000000 --- a/test/fixtures/day12.txt +++ /dev/null @@ -1,4 +0,0 @@ -AAAA -BBCD -BBCC -EEEC diff --git a/test/reachable_test.exs b/test/reachable_test.exs new file mode 100644 index 0000000..ea1a801 --- /dev/null +++ b/test/reachable_test.exs @@ -0,0 +1,29 @@ +defmodule ReachableTest do + use ExUnit.Case, async: true + + test "reachable in undirected graph" do + graph = + Graph.new(type: :undirected) + |> Graph.add_edges([ + {:a, :b}, + {:b, :c}, + # Separate component + {:d, :e} + ]) + + result = + Graph.vertices(graph) + |> Enum.map(fn vertex -> + Graph.reachable(graph, [vertex]) |> Enum.sort() + end) + |> Enum.uniq() + + assert result == [ + [:a, :b, :c], + [:a, :b, :c], + [:a, :b, :c], + [:d, :e], + [:d, :e] + ] + end +end From af731d39fcfddd103f0919be24f5aea0d2ea226c Mon Sep 17 00:00:00 2001 From: Dunya Kirkali Date: Sun, 29 Dec 2024 22:14:02 +0100 Subject: [PATCH 3/6] Add implementation --- lib/graph/undirected.ex | 22 +++++++++++++++++++ ...reachable_test.exs => undirected_test.exs} | 7 +++--- 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 lib/graph/undirected.ex rename test/{reachable_test.exs => undirected_test.exs} (81%) diff --git a/lib/graph/undirected.ex b/lib/graph/undirected.ex new file mode 100644 index 0000000..bc4f3d8 --- /dev/null +++ b/lib/graph/undirected.ex @@ -0,0 +1,22 @@ +defmodule Graph.Undirected do + @moduledoc false + + def reachable(%Graph{type: :undirected} = graph, vertices) when is_list(vertices) do + vertices + |> collect_reachable(graph, MapSet.new()) + |> MapSet.to_list() + end + + defp collect_reachable([], _graph, seen), do: seen + + defp collect_reachable([vertex | rest], graph, seen) do + if MapSet.member?(seen, vertex) do + collect_reachable(rest, graph, seen) + else + new_seen = MapSet.put(seen, vertex) + neighbors = Graph.neighbors(graph, vertex) + neighbors_seen = collect_reachable(neighbors, graph, new_seen) + collect_reachable(rest, graph, neighbors_seen) + end + end +end diff --git a/test/reachable_test.exs b/test/undirected_test.exs similarity index 81% rename from test/reachable_test.exs rename to test/undirected_test.exs index ea1a801..4ff20b2 100644 --- a/test/reachable_test.exs +++ b/test/undirected_test.exs @@ -1,4 +1,4 @@ -defmodule ReachableTest do +defmodule UndirectedTest do use ExUnit.Case, async: true test "reachable in undirected graph" do @@ -14,9 +14,10 @@ defmodule ReachableTest do result = Graph.vertices(graph) |> Enum.map(fn vertex -> - Graph.reachable(graph, [vertex]) |> Enum.sort() + graph + |> Graph.Undirected.reachable([vertex]) + |> Enum.sort() end) - |> Enum.uniq() assert result == [ [:a, :b, :c], From 9a2698df53b3b8e1e47f0337c0563596b3e7793c Mon Sep 17 00:00:00 2001 From: Dunya Kirkali Date: Sun, 29 Dec 2024 22:19:24 +0100 Subject: [PATCH 4/6] Increase coverage --- test/undirected_test.exs | 90 +++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/test/undirected_test.exs b/test/undirected_test.exs index 4ff20b2..aa721bc 100644 --- a/test/undirected_test.exs +++ b/test/undirected_test.exs @@ -1,30 +1,70 @@ defmodule UndirectedTest do use ExUnit.Case, async: true - test "reachable in undirected graph" do - graph = - Graph.new(type: :undirected) - |> Graph.add_edges([ - {:a, :b}, - {:b, :c}, - # Separate component - {:d, :e} - ]) - - result = - Graph.vertices(graph) - |> Enum.map(fn vertex -> - graph - |> Graph.Undirected.reachable([vertex]) - |> Enum.sort() - end) - - assert result == [ - [:a, :b, :c], - [:a, :b, :c], - [:a, :b, :c], - [:d, :e], - [:d, :e] - ] + describe "reachable in undirected graph" do + test "includes all vertices in connected component" do + graph = + Graph.new(type: :undirected) + |> Graph.add_edges([ + {:a, :b}, + {:b, :c} + ]) + + assert Enum.sort(Graph.Undirected.reachable(graph, [:a])) == [:a, :b, :c] + end + + test "handles multiple starting vertices" do + graph = + Graph.new(type: :undirected) + |> Graph.add_edges([ + {:a, :b}, + {:c, :d} + ]) + + assert Enum.sort(Graph.Undirected.reachable(graph, [:a, :c])) == [:a, :b, :c, :d] + end + + test "returns only starting vertex if isolated" do + graph = + Graph.new(type: :undirected) + |> Graph.add_vertex(:a) + |> Graph.add_vertex(:b) + + assert Graph.Undirected.reachable(graph, [:a]) == [:a] + end + + test "handles empty starting set" do + graph = + Graph.new(type: :undirected) + |> Graph.add_vertex(:a) + + assert Graph.Undirected.reachable(graph, []) == [] + end + + test "handles multiple components" do + graph = + Graph.new(type: :undirected) + |> Graph.add_edges([ + {:a, :b}, + {:b, :c}, + {:d, :e} + ]) + + result = + Graph.vertices(graph) + |> Enum.map(fn vertex -> + graph + |> Graph.Undirected.reachable([vertex]) + |> Enum.sort() + end) + + assert result == [ + [:a, :b, :c], + [:a, :b, :c], + [:a, :b, :c], + [:d, :e], + [:d, :e] + ] + end end end From 93bb05d3ef2a48103b0ea3cd1119f1e0b253f917 Mon Sep 17 00:00:00 2001 From: Dunya Kirkali Date: Sun, 29 Dec 2024 22:21:57 +0100 Subject: [PATCH 5/6] Increase coverage --- test/directed_test.exs | 36 ++++++++++++++++++++++++++++++++++++ test/undirected_test.exs | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 test/directed_test.exs diff --git a/test/directed_test.exs b/test/directed_test.exs new file mode 100644 index 0000000..512bb4c --- /dev/null +++ b/test/directed_test.exs @@ -0,0 +1,36 @@ +defmodule DirectedTest do + use ExUnit.Case, async: true + + describe "reachable/2" do + test "returns empty list for empty graph" do + assert Graph.new() |> Graph.reachable([]) == [] + end + + test "returns starting vertex if no edges" do + graph = Graph.new() |> Graph.add_vertex(:a) + assert Graph.reachable(graph, [:a]) == [:a] + end + + test "returns reachable vertices" do + graph = + Graph.new() + |> Graph.add_edge(:a, :b) + |> Graph.add_edge(:b, :c) + |> Graph.add_edge(:d, :e) + + assert Graph.reachable(graph, [:a]) |> Enum.sort() == [:a, :b, :c] + assert Graph.reachable(graph, [:d]) |> Enum.sort() == [:d, :e] + assert Graph.reachable(graph, [:a, :d]) |> Enum.sort() == [:a, :b, :c, :d, :e] + end + + test "handles cycles" do + graph = + Graph.new() + |> Graph.add_edge(:a, :b) + |> Graph.add_edge(:b, :c) + |> Graph.add_edge(:c, :a) + + assert Graph.reachable(graph, [:a]) |> Enum.sort() == [:a, :b, :c] + end + end +end diff --git a/test/undirected_test.exs b/test/undirected_test.exs index aa721bc..a58d8a0 100644 --- a/test/undirected_test.exs +++ b/test/undirected_test.exs @@ -1,7 +1,7 @@ defmodule UndirectedTest do use ExUnit.Case, async: true - describe "reachable in undirected graph" do + describe "reachable/2" do test "includes all vertices in connected component" do graph = Graph.new(type: :undirected) From 288a2d9c85e337a625d5a52bfe2d2b962fb42aa2 Mon Sep 17 00:00:00 2001 From: Dunya Kirkali Date: Sun, 29 Dec 2024 22:22:12 +0100 Subject: [PATCH 6/6] Use the correct implementation depending on type --- lib/graph.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/graph.ex b/lib/graph.ex index b66670f..8e8c959 100644 --- a/lib/graph.ex +++ b/lib/graph.ex @@ -1579,9 +1579,15 @@ defmodule Graph do ...> g = Graph.add_edges(g, [{:a, :b}, {:a, :c}, {:b, :c}, {:c, :d}]) ...> Graph.reachable(g, [:a]) [:d, :c, :b, :a] + + iex> g = Graph.new(type: :undirected) |> Graph.add_vertices([:a, :b, :c, :d]) + ...> g = Graph.add_edges(g, [{:a, :b}, {:c, :d}]) + ...> Graph.reachable(g, [:a]) + [:b, :a] """ @spec reachable(t, [vertex]) :: [[vertex]] - defdelegate reachable(g, vs), to: Graph.Directed + def reachable(%__MODULE__{type: :undirected} = g, vs), do: Graph.Undirected.reachable(g, vs) + def reachable(%__MODULE__{} = g, vs), do: Graph.Directed.reachable(g, vs) @doc """ Returns an unsorted list of vertices from the graph, such that for each vertex in the list (call it `v`),