Skip to content

Commit aed1317

Browse files
1995parhamclaude
andcommitted
feat: add tree-traversal, longest-word, and dict-resolve challenges
Turn three draft exercises into standalone cargo packages with idiomatic typing, tests, and doc comments explaining the type choices: - tree-traversal: Option<Box<Node>> for owned, sized recursive children - longest-word: Option<&str> return with explicit borrow lifetimes - dict-resolve: enum for number-or-reference values, cycle detection via seen set Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d36b4af commit aed1317

10 files changed

Lines changed: 373 additions & 0 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,24 @@ This application receives a delimiter and some entries
213213
(i.e. some text files or array of strings) and after separation based on a delimiter,
214214
it will count and calculate the number of each word in the entries.
215215

216+
### Tree Traversal 🌳
217+
218+
Traverses a binary tree in pre-order, in-order, and post-order.
219+
The node structure stores each child as an `Option<Box<Node>>`,
220+
so `None` marks an empty subtree and the recursive type keeps a known size.
221+
222+
### Longest Word 📏
223+
224+
Given a `&str`, returns its longest whitespace-separated word,
225+
plus a variant that returns the longest word across two strings.
226+
It is a small exercise in lifetimes: the returned slice borrows from the input.
227+
228+
### Dict Resolve 🔗
229+
230+
Retrieves the numeric value for a key from a dictionary where a value is
231+
either a number or a reference to another key (e.g. deployment-stage timeouts).
232+
Reference chains can form cycles, so visited keys are tracked to avoid looping forever.
233+
216234
### Data in Depth
217235

218236
This example is based on _Rust in Action_ book and shows how data is stored.

dict-resolve/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

dict-resolve/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "dict-resolve"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]

dict-resolve/src/main.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//! Retrieve the numeric value for a key from a dictionary where a value is
2+
//! either a number or a *reference* to another key.
3+
//!
4+
//! Type choices:
5+
//! - Rust has no `String | i32` union type, so "number or reference" is a
6+
//! tagged `enum Value`. `match` on it is exhaustive and checked at compile
7+
//! time — replacing the draft's runtime `type(val) == "String"` test.
8+
//! - Visited keys are a `HashSet<String>` (one type parameter — the element),
9+
//! not the draft's `HashSet<String, bool>`; a set already answers "seen it?".
10+
//! - `resolve` returns `Option<i64>`: `None` covers both a missing key and a
11+
//! reference cycle (`local -> default -> local`) that never reaches a number.
12+
13+
use std::collections::{HashMap, HashSet};
14+
15+
/// A dictionary entry: a concrete number, or a pointer to another key.
16+
/// Encodes the draft's `String | i32` as a proper sum type.
17+
#[derive(Debug, Clone)]
18+
enum Value {
19+
Num(i64),
20+
Ref(String),
21+
}
22+
23+
/// Resolve `key` to its numeric value, following references transitively.
24+
///
25+
/// Returns `None` when the key is missing, or when the reference chain forms a
26+
/// cycle and therefore never bottoms out at a number.
27+
fn resolve(map: &HashMap<String, Value>, key: &str) -> Option<i64> {
28+
let mut seen = HashSet::new();
29+
let mut current = key;
30+
31+
loop {
32+
// A key we've already visited means we're going in circles.
33+
if !seen.insert(current.to_string()) {
34+
return None;
35+
}
36+
37+
match map.get(current)? {
38+
Value::Num(n) => return Some(*n),
39+
Value::Ref(next) => current = next,
40+
}
41+
}
42+
}
43+
44+
fn main() {
45+
// Timeout (seconds) per deployment stage; some stages point at another.
46+
let config = HashMap::from([
47+
("prod".to_string(), Value::Num(2)),
48+
("test".to_string(), Value::Num(5)),
49+
("local".to_string(), Value::Ref("default".to_string())),
50+
("default".to_string(), Value::Ref("local".to_string())),
51+
("staging".to_string(), Value::Ref("test".to_string())),
52+
]);
53+
54+
for key in ["prod", "test", "staging", "local", "missing"] {
55+
println!("{key:>8} -> {:?}", resolve(&config, key));
56+
}
57+
}
58+
59+
#[cfg(test)]
60+
mod tests {
61+
use super::*;
62+
63+
fn config() -> HashMap<String, Value> {
64+
HashMap::from([
65+
("prod".to_string(), Value::Num(2)),
66+
("test".to_string(), Value::Num(5)),
67+
("local".to_string(), Value::Ref("default".to_string())),
68+
("default".to_string(), Value::Ref("local".to_string())),
69+
("staging".to_string(), Value::Ref("test".to_string())),
70+
])
71+
}
72+
73+
#[test]
74+
fn direct_number() {
75+
assert_eq!(resolve(&config(), "prod"), Some(2));
76+
assert_eq!(resolve(&config(), "test"), Some(5));
77+
}
78+
79+
#[test]
80+
fn single_reference() {
81+
assert_eq!(resolve(&config(), "staging"), Some(5));
82+
}
83+
84+
#[test]
85+
fn cyclic_reference_returns_none() {
86+
assert_eq!(resolve(&config(), "local"), None);
87+
assert_eq!(resolve(&config(), "default"), None);
88+
}
89+
90+
#[test]
91+
fn missing_key_returns_none() {
92+
assert_eq!(resolve(&config(), "missing"), None);
93+
}
94+
95+
#[test]
96+
fn longer_chain() {
97+
let map = HashMap::from([
98+
("a".to_string(), Value::Ref("b".to_string())),
99+
("b".to_string(), Value::Ref("c".to_string())),
100+
("c".to_string(), Value::Num(42)),
101+
]);
102+
assert_eq!(resolve(&map, "a"), Some(42));
103+
}
104+
}

longest-word/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

longest-word/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "longest-word"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]

longest-word/src/main.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//! Given a `&str`, return its longest whitespace-separated word.
2+
//!
3+
//! Type choices:
4+
//! - Returns `Option<&str>`, not `&str`. The result is a *slice into the
5+
//! input*, and empty/whitespace-only input has no longest word — `None`
6+
//! makes that a type-level outcome instead of panicking on `words[0]`.
7+
//! - No intermediate `Vec<&str>`: `split_whitespace()` yields an iterator we
8+
//! reduce directly with `max_by_key`.
9+
10+
/// Longest word in a single string. Ties resolve to the last such word
11+
/// (`max_by_key` keeps the last maximum).
12+
fn longest_word(s: &str) -> Option<&str> {
13+
s.split_whitespace().max_by_key(|word| word.len())
14+
}
15+
16+
/// Longest word across two strings.
17+
///
18+
/// The `'a` lifetime is the point of this signature: the returned slice borrows
19+
/// from *one of* the inputs, so both `s1` and `s2` must share `'a` and outlive
20+
/// the result. Writing `-> &str` with no `'a` (as the draft did) wouldn't
21+
/// compile — the compiler can't infer which input the borrow comes from.
22+
fn longest_word_couple<'a>(s1: &'a str, s2: &'a str) -> Option<&'a str> {
23+
[longest_word(s1), longest_word(s2)]
24+
.into_iter()
25+
.flatten()
26+
.max_by_key(|word| word.len())
27+
}
28+
29+
fn main() {
30+
let sentence = "the quick brown fox jumped";
31+
println!("longest in {sentence:?} -> {:?}", longest_word(sentence));
32+
33+
let a = "a lazy dog";
34+
let b = "an energetic caterpillar";
35+
println!(
36+
"longest across {a:?} and {b:?} -> {:?}",
37+
longest_word_couple(a, b)
38+
);
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use super::*;
44+
45+
#[test]
46+
fn picks_longest() {
47+
assert_eq!(longest_word("the quick brownish fox"), Some("brownish"));
48+
}
49+
50+
#[test]
51+
fn ties_resolve_to_last() {
52+
// "quick" and "brown" are both 5 chars; max_by_key keeps the last.
53+
assert_eq!(longest_word("the quick brown fox"), Some("brown"));
54+
}
55+
56+
#[test]
57+
fn single_word() {
58+
assert_eq!(longest_word("hello"), Some("hello"));
59+
}
60+
61+
#[test]
62+
fn empty_and_whitespace() {
63+
assert_eq!(longest_word(""), None);
64+
assert_eq!(longest_word(" \t\n "), None);
65+
}
66+
67+
#[test]
68+
fn ignores_extra_whitespace() {
69+
assert_eq!(longest_word(" a bb ccc "), Some("ccc"));
70+
}
71+
72+
#[test]
73+
fn couple_picks_global_longest() {
74+
assert_eq!(
75+
longest_word_couple("a lazy dog", "an energetic caterpillar"),
76+
Some("caterpillar")
77+
);
78+
}
79+
80+
#[test]
81+
fn couple_handles_one_empty_side() {
82+
assert_eq!(longest_word_couple("", "solo"), Some("solo"));
83+
assert_eq!(longest_word_couple("", ""), None);
84+
}
85+
}

tree-traversal/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

tree-traversal/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "tree-traversal"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]

tree-traversal/src/main.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//! Traverse a binary tree in in-order / pre-order / post-order.
2+
//!
3+
//! Type choices:
4+
//! - `val: i64` — Rust's 64-bit signed integer (the draft's `int64` isn't a
5+
//! real type name).
6+
//! - Children are `Option<Box<Node>>`, not `Option<&Node>`. A `&Node` only
7+
//! *borrows* a node owned elsewhere; a tree needs to *own* its children, and
8+
//! `Box` is an owning heap pointer. `Box` is also required for the type to
9+
//! exist at all: a `Node` holding a bare `Node` would be infinitely sized,
10+
//! and the fixed-size `Box` pointer breaks that recursion.
11+
//! - `Option` distinguishes an empty subtree (`None`) from a present child.
12+
13+
#[derive(Debug)]
14+
struct Node {
15+
val: i64,
16+
// `Box` owns the child on the heap; `Option` allows an absent subtree.
17+
left: Option<Box<Node>>,
18+
right: Option<Box<Node>>,
19+
}
20+
21+
impl Node {
22+
fn leaf(val: i64) -> Box<Node> {
23+
Box::new(Node {
24+
val,
25+
left: None,
26+
right: None,
27+
})
28+
}
29+
30+
fn new(val: i64, left: Option<Box<Node>>, right: Option<Box<Node>>) -> Box<Node> {
31+
Box::new(Node { val, left, right })
32+
}
33+
}
34+
35+
/// Root, then left subtree, then right subtree.
36+
fn pre_order(root: &Option<Box<Node>>, out: &mut Vec<i64>) {
37+
if let Some(node) = root {
38+
out.push(node.val);
39+
pre_order(&node.left, out);
40+
pre_order(&node.right, out);
41+
}
42+
}
43+
44+
/// Left subtree, then root, then right subtree.
45+
fn in_order(root: &Option<Box<Node>>, out: &mut Vec<i64>) {
46+
if let Some(node) = root {
47+
in_order(&node.left, out);
48+
out.push(node.val);
49+
in_order(&node.right, out);
50+
}
51+
}
52+
53+
/// Left subtree, then right subtree, then root.
54+
fn post_order(root: &Option<Box<Node>>, out: &mut Vec<i64>) {
55+
if let Some(node) = root {
56+
post_order(&node.left, out);
57+
post_order(&node.right, out);
58+
out.push(node.val);
59+
}
60+
}
61+
62+
/// Small helper so callers don't have to allocate the output vector themselves.
63+
fn collect(root: &Option<Box<Node>>, order: fn(&Option<Box<Node>>, &mut Vec<i64>)) -> Vec<i64> {
64+
let mut out = Vec::new();
65+
order(root, &mut out);
66+
out
67+
}
68+
69+
fn main() {
70+
// 1
71+
// / \
72+
// 2 3
73+
// / \
74+
// 4 5
75+
let tree = Some(Node::new(
76+
1,
77+
Some(Node::new(2, Some(Node::leaf(4)), Some(Node::leaf(5)))),
78+
Some(Node::leaf(3)),
79+
));
80+
81+
println!("pre-order: {:?}", collect(&tree, pre_order));
82+
println!("in-order: {:?}", collect(&tree, in_order));
83+
println!("post-order: {:?}", collect(&tree, post_order));
84+
85+
let empty: Option<Box<Node>> = None;
86+
println!("empty: {:?}", collect(&empty, in_order));
87+
}
88+
89+
#[cfg(test)]
90+
mod tests {
91+
use super::*;
92+
93+
#[test]
94+
fn empty_tree_yields_nothing() {
95+
let empty: Option<Box<Node>> = None;
96+
assert!(collect(&empty, pre_order).is_empty());
97+
assert!(collect(&empty, in_order).is_empty());
98+
assert!(collect(&empty, post_order).is_empty());
99+
}
100+
101+
#[test]
102+
fn single_root() {
103+
let tree = Some(Node::leaf(1));
104+
assert_eq!(collect(&tree, pre_order), [1]);
105+
assert_eq!(collect(&tree, in_order), [1]);
106+
assert_eq!(collect(&tree, post_order), [1]);
107+
}
108+
109+
#[test]
110+
fn root_with_left_child() {
111+
let tree = Some(Node::new(1, Some(Node::leaf(2)), None));
112+
assert_eq!(collect(&tree, pre_order), [1, 2]);
113+
assert_eq!(collect(&tree, in_order), [2, 1]);
114+
assert_eq!(collect(&tree, post_order), [2, 1]);
115+
}
116+
117+
#[test]
118+
fn root_with_right_child() {
119+
let tree = Some(Node::new(1, None, Some(Node::leaf(2))));
120+
assert_eq!(collect(&tree, pre_order), [1, 2]);
121+
assert_eq!(collect(&tree, in_order), [1, 2]);
122+
assert_eq!(collect(&tree, post_order), [2, 1]);
123+
}
124+
125+
#[test]
126+
fn root_left_then_right() {
127+
// root -> left -> right
128+
let tree = Some(Node::new(1, Some(Node::new(2, None, Some(Node::leaf(3)))), None));
129+
assert_eq!(collect(&tree, pre_order), [1, 2, 3]);
130+
assert_eq!(collect(&tree, in_order), [2, 3, 1]);
131+
assert_eq!(collect(&tree, post_order), [3, 2, 1]);
132+
}
133+
134+
#[test]
135+
fn full_tree() {
136+
let tree = Some(Node::new(
137+
1,
138+
Some(Node::new(2, Some(Node::leaf(4)), Some(Node::leaf(5)))),
139+
Some(Node::leaf(3)),
140+
));
141+
assert_eq!(collect(&tree, pre_order), [1, 2, 4, 5, 3]);
142+
assert_eq!(collect(&tree, in_order), [4, 2, 5, 1, 3]);
143+
assert_eq!(collect(&tree, post_order), [4, 5, 2, 3, 1]);
144+
}
145+
}

0 commit comments

Comments
 (0)