Skip to content

Commit 5496b40

Browse files
authoredApr 4, 2025··
New practice exercise relative-distance (#1554)
* run generator mix format from exercise folder * inspect limit to infinity * WIP * finish up with updatet canonical data * too many exercises with recursion
1 parent c2a9e89 commit 5496b40

13 files changed

+469
-17
lines changed
 

‎bin/bootstrap_practice_exercise.exs

+18-17
Original file line numberDiff line numberDiff line change
@@ -73,24 +73,24 @@ defmodule Generate do
7373
"# #{Enum.map_join(values, "\n# ", &String.trim/1)}"
7474

7575
{field, values} when is_list(values) ->
76-
"#\n# --#{field} --\n# #{Enum.map_join(values, "\n# ", &inspect/1)}"
76+
"#\n# --#{field} --\n# #{Enum.map_join(values, "\n# ", &inspect(&1, limit: :infinity))}"
7777

7878
{field, value} ->
79-
"#\n# -- #{field} --\n# #{inspect(value)}"
79+
"#\n# -- #{field} --\n# #{inspect(value, limit: :infinity)}"
8080
end)
8181
end
8282

8383
def print_input(%{} = input),
8484
do:
8585
Enum.map_join(input, "\n", fn {variable, value} ->
86-
"#{Macro.underscore(variable)} = #{inspect(value)}"
86+
"#{Macro.underscore(variable)} = #{inspect(value, limit: :infinity)}"
8787
end)
8888

89-
def print_input(input), do: "input = #{inspect(input)}"
89+
def print_input(input), do: "input = #{inspect(input, limit: :infinity)}"
9090

91-
def print_expected(%{"error" => err}, _error), do: "{:error, #{inspect(err)}}"
92-
def print_expected(expected, true), do: "{:ok, #{inspect(expected)}}"
93-
def print_expected(expected, false), do: inspect(expected)
91+
def print_expected(%{"error" => err}, _error), do: "{:error, #{inspect(err, limit: :infinity)}}"
92+
def print_expected(expected, true), do: "{:ok, #{inspect(expected, limit: :infinity)}}"
93+
def print_expected(expected, false), do: inspect(expected, limit: :infinity)
9494

9595
def print_test_case(
9696
%{"description" => description, "cases" => sub_cases} = category,
@@ -143,8 +143,9 @@ module =
143143

144144
## Step 1: create folder structure
145145

146-
Mix.Generator.create_directory("exercises/practice/#{exercise}/lib")
147-
Mix.Generator.create_directory("exercises/practice/#{exercise}/test")
146+
File.cd!("exercises/practice/#{exercise}")
147+
Mix.Generator.create_directory("lib")
148+
Mix.Generator.create_directory("test")
148149

149150
## Step 2: add common files
150151

@@ -156,7 +157,7 @@ format = """
156157
]
157158
"""
158159

159-
Mix.Generator.create_file("exercises/practice/#{exercise}/.formatter.exs", format)
160+
Mix.Generator.create_file(".formatter.exs", format)
160161

161162
# mix.exs
162163
mix = """
@@ -189,7 +190,7 @@ defmodule #{module}.MixProject do
189190
end
190191
"""
191192

192-
Mix.Generator.create_file("exercises/practice/#{exercise}/mix.exs", mix)
193+
Mix.Generator.create_file("mix.exs", mix)
193194

194195
# .gitignore
195196
gitignore = """
@@ -221,15 +222,15 @@ erl_crash.dump
221222
/tmp/
222223
"""
223224

224-
Mix.Generator.create_file("exercises/practice/#{exercise}/.gitignore", gitignore)
225+
Mix.Generator.create_file(".gitignore", gitignore)
225226

226227
# test/test_helper.exs
227228
test_helper = """
228229
ExUnit.start()
229230
ExUnit.configure(exclude: :pending, trace: true)
230231
"""
231232

232-
Mix.Generator.create_file("exercises/practice/#{exercise}/test/test_helper.exs", test_helper)
233+
Mix.Generator.create_file("test/test_helper.exs", test_helper)
233234

234235
## Step 3: write files that depend on problem specifications
235236

@@ -256,10 +257,10 @@ defmodule #{module} do
256257
end
257258
"""
258259

259-
path = "exercises/practice/#{exercise}/lib/#{exercise_snake_case}.ex"
260+
path = "lib/#{exercise_snake_case}.ex"
260261
Mix.Generator.create_file(path, lib_file)
261262

262-
Mix.Generator.copy_file(path, "exercises/practice/#{exercise}/.meta/example.ex")
263+
Mix.Generator.copy_file(path, ".meta/example.ex")
263264

264265
# Generating test file
265266
test_file =
@@ -273,8 +274,8 @@ test_file =
273274
"""
274275
|> String.replace("@tag", "# @tag", global: false)
275276

276-
path = "exercises/practice/#{exercise}/test/#{exercise_snake_case}_test.exs"
277+
path = "test/#{exercise_snake_case}_test.exs"
277278
Mix.Generator.create_file(path, test_file)
278279

279280
# mix format all files
280-
Mix.Tasks.Format.run(["exercises/practice/#{exercise}/**/*.{ex,exs}"])
281+
Mix.Tasks.Format.run(["**/*.{ex,exs}"])

‎config.json

+17
Original file line numberDiff line numberDiff line change
@@ -2445,6 +2445,23 @@
24452445
],
24462446
"difficulty": 6
24472447
},
2448+
{
2449+
"slug": "relative-distance",
2450+
"name": "Relative Distance",
2451+
"uuid": "ba1d165a-774f-4b8f-9763-2d4279769e75",
2452+
"practices": [],
2453+
"prerequisites": [
2454+
"recursion",
2455+
"maps",
2456+
"tuples",
2457+
"lists",
2458+
"list-comprehensions",
2459+
"enum",
2460+
"multiple-clause-functions",
2461+
"nil"
2462+
],
2463+
"difficulty": 6
2464+
},
24482465
{
24492466
"slug": "robot-simulator",
24502467
"name": "Robot Simulator",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Instructions
2+
3+
Your task is to determine the degree of separation between two individuals in a family tree.
4+
5+
- You will be given an input, with all parent names and their children.
6+
- Each name is unique, a child _can_ have one or two parents.
7+
- The degree of separation is defined as the shortest number of connections from one person to another.
8+
- If two individuals are not connected, return a value that represents "no known relationship."
9+
Please see the test cases for the actual implementation.
10+
11+
## Example
12+
13+
Given the following family tree:
14+
15+
```text
16+
┌──────────┐ ┌──────────┐ ┌───────────┐
17+
│ Helena │ │ Erdős │ │ Shusaku │
18+
└───┬───┬──┘ └─────┬────┘ └──────┬────┘
19+
┌───┘ └───────┐ └──────┬──────┘
20+
▼ ▼ ▼
21+
┌──────────┐ ┌────────┐ ┌──────────┐
22+
│ Isla │ │ Tariq │ │ Kevin │
23+
└────┬─────┘ └────┬───┘ └──────────┘
24+
▼ ▼
25+
┌─────────┐ ┌────────┐
26+
│ Uma │ │ Morphy │
27+
└─────────┘ └────────┘
28+
```
29+
30+
The degree of separation between Tariq and Uma is 3 (Tariq → Helena → Isla → Uma).
31+
There's no known relationship between Isla and [Kevin][six-bacons], as there is no connection in the given data.
32+
The degree of separation between Uma and Isla is 1.
33+
34+
~~~~exercism/note
35+
Isla and Tariq are siblings and have a separation of 1.
36+
Similarly, this implementation would report a separation of 2 from you to your father's brother.
37+
~~~~
38+
39+
[six-bacons]: https://en.m.wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Introduction
2+
3+
You've been hired to develop **Noble Knots**, the hottest new dating app for nobility!
4+
With centuries of royal intermarriage, things have gotten… _complicated_.
5+
To avoid any _oops-we're-twins_ situations, your job is to build a system that checks how closely two people are related.
6+
7+
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.
8+
Your algorithm will determine the **degree of separation** between two individuals in the royal family tree.
9+
10+
Will your app help crown a perfect match?
11+
12+
[islendiga-app]: http://www.islendingaapp.is/information-in-english/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
relative_distance-*.tar
24+
25+
# Temporary files, for example, from tests.
26+
/tmp/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"authors": [
3+
"jiegillet"
4+
],
5+
"files": {
6+
"solution": [
7+
"lib/relative_distance.ex"
8+
],
9+
"test": [
10+
"test/relative_distance_test.exs"
11+
],
12+
"example": [
13+
".meta/example.ex"
14+
]
15+
},
16+
"blurb": "Given a family tree, calculate the degree of separation.",
17+
"source": "vaeng",
18+
"source_url": "https://github.com/exercism/problem-specifications/pull/2537"
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
defmodule RelativeDistance do
2+
@doc """
3+
Find the degree of separation of two members given a given family tree.
4+
"""
5+
@spec degree_of_separation(
6+
family_tree :: %{String.t() => [String.t()]},
7+
person_a :: String.t(),
8+
person_b :: String.t()
9+
) :: nil | pos_integer()
10+
def degree_of_separation(family_tree, person_a, person_b) do
11+
family_tree
12+
|> build_family_graph()
13+
|> find_separation(person_b, [{person_a, 0}], MapSet.new())
14+
end
15+
16+
defp build_family_graph(family_tree) do
17+
for {parent, children} <- family_tree, child <- children, reduce: %{} do
18+
graph ->
19+
siblings = children |> MapSet.new() |> MapSet.delete(child)
20+
21+
graph
22+
|> Map.update(parent, MapSet.new([child]), &MapSet.put(&1, child))
23+
|> Map.update(child, MapSet.new([parent]), &MapSet.put(&1, parent))
24+
|> Map.update(child, siblings, &MapSet.union(&1, siblings))
25+
end
26+
end
27+
28+
defp find_separation(_graph, _goal, [], _history), do: nil
29+
30+
defp find_separation(_graph, goal, [{goal, count} | _], _history), do: count
31+
32+
defp find_separation(graph, goal, [{person, count} | rest], history) do
33+
history = MapSet.put(history, person)
34+
35+
next_steps =
36+
graph[person]
37+
|> Enum.reject(fn relative -> MapSet.member?(history, relative) end)
38+
|> Enum.map(fn relative -> {relative, count + 1} end)
39+
40+
find_separation(graph, goal, rest ++ next_steps, history)
41+
end
42+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[4a1ded74-5d32-47fb-8ae5-321f51d06b5b]
13+
description = "Direct parent-child relation"
14+
15+
[30d17269-83e9-4f82-a0d7-8ef9656d8dce]
16+
description = "Sibling relationship"
17+
18+
[8dffa27d-a8ab-496d-80b3-2f21c77648b5]
19+
description = "Two degrees of separation, grandchild"
20+
21+
[34e56ec1-d528-4a42-908e-020a4606ee60]
22+
description = "Unrelated individuals"
23+
24+
[93ffe989-bad2-48c4-878f-3acb1ce2611b]
25+
description = "Complex graph, cousins"
26+
27+
[2cc2e76b-013a-433c-9486-1dbe29bf06e5]
28+
description = "Complex graph, no shortcut, far removed nephew"
29+
30+
[46c9fbcb-e464-455f-a718-049ea3c7400a]
31+
description = "Complex graph, some shortcuts, cross-down and cross-up, cousins several times removed, with unrelated family tree"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
defmodule RelativeDistance do
2+
@doc """
3+
Find the degree of separation of two members given a given family tree.
4+
"""
5+
@spec degree_of_separation(
6+
family_tree :: %{String.t() => [String.t()]},
7+
person_a :: String.t(),
8+
person_b :: String.t()
9+
) :: nil | pos_integer()
10+
def degree_of_separation(family_tree, person_a, person_b) do
11+
end
12+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
defmodule RelativeDistance.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :relative_distance,
7+
version: "0.1.0",
8+
start_permanent: Mix.env() == :prod,
9+
deps: deps()
10+
]
11+
end
12+
13+
# Run "mix help compile.app" to learn about applications.
14+
def application do
15+
[
16+
extra_applications: [:logger]
17+
]
18+
end
19+
20+
# Run "mix help deps" to learn about dependencies.
21+
defp deps do
22+
[
23+
# {:dep_from_hexpm, "~> 0.3.0"},
24+
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
25+
]
26+
end
27+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
defmodule RelativeDistanceTest do
2+
use ExUnit.Case
3+
4+
# @tag :pending
5+
test "Direct parent-child relation" do
6+
family_tree = %{"Vera" => ["Tomoko"], "Tomoko" => ["Aditi"]}
7+
person_a = "Vera"
8+
person_b = "Tomoko"
9+
assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 1
10+
end
11+
12+
@tag :pending
13+
test "Sibling relationship" do
14+
family_tree = %{"Dalia" => ["Olga", "Yassin"]}
15+
person_a = "Olga"
16+
person_b = "Yassin"
17+
assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 1
18+
end
19+
20+
@tag :pending
21+
test "Two degrees of separation, grandchild" do
22+
family_tree = %{"Khadija" => ["Mateo"], "Mateo" => ["Rami"]}
23+
person_a = "Khadija"
24+
person_b = "Rami"
25+
assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 2
26+
end
27+
28+
@tag :pending
29+
test "Unrelated individuals" do
30+
family_tree = %{"Kaito" => ["Elif"], "Priya" => ["Rami"]}
31+
person_a = "Priya"
32+
person_b = "Kaito"
33+
assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == nil
34+
end
35+
36+
@tag :pending
37+
test "Complex graph, cousins" do
38+
family_tree = %{
39+
"Aiko" => ["Bao", "Carlos"],
40+
"Bao" => ["Dalia", "Elias"],
41+
"Carlos" => ["Fatima", "Gustavo"],
42+
"Dalia" => ["Hassan", "Isla"],
43+
"Elias" => ["Javier"],
44+
"Fatima" => ["Khadija", "Liam"],
45+
"Gustavo" => ["Mina"],
46+
"Hassan" => ["Noah", "Olga"],
47+
"Isla" => ["Pedro"],
48+
"Javier" => ["Quynh", "Ravi"],
49+
"Khadija" => ["Sofia"],
50+
"Liam" => ["Tariq", "Uma"],
51+
"Mina" => ["Viktor", "Wang"],
52+
"Noah" => ["Xiomara"],
53+
"Olga" => ["Yuki"],
54+
"Pedro" => ["Zane", "Aditi"],
55+
"Quynh" => ["Boris"],
56+
"Ravi" => ["Celine"],
57+
"Sofia" => ["Diego", "Elif"],
58+
"Tariq" => ["Farah"],
59+
"Uma" => ["Giorgio"],
60+
"Viktor" => ["Hana", "Ian"],
61+
"Wang" => ["Jing"],
62+
"Xiomara" => ["Kaito"],
63+
"Yuki" => ["Leila"],
64+
"Zane" => ["Mateo"],
65+
"Aditi" => ["Nia"],
66+
"Boris" => ["Oscar"],
67+
"Celine" => ["Priya"],
68+
"Diego" => ["Qi"],
69+
"Elif" => ["Rami"],
70+
"Farah" => ["Sven"],
71+
"Giorgio" => ["Tomoko"],
72+
"Hana" => ["Umar"],
73+
"Ian" => ["Vera"],
74+
"Jing" => ["Wyatt"],
75+
"Kaito" => ["Xia"],
76+
"Leila" => ["Yassin"],
77+
"Mateo" => ["Zara"],
78+
"Nia" => ["Antonio"],
79+
"Oscar" => ["Bianca"],
80+
"Priya" => ["Cai"],
81+
"Qi" => ["Dimitri"],
82+
"Rami" => ["Ewa"],
83+
"Sven" => ["Fabio"],
84+
"Tomoko" => ["Gabriela"],
85+
"Umar" => ["Helena"],
86+
"Vera" => ["Igor"],
87+
"Wyatt" => ["Jun"],
88+
"Xia" => ["Kim"],
89+
"Yassin" => ["Lucia"],
90+
"Zara" => ["Mohammed"]
91+
}
92+
93+
person_a = "Dimitri"
94+
person_b = "Fabio"
95+
assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 9
96+
end
97+
98+
@tag :pending
99+
test "Complex graph, no shortcut, far removed nephew" do
100+
family_tree = %{
101+
"Aiko" => ["Bao", "Carlos"],
102+
"Bao" => ["Dalia", "Elias"],
103+
"Carlos" => ["Fatima", "Gustavo"],
104+
"Dalia" => ["Hassan", "Isla"],
105+
"Elias" => ["Javier"],
106+
"Fatima" => ["Khadija", "Liam"],
107+
"Gustavo" => ["Mina"],
108+
"Hassan" => ["Noah", "Olga"],
109+
"Isla" => ["Pedro"],
110+
"Javier" => ["Quynh", "Ravi"],
111+
"Khadija" => ["Sofia"],
112+
"Liam" => ["Tariq", "Uma"],
113+
"Mina" => ["Viktor", "Wang"],
114+
"Noah" => ["Xiomara"],
115+
"Olga" => ["Yuki"],
116+
"Pedro" => ["Zane", "Aditi"],
117+
"Quynh" => ["Boris"],
118+
"Ravi" => ["Celine"],
119+
"Sofia" => ["Diego", "Elif"],
120+
"Tariq" => ["Farah"],
121+
"Uma" => ["Giorgio"],
122+
"Viktor" => ["Hana", "Ian"],
123+
"Wang" => ["Jing"],
124+
"Xiomara" => ["Kaito"],
125+
"Yuki" => ["Leila"],
126+
"Zane" => ["Mateo"],
127+
"Aditi" => ["Nia"],
128+
"Boris" => ["Oscar"],
129+
"Celine" => ["Priya"],
130+
"Diego" => ["Qi"],
131+
"Elif" => ["Rami"],
132+
"Farah" => ["Sven"],
133+
"Giorgio" => ["Tomoko"],
134+
"Hana" => ["Umar"],
135+
"Ian" => ["Vera"],
136+
"Jing" => ["Wyatt"],
137+
"Kaito" => ["Xia"],
138+
"Leila" => ["Yassin"],
139+
"Mateo" => ["Zara"],
140+
"Nia" => ["Antonio"],
141+
"Oscar" => ["Bianca"],
142+
"Priya" => ["Cai"],
143+
"Qi" => ["Dimitri"],
144+
"Rami" => ["Ewa"],
145+
"Sven" => ["Fabio"],
146+
"Tomoko" => ["Gabriela"],
147+
"Umar" => ["Helena"],
148+
"Vera" => ["Igor"],
149+
"Wyatt" => ["Jun"],
150+
"Xia" => ["Kim"],
151+
"Yassin" => ["Lucia"],
152+
"Zara" => ["Mohammed"]
153+
}
154+
155+
person_a = "Lucia"
156+
person_b = "Jun"
157+
assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 14
158+
end
159+
160+
@tag :pending
161+
test "Complex graph, some shortcuts, cross-down and cross-up, cousins several times removed, with unrelated family tree" do
162+
family_tree = %{
163+
"Aiko" => ["Bao", "Carlos"],
164+
"Bao" => ["Dalia"],
165+
"Carlos" => ["Fatima", "Gustavo"],
166+
"Dalia" => ["Hassan", "Isla"],
167+
"Fatima" => ["Khadija", "Liam"],
168+
"Gustavo" => ["Mina"],
169+
"Hassan" => ["Noah", "Olga"],
170+
"Isla" => ["Pedro"],
171+
"Javier" => ["Quynh", "Ravi"],
172+
"Khadija" => ["Sofia"],
173+
"Liam" => ["Tariq", "Uma"],
174+
"Mina" => ["Viktor", "Wang"],
175+
"Noah" => ["Xiomara"],
176+
"Olga" => ["Yuki"],
177+
"Pedro" => ["Zane", "Aditi"],
178+
"Quynh" => ["Boris"],
179+
"Ravi" => ["Celine"],
180+
"Sofia" => ["Diego", "Elif"],
181+
"Tariq" => ["Farah"],
182+
"Uma" => ["Giorgio"],
183+
"Viktor" => ["Hana", "Ian"],
184+
"Wang" => ["Jing"],
185+
"Xiomara" => ["Kaito"],
186+
"Yuki" => ["Leila"],
187+
"Zane" => ["Mateo"],
188+
"Aditi" => ["Nia"],
189+
"Boris" => ["Oscar"],
190+
"Celine" => ["Priya"],
191+
"Diego" => ["Qi"],
192+
"Elif" => ["Rami"],
193+
"Farah" => ["Sven"],
194+
"Giorgio" => ["Tomoko"],
195+
"Hana" => ["Umar"],
196+
"Ian" => ["Vera"],
197+
"Jing" => ["Wyatt"],
198+
"Kaito" => ["Xia"],
199+
"Leila" => ["Yassin"],
200+
"Mateo" => ["Zara"],
201+
"Nia" => ["Antonio"],
202+
"Oscar" => ["Bianca"],
203+
"Priya" => ["Cai"],
204+
"Qi" => ["Dimitri"],
205+
"Rami" => ["Ewa"],
206+
"Sven" => ["Fabio"],
207+
"Tomoko" => ["Gabriela"],
208+
"Umar" => ["Helena"],
209+
"Vera" => ["Igor"],
210+
"Wyatt" => ["Jun"],
211+
"Xia" => ["Kim"],
212+
"Yassin" => ["Lucia"],
213+
"Zara" => ["Mohammed"]
214+
}
215+
216+
person_a = "Wyatt"
217+
person_b = "Xia"
218+
assert RelativeDistance.degree_of_separation(family_tree, person_a, person_b) == 12
219+
end
220+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ExUnit.start()
2+
ExUnit.configure(exclude: :pending, trace: true)

0 commit comments

Comments
 (0)
Please sign in to comment.