Skip to content

New practice exercise relative-distance #1554

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

Merged
merged 5 commits into from
Apr 4, 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
35 changes: 18 additions & 17 deletions bin/bootstrap_practice_exercise.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this because mix format wouldn't run from outside the directory anymore

Mix.Generator.create_directory("lib")
Mix.Generator.create_directory("test")

## Step 2: add common files

Expand All @@ -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 = """
Expand Down Expand Up @@ -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 = """
Expand Down Expand Up @@ -221,15 +222,15 @@ 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 = """
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

Expand All @@ -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 =
Expand All @@ -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}"])
17 changes: 17 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2445,6 +2445,23 @@
],
"difficulty": 6
},
{
"slug": "relative-distance",
"name": "Relative Distance",
"uuid": "ba1d165a-774f-4b8f-9763-2d4279769e75",
"practices": [],
"prerequisites": [
"recursion",
"maps",
"tuples",
"lists",
"list-comprehensions",
"enum",
"multiple-clause-functions",
"nil"
],
"difficulty": 6
},
{
"slug": "robot-simulator",
"name": "Robot Simulator",
Expand Down
39 changes: 39 additions & 0 deletions exercises/practice/relative-distance/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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.

~~~~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
12 changes: 12 additions & 0 deletions exercises/practice/relative-distance/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -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/
4 changes: 4 additions & 0 deletions exercises/practice/relative-distance/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
26 changes: 26 additions & 0 deletions exercises/practice/relative-distance/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
19 changes: 19 additions & 0 deletions exercises/practice/relative-distance/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"authors": [
"jiegillet"
],
"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"
}
42 changes: 42 additions & 0 deletions exercises/practice/relative-distance/.meta/example.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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_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
31 changes: 31 additions & 0 deletions exercises/practice/relative-distance/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 12 additions & 0 deletions exercises/practice/relative-distance/lib/relative_distance.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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
end
end
27 changes: 27 additions & 0 deletions exercises/practice/relative-distance/mix.exs
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading