Skip to content

Commit 5496b40

Browse files
authored
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

0 commit comments

Comments
 (0)