From 213dfec79a4a7a8bd91030808e872d85900ad224 Mon Sep 17 00:00:00 2001 From: devcyjung Date: Thu, 3 Jul 2025 12:49:54 +0900 Subject: [PATCH 1/6] Initial draft of POV, also added camel->snake custom filter --- .gitignore | 1 + config.json | 9 + exercises/practice/pov/.docs/instructions.md | 41 ++ exercises/practice/pov/.gitignore | 2 + exercises/practice/pov/.meta/config.json | 20 + exercises/practice/pov/.meta/example.rs | 35 ++ .../practice/pov/.meta/test_template.tera | 94 ++++ exercises/practice/pov/.meta/tests.toml | 55 +++ exercises/practice/pov/Cargo.toml | 9 + exercises/practice/pov/src/lib.rs | 84 ++++ exercises/practice/pov/tests/pov.rs | 428 ++++++++++++++++++ rust-tooling/generate/src/custom_filters.rs | 22 + 12 files changed, 800 insertions(+) create mode 100644 exercises/practice/pov/.docs/instructions.md create mode 100644 exercises/practice/pov/.gitignore create mode 100644 exercises/practice/pov/.meta/config.json create mode 100644 exercises/practice/pov/.meta/example.rs create mode 100644 exercises/practice/pov/.meta/test_template.tera create mode 100644 exercises/practice/pov/.meta/tests.toml create mode 100644 exercises/practice/pov/Cargo.toml create mode 100644 exercises/practice/pov/src/lib.rs create mode 100644 exercises/practice/pov/tests/pov.rs diff --git a/.gitignore b/.gitignore index 3eea4c1c0..69320c607 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ tmp /bin/configlet exercises/*/*/Cargo.lock clippy.log +.idea .vscode .prob-spec problem-specifications diff --git a/config.json b/config.json index a36553efd..0b7d079f8 100644 --- a/config.json +++ b/config.json @@ -1572,6 +1572,15 @@ "lists", "unsafe" ] + }, + { + "slug": "pov", + "name": "POV", + "uuid": "bf7b7309-3d34-4893-a584-f7742502e012", + "practices": [], + "prerequisites": [], + "difficulty": 10, + "topics": [] } ], "foregone": [ diff --git a/exercises/practice/pov/.docs/instructions.md b/exercises/practice/pov/.docs/instructions.md new file mode 100644 index 000000000..0fdeed225 --- /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/.gitignore b/exercises/practice/pov/.gitignore new file mode 100644 index 000000000..96ef6c0b9 --- /dev/null +++ b/exercises/practice/pov/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/exercises/practice/pov/.meta/config.json b/exercises/practice/pov/.meta/config.json new file mode 100644 index 000000000..076d72325 --- /dev/null +++ b/exercises/practice/pov/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "devcyjung" + ], + "files": { + "solution": [ + "src/lib.rs", + "Cargo.toml" + ], + "test": [ + "tests/pov.rs" + ], + "example": [ + ".meta/example.rs" + ] + }, + "blurb": "Reparent a graph on a selected node.", + "source": "Adaptation of exercise from 4clojure", + "source_url": "https://github.com/oxalorg/4ever-clojure" +} diff --git a/exercises/practice/pov/.meta/example.rs b/exercises/practice/pov/.meta/example.rs new file mode 100644 index 000000000..63d6e6b13 --- /dev/null +++ b/exercises/practice/pov/.meta/example.rs @@ -0,0 +1,35 @@ +#[derive(Clone, PartialEq, Eq)] +pub struct Tree {} + +impl Tree { + #[must_use] + pub fn new(_label: &str) -> Self { + todo!("Implement a function that creates a new Tree with given label"); + } + + #[must_use] + pub fn with_children(_label: &str, _children: &[Tree]) -> Self { + todo!("Implement a function that creates a new Tree with given label and Children"); + } + + #[must_use] + pub fn get_label(&self) -> String { + todo!("Implement getter for label."); + } + + #[must_use] + pub fn get_children(&self) -> Vec<&Self> { + todo!("Implement getter for children."); + } + + #[must_use] + pub fn pov_from(self, _from: &str) -> Option { + todo!("Implement a function that reparents Tree with 'from' as root."); + } + + #[must_use] + pub fn path_to(self, _from: &str, _to: &str) -> Option> + { + todo!("Implement a function that returns the list of labels in the shortest path from 'from' to 'to'"); + } +} diff --git a/exercises/practice/pov/.meta/test_template.tera b/exercises/practice/pov/.meta/test_template.tera new file mode 100644 index 000000000..840f8b2c1 --- /dev/null +++ b/exercises/practice/pov/.meta/test_template.tera @@ -0,0 +1,94 @@ +{%- macro render_tree(tree) -%} +{%- if tree.children -%} + Tree::with_children( + "{{ tree.label }}".to_string(), + vec![ + {%- for child in tree.children -%} + {{ self::render_tree(tree=child) }}, + {%- endfor -%} + ], + ) +{%- else -%} + Tree::new("{{ tree.label }}".to_string()) +{%- endif -%} +{%- endmacro -%} + +{%- macro render_vec(values) -%} +vec![ + {%- for value in values -%} + "{{ value }}".to_string(), + {%- endfor -%} +] +{%- endmacro -%} + +{% for test_group in cases %} +/// {{ test_group.cases[0].property | camel_to_snake }}() tests +/// {{ test_group.description }} +{%- for comment in test_group.comments %} +/// {{ comment }} +{%- endfor %} +mod {{ test_group.description | make_ident }} { + use pov::*; + +{% for test in test_group.cases %} +#[test] +#[ignore] +fn {{ test.description | make_ident }}() { + let input = {{ self::render_tree(tree=test.input.tree) }}; + let from = "{{ test.input.from }}".to_string(); + {%- if test.property == "fromPov" -%} + let result = input.pov_from(&from); + {%- if not test.expected -%} + let expected: Option> = None; + {%- else -%} + let expected = Some({{ self::render_tree(tree=test.expected) }}); + {%- endif -%} + assert!(crate::test_util::tree_option_eq(result, expected)); + {%- elif test.property == "pathTo" -%} + let to = "{{ test.input.to }}".to_string(); + let result = input.path_to(&from, &to); + {%- if not test.expected -%} + let expected: Option> = None; + {%- else -%} + let expected = Some({{ self::render_vec(values=test.expected) }}); + {%- endif -%} + assert_eq!(result, expected); + {%- else -%} + Invalid property: {{ test.property }} + {%- endif -%} +} +{% endfor %} +} +{% endfor %} +mod test_util { + use pov::*; + + pub fn tree_option_eq(lhs: Option>, rhs: Option>) -> bool { + match (lhs, rhs) { + (None, None) => true, + (Some(l_inner), Some(r_inner)) => tree_eq(&l_inner, &r_inner), + _ => false, + } + } + + pub fn tree_eq(lhs: &Tree, rhs: &Tree) -> bool { + let (l_label, r_label) = (lhs.get_label(), rhs.get_label()); + let (mut l_children, mut r_children) = ( + lhs.get_children(), + rhs.get_children(), + ); + if l_label == r_label && l_children.len() == r_children.len() { + if l_children.len() == 0 { + return true; + } + let key_fn = |child: &&Tree| child.get_label(); + l_children.sort_unstable_by_key(key_fn); + r_children.sort_unstable_by_key(key_fn); + return l_children + .iter() + .zip(r_children.iter()) + .all(|(&lc, &rc)| tree_eq(lc, rc)); + } + false + } +} \ No newline at end of file diff --git a/exercises/practice/pov/.meta/tests.toml b/exercises/practice/pov/.meta/tests.toml new file mode 100644 index 000000000..bfa0bb630 --- /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/Cargo.toml b/exercises/practice/pov/Cargo.toml new file mode 100644 index 000000000..87c6c85d1 --- /dev/null +++ b/exercises/practice/pov/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pov" +version = "0.1.0" +edition = "2024" + +# Not all libraries from crates.io are available in Exercism's test runner. +# The full list of available libraries is here: +# https://github.com/exercism/rust-test-runner/blob/main/local-registry/Cargo.toml +[dependencies] diff --git a/exercises/practice/pov/src/lib.rs b/exercises/practice/pov/src/lib.rs new file mode 100644 index 000000000..33ce33a04 --- /dev/null +++ b/exercises/practice/pov/src/lib.rs @@ -0,0 +1,84 @@ +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, PartialEq)] +pub struct Tree { + label: T, + children: Vec>>, +} + +impl Tree { + #[must_use] + pub fn new(label: T) -> Self { + Self { label, children: Default::default() } + } + + #[must_use] + pub fn with_children(label: T, children: Vec>) -> Self { + Self { label, children: children.into_iter().map(Box::new).collect() } + } + + #[must_use] + pub fn get_label(&self) -> T { + self.label.clone() + } + + #[must_use] + pub fn get_children(&self) -> Vec<&Self> { + self.children.iter().map(Box::deref).collect() + } + + #[must_use] + pub fn pov_from(&self, from: &T) -> Option { + // list of (child, parent, child's index in parent.children) + let mut lookup = vec![(self, None, None)]; + let mut stack = vec![self]; + while let Some(parent) = stack.pop() { + if &parent.label == from { + return self.reparent(parent, lookup.as_slice()).into(); + } + lookup.extend( + parent.children.iter() + .map(Box::deref).enumerate() + .map(|(i, child)| (child, Some(parent), Some(i))) + ); + stack.extend(parent.children.iter().map(Box::deref)); + } + None + } + + // lookup is list of (child, parent, child's index in parent.children) + #[must_use] + fn reparent(&self, parent: &Self, lookup: &[(&Self, Option<&Self>, Option)]) -> Self { + let mut new_root = parent.clone(); + let mut current = parent; + let mut clone_mut = &mut new_root; + let find_parent = |child| lookup.iter().find(|(c, _p, _i)| *c == child); + while let Some(&(_, Some(parent), Some(index))) = find_parent(current) { + let mut parent_clone = parent.clone(); + parent_clone.children.swap_remove(index); + clone_mut.children.push(Box::new(parent_clone)); + current = parent; + let new_box = clone_mut.children.last_mut().expect("We just inserted node, this is not empty"); + clone_mut = new_box.deref_mut(); + } + new_root + } + + #[must_use] + pub fn path_to(&self, from: &T, to: &T) -> Option> + { + if from != &self.label { + return self.pov_from(from).and_then(|pov| pov.path_to(from, to)); + } + if to == &self.label { + return Some(vec![self.label.clone()]); + } + for child in self.children.iter() { + if let Some(mut path) = child.path_to(&child.label, to) { + path.insert(0, self.label.clone()); + return Some(path); + } + } + None + } +} diff --git a/exercises/practice/pov/tests/pov.rs b/exercises/practice/pov/tests/pov.rs new file mode 100644 index 000000000..e1dc8f7ed --- /dev/null +++ b/exercises/practice/pov/tests/pov.rs @@ -0,0 +1,428 @@ +/// from_pov() tests +/// Reroot a tree so that its root is the specified node. +/// In this way, the tree is presented from the point of view of the specified node. +/// +/// If appropriate for your track, you may test that the input tree is not modified. +/// +/// Note that when rerooting upon a target node that has both parents and children, +/// it does not matter whether the former parent comes before or after the former children. +/// Please account for this when checking correctness of the resulting trees. +/// One suggested method is to only check two things: +/// 1) The root of the returned tree is the root that was passed in to from_pov. +/// 2) The sorted edge list of the returned tree is the same as the sorted edge list of the expected tree. +mod reroot_a_tree_so_that_its_root_is_the_specified_node { + use pov::*; + + #[test] + fn results_in_the_same_tree_if_the_input_tree_is_a_singleton() { + let input = Tree::new("x".to_string()); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::new("x".to_string())); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn can_reroot_a_tree_with_a_parent_and_one_sibling() { + let input = Tree::with_children( + "parent".to_string(), + vec![Tree::new("x".to_string()), Tree::new("sibling".to_string())], + ); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::with_children( + "x".to_string(), + vec![Tree::with_children( + "parent".to_string(), + vec![Tree::new("sibling".to_string())], + )], + )); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn can_reroot_a_tree_with_a_parent_and_many_siblings() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("a".to_string()), + Tree::new("x".to_string()), + Tree::new("b".to_string()), + Tree::new("c".to_string()), + ], + ); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::with_children( + "x".to_string(), + vec![Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("a".to_string()), + Tree::new("b".to_string()), + Tree::new("c".to_string()), + ], + )], + )); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn can_reroot_a_tree_with_new_root_deeply_nested_in_tree() { + let input = Tree::with_children( + "level-0".to_string(), + vec![Tree::with_children( + "level-1".to_string(), + vec![Tree::with_children( + "level-2".to_string(), + vec![Tree::with_children( + "level-3".to_string(), + vec![Tree::new("x".to_string())], + )], + )], + )], + ); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::with_children( + "x".to_string(), + vec![Tree::with_children( + "level-3".to_string(), + vec![Tree::with_children( + "level-2".to_string(), + vec![Tree::with_children( + "level-1".to_string(), + vec![Tree::new("level-0".to_string())], + )], + )], + )], + )); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn moves_children_of_the_new_root_to_same_level_as_former_parent() { + let input = Tree::with_children( + "parent".to_string(), + vec![Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + )], + ); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + Tree::new("parent".to_string()), + ], + )); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn can_reroot_a_complex_tree_with_cousins() { + let input = Tree::with_children( + "grandparent".to_string(), + vec![ + Tree::with_children( + "parent".to_string(), + vec![ + Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + ), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + ), + Tree::with_children( + "uncle".to_string(), + vec![ + Tree::new("cousin-0".to_string()), + Tree::new("cousin-1".to_string()), + ], + ), + ], + ); + let from = "x".to_string(); + let result = input.pov_from(&from); + let expected = Some(Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-1".to_string()), + Tree::new("kid-0".to_string()), + Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + Tree::with_children( + "grandparent".to_string(), + vec![Tree::with_children( + "uncle".to_string(), + vec![ + Tree::new("cousin-0".to_string()), + Tree::new("cousin-1".to_string()), + ], + )], + ), + ], + ), + ], + )); + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn errors_if_target_does_not_exist_in_a_singleton_tree() { + let input = Tree::new("x".to_string()); + let from = "nonexistent".to_string(); + let result = input.pov_from(&from); + let expected: Option> = None; + assert!(crate::test_util::tree_option_eq(result, expected)); + } + + #[test] + #[ignore] + fn errors_if_target_does_not_exist_in_a_large_tree() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + ), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + ); + let from = "nonexistent".to_string(); + let result = input.pov_from(&from); + let expected: Option> = None; + assert!(crate::test_util::tree_option_eq(result, expected)); + } +} + +/// path_to() tests +/// Given two nodes, find the path between them +/// A typical implementation would first reroot the tree on one of the two nodes. +/// +/// If appropriate for your track, you may test that the input tree is not modified. +mod given_two_nodes_find_the_path_between_them { + use pov::*; + + #[test] + #[ignore] + fn can_find_path_to_parent() { + let input = Tree::with_children( + "parent".to_string(), + vec![Tree::new("x".to_string()), Tree::new("sibling".to_string())], + ); + let from = "x".to_string(); + let to = "parent".to_string(); + let result = input.path_to(&from, &to); + let expected = Some(vec!["x".to_string(), "parent".to_string()]); + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn can_find_path_to_sibling() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("a".to_string()), + Tree::new("x".to_string()), + Tree::new("b".to_string()), + Tree::new("c".to_string()), + ], + ); + let from = "x".to_string(); + let to = "b".to_string(); + let result = input.path_to(&from, &to); + let expected = Some(vec!["x".to_string(), "parent".to_string(), "b".to_string()]); + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn can_find_path_to_cousin() { + let input = Tree::with_children( + "grandparent".to_string(), + vec![ + Tree::with_children( + "parent".to_string(), + vec![ + Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + ), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + ), + Tree::with_children( + "uncle".to_string(), + vec![ + Tree::new("cousin-0".to_string()), + Tree::new("cousin-1".to_string()), + ], + ), + ], + ); + let from = "x".to_string(); + let to = "cousin-1".to_string(); + let result = input.path_to(&from, &to); + let expected = Some(vec![ + "x".to_string(), + "parent".to_string(), + "grandparent".to_string(), + "uncle".to_string(), + "cousin-1".to_string(), + ]); + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn can_find_path_not_involving_root() { + let input = Tree::with_children( + "grandparent".to_string(), + vec![Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("x".to_string()), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + )], + ); + let from = "x".to_string(); + let to = "sibling-1".to_string(); + let result = input.path_to(&from, &to); + let expected = Some(vec![ + "x".to_string(), + "parent".to_string(), + "sibling-1".to_string(), + ]); + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn can_find_path_from_nodes_other_than_x() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::new("a".to_string()), + Tree::new("x".to_string()), + Tree::new("b".to_string()), + Tree::new("c".to_string()), + ], + ); + let from = "a".to_string(); + let to = "c".to_string(); + let result = input.path_to(&from, &to); + let expected = Some(vec!["a".to_string(), "parent".to_string(), "c".to_string()]); + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn errors_if_destination_does_not_exist() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + ), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + ); + let from = "x".to_string(); + let to = "nonexistent".to_string(); + let result = input.path_to(&from, &to); + let expected: Option> = None; + assert_eq!(result, expected); + } + + #[test] + #[ignore] + fn errors_if_source_does_not_exist() { + let input = Tree::with_children( + "parent".to_string(), + vec![ + Tree::with_children( + "x".to_string(), + vec![ + Tree::new("kid-0".to_string()), + Tree::new("kid-1".to_string()), + ], + ), + Tree::new("sibling-0".to_string()), + Tree::new("sibling-1".to_string()), + ], + ); + let from = "nonexistent".to_string(); + let to = "x".to_string(); + let result = input.path_to(&from, &to); + let expected: Option> = None; + assert_eq!(result, expected); + } +} + +mod test_util { + use pov::*; + + pub fn tree_option_eq(lhs: Option>, rhs: Option>) -> bool { + match (lhs, rhs) { + (None, None) => true, + (Some(l_inner), Some(r_inner)) => tree_eq(&l_inner, &r_inner), + _ => false, + } + } + + pub fn tree_eq(lhs: &Tree, rhs: &Tree) -> bool { + let (l_label, r_label) = (lhs.get_label(), rhs.get_label()); + let (mut l_children, mut r_children) = (lhs.get_children(), rhs.get_children()); + if l_label == r_label && l_children.len() == r_children.len() { + if l_children.len() == 0 { + return true; + } + let key_fn = |child: &&Tree| child.get_label(); + l_children.sort_unstable_by_key(key_fn); + r_children.sort_unstable_by_key(key_fn); + return l_children + .iter() + .zip(r_children.iter()) + .all(|(&lc, &rc)| tree_eq(lc, rc)); + } + false + } +} diff --git a/rust-tooling/generate/src/custom_filters.rs b/rust-tooling/generate/src/custom_filters.rs index dcef8b45c..b92eeef27 100644 --- a/rust-tooling/generate/src/custom_filters.rs +++ b/rust-tooling/generate/src/custom_filters.rs @@ -8,6 +8,7 @@ pub static CUSTOM_FILTERS: &[(&str, Filter)] = &[ ("to_hex", to_hex), ("make_ident", make_ident), ("fmt_num", fmt_num), + ("camel_to_snake", camel_to_snake), ]; pub fn to_hex(value: &Value, _args: &HashMap) -> Result { @@ -56,3 +57,24 @@ pub fn fmt_num(value: &Value, _args: &HashMap) -> Result { let pretty_num = String::from_utf8(pretty_digits).unwrap_or_default(); Ok(Value::String(pretty_num)) } + +pub fn camel_to_snake(value: &Value, _args: &HashMap) -> Result { + let Some(input) = value.as_str() else { + return Err(tera::Error::call_filter( + "camel_to_snake filter expects a string", + "serde_json::value::Value::as_str", + )); + }; + let mut result = String::with_capacity(input.len() << 1); + let mut peek_iter = input.chars().peekable(); + while let Some(ch) = peek_iter.next() { + result.push(ch); + if let Some(&next_ch) = peek_iter.peek() { + if ch.is_lowercase() && next_ch.is_uppercase() { + result.push('_'); + } + } + } + result.shrink_to_fit(); + Ok(Value::String(result.to_lowercase())) +} \ No newline at end of file From 721c5bf8ca3cb2bc3cdcaaba37abbc578bb85b43 Mon Sep 17 00:00:00 2001 From: devcyjung Date: Thu, 3 Jul 2025 13:08:44 +0900 Subject: [PATCH 2/6] swapped .meta/example.rs with src/lib.rs properly --- exercises/practice/pov/.meta/example.rs | 90 ++++++++++++++++++++----- exercises/practice/pov/src/lib.rs | 72 ++++---------------- 2 files changed, 88 insertions(+), 74 deletions(-) diff --git a/exercises/practice/pov/.meta/example.rs b/exercises/practice/pov/.meta/example.rs index 63d6e6b13..e19263d2c 100644 --- a/exercises/practice/pov/.meta/example.rs +++ b/exercises/practice/pov/.meta/example.rs @@ -1,35 +1,95 @@ -#[derive(Clone, PartialEq, Eq)] -pub struct Tree {} +use std::ops::{Deref, DerefMut}; -impl Tree { +#[derive(Clone, PartialEq)] +pub struct Tree { + label: T, + children: Vec>>, +} + +impl Tree { #[must_use] - pub fn new(_label: &str) -> Self { - todo!("Implement a function that creates a new Tree with given label"); + pub fn new(label: T) -> Self { + Self { + label, + children: Default::default(), + } } #[must_use] - pub fn with_children(_label: &str, _children: &[Tree]) -> Self { - todo!("Implement a function that creates a new Tree with given label and Children"); + pub fn with_children(label: T, children: Vec) -> Self { + Self { + label, + children: children.into_iter().map(Box::new).collect(), + } } #[must_use] - pub fn get_label(&self) -> String { - todo!("Implement getter for label."); + pub fn get_label(&self) -> T { + self.label.clone() } #[must_use] pub fn get_children(&self) -> Vec<&Self> { - todo!("Implement getter for children."); + self.children.iter().map(Box::deref).collect() + } + + #[must_use] + pub fn pov_from(&self, from: &T) -> Option { + // list of (child, parent, child's index in parent.children) + let mut lookup = vec![(self, None, None)]; + let mut stack = vec![self]; + while let Some(parent) = stack.pop() { + if &parent.label == from { + return self.reparent(parent, lookup.as_slice()).into(); + } + lookup.extend( + parent + .children + .iter() + .map(Box::deref) + .enumerate() + .map(|(i, child)| (child, Some(parent), Some(i))), + ); + stack.extend(parent.children.iter().map(Box::deref)); + } + None } + // lookup is list of (child, parent, child's index in parent.children) #[must_use] - pub fn pov_from(self, _from: &str) -> Option { - todo!("Implement a function that reparents Tree with 'from' as root."); + fn reparent(&self, parent: &Self, lookup: &[(&Self, Option<&Self>, Option)]) -> Self { + let mut new_root = parent.clone(); + let mut current = parent; + let mut clone_mut = &mut new_root; + let find_parent = |child| lookup.iter().find(|(c, _p, _i)| *c == child); + while let Some(&(_, Some(parent), Some(index))) = find_parent(current) { + let mut parent_clone = parent.clone(); + parent_clone.children.swap_remove(index); + clone_mut.children.push(Box::new(parent_clone)); + current = parent; + let new_box = clone_mut + .children + .last_mut() + .expect("We just inserted node, this is not empty"); + clone_mut = new_box.deref_mut(); + } + new_root } #[must_use] - pub fn path_to(self, _from: &str, _to: &str) -> Option> - { - todo!("Implement a function that returns the list of labels in the shortest path from 'from' to 'to'"); + pub fn path_to(&self, from: &T, to: &T) -> Option> { + if from != &self.label { + return self.pov_from(from).and_then(|pov| pov.path_to(from, to)); + } + if to == &self.label { + return Some(vec![self.label.clone()]); + } + for child in self.children.iter() { + if let Some(mut path) = child.path_to(&child.label, to) { + path.insert(0, self.label.clone()); + return Some(path); + } + } + None } } diff --git a/exercises/practice/pov/src/lib.rs b/exercises/practice/pov/src/lib.rs index 33ce33a04..3eea2a7cd 100644 --- a/exercises/practice/pov/src/lib.rs +++ b/exercises/practice/pov/src/lib.rs @@ -1,84 +1,38 @@ -use std::ops::{Deref, DerefMut}; - #[derive(Clone, PartialEq)] pub struct Tree { - label: T, - children: Vec>>, + _remove_this: std::marker::PhantomData, } impl Tree { #[must_use] - pub fn new(label: T) -> Self { - Self { label, children: Default::default() } + pub fn new(_label: T) -> Self { + todo!("Implement a function that creates a new Tree with given label"); } #[must_use] - pub fn with_children(label: T, children: Vec>) -> Self { - Self { label, children: children.into_iter().map(Box::new).collect() } + pub fn with_children(_label: T, _children: Vec) -> Self { + todo!("Implement a function that creates a new Tree with given label and Children"); } #[must_use] pub fn get_label(&self) -> T { - self.label.clone() + todo!("Implement getter for label."); } #[must_use] pub fn get_children(&self) -> Vec<&Self> { - self.children.iter().map(Box::deref).collect() + todo!("Implement getter for children."); } #[must_use] - pub fn pov_from(&self, from: &T) -> Option { - // list of (child, parent, child's index in parent.children) - let mut lookup = vec![(self, None, None)]; - let mut stack = vec![self]; - while let Some(parent) = stack.pop() { - if &parent.label == from { - return self.reparent(parent, lookup.as_slice()).into(); - } - lookup.extend( - parent.children.iter() - .map(Box::deref).enumerate() - .map(|(i, child)| (child, Some(parent), Some(i))) - ); - stack.extend(parent.children.iter().map(Box::deref)); - } - None - } - - // lookup is list of (child, parent, child's index in parent.children) - #[must_use] - fn reparent(&self, parent: &Self, lookup: &[(&Self, Option<&Self>, Option)]) -> Self { - let mut new_root = parent.clone(); - let mut current = parent; - let mut clone_mut = &mut new_root; - let find_parent = |child| lookup.iter().find(|(c, _p, _i)| *c == child); - while let Some(&(_, Some(parent), Some(index))) = find_parent(current) { - let mut parent_clone = parent.clone(); - parent_clone.children.swap_remove(index); - clone_mut.children.push(Box::new(parent_clone)); - current = parent; - let new_box = clone_mut.children.last_mut().expect("We just inserted node, this is not empty"); - clone_mut = new_box.deref_mut(); - } - new_root + pub fn pov_from(&self, _from: &T) -> Option { + todo!("Implement a function that reparents Tree with 'from' as root."); } #[must_use] - pub fn path_to(&self, from: &T, to: &T) -> Option> - { - if from != &self.label { - return self.pov_from(from).and_then(|pov| pov.path_to(from, to)); - } - if to == &self.label { - return Some(vec![self.label.clone()]); - } - for child in self.children.iter() { - if let Some(mut path) = child.path_to(&child.label, to) { - path.insert(0, self.label.clone()); - return Some(path); - } - } - None + pub fn path_to(&self, _from: &T, _to: &T) -> Option> { + todo!( + "Implement a function that returns the list of labels in the shortest path from 'from' to 'to'" + ); } } From a51185824df2e540f29c581578db28faf950e572 Mon Sep 17 00:00:00 2001 From: devcyjung Date: Thu, 3 Jul 2025 15:52:01 +0900 Subject: [PATCH 3/6] Applied the initial reviews --- .../practice/pov/.meta/test_template.tera | 42 ++------- exercises/practice/pov/src/lib.rs | 30 +++---- exercises/practice/pov/tests/pov.rs | 88 ++++++++----------- rust-tooling/generate/src/custom_filters.rs | 22 ----- 4 files changed, 58 insertions(+), 124 deletions(-) diff --git a/exercises/practice/pov/.meta/test_template.tera b/exercises/practice/pov/.meta/test_template.tera index 840f8b2c1..ad6b7690c 100644 --- a/exercises/practice/pov/.meta/test_template.tera +++ b/exercises/practice/pov/.meta/test_template.tera @@ -22,12 +22,8 @@ vec![ {%- endmacro -%} {% for test_group in cases %} -/// {{ test_group.cases[0].property | camel_to_snake }}() tests /// {{ test_group.description }} -{%- for comment in test_group.comments %} -/// {{ comment }} -{%- endfor %} -mod {{ test_group.description | make_ident }} { +mod {{ test_group.cases[0].property | make_ident }} { use pov::*; {% for test in test_group.cases %} @@ -43,7 +39,7 @@ fn {{ test.description | make_ident }}() { {%- else -%} let expected = Some({{ self::render_tree(tree=test.expected) }}); {%- endif -%} - assert!(crate::test_util::tree_option_eq(result, expected)); + assert_eq!(result.map(|t| crate::test_util::tree_to_sorted(&t)), expected.map(|t| crate::test_util::tree_to_sorted(&t))); {%- elif test.property == "pathTo" -%} let to = "{{ test.input.to }}".to_string(); let result = input.path_to(&from, &to); @@ -62,33 +58,9 @@ fn {{ test.description | make_ident }}() { {% endfor %} mod test_util { use pov::*; - - pub fn tree_option_eq(lhs: Option>, rhs: Option>) -> bool { - match (lhs, rhs) { - (None, None) => true, - (Some(l_inner), Some(r_inner)) => tree_eq(&l_inner, &r_inner), - _ => false, - } - } - - pub fn tree_eq(lhs: &Tree, rhs: &Tree) -> bool { - let (l_label, r_label) = (lhs.get_label(), rhs.get_label()); - let (mut l_children, mut r_children) = ( - lhs.get_children(), - rhs.get_children(), - ); - if l_label == r_label && l_children.len() == r_children.len() { - if l_children.len() == 0 { - return true; - } - let key_fn = |child: &&Tree| child.get_label(); - l_children.sort_unstable_by_key(key_fn); - r_children.sort_unstable_by_key(key_fn); - return l_children - .iter() - .zip(r_children.iter()) - .all(|(&lc, &rc)| tree_eq(lc, rc)); - } - false + pub fn tree_to_sorted(tree: &Tree) -> Tree { + let mut children = tree.get_children(); + children.sort_unstable_by_key(|child| child.get_label()); + Tree::with_children(tree.get_label(), children.into_iter().cloned().collect()) } -} \ No newline at end of file +} diff --git a/exercises/practice/pov/src/lib.rs b/exercises/practice/pov/src/lib.rs index 3eea2a7cd..80142dd13 100644 --- a/exercises/practice/pov/src/lib.rs +++ b/exercises/practice/pov/src/lib.rs @@ -1,38 +1,34 @@ -#[derive(Clone, PartialEq)] -pub struct Tree { +use std::fmt::Debug; + +#[derive(Clone, Debug, PartialEq)] +pub struct Tree { _remove_this: std::marker::PhantomData, } -impl Tree { - #[must_use] - pub fn new(_label: T) -> Self { - todo!("Implement a function that creates a new Tree with given label"); +impl Tree { + pub fn new(label: T) -> Self { + todo!("Implement a function that creates a new Tree with given {label:?}"); } - #[must_use] - pub fn with_children(_label: T, _children: Vec) -> Self { - todo!("Implement a function that creates a new Tree with given label and Children"); + pub fn with_children(label: T, children: Vec) -> Self { + todo!("Implement a function that creates a new Tree with given {label:?} and {children:?}"); } - #[must_use] pub fn get_label(&self) -> T { todo!("Implement getter for label."); } - #[must_use] pub fn get_children(&self) -> Vec<&Self> { todo!("Implement getter for children."); } - #[must_use] - pub fn pov_from(&self, _from: &T) -> Option { - todo!("Implement a function that reparents Tree with 'from' as root."); + pub fn pov_from(&self, from: &T) -> Option { + todo!("Implement a function that reparents Tree with {from:?} as root."); } - #[must_use] - pub fn path_to(&self, _from: &T, _to: &T) -> Option> { + pub fn path_to(&self, from: &T, to: &T) -> Option> { todo!( - "Implement a function that returns the list of labels in the shortest path from 'from' to 'to'" + "Implement a function that returns the list of labels in the shortest path from {from:?} to {to:?}" ); } } diff --git a/exercises/practice/pov/tests/pov.rs b/exercises/practice/pov/tests/pov.rs index e1dc8f7ed..915281c27 100644 --- a/exercises/practice/pov/tests/pov.rs +++ b/exercises/practice/pov/tests/pov.rs @@ -1,16 +1,5 @@ -/// from_pov() tests /// Reroot a tree so that its root is the specified node. -/// In this way, the tree is presented from the point of view of the specified node. -/// -/// If appropriate for your track, you may test that the input tree is not modified. -/// -/// Note that when rerooting upon a target node that has both parents and children, -/// it does not matter whether the former parent comes before or after the former children. -/// Please account for this when checking correctness of the resulting trees. -/// One suggested method is to only check two things: -/// 1) The root of the returned tree is the root that was passed in to from_pov. -/// 2) The sorted edge list of the returned tree is the same as the sorted edge list of the expected tree. -mod reroot_a_tree_so_that_its_root_is_the_specified_node { +mod frompov { use pov::*; #[test] @@ -19,7 +8,10 @@ mod reroot_a_tree_so_that_its_root_is_the_specified_node { let from = "x".to_string(); let result = input.pov_from(&from); let expected = Some(Tree::new("x".to_string())); - assert!(crate::test_util::tree_option_eq(result, expected)); + assert_eq!( + result.map(|t| crate::test_util::tree_to_sorted(&t)), + expected.map(|t| crate::test_util::tree_to_sorted(&t)) + ); } #[test] @@ -38,7 +30,10 @@ mod reroot_a_tree_so_that_its_root_is_the_specified_node { vec![Tree::new("sibling".to_string())], )], )); - assert!(crate::test_util::tree_option_eq(result, expected)); + assert_eq!( + result.map(|t| crate::test_util::tree_to_sorted(&t)), + expected.map(|t| crate::test_util::tree_to_sorted(&t)) + ); } #[test] @@ -66,7 +61,10 @@ mod reroot_a_tree_so_that_its_root_is_the_specified_node { ], )], )); - assert!(crate::test_util::tree_option_eq(result, expected)); + assert_eq!( + result.map(|t| crate::test_util::tree_to_sorted(&t)), + expected.map(|t| crate::test_util::tree_to_sorted(&t)) + ); } #[test] @@ -100,7 +98,10 @@ mod reroot_a_tree_so_that_its_root_is_the_specified_node { )], )], )); - assert!(crate::test_util::tree_option_eq(result, expected)); + assert_eq!( + result.map(|t| crate::test_util::tree_to_sorted(&t)), + expected.map(|t| crate::test_util::tree_to_sorted(&t)) + ); } #[test] @@ -126,7 +127,10 @@ mod reroot_a_tree_so_that_its_root_is_the_specified_node { Tree::new("parent".to_string()), ], )); - assert!(crate::test_util::tree_option_eq(result, expected)); + assert_eq!( + result.map(|t| crate::test_util::tree_to_sorted(&t)), + expected.map(|t| crate::test_util::tree_to_sorted(&t)) + ); } #[test] @@ -184,7 +188,10 @@ mod reroot_a_tree_so_that_its_root_is_the_specified_node { ), ], )); - assert!(crate::test_util::tree_option_eq(result, expected)); + assert_eq!( + result.map(|t| crate::test_util::tree_to_sorted(&t)), + expected.map(|t| crate::test_util::tree_to_sorted(&t)) + ); } #[test] @@ -194,7 +201,10 @@ mod reroot_a_tree_so_that_its_root_is_the_specified_node { let from = "nonexistent".to_string(); let result = input.pov_from(&from); let expected: Option> = None; - assert!(crate::test_util::tree_option_eq(result, expected)); + assert_eq!( + result.map(|t| crate::test_util::tree_to_sorted(&t)), + expected.map(|t| crate::test_util::tree_to_sorted(&t)) + ); } #[test] @@ -217,16 +227,15 @@ mod reroot_a_tree_so_that_its_root_is_the_specified_node { let from = "nonexistent".to_string(); let result = input.pov_from(&from); let expected: Option> = None; - assert!(crate::test_util::tree_option_eq(result, expected)); + assert_eq!( + result.map(|t| crate::test_util::tree_to_sorted(&t)), + expected.map(|t| crate::test_util::tree_to_sorted(&t)) + ); } } -/// path_to() tests /// Given two nodes, find the path between them -/// A typical implementation would first reroot the tree on one of the two nodes. -/// -/// If appropriate for your track, you may test that the input tree is not modified. -mod given_two_nodes_find_the_path_between_them { +mod pathto { use pov::*; #[test] @@ -399,30 +408,9 @@ mod given_two_nodes_find_the_path_between_them { mod test_util { use pov::*; - - pub fn tree_option_eq(lhs: Option>, rhs: Option>) -> bool { - match (lhs, rhs) { - (None, None) => true, - (Some(l_inner), Some(r_inner)) => tree_eq(&l_inner, &r_inner), - _ => false, - } - } - - pub fn tree_eq(lhs: &Tree, rhs: &Tree) -> bool { - let (l_label, r_label) = (lhs.get_label(), rhs.get_label()); - let (mut l_children, mut r_children) = (lhs.get_children(), rhs.get_children()); - if l_label == r_label && l_children.len() == r_children.len() { - if l_children.len() == 0 { - return true; - } - let key_fn = |child: &&Tree| child.get_label(); - l_children.sort_unstable_by_key(key_fn); - r_children.sort_unstable_by_key(key_fn); - return l_children - .iter() - .zip(r_children.iter()) - .all(|(&lc, &rc)| tree_eq(lc, rc)); - } - false + pub fn tree_to_sorted(tree: &Tree) -> Tree { + let mut children = tree.get_children(); + children.sort_unstable_by_key(|child| child.get_label()); + Tree::with_children(tree.get_label(), children.into_iter().cloned().collect()) } } diff --git a/rust-tooling/generate/src/custom_filters.rs b/rust-tooling/generate/src/custom_filters.rs index b92eeef27..76b5225d2 100644 --- a/rust-tooling/generate/src/custom_filters.rs +++ b/rust-tooling/generate/src/custom_filters.rs @@ -8,7 +8,6 @@ pub static CUSTOM_FILTERS: &[(&str, Filter)] = &[ ("to_hex", to_hex), ("make_ident", make_ident), ("fmt_num", fmt_num), - ("camel_to_snake", camel_to_snake), ]; pub fn to_hex(value: &Value, _args: &HashMap) -> Result { @@ -56,25 +55,4 @@ pub fn fmt_num(value: &Value, _args: &HashMap) -> Result { pretty_digits.reverse(); let pretty_num = String::from_utf8(pretty_digits).unwrap_or_default(); Ok(Value::String(pretty_num)) -} - -pub fn camel_to_snake(value: &Value, _args: &HashMap) -> Result { - let Some(input) = value.as_str() else { - return Err(tera::Error::call_filter( - "camel_to_snake filter expects a string", - "serde_json::value::Value::as_str", - )); - }; - let mut result = String::with_capacity(input.len() << 1); - let mut peek_iter = input.chars().peekable(); - while let Some(ch) = peek_iter.next() { - result.push(ch); - if let Some(&next_ch) = peek_iter.peek() { - if ch.is_lowercase() && next_ch.is_uppercase() { - result.push('_'); - } - } - } - result.shrink_to_fit(); - Ok(Value::String(result.to_lowercase())) } \ No newline at end of file From 623f98ca806f482ec6be069d5d0a79063eb46c72 Mon Sep 17 00:00:00 2001 From: devcyjung Date: Thu, 3 Jul 2025 16:17:41 +0900 Subject: [PATCH 4/6] Fixed example solution accordingly --- exercises/practice/pov/.meta/example.rs | 7 ++++--- exercises/practice/pov/.meta/test_template.tera | 4 +++- exercises/practice/pov/Cargo.toml | 3 +++ exercises/practice/pov/tests/pov.rs | 7 ++++++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/exercises/practice/pov/.meta/example.rs b/exercises/practice/pov/.meta/example.rs index e19263d2c..fb5e3f906 100644 --- a/exercises/practice/pov/.meta/example.rs +++ b/exercises/practice/pov/.meta/example.rs @@ -1,12 +1,13 @@ +use std::fmt::Debug; use std::ops::{Deref, DerefMut}; -#[derive(Clone, PartialEq)] -pub struct Tree { +#[derive(Clone, Debug, PartialEq)] +pub struct Tree { label: T, children: Vec>>, } -impl Tree { +impl Tree { #[must_use] pub fn new(label: T) -> Self { Self { diff --git a/exercises/practice/pov/.meta/test_template.tera b/exercises/practice/pov/.meta/test_template.tera index ad6b7690c..4b4da883b 100644 --- a/exercises/practice/pov/.meta/test_template.tera +++ b/exercises/practice/pov/.meta/test_template.tera @@ -25,6 +25,7 @@ vec![ /// {{ test_group.description }} mod {{ test_group.cases[0].property | make_ident }} { use pov::*; + use pretty_assertions::assert_eq; {% for test in test_group.cases %} #[test] @@ -56,11 +57,12 @@ fn {{ test.description | make_ident }}() { {% endfor %} } {% endfor %} + mod test_util { use pov::*; pub fn tree_to_sorted(tree: &Tree) -> Tree { let mut children = tree.get_children(); children.sort_unstable_by_key(|child| child.get_label()); - Tree::with_children(tree.get_label(), children.into_iter().cloned().collect()) + Tree::with_children(tree.get_label(), children.iter().map(|c| tree_to_sorted(c)).collect()) } } diff --git a/exercises/practice/pov/Cargo.toml b/exercises/practice/pov/Cargo.toml index 87c6c85d1..34f6633ce 100644 --- a/exercises/practice/pov/Cargo.toml +++ b/exercises/practice/pov/Cargo.toml @@ -7,3 +7,6 @@ edition = "2024" # The full list of available libraries is here: # https://github.com/exercism/rust-test-runner/blob/main/local-registry/Cargo.toml [dependencies] + +[dev-dependencies] +pretty_assertions = "1.4.1" \ No newline at end of file diff --git a/exercises/practice/pov/tests/pov.rs b/exercises/practice/pov/tests/pov.rs index 915281c27..4212c002c 100644 --- a/exercises/practice/pov/tests/pov.rs +++ b/exercises/practice/pov/tests/pov.rs @@ -1,6 +1,7 @@ /// Reroot a tree so that its root is the specified node. mod frompov { use pov::*; + use pretty_assertions::assert_eq; #[test] fn results_in_the_same_tree_if_the_input_tree_is_a_singleton() { @@ -237,6 +238,7 @@ mod frompov { /// Given two nodes, find the path between them mod pathto { use pov::*; + use pretty_assertions::assert_eq; #[test] #[ignore] @@ -411,6 +413,9 @@ mod test_util { pub fn tree_to_sorted(tree: &Tree) -> Tree { let mut children = tree.get_children(); children.sort_unstable_by_key(|child| child.get_label()); - Tree::with_children(tree.get_label(), children.into_iter().cloned().collect()) + Tree::with_children( + tree.get_label(), + children.iter().map(|c| tree_to_sorted(c)).collect(), + ) } } From 19ea634144c33b0fae2af6d50117d9e0c265dfc0 Mon Sep 17 00:00:00 2001 From: devcyjung Date: Thu, 3 Jul 2025 18:24:07 +0900 Subject: [PATCH 5/6] Iteration 2: added camelCase support in make_ident --- exercises/practice/pov/.meta/example.rs | 11 +--- .../practice/pov/.meta/test_template.tera | 24 +++++-- exercises/practice/pov/Cargo.toml | 2 +- exercises/practice/pov/src/lib.rs | 4 +- exercises/practice/pov/tests/pov.rs | 63 +++++++------------ rust-tooling/Cargo.lock | 15 ++--- rust-tooling/generate/Cargo.toml | 1 + rust-tooling/generate/src/custom_filters.rs | 13 +++- 8 files changed, 66 insertions(+), 67 deletions(-) diff --git a/exercises/practice/pov/.meta/example.rs b/exercises/practice/pov/.meta/example.rs index fb5e3f906..31dcba1b4 100644 --- a/exercises/practice/pov/.meta/example.rs +++ b/exercises/practice/pov/.meta/example.rs @@ -8,7 +8,6 @@ pub struct Tree { } impl Tree { - #[must_use] pub fn new(label: T) -> Self { Self { label, @@ -16,7 +15,6 @@ impl Tree { } } - #[must_use] pub fn with_children(label: T, children: Vec) -> Self { Self { label, @@ -24,17 +22,14 @@ impl Tree { } } - #[must_use] - pub fn get_label(&self) -> T { + pub fn label(&self) -> T { self.label.clone() } - #[must_use] - pub fn get_children(&self) -> Vec<&Self> { + pub fn children(&self) -> Vec<&Self> { self.children.iter().map(Box::deref).collect() } - #[must_use] pub fn pov_from(&self, from: &T) -> Option { // list of (child, parent, child's index in parent.children) let mut lookup = vec![(self, None, None)]; @@ -57,7 +52,6 @@ impl Tree { } // lookup is list of (child, parent, child's index in parent.children) - #[must_use] fn reparent(&self, parent: &Self, lookup: &[(&Self, Option<&Self>, Option)]) -> Self { let mut new_root = parent.clone(); let mut current = parent; @@ -77,7 +71,6 @@ impl Tree { new_root } - #[must_use] pub fn path_to(&self, from: &T, to: &T) -> Option> { if from != &self.label { return self.pov_from(from).and_then(|pov| pov.path_to(from, to)); diff --git a/exercises/practice/pov/.meta/test_template.tera b/exercises/practice/pov/.meta/test_template.tera index 4b4da883b..15f53a3ed 100644 --- a/exercises/practice/pov/.meta/test_template.tera +++ b/exercises/practice/pov/.meta/test_template.tera @@ -26,6 +26,9 @@ vec![ mod {{ test_group.cases[0].property | make_ident }} { use pov::*; use pretty_assertions::assert_eq; +{%- if test_group.cases[0].property == "fromPov" -%} + use super::test_util::tree_to_sorted; +{%- endif -%} {% for test in test_group.cases %} #[test] @@ -40,7 +43,7 @@ fn {{ test.description | make_ident }}() { {%- else -%} let expected = Some({{ self::render_tree(tree=test.expected) }}); {%- endif -%} - assert_eq!(result.map(|t| crate::test_util::tree_to_sorted(&t)), expected.map(|t| crate::test_util::tree_to_sorted(&t))); + assert_eq!(tree_to_sorted(result), tree_to_sorted(expected)); {%- elif test.property == "pathTo" -%} let to = "{{ test.input.to }}".to_string(); let result = input.path_to(&from, &to); @@ -60,9 +63,20 @@ fn {{ test.description | make_ident }}() { mod test_util { use pov::*; - pub fn tree_to_sorted(tree: &Tree) -> Tree { - let mut children = tree.get_children(); - children.sort_unstable_by_key(|child| child.get_label()); - Tree::with_children(tree.get_label(), children.iter().map(|c| tree_to_sorted(c)).collect()) + use std::fmt::Debug; + + pub fn tree_to_sorted( + tree_opt: Option>, + ) -> Option> { + tree_opt.map(sorter) + } + + fn sorter(tree: Tree) -> Tree { + let mut children = tree.children(); + children.sort_unstable_by_key(|child| child.label()); + Tree::with_children( + tree.label(), + children.into_iter().cloned().map(|c| sorter(c)).collect(), + ) } } diff --git a/exercises/practice/pov/Cargo.toml b/exercises/practice/pov/Cargo.toml index 34f6633ce..4c3572c9f 100644 --- a/exercises/practice/pov/Cargo.toml +++ b/exercises/practice/pov/Cargo.toml @@ -9,4 +9,4 @@ edition = "2024" [dependencies] [dev-dependencies] -pretty_assertions = "1.4.1" \ No newline at end of file +pretty_assertions = "*" \ No newline at end of file diff --git a/exercises/practice/pov/src/lib.rs b/exercises/practice/pov/src/lib.rs index 80142dd13..6d69b82a7 100644 --- a/exercises/practice/pov/src/lib.rs +++ b/exercises/practice/pov/src/lib.rs @@ -14,11 +14,11 @@ impl Tree { todo!("Implement a function that creates a new Tree with given {label:?} and {children:?}"); } - pub fn get_label(&self) -> T { + pub fn label(&self) -> T { todo!("Implement getter for label."); } - pub fn get_children(&self) -> Vec<&Self> { + pub fn children(&self) -> Vec<&Self> { todo!("Implement getter for children."); } diff --git a/exercises/practice/pov/tests/pov.rs b/exercises/practice/pov/tests/pov.rs index 4212c002c..ac19a9b02 100644 --- a/exercises/practice/pov/tests/pov.rs +++ b/exercises/practice/pov/tests/pov.rs @@ -1,18 +1,15 @@ /// Reroot a tree so that its root is the specified node. -mod frompov { +mod from_pov { + use super::test_util::tree_to_sorted; use pov::*; use pretty_assertions::assert_eq; - #[test] fn results_in_the_same_tree_if_the_input_tree_is_a_singleton() { let input = Tree::new("x".to_string()); let from = "x".to_string(); let result = input.pov_from(&from); let expected = Some(Tree::new("x".to_string())); - assert_eq!( - result.map(|t| crate::test_util::tree_to_sorted(&t)), - expected.map(|t| crate::test_util::tree_to_sorted(&t)) - ); + assert_eq!(tree_to_sorted(result), tree_to_sorted(expected)); } #[test] @@ -31,10 +28,7 @@ mod frompov { vec![Tree::new("sibling".to_string())], )], )); - assert_eq!( - result.map(|t| crate::test_util::tree_to_sorted(&t)), - expected.map(|t| crate::test_util::tree_to_sorted(&t)) - ); + assert_eq!(tree_to_sorted(result), tree_to_sorted(expected)); } #[test] @@ -62,10 +56,7 @@ mod frompov { ], )], )); - assert_eq!( - result.map(|t| crate::test_util::tree_to_sorted(&t)), - expected.map(|t| crate::test_util::tree_to_sorted(&t)) - ); + assert_eq!(tree_to_sorted(result), tree_to_sorted(expected)); } #[test] @@ -99,10 +90,7 @@ mod frompov { )], )], )); - assert_eq!( - result.map(|t| crate::test_util::tree_to_sorted(&t)), - expected.map(|t| crate::test_util::tree_to_sorted(&t)) - ); + assert_eq!(tree_to_sorted(result), tree_to_sorted(expected)); } #[test] @@ -128,10 +116,7 @@ mod frompov { Tree::new("parent".to_string()), ], )); - assert_eq!( - result.map(|t| crate::test_util::tree_to_sorted(&t)), - expected.map(|t| crate::test_util::tree_to_sorted(&t)) - ); + assert_eq!(tree_to_sorted(result), tree_to_sorted(expected)); } #[test] @@ -189,10 +174,7 @@ mod frompov { ), ], )); - assert_eq!( - result.map(|t| crate::test_util::tree_to_sorted(&t)), - expected.map(|t| crate::test_util::tree_to_sorted(&t)) - ); + assert_eq!(tree_to_sorted(result), tree_to_sorted(expected)); } #[test] @@ -202,10 +184,7 @@ mod frompov { let from = "nonexistent".to_string(); let result = input.pov_from(&from); let expected: Option> = None; - assert_eq!( - result.map(|t| crate::test_util::tree_to_sorted(&t)), - expected.map(|t| crate::test_util::tree_to_sorted(&t)) - ); + assert_eq!(tree_to_sorted(result), tree_to_sorted(expected)); } #[test] @@ -228,18 +207,14 @@ mod frompov { let from = "nonexistent".to_string(); let result = input.pov_from(&from); let expected: Option> = None; - assert_eq!( - result.map(|t| crate::test_util::tree_to_sorted(&t)), - expected.map(|t| crate::test_util::tree_to_sorted(&t)) - ); + assert_eq!(tree_to_sorted(result), tree_to_sorted(expected)); } } /// Given two nodes, find the path between them -mod pathto { +mod path_to { use pov::*; use pretty_assertions::assert_eq; - #[test] #[ignore] fn can_find_path_to_parent() { @@ -410,12 +385,18 @@ mod pathto { mod test_util { use pov::*; - pub fn tree_to_sorted(tree: &Tree) -> Tree { - let mut children = tree.get_children(); - children.sort_unstable_by_key(|child| child.get_label()); + use std::fmt::Debug; + + pub fn tree_to_sorted(tree_opt: Option>) -> Option> { + tree_opt.map(sorter) + } + + fn sorter(tree: Tree) -> Tree { + let mut children = tree.children(); + children.sort_unstable_by_key(|child| child.label()); Tree::with_children( - tree.get_label(), - children.iter().map(|c| tree_to_sorted(c)).collect(), + tree.label(), + children.into_iter().cloned().map(|c| sorter(c)).collect(), ) } } diff --git a/rust-tooling/Cargo.lock b/rust-tooling/Cargo.lock index 2e98226bc..f54d88314 100644 --- a/rust-tooling/Cargo.lock +++ b/rust-tooling/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -370,6 +370,7 @@ dependencies = [ "glob", "inquire", "models", + "regex", "serde_json", "slug", "tera", @@ -816,9 +817,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -828,9 +829,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -839,9 +840,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" diff --git a/rust-tooling/generate/Cargo.toml b/rust-tooling/generate/Cargo.toml index 2525ee049..6c7ace8e7 100644 --- a/rust-tooling/generate/Cargo.toml +++ b/rust-tooling/generate/Cargo.toml @@ -20,6 +20,7 @@ inquire = "0.6.2" models = { version = "0.1.0", path = "../models" } serde_json = { version = "1.0.105", features = ["arbitrary_precision", "preserve_order"] } slug = "0.1.5" +regex = "1.11.1" tera = "1.19.1" utils = { version = "0.1.0", path = "../utils" } uuid = { version = "1.4.1", features = ["v4"] } diff --git a/rust-tooling/generate/src/custom_filters.rs b/rust-tooling/generate/src/custom_filters.rs index 76b5225d2..27c3d3336 100644 --- a/rust-tooling/generate/src/custom_filters.rs +++ b/rust-tooling/generate/src/custom_filters.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; - +use regex::Regex; use tera::{Result, Value}; type Filter = fn(&Value, &HashMap) -> Result; @@ -27,6 +27,7 @@ pub fn make_ident(value: &Value, _args: &HashMap) -> Result) -> Result String { + let re = Regex::new(r"([a-z0-9])([A-Z])").unwrap(); + let value = re.replace_all(value, "${1}_${2}"); + let re2 = Regex::new(r"([A-Z])([A-Z][a-z])").unwrap(); + let value = re2.replace_all(&value, "${1}_${2}"); + String::from(value) +} + pub fn fmt_num(value: &Value, _args: &HashMap) -> Result { let Some(value) = value.as_number() else { return Err(tera::Error::call_filter( @@ -55,4 +64,4 @@ pub fn fmt_num(value: &Value, _args: &HashMap) -> Result { pretty_digits.reverse(); let pretty_num = String::from_utf8(pretty_digits).unwrap_or_default(); Ok(Value::String(pretty_num)) -} \ No newline at end of file +} From ffbc4b9b1602b6eecf4ca28c56fd8a208c46f4e0 Mon Sep 17 00:00:00 2001 From: devcyjung Date: Thu, 3 Jul 2025 18:39:15 +0900 Subject: [PATCH 6/6] Fixed newlines --- exercises/practice/pov/.meta/test_template.tera | 2 +- exercises/practice/pov/tests/pov.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/exercises/practice/pov/.meta/test_template.tera b/exercises/practice/pov/.meta/test_template.tera index 15f53a3ed..51d2a95b4 100644 --- a/exercises/practice/pov/.meta/test_template.tera +++ b/exercises/practice/pov/.meta/test_template.tera @@ -28,7 +28,7 @@ mod {{ test_group.cases[0].property | make_ident }} { use pretty_assertions::assert_eq; {%- if test_group.cases[0].property == "fromPov" -%} use super::test_util::tree_to_sorted; -{%- endif -%} +{%- endif %} {% for test in test_group.cases %} #[test] diff --git a/exercises/practice/pov/tests/pov.rs b/exercises/practice/pov/tests/pov.rs index ac19a9b02..66594895a 100644 --- a/exercises/practice/pov/tests/pov.rs +++ b/exercises/practice/pov/tests/pov.rs @@ -3,6 +3,7 @@ mod from_pov { use super::test_util::tree_to_sorted; use pov::*; use pretty_assertions::assert_eq; + #[test] fn results_in_the_same_tree_if_the_input_tree_is_a_singleton() { let input = Tree::new("x".to_string()); @@ -215,6 +216,7 @@ mod from_pov { mod path_to { use pov::*; use pretty_assertions::assert_eq; + #[test] #[ignore] fn can_find_path_to_parent() {