diff --git a/config.json b/config.json index 8b6f3b46..08e75592 100644 --- a/config.json +++ b/config.json @@ -892,6 +892,14 @@ "prerequisites": [], "difficulty": 8 }, + { + "slug": "pov", + "name": "POV", + "uuid": "6cbdabf6-4a22-43ec-b2e4-6611ea253237", + "practices": [], + "prerequisites": [], + "difficulty": 8 + }, { "slug": "rest-api", "name": "REST API", diff --git a/exercises/practice/pov/.docs/instructions.md b/exercises/practice/pov/.docs/instructions.md new file mode 100644 index 00000000..0fdeed22 --- /dev/null +++ b/exercises/practice/pov/.docs/instructions.md @@ -0,0 +1,41 @@ +# Instructions + +Reparent a tree on a selected node. + +A [tree][wiki-tree] is a special type of [graph][wiki-graph] where all nodes are connected but there are no cycles. +That means, there is exactly one path to get from one node to another for any pair of nodes. + +This exercise is all about re-orientating a tree to see things from a different point of view. +For example family trees are usually presented from the ancestor's perspective: + +```text + +------0------+ + | | | + +-1-+ +-2-+ +-3-+ + | | | | | | + 4 5 6 7 8 9 +``` + +But there is no inherent direction in a tree. +The same information can be presented from the perspective of any other node in the tree, by pulling it up to the root and dragging its relationships along with it. +So the same tree from 6's perspective would look like: + +```text + 6 + | + +-----2-----+ + | | + 7 +-----0-----+ + | | + +-1-+ +-3-+ + | | | | + 4 5 8 9 +``` + +This lets us more simply describe the paths between two nodes. +So for example the path from 6-9 (which in the first tree goes up to the root and then down to a different leaf node) can be seen to follow the path 6-2-0-3-9. + +This exercise involves taking an input tree and re-orientating it from the point of view of one of the nodes. + +[wiki-graph]: https://en.wikipedia.org/wiki/Tree_(graph_theory) +[wiki-tree]: https://en.wikipedia.org/wiki/Graph_(discrete_mathematics) diff --git a/exercises/practice/pov/.meta/Example.roc b/exercises/practice/pov/.meta/Example.roc new file mode 100644 index 00000000..3f95c055 --- /dev/null +++ b/exercises/practice/pov/.meta/Example.roc @@ -0,0 +1,99 @@ +Pov :: {}.{ + Tree := [Empty, Node({ label : Str, children : Set(Tree) })].{ + + ## Are two trees equal? + is_eq : _ # enable the default is_eq implementation for the Tree type + + ## Return the tree from the point of view of the node with the given label. + ## Return Err(NotFound) if no such node is found. + from_pov : Tree, Str -> Try(Tree, [NotFound, ..]) + from_pov = |tree, from| { + root_path = tree |> nodes_to_root(from)? + match root_path.first() { + Err(ListWasEmpty) => { + crash "Unreachable: nodes_to_root cannot return Ok([])" + } + Ok(Empty) => { + crash "Unreachable: target cannot be Empty" + } + Ok(Node(target)) => { + parent = root_path.get(1) ?? Empty + match parent { + Empty => Ok(tree) # the target node is already the root of the tree + Node(parent_node) => { + tree_without_target = tree |> drop(from) + from_parent_pov = tree_without_target |> from_pov(parent_node.label)? + new_children = target.children.insert(from_parent_pov) + Node({ label: from, children: new_children }) |> Ok + } + } + } + } + } + + ## Find the list of nodes between the two given nodes and return their labels + ## If either of these nodes don't exist, return Err(NotFound) + path_to : Tree, Str, Str -> Try(List(Str), [NotFound, ..]) + path_to = |tree, from, to| { + from_tree = tree |> from_pov(to)? + from_tree + |> nodes_to_root(from)? + .map( + |child| { + match child { + Empty => { + crash "Unreachable: trees must never contain Empty children" + } + Node(node) => node.label + } + }, + ) + |> Ok + } + } +} + +## Return all nodes on the path from the target node up to the root. +## If the node is not found, Err(NotFound) is returned. +nodes_to_root : Pov.Tree, Str -> Try(List(Pov.Tree), [NotFound, ..]) +nodes_to_root = |tree, label| { + match tree { + Empty => Err(NotFound) + Node(node) => { + if node.label == label + Ok([Node(node)]) + else { + node.children + .to_list() + .fold_until( + Err(NotFound), + |state, child| { + match child |> nodes_to_root(label) { + Ok(subPath) => Break(Ok(subPath.append(Node(node)))) + Err(NotFound) => Continue(state) + } + }, + ) + } + } + } +} + +## Drop the node with the given label, if it exists +drop : Pov.Tree, Str -> Pov.Tree +drop = |tree, label| { + match tree { + Empty => Empty + Node(node) => { + if node.label == label { + Empty + } else { + filtered_children = + node.children + .map(|child| drop(child, label)) + .drop_if(|child| child == Empty) + Node({ label: node.label, children: filtered_children }) + } + } + } +} diff --git a/exercises/practice/pov/.meta/config.json b/exercises/practice/pov/.meta/config.json new file mode 100644 index 00000000..a1463bae --- /dev/null +++ b/exercises/practice/pov/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "ageron" + ], + "files": { + "solution": [ + "Pov.roc" + ], + "test": [ + "pov-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Reparent a graph on a selected node.", + "source": "Adaptation of exercise from 4clojure", + "source_url": "https://www.4clojure.com/" +} diff --git a/exercises/practice/pov/.meta/template.j2 b/exercises/practice/pov/.meta/template.j2 new file mode 100644 index 00000000..bcc40271 --- /dev/null +++ b/exercises/practice/pov/.meta/template.j2 @@ -0,0 +1,59 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +import {{ exercise | to_pascal }} exposing [Tree] + +{% macro roc_tree(tree) -%} +{%- set children = tree["children"] or [] -%} +{%- if children == [] -%} +Node({ label: {{ tree["label"] | to_roc }}, children: Set.empty() }) +{%- else %} +Node({ + label: {{ tree["label"] | to_roc }}, + children: Set.from_list([ +{%- for child in (tree["children"]) %} + {{ roc_tree(child) }}, +{%- endfor %} + ]) +}) +{%- endif %} +{%- endmacro %} + + +{% for supercase in cases %} +## +## {{ supercase["description"] }} +## + +{% for case in supercase["cases"] -%} +# {{ case["description"] }} +{% if case["property"] == "fromPov" %} +expect { + tree = Tree.({{ roc_tree(case["input"]["tree"]) | indent(8) }}) + result = tree.from_pov({{ case["input"]["from"] | to_roc }}) + {%- if case["expected"] %} + expected = Ok({{ roc_tree(case["expected"]) | indent(8) }}) + result == expected + {%- else %} + result == Err(NotFound) + {%- endif %} +} +{% elif case["property"] == "pathTo" %} +expect { + tree = Tree.({{ roc_tree(case["input"]["tree"]) | indent(8) }}) + result = tree.path_to({{ case["input"]["from"] | to_roc }}, {{ case["input"]["to"] | to_roc }}) + {%- if case["expected"] %} + expected = Ok({{ case["expected"] | to_roc }}) + result == expected + {%- else %} + result == Err(NotFound) + {%- endif %} +} +{% else %} +# This test case is not supported yet: perhaps you can try implementing it? +{% endif %} +{% endfor %} +{% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/pov/.meta/tests.toml b/exercises/practice/pov/.meta/tests.toml new file mode 100644 index 00000000..bfa0bb63 --- /dev/null +++ b/exercises/practice/pov/.meta/tests.toml @@ -0,0 +1,55 @@ +# 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. + +[1b3cd134-49ad-4a7d-8376-7087b7e70792] +description = "Reroot a tree so that its root is the specified node. -> Results in the same tree if the input tree is a singleton" + +[0778c745-0636-40de-9edd-25a8f40426f6] +description = "Reroot a tree so that its root is the specified node. -> Can reroot a tree with a parent and one sibling" + +[fdfdef0a-4472-4248-8bcf-19cf33f9c06e] +description = "Reroot a tree so that its root is the specified node. -> Can reroot a tree with a parent and many siblings" + +[cbcf52db-8667-43d8-a766-5d80cb41b4bb] +description = "Reroot a tree so that its root is the specified node. -> Can reroot a tree with new root deeply nested in tree" + +[e27fa4fa-648d-44cd-90af-d64a13d95e06] +description = "Reroot a tree so that its root is the specified node. -> Moves children of the new root to same level as former parent" + +[09236c7f-7c83-42cc-87a1-25afa60454a3] +description = "Reroot a tree so that its root is the specified node. -> Can reroot a complex tree with cousins" + +[f41d5eeb-8973-448f-a3b0-cc1e019a4193] +description = "Reroot a tree so that its root is the specified node. -> Errors if target does not exist in a singleton tree" + +[9dc0a8b3-df02-4267-9a41-693b6aff75e7] +description = "Reroot a tree so that its root is the specified node. -> Errors if target does not exist in a large tree" + +[02d1f1d9-428d-4395-b026-2db35ffa8f0a] +description = "Given two nodes, find the path between them -> Can find path to parent" + +[d0002674-fcfb-4cdc-9efa-bfc54e3c31b5] +description = "Given two nodes, find the path between them -> Can find path to sibling" + +[c9877cd1-0a69-40d4-b362-725763a5c38f] +description = "Given two nodes, find the path between them -> Can find path to cousin" + +[9fb17a82-2c14-4261-baa3-2f3f234ffa03] +description = "Given two nodes, find the path between them -> Can find path not involving root" + +[5124ed49-7845-46ad-bc32-97d5ac7451b2] +description = "Given two nodes, find the path between them -> Can find path from nodes other than x" + +[f52a183c-25cc-4c87-9fc9-0e7f81a5725c] +description = "Given two nodes, find the path between them -> Errors if destination does not exist" + +[f4fe18b9-b4a2-4bd5-a694-e179155c2149] +description = "Given two nodes, find the path between them -> Errors if source does not exist" diff --git a/exercises/practice/pov/Pov.roc b/exercises/practice/pov/Pov.roc new file mode 100644 index 00000000..f369f483 --- /dev/null +++ b/exercises/practice/pov/Pov.roc @@ -0,0 +1,21 @@ +Pov :: {}.{ + Tree := [Empty, Node({ label : Str, children : Set(Tree) })].{ + + ## Are two trees equal? + is_eq : _ # enable the default is_eq implementation for the Tree type + + ## Return the tree from the point of view of the node with the given label. + ## Return Err(NotFound) if no such node is found. + from_pov : Tree, Str -> Try(Tree, [NotFound, ..]) + from_pov = |tree, from| { + crash "Please implement the 'from_pov' function" + } + + ## Return the labels of the nodes between the two given nodes + ## If either of these nodes don't exist, return Err(NotFound) + path_to : Tree, Str, Str -> Try(List(Str), [NotFound, ..]) + path_to = |tree, from, to| { + crash "Please implement the 'path_to' function" + } + } +} diff --git a/exercises/practice/pov/pov-test.roc b/exercises/practice/pov/pov-test.roc new file mode 100644 index 00000000..cda2afc6 --- /dev/null +++ b/exercises/practice/pov/pov-test.roc @@ -0,0 +1,449 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/pov/canonical-data.json +# File last updated on 2026-08-08 + +import Pov exposing [Tree] + +## +## Reroot a tree so that its root is the specified node. +## + +# Results in the same tree if the input tree is a singleton + +expect { + tree = Tree.(Node({ label: "x", children: Set.empty() })) + result = tree.from_pov("x") + expected = Ok(Node({ label: "x", children: Set.empty() })) + result == expected +} + +# Can reroot a tree with a parent and one sibling + +expect { + tree = Tree.( + Node({ + label: "parent", + children: Set.from_list([ + Node({ label: "x", children: Set.empty() }), + Node({ label: "sibling", children: Set.empty() }), + ]), + }), + ) + result = tree.from_pov("x") + expected = Ok( + Node({ + label: "x", + children: Set.from_list([ + + Node({ + label: "parent", + children: Set.from_list([ + Node({ label: "sibling", children: Set.empty() }), + ]), + }), + ]), + }), + ) + result == expected +} + +# Can reroot a tree with a parent and many siblings + +expect { + tree = Tree.( + Node({ + label: "parent", + children: Set.from_list([ + Node({ label: "a", children: Set.empty() }), + Node({ label: "x", children: Set.empty() }), + Node({ label: "b", children: Set.empty() }), + Node({ label: "c", children: Set.empty() }), + ]), + }), + ) + result = tree.from_pov("x") + expected = Ok( + Node({ + label: "x", + children: Set.from_list([ + + Node({ + label: "parent", + children: Set.from_list([ + Node({ label: "a", children: Set.empty() }), + Node({ label: "b", children: Set.empty() }), + Node({ label: "c", children: Set.empty() }), + ]), + }), + ]), + }), + ) + result == expected +} + +# Can reroot a tree with new root deeply nested in tree + +expect { + tree = Tree.( + Node({ + label: "level-0", + children: Set.from_list([ + + Node({ + label: "level-1", + children: Set.from_list([ + + Node({ + label: "level-2", + children: Set.from_list([ + + Node({ + label: "level-3", + children: Set.from_list([ + Node({ label: "x", children: Set.empty() }), + ]), + }), + ]), + }), + ]), + }), + ]), + }), + ) + result = tree.from_pov("x") + expected = Ok( + Node({ + label: "x", + children: Set.from_list([ + + Node({ + label: "level-3", + children: Set.from_list([ + + Node({ + label: "level-2", + children: Set.from_list([ + + Node({ + label: "level-1", + children: Set.from_list([ + Node({ label: "level-0", children: Set.empty() }), + ]), + }), + ]), + }), + ]), + }), + ]), + }), + ) + result == expected +} + +# Moves children of the new root to same level as former parent + +expect { + tree = Tree.( + Node({ + label: "parent", + children: Set.from_list([ + + Node({ + label: "x", + children: Set.from_list([ + Node({ label: "kid-0", children: Set.empty() }), + Node({ label: "kid-1", children: Set.empty() }), + ]), + }), + ]), + }), + ) + result = tree.from_pov("x") + expected = Ok( + Node({ + label: "x", + children: Set.from_list([ + Node({ label: "kid-0", children: Set.empty() }), + Node({ label: "kid-1", children: Set.empty() }), + Node({ label: "parent", children: Set.empty() }), + ]), + }), + ) + result == expected +} + +# Can reroot a complex tree with cousins + +expect { + tree = Tree.( + Node({ + label: "grandparent", + children: Set.from_list([ + + Node({ + label: "parent", + children: Set.from_list([ + + Node({ + label: "x", + children: Set.from_list([ + Node({ label: "kid-0", children: Set.empty() }), + Node({ label: "kid-1", children: Set.empty() }), + ]), + }), + Node({ label: "sibling-0", children: Set.empty() }), + Node({ label: "sibling-1", children: Set.empty() }), + ]), + }), + + Node({ + label: "uncle", + children: Set.from_list([ + Node({ label: "cousin-0", children: Set.empty() }), + Node({ label: "cousin-1", children: Set.empty() }), + ]), + }), + ]), + }), + ) + result = tree.from_pov("x") + expected = Ok( + Node({ + label: "x", + children: Set.from_list([ + Node({ label: "kid-1", children: Set.empty() }), + Node({ label: "kid-0", children: Set.empty() }), + + Node({ + label: "parent", + children: Set.from_list([ + Node({ label: "sibling-0", children: Set.empty() }), + Node({ label: "sibling-1", children: Set.empty() }), + + Node({ + label: "grandparent", + children: Set.from_list([ + + Node({ + label: "uncle", + children: Set.from_list([ + Node({ label: "cousin-0", children: Set.empty() }), + Node({ label: "cousin-1", children: Set.empty() }), + ]), + }), + ]), + }), + ]), + }), + ]), + }), + ) + result == expected +} + +# Errors if target does not exist in a singleton tree + +expect { + tree = Tree.(Node({ label: "x", children: Set.empty() })) + result = tree.from_pov("nonexistent") + result == Err(NotFound) +} + +# Errors if target does not exist in a large tree + +expect { + tree = Tree.( + Node({ + label: "parent", + children: Set.from_list([ + + Node({ + label: "x", + children: Set.from_list([ + Node({ label: "kid-0", children: Set.empty() }), + Node({ label: "kid-1", children: Set.empty() }), + ]), + }), + Node({ label: "sibling-0", children: Set.empty() }), + Node({ label: "sibling-1", children: Set.empty() }), + ]), + }), + ) + result = tree.from_pov("nonexistent") + result == Err(NotFound) +} + +## +## Given two nodes, find the path between them +## + +# Can find path to parent + +expect { + tree = Tree.( + Node({ + label: "parent", + children: Set.from_list([ + Node({ label: "x", children: Set.empty() }), + Node({ label: "sibling", children: Set.empty() }), + ]), + }), + ) + result = tree.path_to("x", "parent") + expected = Ok(["x", "parent"]) + result == expected +} + +# Can find path to sibling + +expect { + tree = Tree.( + Node({ + label: "parent", + children: Set.from_list([ + Node({ label: "a", children: Set.empty() }), + Node({ label: "x", children: Set.empty() }), + Node({ label: "b", children: Set.empty() }), + Node({ label: "c", children: Set.empty() }), + ]), + }), + ) + result = tree.path_to("x", "b") + expected = Ok(["x", "parent", "b"]) + result == expected +} + +# Can find path to cousin + +expect { + tree = Tree.( + Node({ + label: "grandparent", + children: Set.from_list([ + + Node({ + label: "parent", + children: Set.from_list([ + + Node({ + label: "x", + children: Set.from_list([ + Node({ label: "kid-0", children: Set.empty() }), + Node({ label: "kid-1", children: Set.empty() }), + ]), + }), + Node({ label: "sibling-0", children: Set.empty() }), + Node({ label: "sibling-1", children: Set.empty() }), + ]), + }), + + Node({ + label: "uncle", + children: Set.from_list([ + Node({ label: "cousin-0", children: Set.empty() }), + Node({ label: "cousin-1", children: Set.empty() }), + ]), + }), + ]), + }), + ) + result = tree.path_to("x", "cousin-1") + expected = Ok(["x", "parent", "grandparent", "uncle", "cousin-1"]) + result == expected +} + +# Can find path not involving root + +expect { + tree = Tree.( + Node({ + label: "grandparent", + children: Set.from_list([ + + Node({ + label: "parent", + children: Set.from_list([ + Node({ label: "x", children: Set.empty() }), + Node({ label: "sibling-0", children: Set.empty() }), + Node({ label: "sibling-1", children: Set.empty() }), + ]), + }), + ]), + }), + ) + result = tree.path_to("x", "sibling-1") + expected = Ok(["x", "parent", "sibling-1"]) + result == expected +} + +# Can find path from nodes other than x + +expect { + tree = Tree.( + Node({ + label: "parent", + children: Set.from_list([ + Node({ label: "a", children: Set.empty() }), + Node({ label: "x", children: Set.empty() }), + Node({ label: "b", children: Set.empty() }), + Node({ label: "c", children: Set.empty() }), + ]), + }), + ) + result = tree.path_to("a", "c") + expected = Ok(["a", "parent", "c"]) + result == expected +} + +# Errors if destination does not exist + +expect { + tree = Tree.( + Node({ + label: "parent", + children: Set.from_list([ + + Node({ + label: "x", + children: Set.from_list([ + Node({ label: "kid-0", children: Set.empty() }), + Node({ label: "kid-1", children: Set.empty() }), + ]), + }), + Node({ label: "sibling-0", children: Set.empty() }), + Node({ label: "sibling-1", children: Set.empty() }), + ]), + }), + ) + result = tree.path_to("x", "nonexistent") + result == Err(NotFound) +} + +# Errors if source does not exist + +expect { + tree = Tree.( + Node({ + label: "parent", + children: Set.from_list([ + + Node({ + label: "x", + children: Set.from_list([ + Node({ label: "kid-0", children: Set.empty() }), + Node({ label: "kid-1", children: Set.empty() }), + ]), + }), + Node({ label: "sibling-0", children: Set.empty() }), + Node({ label: "sibling-1", children: Set.empty() }), + ]), + }), + ) + result = tree.path_to("nonexistent", "x") + result == Err(NotFound) +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +}