From fabfb999d10f9859ffff5bf775397961671b8326 Mon Sep 17 00:00:00 2001 From: Jeremie Gillet Date: Fri, 7 Mar 2025 11:13:47 +0900 Subject: [PATCH 1/5] run generator mix format from exercise folder --- bin/bootstrap_practice_exercise.exs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/bin/bootstrap_practice_exercise.exs b/bin/bootstrap_practice_exercise.exs index 94cc5d4bc..5dfd41c8c 100644 --- a/bin/bootstrap_practice_exercise.exs +++ b/bin/bootstrap_practice_exercise.exs @@ -143,8 +143,9 @@ module = ## Step 1: create folder structure -Mix.Generator.create_directory("exercises/practice/#{exercise}/lib") -Mix.Generator.create_directory("exercises/practice/#{exercise}/test") +File.cd!("exercises/practice/#{exercise}") +Mix.Generator.create_directory("lib") +Mix.Generator.create_directory("test") ## Step 2: add common files @@ -156,7 +157,7 @@ format = """ ] """ -Mix.Generator.create_file("exercises/practice/#{exercise}/.formatter.exs", format) +Mix.Generator.create_file(".formatter.exs", format) # mix.exs mix = """ @@ -189,7 +190,7 @@ defmodule #{module}.MixProject do end """ -Mix.Generator.create_file("exercises/practice/#{exercise}/mix.exs", mix) +Mix.Generator.create_file("mix.exs", mix) # .gitignore gitignore = """ @@ -221,7 +222,7 @@ erl_crash.dump /tmp/ """ -Mix.Generator.create_file("exercises/practice/#{exercise}/.gitignore", gitignore) +Mix.Generator.create_file(".gitignore", gitignore) # test/test_helper.exs test_helper = """ @@ -229,7 +230,7 @@ ExUnit.start() ExUnit.configure(exclude: :pending, trace: true) """ -Mix.Generator.create_file("exercises/practice/#{exercise}/test/test_helper.exs", test_helper) +Mix.Generator.create_file("test/test_helper.exs", test_helper) ## Step 3: write files that depend on problem specifications @@ -256,10 +257,10 @@ defmodule #{module} do end """ -path = "exercises/practice/#{exercise}/lib/#{exercise_snake_case}.ex" +path = "lib/#{exercise_snake_case}.ex" Mix.Generator.create_file(path, lib_file) -Mix.Generator.copy_file(path, "exercises/practice/#{exercise}/.meta/example.ex") +Mix.Generator.copy_file(path, ".meta/example.ex") # Generating test file test_file = @@ -273,8 +274,8 @@ test_file = """ |> String.replace("@tag", "# @tag", global: false) -path = "exercises/practice/#{exercise}/test/#{exercise_snake_case}_test.exs" +path = "test/#{exercise_snake_case}_test.exs" Mix.Generator.create_file(path, test_file) # mix format all files -Mix.Tasks.Format.run(["exercises/practice/#{exercise}/**/*.{ex,exs}"]) +Mix.Tasks.Format.run(["**/*.{ex,exs}"]) From ba8629d86b94eeec8bf9c45516f4d32e383d46e0 Mon Sep 17 00:00:00 2001 From: Jeremie Gillet Date: Fri, 7 Mar 2025 15:07:58 +0900 Subject: [PATCH 2/5] inspect limit to infinity --- bin/bootstrap_practice_exercise.exs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/bootstrap_practice_exercise.exs b/bin/bootstrap_practice_exercise.exs index 5dfd41c8c..64af223f7 100644 --- a/bin/bootstrap_practice_exercise.exs +++ b/bin/bootstrap_practice_exercise.exs @@ -73,24 +73,24 @@ defmodule Generate do "# #{Enum.map_join(values, "\n# ", &String.trim/1)}" {field, values} when is_list(values) -> - "#\n# --#{field} --\n# #{Enum.map_join(values, "\n# ", &inspect/1)}" + "#\n# --#{field} --\n# #{Enum.map_join(values, "\n# ", &inspect(&1, limit: :infinity))}" {field, value} -> - "#\n# -- #{field} --\n# #{inspect(value)}" + "#\n# -- #{field} --\n# #{inspect(value, limit: :infinity)}" end) end def print_input(%{} = input), do: Enum.map_join(input, "\n", fn {variable, value} -> - "#{Macro.underscore(variable)} = #{inspect(value)}" + "#{Macro.underscore(variable)} = #{inspect(value, limit: :infinity)}" end) - def print_input(input), do: "input = #{inspect(input)}" + def print_input(input), do: "input = #{inspect(input, limit: :infinity)}" - def print_expected(%{"error" => err}, _error), do: "{:error, #{inspect(err)}}" - def print_expected(expected, true), do: "{:ok, #{inspect(expected)}}" - def print_expected(expected, false), do: inspect(expected) + def print_expected(%{"error" => err}, _error), do: "{:error, #{inspect(err, limit: :infinity)}}" + def print_expected(expected, true), do: "{:ok, #{inspect(expected, limit: :infinity)}}" + def print_expected(expected, false), do: inspect(expected, limit: :infinity) def print_test_case( %{"description" => description, "cases" => sub_cases} = category, From e615a1fc40cebe87dc1139f9054aebc3bfe6ced0 Mon Sep 17 00:00:00 2001 From: Jeremie Gillet Date: Fri, 7 Mar 2025 15:08:48 +0900 Subject: [PATCH 3/5] WIP --- config.json | 8 + .../relative-distance/.docs/instructions.md | 34 +++ .../relative-distance/.docs/introduction.md | 12 + .../practice/relative-distance/.formatter.exs | 4 + .../practice/relative-distance/.gitignore | 26 ++ .../relative-distance/.meta/config.json | 17 ++ .../relative-distance/.meta/example.ex | 65 +++++ .../lib/relative_distance.ex | 43 ++++ exercises/practice/relative-distance/mix.exs | 27 +++ .../test/relative_distance_test.exs | 223 ++++++++++++++++++ .../relative-distance/test/test_helper.exs | 2 + 11 files changed, 461 insertions(+) create mode 100644 exercises/practice/relative-distance/.docs/instructions.md create mode 100644 exercises/practice/relative-distance/.docs/introduction.md create mode 100644 exercises/practice/relative-distance/.formatter.exs create mode 100644 exercises/practice/relative-distance/.gitignore create mode 100644 exercises/practice/relative-distance/.meta/config.json create mode 100644 exercises/practice/relative-distance/.meta/example.ex create mode 100644 exercises/practice/relative-distance/lib/relative_distance.ex create mode 100644 exercises/practice/relative-distance/mix.exs create mode 100644 exercises/practice/relative-distance/test/relative_distance_test.exs create mode 100644 exercises/practice/relative-distance/test/test_helper.exs diff --git a/config.json b/config.json index 0ba0c0070..4a24a8aab 100644 --- a/config.json +++ b/config.json @@ -3036,6 +3036,14 @@ "exceptions" ], "difficulty": 10 + }, + { + "slug": "relative-distance", + "name": "Relative Distance", + "uuid": "ba1d165a-774f-4b8f-9763-2d4279769e75", + "practices": [], + "prerequisites": [], + "difficulty": 5 } ], "foregone": [ diff --git a/exercises/practice/relative-distance/.docs/instructions.md b/exercises/practice/relative-distance/.docs/instructions.md new file mode 100644 index 000000000..9be1b2a90 --- /dev/null +++ b/exercises/practice/relative-distance/.docs/instructions.md @@ -0,0 +1,34 @@ +# Instructions + +Your task is to determine the degree of separation between two individuals in a family tree. + +- You will be given an input, with all parent names and their children. +- Each name is unique, a child _can_ have one or two parents. +- The degree of separation is defined as the shortest number of connections from one person to another. +- If two individuals are not connected, return a value that represents "no known relationship." + Please see the test cases for the actual implementation. + +## Example + +Given the following family tree: + +```text + ┌──────────┐ ┌──────────┐ ┌───────────┐ + │ Helena │ │ Erdős │ │ Shusaku │ + └───┬───┬──┘ └─────┬────┘ └──────┬────┘ + ┌───┘ └───────┐ └──────┬──────┘ + ▼ ▼ ▼ +┌──────────┐ ┌────────┐ ┌──────────┐ +│ Isla │ │ Tariq │ │ Kevin │ +└────┬─────┘ └────┬───┘ └──────────┘ + ▼ ▼ +┌─────────┐ ┌────────┐ +│ Uma │ │ Morphy │ +└─────────┘ └────────┘ +``` + +The degree of separation between Tariq and Uma is 3 (Tariq → Helena → Isla → Uma). +There's no known relationship between Isla and [Kevin][six-bacons], as there is no connection in the given data. +The degree of separation between Uma and Isla is 1. + +[six-bacons]: https://en.m.wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon diff --git a/exercises/practice/relative-distance/.docs/introduction.md b/exercises/practice/relative-distance/.docs/introduction.md new file mode 100644 index 000000000..cb9fee6c7 --- /dev/null +++ b/exercises/practice/relative-distance/.docs/introduction.md @@ -0,0 +1,12 @@ +# Introduction + +You've been hired to develop **Noble Knots**, the hottest new dating app for nobility! +With centuries of royal intermarriage, things have gotten… _complicated_. +To avoid any _oops-we're-twins_ situations, your job is to build a system that checks how closely two people are related. + +Noble Knots is inspired by Iceland's "[Islendinga-App][islendiga-app]," which is backed up by a database that traces all known family connections between Icelanders from the time of the settlement of Iceland. +Your algorithm will determine the **degree of separation** between two individuals in the royal family tree. + +Will your app help crown a perfect match? + +[islendiga-app]: http://www.islendingaapp.is/information-in-english/ diff --git a/exercises/practice/relative-distance/.formatter.exs b/exercises/practice/relative-distance/.formatter.exs new file mode 100644 index 000000000..d2cda26ed --- /dev/null +++ b/exercises/practice/relative-distance/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/exercises/practice/relative-distance/.gitignore b/exercises/practice/relative-distance/.gitignore new file mode 100644 index 000000000..93d4668ce --- /dev/null +++ b/exercises/practice/relative-distance/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +relative_distance-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/exercises/practice/relative-distance/.meta/config.json b/exercises/practice/relative-distance/.meta/config.json new file mode 100644 index 000000000..cb36fe737 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [], + "files": { + "solution": [ + "lib/relative_distance.ex" + ], + "test": [ + "test/relative_distance_test.exs" + ], + "example": [ + ".meta/example.ex" + ] + }, + "blurb": "Given a family tree, calculate the degree of separation.", + "source": "vaeng", + "source_url": "https://github.com/exercism/problem-specifications/pull/2537" +} diff --git a/exercises/practice/relative-distance/.meta/example.ex b/exercises/practice/relative-distance/.meta/example.ex new file mode 100644 index 000000000..28822f909 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/example.ex @@ -0,0 +1,65 @@ +defmodule RelativeDistance do + @doc """ + TODO: add function description and replace types in @spec + """ + @spec degree_of_separation( + family_tree :: %{ + ian: TODO.t(), + tariq: TODO.t(), + umar: TODO.t(), + yuki: TODO.t(), + viktor: TODO.t(), + pedro: TODO.t(), + wang: TODO.t(), + tomoko: TODO.t(), + diego: TODO.t(), + elif: TODO.t(), + liam: TODO.t(), + farah: TODO.t(), + jing: TODO.t(), + aiko: TODO.t(), + uma: TODO.t(), + hana: TODO.t(), + zara: TODO.t(), + zane: TODO.t(), + sofia: TODO.t(), + boris: TODO.t(), + hassan: TODO.t(), + aditi: TODO.t(), + ravi: TODO.t(), + leila: TODO.t(), + mateo: TODO.t(), + giorgio: TODO.t(), + vera: TODO.t(), + nia: TODO.t(), + olga: TODO.t(), + rami: TODO.t(), + qi: TODO.t(), + bao: TODO.t(), + elias: TODO.t(), + carlos: TODO.t(), + gustavo: TODO.t(), + oscar: TODO.t(), + isla: TODO.t(), + quynh: TODO.t(), + khadija: TODO.t(), + sven: TODO.t(), + javier: TODO.t(), + fatima: TODO.t(), + xia: TODO.t(), + yassin: TODO.t(), + wyatt: TODO.t(), + dalia: TODO.t(), + mina: TODO.t(), + noah: TODO.t(), + priya: TODO.t(), + xiomara: TODO.t(), + celine: TODO.t(), + kaito: TODO.t() + }, + person_a :: TODO.t(), + person_b :: TODO.t() + ) :: TODO.t() + def degree_of_separation(family_tree, person_a, person_b) do + end +end diff --git a/exercises/practice/relative-distance/lib/relative_distance.ex b/exercises/practice/relative-distance/lib/relative_distance.ex new file mode 100644 index 000000000..3fb66c2eb --- /dev/null +++ b/exercises/practice/relative-distance/lib/relative_distance.ex @@ -0,0 +1,43 @@ +defmodule RelativeDistance do + @doc """ + Find the degree of separation of two members given a given family tree. + """ + @spec degree_of_separation( + family_tree :: %{String.t() => [String.t()]}, + person_a :: String.t(), + person_b :: String.t() + ) :: nil | pos_integer() + def degree_of_separation(family_tree, person_a, person_b) do + family_tree + |> build_family_graph() + |> find_degree(person_b, [{person_a, 0}], MapSet.new()) + end + + defp build_family_graph(family_tree) do + for {parent, children} <- family_tree, child <- children, reduce: %{} do + graph -> + siblings = children |> MapSet.new() |> MapSet.delete(child) + + graph + |> Map.update(parent, MapSet.new([child]), &MapSet.put(&1, child)) + |> Map.update(child, MapSet.new([parent]), &MapSet.put(&1, parent)) + # sibling rule, inferred from "Sibling relationship" + |> Map.update(child, siblings, &MapSet.union(&1, siblings)) + end + end + + defp find_degree(_graph, _goal, [], _history), do: nil + + defp find_degree(_graph, goal, [{goal, count} | _], _history), do: count + + defp find_degree(graph, goal, [{person, count} | rest], history) do + history = MapSet.put(history, person) + + next_steps = + graph[person] + |> Enum.reject(fn relative -> MapSet.member?(history, relative) end) + |> Enum.map(fn relative -> {relative, count + 1} end) + + find_degree(graph, goal, rest ++ next_steps, history) + end +end diff --git a/exercises/practice/relative-distance/mix.exs b/exercises/practice/relative-distance/mix.exs new file mode 100644 index 000000000..8034d82dd --- /dev/null +++ b/exercises/practice/relative-distance/mix.exs @@ -0,0 +1,27 @@ +defmodule RelativeDistance.MixProject do + use Mix.Project + + def project do + [ + app: :relative_distance, + version: "0.1.0", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/exercises/practice/relative-distance/test/relative_distance_test.exs b/exercises/practice/relative-distance/test/relative_distance_test.exs new file mode 100644 index 000000000..a7e298c26 --- /dev/null +++ b/exercises/practice/relative-distance/test/relative_distance_test.exs @@ -0,0 +1,223 @@ +defmodule RelativeDistanceTest do + use ExUnit.Case + + # TODO: preserve the ordering of the original specs + + # @tag :pending + test "Direct parent-child relation" do + family_tree = %{"Tomoko" => ["Aditi"], "Vera" => ["Tomoko"]} + person_a = "Vera" + person_b = "Tomoko" + assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 1 + end + + @tag :pending + test "Sibling relationship" do + family_tree = %{"Dalia" => ["Olga", "Yassin"]} + person_a = "Olga" + person_b = "Yassin" + assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 1 + end + + @tag :pending + test "Two degrees of separation, grandchild" do + family_tree = %{"Khadija" => ["Mateo"], "Mateo" => ["Rami"]} + person_a = "Khadija" + person_b = "Rami" + assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 2 + end + + @tag :pending + test "Unrelated individuals" do + family_tree = %{"Kaito" => ["Elif"], "Priya" => ["Rami"]} + person_a = "Priya" + person_b = "Kaito" + assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == nil + end + + @tag :pending + test "Complex graph, cousins" do + family_tree = %{ + "Kaito" => ["Xia"], + "Celine" => ["Priya"], + "Xiomara" => ["Kaito"], + "Priya" => ["Cai"], + "Noah" => ["Xiomara"], + "Mina" => ["Viktor", "Wang"], + "Dalia" => ["Hassan", "Isla"], + "Wyatt" => ["Jun"], + "Yassin" => ["Lucia"], + "Xia" => ["Kim"], + "Fatima" => ["Khadija", "Liam"], + "Javier" => ["Quynh", "Ravi"], + "Sven" => ["Fabio"], + "Khadija" => ["Sofia"], + "Quynh" => ["Boris"], + "Isla" => ["Pedro"], + "Oscar" => ["Bianca"], + "Gustavo" => ["Mina"], + "Carlos" => ["Fatima", "Gustavo"], + "Elias" => ["Javier"], + "Bao" => ["Dalia", "Elias"], + "Qi" => ["Dimitri"], + "Rami" => ["Ewa"], + "Olga" => ["Yuki"], + "Nia" => ["Antonio"], + "Vera" => ["Igor"], + "Giorgio" => ["Tomoko"], + "Mateo" => ["Zara"], + "Leila" => ["Yassin"], + "Ravi" => ["Celine"], + "Aditi" => ["Nia"], + "Hassan" => ["Noah", "Olga"], + "Boris" => ["Oscar"], + "Sofia" => ["Diego", "Elif"], + "Zane" => ["Mateo"], + "Zara" => ["Mohammed"], + "Hana" => ["Umar"], + "Uma" => ["Giorgio"], + "Aiko" => ["Bao", "Carlos"], + "Jing" => ["Wyatt"], + "Farah" => ["Sven"], + "Liam" => ["Tariq", "Uma"], + "Elif" => ["Rami"], + "Diego" => ["Qi"], + "Tomoko" => ["Gabriela"], + "Wang" => ["Jing"], + "Pedro" => ["Zane", "Aditi"], + "Viktor" => ["Hana", "Ian"], + "Yuki" => ["Leila"], + "Umar" => ["Helena"], + "Tariq" => ["Farah"], + "Ian" => ["Vera"] + } + + person_a = "Dimitri" + person_b = "Fabio" + assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 4 + end + + @tag :pending + test "Complex graph, no shortcut, far removed nephew" do + family_tree = %{ + "Kaito" => ["Xia"], + "Celine" => ["Priya"], + "Xiomara" => ["Kaito"], + "Priya" => ["Cai"], + "Noah" => ["Xiomara"], + "Mina" => ["Viktor", "Wang"], + "Dalia" => ["Hassan", "Isla"], + "Wyatt" => ["Jun"], + "Yassin" => ["Lucia"], + "Xia" => ["Kim"], + "Fatima" => ["Khadija", "Liam"], + "Javier" => ["Quynh", "Ravi"], + "Sven" => ["Fabio"], + "Khadija" => ["Sofia"], + "Quynh" => ["Boris"], + "Isla" => ["Pedro"], + "Oscar" => ["Bianca"], + "Gustavo" => ["Mina"], + "Carlos" => ["Fatima", "Gustavo"], + "Elias" => ["Javier"], + "Bao" => ["Dalia", "Elias"], + "Qi" => ["Dimitri"], + "Rami" => ["Ewa"], + "Olga" => ["Yuki"], + "Nia" => ["Antonio"], + "Vera" => ["Igor"], + "Giorgio" => ["Tomoko"], + "Mateo" => ["Zara"], + "Leila" => ["Yassin"], + "Ravi" => ["Celine"], + "Aditi" => ["Nia"], + "Hassan" => ["Noah", "Olga"], + "Boris" => ["Oscar"], + "Sofia" => ["Diego", "Elif"], + "Zane" => ["Mateo"], + "Zara" => ["Mohammed"], + "Hana" => ["Umar"], + "Uma" => ["Giorgio"], + "Aiko" => ["Bao", "Carlos"], + "Jing" => ["Wyatt"], + "Farah" => ["Sven"], + "Liam" => ["Tariq", "Uma"], + "Elif" => ["Rami"], + "Diego" => ["Qi"], + "Tomoko" => ["Gabriela"], + "Wang" => ["Jing"], + "Pedro" => ["Zane", "Aditi"], + "Viktor" => ["Hana", "Ian"], + "Yuki" => ["Leila"], + "Umar" => ["Helena"], + "Tariq" => ["Farah"], + "Ian" => ["Vera"] + } + + person_a = "Lucia" + person_b = "Jun" + assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 15 + end + + @tag :pending + test "Complex graph, some shortcuts, cross-down and cross-up, cousins three times removed" do + family_tree = %{ + "Kaito" => ["Xia"], + "Celine" => ["Priya"], + "Xiomara" => ["Kaito"], + "Priya" => ["Cai"], + "Noah" => ["Xiomara"], + "Mina" => ["Viktor", "Wang"], + "Dalia" => ["Hassan", "Isla"], + "Wyatt" => ["Jun"], + "Yassin" => ["Lucia"], + "Xia" => ["Kim"], + "Fatima" => ["Khadija", "Liam"], + "Javier" => ["Quynh", "Ravi"], + "Sven" => ["Fabio"], + "Khadija" => ["Viktor"], + "Quynh" => ["Boris"], + "Isla" => ["Pedro"], + "Oscar" => ["Bianca"], + "Gustavo" => ["Mina"], + "Carlos" => ["Fatima", "Gustavo"], + "Elias" => ["Javier"], + "Bao" => ["Dalia", "Elias"], + "Qi" => ["Dimitri"], + "Rami" => ["Ewa"], + "Olga" => ["Yuki"], + "Nia" => ["Antonio"], + "Vera" => ["Igor"], + "Giorgio" => ["Tomoko"], + "Mateo" => ["Zara"], + "Leila" => ["Yassin"], + "Ravi" => ["Celine"], + "Aditi" => ["Nia"], + "Hassan" => ["Noah", "Olga"], + "Boris" => ["Oscar"], + "Sofia" => ["Diego", "Elif"], + "Zane" => ["Mateo"], + "Zara" => ["Mohammed"], + "Hana" => ["Umar"], + "Uma" => ["Giorgio"], + "Aiko" => ["Bao", "Carlos"], + "Jing" => ["Wyatt"], + "Farah" => ["Sven"], + "Liam" => ["Tariq", "Uma"], + "Elif" => ["Rami"], + "Diego" => ["Qi"], + "Tomoko" => ["Gabriela"], + "Wang" => ["Jing"], + "Pedro" => ["Zane", "Aditi"], + "Viktor" => ["Hana", "Ian"], + "Yuki" => ["Leila"], + "Umar" => ["Helena"], + "Tariq" => ["Farah"], + "Ian" => ["Vera"] + } + + person_a = "Tomoko" + person_b = "Qi" + assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 8 + end +end diff --git a/exercises/practice/relative-distance/test/test_helper.exs b/exercises/practice/relative-distance/test/test_helper.exs new file mode 100644 index 000000000..35fc5bff8 --- /dev/null +++ b/exercises/practice/relative-distance/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true) From 13814c6e577c40c7d9abf94eec2fed66ae787f66 Mon Sep 17 00:00:00 2001 From: Jeremie Gillet Date: Fri, 4 Apr 2025 09:15:37 +0900 Subject: [PATCH 4/5] finish up with updatet canonical data --- config.json | 26 +- .../relative-distance/.docs/instructions.md | 5 + .../relative-distance/.meta/config.json | 4 +- .../relative-distance/.meta/example.ex | 93 +++---- .../relative-distance/.meta/tests.toml | 31 +++ .../lib/relative_distance.ex | 31 --- .../test/relative_distance_test.exs | 257 +++++++++--------- 7 files changed, 219 insertions(+), 228 deletions(-) create mode 100644 exercises/practice/relative-distance/.meta/tests.toml diff --git a/config.json b/config.json index 4a24a8aab..db989ec99 100644 --- a/config.json +++ b/config.json @@ -2445,6 +2445,24 @@ ], "difficulty": 6 }, + { + "slug": "relative-distance", + "name": "Relative Distance", + "uuid": "ba1d165a-774f-4b8f-9763-2d4279769e75", + "practices": [ + "recursion" + ], + "prerequisites": [ + "maps", + "tuples", + "lists", + "list-comprehensions", + "enum", + "multiple-clause-functions", + "nil" + ], + "difficulty": 6 + }, { "slug": "robot-simulator", "name": "Robot Simulator", @@ -3036,14 +3054,6 @@ "exceptions" ], "difficulty": 10 - }, - { - "slug": "relative-distance", - "name": "Relative Distance", - "uuid": "ba1d165a-774f-4b8f-9763-2d4279769e75", - "practices": [], - "prerequisites": [], - "difficulty": 5 } ], "foregone": [ diff --git a/exercises/practice/relative-distance/.docs/instructions.md b/exercises/practice/relative-distance/.docs/instructions.md index 9be1b2a90..91dd5762b 100644 --- a/exercises/practice/relative-distance/.docs/instructions.md +++ b/exercises/practice/relative-distance/.docs/instructions.md @@ -31,4 +31,9 @@ The degree of separation between Tariq and Uma is 3 (Tariq → Helena → Isla There's no known relationship between Isla and [Kevin][six-bacons], as there is no connection in the given data. The degree of separation between Uma and Isla is 1. +~~~~exercism/note +Isla and Tariq are siblings and have a separation of 1. +Similarly, this implementation would report a separation of 2 from you to your father's brother. +~~~~ + [six-bacons]: https://en.m.wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon diff --git a/exercises/practice/relative-distance/.meta/config.json b/exercises/practice/relative-distance/.meta/config.json index cb36fe737..01392c18a 100644 --- a/exercises/practice/relative-distance/.meta/config.json +++ b/exercises/practice/relative-distance/.meta/config.json @@ -1,5 +1,7 @@ { - "authors": [], + "authors": [ + "jiegillet" + ], "files": { "solution": [ "lib/relative_distance.ex" diff --git a/exercises/practice/relative-distance/.meta/example.ex b/exercises/practice/relative-distance/.meta/example.ex index 28822f909..e0d7c323d 100644 --- a/exercises/practice/relative-distance/.meta/example.ex +++ b/exercises/practice/relative-distance/.meta/example.ex @@ -1,65 +1,42 @@ defmodule RelativeDistance do @doc """ - TODO: add function description and replace types in @spec + Find the degree of separation of two members given a given family tree. """ @spec degree_of_separation( - family_tree :: %{ - ian: TODO.t(), - tariq: TODO.t(), - umar: TODO.t(), - yuki: TODO.t(), - viktor: TODO.t(), - pedro: TODO.t(), - wang: TODO.t(), - tomoko: TODO.t(), - diego: TODO.t(), - elif: TODO.t(), - liam: TODO.t(), - farah: TODO.t(), - jing: TODO.t(), - aiko: TODO.t(), - uma: TODO.t(), - hana: TODO.t(), - zara: TODO.t(), - zane: TODO.t(), - sofia: TODO.t(), - boris: TODO.t(), - hassan: TODO.t(), - aditi: TODO.t(), - ravi: TODO.t(), - leila: TODO.t(), - mateo: TODO.t(), - giorgio: TODO.t(), - vera: TODO.t(), - nia: TODO.t(), - olga: TODO.t(), - rami: TODO.t(), - qi: TODO.t(), - bao: TODO.t(), - elias: TODO.t(), - carlos: TODO.t(), - gustavo: TODO.t(), - oscar: TODO.t(), - isla: TODO.t(), - quynh: TODO.t(), - khadija: TODO.t(), - sven: TODO.t(), - javier: TODO.t(), - fatima: TODO.t(), - xia: TODO.t(), - yassin: TODO.t(), - wyatt: TODO.t(), - dalia: TODO.t(), - mina: TODO.t(), - noah: TODO.t(), - priya: TODO.t(), - xiomara: TODO.t(), - celine: TODO.t(), - kaito: TODO.t() - }, - person_a :: TODO.t(), - person_b :: TODO.t() - ) :: TODO.t() + family_tree :: %{String.t() => [String.t()]}, + person_a :: String.t(), + person_b :: String.t() + ) :: nil | pos_integer() def degree_of_separation(family_tree, person_a, person_b) do + family_tree + |> build_family_graph() + |> find_separation(person_b, [{person_a, 0}], MapSet.new()) + end + + defp build_family_graph(family_tree) do + for {parent, children} <- family_tree, child <- children, reduce: %{} do + graph -> + siblings = children |> MapSet.new() |> MapSet.delete(child) + + graph + |> Map.update(parent, MapSet.new([child]), &MapSet.put(&1, child)) + |> Map.update(child, MapSet.new([parent]), &MapSet.put(&1, parent)) + |> Map.update(child, siblings, &MapSet.union(&1, siblings)) + end + end + + defp find_separation(_graph, _goal, [], _history), do: nil + + defp find_separation(_graph, goal, [{goal, count} | _], _history), do: count + + defp find_separation(graph, goal, [{person, count} | rest], history) do + history = MapSet.put(history, person) + + next_steps = + graph[person] + |> Enum.reject(fn relative -> MapSet.member?(history, relative) end) + |> Enum.map(fn relative -> {relative, count + 1} end) + + find_separation(graph, goal, rest ++ next_steps, history) end end diff --git a/exercises/practice/relative-distance/.meta/tests.toml b/exercises/practice/relative-distance/.meta/tests.toml new file mode 100644 index 000000000..66c91ba09 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/tests.toml @@ -0,0 +1,31 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[4a1ded74-5d32-47fb-8ae5-321f51d06b5b] +description = "Direct parent-child relation" + +[30d17269-83e9-4f82-a0d7-8ef9656d8dce] +description = "Sibling relationship" + +[8dffa27d-a8ab-496d-80b3-2f21c77648b5] +description = "Two degrees of separation, grandchild" + +[34e56ec1-d528-4a42-908e-020a4606ee60] +description = "Unrelated individuals" + +[93ffe989-bad2-48c4-878f-3acb1ce2611b] +description = "Complex graph, cousins" + +[2cc2e76b-013a-433c-9486-1dbe29bf06e5] +description = "Complex graph, no shortcut, far removed nephew" + +[46c9fbcb-e464-455f-a718-049ea3c7400a] +description = "Complex graph, some shortcuts, cross-down and cross-up, cousins several times removed, with unrelated family tree" diff --git a/exercises/practice/relative-distance/lib/relative_distance.ex b/exercises/practice/relative-distance/lib/relative_distance.ex index 3fb66c2eb..a063157b8 100644 --- a/exercises/practice/relative-distance/lib/relative_distance.ex +++ b/exercises/practice/relative-distance/lib/relative_distance.ex @@ -8,36 +8,5 @@ defmodule RelativeDistance do person_b :: String.t() ) :: nil | pos_integer() def degree_of_separation(family_tree, person_a, person_b) do - family_tree - |> build_family_graph() - |> find_degree(person_b, [{person_a, 0}], MapSet.new()) - end - - defp build_family_graph(family_tree) do - for {parent, children} <- family_tree, child <- children, reduce: %{} do - graph -> - siblings = children |> MapSet.new() |> MapSet.delete(child) - - graph - |> Map.update(parent, MapSet.new([child]), &MapSet.put(&1, child)) - |> Map.update(child, MapSet.new([parent]), &MapSet.put(&1, parent)) - # sibling rule, inferred from "Sibling relationship" - |> Map.update(child, siblings, &MapSet.union(&1, siblings)) - end - end - - defp find_degree(_graph, _goal, [], _history), do: nil - - defp find_degree(_graph, goal, [{goal, count} | _], _history), do: count - - defp find_degree(graph, goal, [{person, count} | rest], history) do - history = MapSet.put(history, person) - - next_steps = - graph[person] - |> Enum.reject(fn relative -> MapSet.member?(history, relative) end) - |> Enum.map(fn relative -> {relative, count + 1} end) - - find_degree(graph, goal, rest ++ next_steps, history) end end diff --git a/exercises/practice/relative-distance/test/relative_distance_test.exs b/exercises/practice/relative-distance/test/relative_distance_test.exs index a7e298c26..eaa452db8 100644 --- a/exercises/practice/relative-distance/test/relative_distance_test.exs +++ b/exercises/practice/relative-distance/test/relative_distance_test.exs @@ -1,11 +1,9 @@ defmodule RelativeDistanceTest do use ExUnit.Case - # TODO: preserve the ordering of the original specs - # @tag :pending test "Direct parent-child relation" do - family_tree = %{"Tomoko" => ["Aditi"], "Vera" => ["Tomoko"]} + family_tree = %{"Vera" => ["Tomoko"], "Tomoko" => ["Aditi"]} person_a = "Vera" person_b = "Tomoko" assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 1 @@ -38,186 +36,185 @@ defmodule RelativeDistanceTest do @tag :pending test "Complex graph, cousins" do family_tree = %{ - "Kaito" => ["Xia"], - "Celine" => ["Priya"], - "Xiomara" => ["Kaito"], - "Priya" => ["Cai"], - "Noah" => ["Xiomara"], - "Mina" => ["Viktor", "Wang"], + "Aiko" => ["Bao", "Carlos"], + "Bao" => ["Dalia", "Elias"], + "Carlos" => ["Fatima", "Gustavo"], "Dalia" => ["Hassan", "Isla"], - "Wyatt" => ["Jun"], - "Yassin" => ["Lucia"], - "Xia" => ["Kim"], + "Elias" => ["Javier"], "Fatima" => ["Khadija", "Liam"], + "Gustavo" => ["Mina"], + "Hassan" => ["Noah", "Olga"], + "Isla" => ["Pedro"], "Javier" => ["Quynh", "Ravi"], - "Sven" => ["Fabio"], "Khadija" => ["Sofia"], - "Quynh" => ["Boris"], - "Isla" => ["Pedro"], - "Oscar" => ["Bianca"], - "Gustavo" => ["Mina"], - "Carlos" => ["Fatima", "Gustavo"], - "Elias" => ["Javier"], - "Bao" => ["Dalia", "Elias"], - "Qi" => ["Dimitri"], - "Rami" => ["Ewa"], + "Liam" => ["Tariq", "Uma"], + "Mina" => ["Viktor", "Wang"], + "Noah" => ["Xiomara"], "Olga" => ["Yuki"], - "Nia" => ["Antonio"], - "Vera" => ["Igor"], - "Giorgio" => ["Tomoko"], - "Mateo" => ["Zara"], - "Leila" => ["Yassin"], + "Pedro" => ["Zane", "Aditi"], + "Quynh" => ["Boris"], "Ravi" => ["Celine"], - "Aditi" => ["Nia"], - "Hassan" => ["Noah", "Olga"], - "Boris" => ["Oscar"], "Sofia" => ["Diego", "Elif"], + "Tariq" => ["Farah"], + "Uma" => ["Giorgio"], + "Viktor" => ["Hana", "Ian"], + "Wang" => ["Jing"], + "Xiomara" => ["Kaito"], + "Yuki" => ["Leila"], "Zane" => ["Mateo"], - "Zara" => ["Mohammed"], + "Aditi" => ["Nia"], + "Boris" => ["Oscar"], + "Celine" => ["Priya"], + "Diego" => ["Qi"], + "Elif" => ["Rami"], + "Farah" => ["Sven"], + "Giorgio" => ["Tomoko"], "Hana" => ["Umar"], - "Uma" => ["Giorgio"], - "Aiko" => ["Bao", "Carlos"], + "Ian" => ["Vera"], "Jing" => ["Wyatt"], - "Farah" => ["Sven"], - "Liam" => ["Tariq", "Uma"], - "Elif" => ["Rami"], - "Diego" => ["Qi"], + "Kaito" => ["Xia"], + "Leila" => ["Yassin"], + "Mateo" => ["Zara"], + "Nia" => ["Antonio"], + "Oscar" => ["Bianca"], + "Priya" => ["Cai"], + "Qi" => ["Dimitri"], + "Rami" => ["Ewa"], + "Sven" => ["Fabio"], "Tomoko" => ["Gabriela"], - "Wang" => ["Jing"], - "Pedro" => ["Zane", "Aditi"], - "Viktor" => ["Hana", "Ian"], - "Yuki" => ["Leila"], "Umar" => ["Helena"], - "Tariq" => ["Farah"], - "Ian" => ["Vera"] + "Vera" => ["Igor"], + "Wyatt" => ["Jun"], + "Xia" => ["Kim"], + "Yassin" => ["Lucia"], + "Zara" => ["Mohammed"] } person_a = "Dimitri" person_b = "Fabio" - assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 4 + assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 9 end @tag :pending test "Complex graph, no shortcut, far removed nephew" do family_tree = %{ - "Kaito" => ["Xia"], - "Celine" => ["Priya"], - "Xiomara" => ["Kaito"], - "Priya" => ["Cai"], - "Noah" => ["Xiomara"], - "Mina" => ["Viktor", "Wang"], + "Aiko" => ["Bao", "Carlos"], + "Bao" => ["Dalia", "Elias"], + "Carlos" => ["Fatima", "Gustavo"], "Dalia" => ["Hassan", "Isla"], - "Wyatt" => ["Jun"], - "Yassin" => ["Lucia"], - "Xia" => ["Kim"], + "Elias" => ["Javier"], "Fatima" => ["Khadija", "Liam"], + "Gustavo" => ["Mina"], + "Hassan" => ["Noah", "Olga"], + "Isla" => ["Pedro"], "Javier" => ["Quynh", "Ravi"], - "Sven" => ["Fabio"], "Khadija" => ["Sofia"], - "Quynh" => ["Boris"], - "Isla" => ["Pedro"], - "Oscar" => ["Bianca"], - "Gustavo" => ["Mina"], - "Carlos" => ["Fatima", "Gustavo"], - "Elias" => ["Javier"], - "Bao" => ["Dalia", "Elias"], - "Qi" => ["Dimitri"], - "Rami" => ["Ewa"], + "Liam" => ["Tariq", "Uma"], + "Mina" => ["Viktor", "Wang"], + "Noah" => ["Xiomara"], "Olga" => ["Yuki"], - "Nia" => ["Antonio"], - "Vera" => ["Igor"], - "Giorgio" => ["Tomoko"], - "Mateo" => ["Zara"], - "Leila" => ["Yassin"], + "Pedro" => ["Zane", "Aditi"], + "Quynh" => ["Boris"], "Ravi" => ["Celine"], - "Aditi" => ["Nia"], - "Hassan" => ["Noah", "Olga"], - "Boris" => ["Oscar"], "Sofia" => ["Diego", "Elif"], + "Tariq" => ["Farah"], + "Uma" => ["Giorgio"], + "Viktor" => ["Hana", "Ian"], + "Wang" => ["Jing"], + "Xiomara" => ["Kaito"], + "Yuki" => ["Leila"], "Zane" => ["Mateo"], - "Zara" => ["Mohammed"], + "Aditi" => ["Nia"], + "Boris" => ["Oscar"], + "Celine" => ["Priya"], + "Diego" => ["Qi"], + "Elif" => ["Rami"], + "Farah" => ["Sven"], + "Giorgio" => ["Tomoko"], "Hana" => ["Umar"], - "Uma" => ["Giorgio"], - "Aiko" => ["Bao", "Carlos"], + "Ian" => ["Vera"], "Jing" => ["Wyatt"], - "Farah" => ["Sven"], - "Liam" => ["Tariq", "Uma"], - "Elif" => ["Rami"], - "Diego" => ["Qi"], + "Kaito" => ["Xia"], + "Leila" => ["Yassin"], + "Mateo" => ["Zara"], + "Nia" => ["Antonio"], + "Oscar" => ["Bianca"], + "Priya" => ["Cai"], + "Qi" => ["Dimitri"], + "Rami" => ["Ewa"], + "Sven" => ["Fabio"], "Tomoko" => ["Gabriela"], - "Wang" => ["Jing"], - "Pedro" => ["Zane", "Aditi"], - "Viktor" => ["Hana", "Ian"], - "Yuki" => ["Leila"], "Umar" => ["Helena"], - "Tariq" => ["Farah"], - "Ian" => ["Vera"] + "Vera" => ["Igor"], + "Wyatt" => ["Jun"], + "Xia" => ["Kim"], + "Yassin" => ["Lucia"], + "Zara" => ["Mohammed"] } person_a = "Lucia" person_b = "Jun" - assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 15 + assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 14 end @tag :pending - test "Complex graph, some shortcuts, cross-down and cross-up, cousins three times removed" do + test "Complex graph, some shortcuts, cross-down and cross-up, cousins several times removed, with unrelated family tree" do family_tree = %{ - "Kaito" => ["Xia"], - "Celine" => ["Priya"], - "Xiomara" => ["Kaito"], - "Priya" => ["Cai"], - "Noah" => ["Xiomara"], - "Mina" => ["Viktor", "Wang"], + "Aiko" => ["Bao", "Carlos"], + "Bao" => ["Dalia"], + "Carlos" => ["Fatima", "Gustavo"], "Dalia" => ["Hassan", "Isla"], - "Wyatt" => ["Jun"], - "Yassin" => ["Lucia"], - "Xia" => ["Kim"], "Fatima" => ["Khadija", "Liam"], - "Javier" => ["Quynh", "Ravi"], - "Sven" => ["Fabio"], - "Khadija" => ["Viktor"], - "Quynh" => ["Boris"], - "Isla" => ["Pedro"], - "Oscar" => ["Bianca"], "Gustavo" => ["Mina"], - "Carlos" => ["Fatima", "Gustavo"], - "Elias" => ["Javier"], - "Bao" => ["Dalia", "Elias"], - "Qi" => ["Dimitri"], - "Rami" => ["Ewa"], + "Hassan" => ["Noah", "Olga"], + "Isla" => ["Pedro"], + "Javier" => ["Quynh", "Ravi"], + "Khadija" => ["Sofia"], + "Liam" => ["Tariq", "Uma"], + "Mina" => ["Viktor", "Wang"], + "Noah" => ["Xiomara"], "Olga" => ["Yuki"], - "Nia" => ["Antonio"], - "Vera" => ["Igor"], - "Giorgio" => ["Tomoko"], - "Mateo" => ["Zara"], - "Leila" => ["Yassin"], + "Pedro" => ["Zane", "Aditi"], + "Quynh" => ["Boris"], "Ravi" => ["Celine"], - "Aditi" => ["Nia"], - "Hassan" => ["Noah", "Olga"], - "Boris" => ["Oscar"], "Sofia" => ["Diego", "Elif"], + "Tariq" => ["Farah"], + "Uma" => ["Giorgio"], + "Viktor" => ["Hana", "Ian"], + "Wang" => ["Jing"], + "Xiomara" => ["Kaito"], + "Yuki" => ["Leila"], "Zane" => ["Mateo"], - "Zara" => ["Mohammed"], + "Aditi" => ["Nia"], + "Boris" => ["Oscar"], + "Celine" => ["Priya"], + "Diego" => ["Qi"], + "Elif" => ["Rami"], + "Farah" => ["Sven"], + "Giorgio" => ["Tomoko"], "Hana" => ["Umar"], - "Uma" => ["Giorgio"], - "Aiko" => ["Bao", "Carlos"], + "Ian" => ["Vera"], "Jing" => ["Wyatt"], - "Farah" => ["Sven"], - "Liam" => ["Tariq", "Uma"], - "Elif" => ["Rami"], - "Diego" => ["Qi"], + "Kaito" => ["Xia"], + "Leila" => ["Yassin"], + "Mateo" => ["Zara"], + "Nia" => ["Antonio"], + "Oscar" => ["Bianca"], + "Priya" => ["Cai"], + "Qi" => ["Dimitri"], + "Rami" => ["Ewa"], + "Sven" => ["Fabio"], "Tomoko" => ["Gabriela"], - "Wang" => ["Jing"], - "Pedro" => ["Zane", "Aditi"], - "Viktor" => ["Hana", "Ian"], - "Yuki" => ["Leila"], "Umar" => ["Helena"], - "Tariq" => ["Farah"], - "Ian" => ["Vera"] + "Vera" => ["Igor"], + "Wyatt" => ["Jun"], + "Xia" => ["Kim"], + "Yassin" => ["Lucia"], + "Zara" => ["Mohammed"] } - person_a = "Tomoko" - person_b = "Qi" - assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 8 + person_a = "Wyatt" + person_b = "Xia" + assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 12 end end From 1bc86317d51af0687f057f3bc38aa31cca4fb965 Mon Sep 17 00:00:00 2001 From: Jeremie Gillet Date: Fri, 4 Apr 2025 09:22:12 +0900 Subject: [PATCH 5/5] too many exercises with recursion --- config.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config.json b/config.json index db989ec99..712dbd6ee 100644 --- a/config.json +++ b/config.json @@ -2449,10 +2449,9 @@ "slug": "relative-distance", "name": "Relative Distance", "uuid": "ba1d165a-774f-4b8f-9763-2d4279769e75", - "practices": [ - "recursion" - ], + "practices": [], "prerequisites": [ + "recursion", "maps", "tuples", "lists",