Skip to content

Commit 13eef73

Browse files
[#725] New practice exercise pov (#752)
Co-authored-by: Angelika Tyborska <angelikatyborska@fastmail.com>
1 parent fe6223a commit 13eef73

File tree

10 files changed

+464
-0
lines changed

10 files changed

+464
-0
lines changed

config.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2357,6 +2357,28 @@
23572357
"macros"
23582358
],
23592359
"difficulty": 8
2360+
},
2361+
{
2362+
"slug": "pov",
2363+
"name": "POV",
2364+
"uuid": "abc93451-451d-4497-b590-68cbf82e0a20",
2365+
"prerequisites": [
2366+
"pattern-matching",
2367+
"case",
2368+
"if",
2369+
"errors",
2370+
"recursion",
2371+
"enum",
2372+
"protocols",
2373+
"nil",
2374+
"structs",
2375+
"multiple-clause-functions"
2376+
],
2377+
"practices": [
2378+
"recursion",
2379+
"pattern-matching"
2380+
],
2381+
"difficulty": 9
23602382
}
23612383
],
23622384
"foregone": [
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Description
2+
3+
Reparent a graph on a selected node.
4+
5+
This exercise is all about re-orientating a graph to see things from a different
6+
point of view. For example family trees are usually presented from the
7+
ancestor's perspective:
8+
9+
```text
10+
+------0------+
11+
| | |
12+
+-1-+ +-2-+ +-3-+
13+
| | | | | |
14+
4 5 6 7 8 9
15+
```
16+
17+
But the same information can be presented from the perspective of any other node
18+
in the graph, by pulling it up to the root and dragging its relationships along
19+
with it. So the same graph from 6's perspective would look like:
20+
21+
```text
22+
6
23+
|
24+
+-----2-----+
25+
| |
26+
7 +-----0-----+
27+
| |
28+
+-1-+ +-3-+
29+
| | | |
30+
4 5 8 9
31+
```
32+
33+
This lets us more simply describe the paths between two nodes. So for example
34+
the path from 6-9 (which in the first graph goes up to the root and then down to
35+
a different leaf node) can be seen to follow the path 6-2-0-3-9
36+
37+
This exercise involves taking an input graph and re-orientating it from the point
38+
of view of one of the nodes.

exercises/practice/pov/.formatter.exs

Lines changed: 4 additions & 0 deletions
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+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"blurb": "Reparent a graph on a selected node.",
3+
"authors": [
4+
"jiegillet"
5+
],
6+
"contributors": [
7+
"neenjaw",
8+
"angelikatyborska"
9+
],
10+
"files": {
11+
"solution": [
12+
"lib/pov.ex"
13+
],
14+
"test": [
15+
"test/pov_test.exs"
16+
],
17+
"example": [
18+
".meta/example.ex"
19+
]
20+
},
21+
"source": "Adaptation of exercise from 4clojure",
22+
"source_url": "https://www.4clojure.com/"
23+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
defmodule Pov do
2+
# Structs and types
3+
4+
@typedoc """
5+
A tree, which is made of a node with several branches
6+
"""
7+
@type tree :: {any, [tree]}
8+
9+
defmodule Crumb do
10+
defstruct [:parent, left_siblings: [], right_siblings: []]
11+
@type t :: %Crumb{parent: any, left_siblings: [Pov.tree()], right_siblings: [Pov.tree()]}
12+
end
13+
14+
defmodule Zipper do
15+
defstruct [:focus, genealogy: []]
16+
@type t :: %Zipper{focus: Pov.tree(), genealogy: [Crumb.t()]}
17+
end
18+
19+
# Core functions
20+
21+
@doc """
22+
Reparent a tree on a selected node.
23+
"""
24+
@spec from_pov(tree :: tree, node :: any) :: tree | {:error, atom}
25+
def from_pov(tree, node) do
26+
case tree |> zip |> search(node) do
27+
{:ok, zipper} -> reparent(zipper)
28+
_ -> {:error, :nonexistent_target}
29+
end
30+
end
31+
32+
@doc """
33+
Finds a path between two nodes
34+
"""
35+
@spec path_between(tree :: tree, from :: any, to :: any) :: [any] | {:error, atom}
36+
def path_between(tree, from, to) do
37+
case tree |> zip |> search(from) do
38+
{:ok, zipper} ->
39+
case zipper |> reparent |> zip |> search(to) do
40+
{:ok, zipper_path} -> get_path(zipper_path)
41+
_ -> {:error, :nonexistent_destination}
42+
end
43+
44+
_ ->
45+
{:error, :nonexistent_source}
46+
end
47+
end
48+
49+
def search(%Zipper{focus: {node, _children}} = zipper, node), do: {:ok, zipper}
50+
51+
def search(%Zipper{} = zipper, node) do
52+
case zipper |> down |> search(node) do
53+
{:ok, z} -> {:ok, z}
54+
_ -> zipper |> right |> search(node)
55+
end
56+
end
57+
58+
def search(nil, _node), do: nil
59+
60+
def reparent(%Zipper{focus: tree, genealogy: []}), do: tree
61+
62+
def reparent(%Zipper{
63+
focus: {node, children},
64+
genealogy: [
65+
%Crumb{parent: parent, left_siblings: left, right_siblings: right} | grandparent
66+
]
67+
}) do
68+
{node, [reparent(%Zipper{focus: {parent, left ++ right}, genealogy: grandparent}) | children]}
69+
end
70+
71+
def get_path(%Zipper{focus: {node, _children}, genealogy: genealogy}) do
72+
parents = Enum.map(genealogy, fn %Crumb{parent: parent} -> parent end)
73+
74+
Enum.reverse([node | parents])
75+
end
76+
77+
# Zipper navigation
78+
# up and left are not actually required for this problem
79+
80+
def zip(tree), do: %Zipper{focus: tree}
81+
82+
def down(%Zipper{focus: {value, [child | children]}, genealogy: genealogy}) do
83+
%Zipper{
84+
focus: child,
85+
genealogy: [%Crumb{parent: value, right_siblings: children} | genealogy]
86+
}
87+
end
88+
89+
def down(_zipper), do: nil
90+
91+
def up(%Zipper{
92+
focus: tree,
93+
genealogy: [
94+
%Crumb{parent: parent, left_siblings: left, right_siblings: right} | grandparents
95+
]
96+
}) do
97+
%Zipper{focus: {parent, left ++ [tree | right]}, genealogy: grandparents}
98+
end
99+
100+
def up(_zipper), do: nil
101+
102+
def left(%Zipper{
103+
focus: tree,
104+
genealogy: [
105+
%Crumb{left_siblings: [left | lefties], right_siblings: right} = crumb | grandparents
106+
]
107+
}) do
108+
%Zipper{
109+
focus: left,
110+
genealogy: [
111+
%Crumb{crumb | left_siblings: lefties, right_siblings: [tree | right]} | grandparents
112+
]
113+
}
114+
end
115+
116+
def left(_zipper), do: nil
117+
118+
def right(%Zipper{
119+
focus: tree,
120+
genealogy: [
121+
%Crumb{left_siblings: left, right_siblings: [right | righties]} = crumb | grandparents
122+
]
123+
}) do
124+
%Zipper{
125+
focus: right,
126+
genealogy: [
127+
%Crumb{crumb | left_siblings: [tree | left], right_siblings: righties} | grandparents
128+
]
129+
}
130+
end
131+
132+
def right(_zipper), do: nil
133+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# This is an auto-generated file. Regular comments will be removed when this
2+
# file is regenerated. Regenerating will not touch any manually added keys,
3+
# so comments can be added in a "comment" key.
4+
5+
[1b3cd134-49ad-4a7d-8376-7087b7e70792]
6+
description = "Results in the same tree if the input tree is a singleton"
7+
8+
[0778c745-0636-40de-9edd-25a8f40426f6]
9+
description = "Can reroot a tree with a parent and one sibling"
10+
11+
[fdfdef0a-4472-4248-8bcf-19cf33f9c06e]
12+
description = "Can reroot a tree with a parent and many siblings"
13+
14+
[cbcf52db-8667-43d8-a766-5d80cb41b4bb]
15+
description = "Can reroot a tree with new root deeply nested in tree"
16+
17+
[e27fa4fa-648d-44cd-90af-d64a13d95e06]
18+
description = "Moves children of the new root to same level as former parent"
19+
20+
[09236c7f-7c83-42cc-87a1-25afa60454a3]
21+
description = "Can reroot a complex tree with cousins"
22+
23+
[f41d5eeb-8973-448f-a3b0-cc1e019a4193]
24+
description = "Errors if target does not exist in a singleton tree"
25+
26+
[9dc0a8b3-df02-4267-9a41-693b6aff75e7]
27+
description = "Errors if target does not exist in a large tree"
28+
29+
[02d1f1d9-428d-4395-b026-2db35ffa8f0a]
30+
description = "Can find path to parent"
31+
32+
[d0002674-fcfb-4cdc-9efa-bfc54e3c31b5]
33+
description = "Can find path to sibling"
34+
35+
[c9877cd1-0a69-40d4-b362-725763a5c38f]
36+
description = "Can find path to cousin"
37+
38+
[9fb17a82-2c14-4261-baa3-2f3f234ffa03]
39+
description = "Can find path not involving root"
40+
41+
[5124ed49-7845-46ad-bc32-97d5ac7451b2]
42+
description = "Can find path from nodes other than x"
43+
44+
[f52a183c-25cc-4c87-9fc9-0e7f81a5725c]
45+
description = "Errors if destination does not exist"
46+
47+
[f4fe18b9-b4a2-4bd5-a694-e179155c2149]
48+
description = "Errors if source does not exist"

exercises/practice/pov/lib/pov.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
defmodule Pov do
2+
@typedoc """
3+
A tree, which is made of a node with several branches
4+
"""
5+
@type tree :: {any, [tree]}
6+
7+
@doc """
8+
Reparent a tree on a selected node.
9+
"""
10+
@spec from_pov(tree :: tree, node :: any) :: tree | {:error, atom}
11+
def from_pov(tree, node) do
12+
end
13+
14+
@doc """
15+
Finds a path between two nodes
16+
"""
17+
@spec path_between(tree :: tree, from :: any, to :: any) :: [any] | {:error, atom}
18+
def path_between(tree, from, to) do
19+
end
20+
end

exercises/practice/pov/mix.exs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule Pov.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :pov,
7+
version: "0.1.0",
8+
# elixir: "~> 1.8",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger]
18+
]
19+
end
20+
21+
# Run "mix help deps" to learn about dependencies.
22+
defp deps do
23+
[
24+
# {:dep_from_hexpm, "~> 0.3.0"},
25+
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
26+
]
27+
end
28+
end

0 commit comments

Comments
 (0)