Skip to content

Commit c592d08

Browse files
committed
Add word counter exercise with solution and documentation
1 parent 718577c commit c592d08

File tree

4 files changed

+259
-0
lines changed

4 files changed

+259
-0
lines changed

src/SUMMARY.md

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@
118118
- [`HashMap`](std-types/hashmap.md)
119119
- [Exercise: Counter](std-types/exercise.md)
120120
- [Solution](std-types/solution.md)
121+
- [Exercise: Word Counter](std-types/word_counter.md)
122+
- [Solution](std-types/word_counter_solution.md)
121123
- [Standard Library Traits](std-traits.md)
122124
- [Comparisons](std-traits/comparisons.md)
123125
- [Operators](std-traits/operators.md)

src/std-types/Cargo.toml

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ version = "0.1.0"
44
edition = "2021"
55
publish = false
66

7+
[dependencies]
8+
clap = { version = "4.4", features = ["derive"] }
9+
710
[[bin]]
811
name = "hashset"
912
path = "exercise.rs"
13+
14+
[[bin]]
15+
name = "word_counter"
16+
path = "word_counter.rs"

src/std-types/word_counter.rs

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// ANCHOR: word_counter
2+
use std::collections::HashMap;
3+
use clap::Parser;
4+
5+
/// A word counting program
6+
#[derive(Parser)]
7+
#[command(author, version, about, long_about = None)]
8+
struct Args {
9+
/// Text to count words in
10+
#[arg(short, long)]
11+
text: Option<String>,
12+
13+
/// File to read text from
14+
#[arg(short, long)]
15+
file: Option<String>,
16+
17+
/// Ignore case when counting words
18+
#[arg(short, long, default_value_t = true)]
19+
ignore_case: bool,
20+
}
21+
22+
/// WordCounter counts the frequency of words in text.
23+
struct WordCounter {
24+
word_counts: HashMap<String, usize>,
25+
ignore_case: bool,
26+
}
27+
28+
impl WordCounter {
29+
/// Create a new WordCounter.
30+
fn new(ignore_case: bool) -> Self {
31+
WordCounter {
32+
word_counts: HashMap::new(),
33+
ignore_case,
34+
}
35+
}
36+
37+
/// Count words in the given text.
38+
fn count_words(&mut self, text: &str) {
39+
for word in text.split_whitespace() {
40+
let word = if self.ignore_case {
41+
word.to_lowercase()
42+
} else {
43+
word.to_string()
44+
};
45+
*self.word_counts.entry(word).or_insert(0) += 1;
46+
}
47+
}
48+
49+
/// Get the count for a specific word.
50+
fn word_count(&self, word: &str) -> usize {
51+
let word = if self.ignore_case {
52+
word.to_lowercase()
53+
} else {
54+
word.to_string()
55+
};
56+
self.word_counts.get(&word).copied().unwrap_or(0)
57+
}
58+
59+
/// Find the most frequent word(s) and their count.
60+
fn most_frequent(&self) -> Vec<(&str, usize)> {
61+
if self.word_counts.is_empty() {
62+
return Vec::new();
63+
}
64+
65+
let max_count = self.word_counts.values().max().unwrap();
66+
self.word_counts
67+
.iter()
68+
.filter(|(_, &count)| count == *max_count)
69+
.map(|(word, &count)| (word.as_str(), count))
70+
.collect()
71+
}
72+
73+
/// Print word counts in alphabetical order
74+
fn print_counts(&self) {
75+
let mut words: Vec<_> = self.word_counts.keys().collect();
76+
words.sort();
77+
for word in words {
78+
println!("{}: {}", word, self.word_counts[word]);
79+
}
80+
}
81+
}
82+
// ANCHOR_END: word_counter
83+
84+
// ANCHOR: tests
85+
#[test]
86+
fn test_empty_counter() {
87+
let counter = WordCounter::new(true);
88+
assert_eq!(counter.word_count("any"), 0);
89+
assert!(counter.most_frequent().is_empty());
90+
}
91+
92+
#[test]
93+
fn test_simple_text() {
94+
let mut counter = WordCounter::new(true);
95+
counter.count_words("Hello world, hello Rust!");
96+
assert_eq!(counter.word_count("hello"), 2);
97+
assert_eq!(counter.word_count("rust"), 1);
98+
assert_eq!(counter.word_count("world"), 1);
99+
}
100+
101+
#[test]
102+
fn test_case_insensitive() {
103+
let mut counter = WordCounter::new(true);
104+
counter.count_words("Hello HELLO hello");
105+
assert_eq!(counter.word_count("hello"), 3);
106+
assert_eq!(counter.word_count("HELLO"), 3);
107+
}
108+
109+
#[test]
110+
fn test_most_frequent() {
111+
let mut counter = WordCounter::new(true);
112+
counter.count_words("hello world hello rust hello");
113+
let most_frequent = counter.most_frequent();
114+
assert_eq!(most_frequent, vec![("hello", 3)]);
115+
}
116+
// ANCHOR_END: tests
117+
118+
fn main() {
119+
let args = Args::parse();
120+
121+
let mut counter = WordCounter::new(args.ignore_case);
122+
123+
if let Some(text) = args.text {
124+
counter.count_words(&text);
125+
} else if let Some(file) = args.file {
126+
match std::fs::read_to_string(file) {
127+
Ok(content) => counter.count_words(&content),
128+
Err(e) => {
129+
eprintln!("Error reading file: {}", e);
130+
std::process::exit(1);
131+
}
132+
}
133+
} else {
134+
eprintln!("Please provide either --text or --file");
135+
std::process::exit(1);
136+
}
137+
138+
println!("Word counts:");
139+
counter.print_counts();
140+
}
+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use std::collections::HashMap;
2+
use std::error::Error;
3+
4+
fn count_words(text: &str) -> Result<HashMap<String, u32>, Box<dyn Error>> {
5+
if text.trim().is_empty() {
6+
return Err("Empty input".into());
7+
}
8+
9+
let mut word_counts = HashMap::new();
10+
11+
for word in text.split_whitespace() {
12+
*word_counts.entry(word.to_lowercase()).or_insert(0) += 1;
13+
}
14+
15+
Ok(word_counts)
16+
}
17+
18+
fn print_word_counts(counts: &HashMap<String, u32>) {
19+
let mut words: Vec<_> = counts.keys().collect();
20+
words.sort();
21+
22+
for word in words {
23+
println!("{}: {}", word, counts[word]);
24+
}
25+
}
26+
27+
fn main() -> Result<(), Box<dyn Error>> {
28+
let text = "The quick brown fox jumps over the lazy dog";
29+
let counts = count_words(text)?;
30+
print_word_counts(&counts);
31+
32+
// Test empty input
33+
match count_words("") {
34+
Ok(_) => println!("Unexpected success with empty input"),
35+
Err(e) => println!("Expected error: {}", e),
36+
}
37+
38+
Ok(())
39+
}
40+
41+
struct WordCounter {
42+
word_counts: HashMap<String, usize>,
43+
}
44+
45+
impl WordCounter {
46+
fn new() -> Self {
47+
WordCounter {
48+
word_counts: HashMap::new(),
49+
}
50+
}
51+
52+
fn count_words(&mut self, text: &str) {
53+
// Convert to lowercase and split into words
54+
for word in text.to_lowercase()
55+
.split(|c: char| !c.is_alphabetic())
56+
.filter(|s| !s.is_empty())
57+
{
58+
*self.word_counts.entry(word.to_string()).or_insert(0) += 1;
59+
}
60+
}
61+
62+
fn word_count(&self, word: &str) -> usize {
63+
self.word_counts.get(&word.to_lowercase()).copied().unwrap_or(0)
64+
}
65+
66+
fn most_frequent(&self) -> Vec<(&str, usize)> {
67+
if self.word_counts.is_empty() {
68+
return Vec::new();
69+
}
70+
71+
let max_count = self.word_counts.values().max().unwrap();
72+
self.word_counts
73+
.iter()
74+
.filter(|(_, &count)| count == *max_count)
75+
.map(|(word, &count)| (word.as_str(), count))
76+
.collect()
77+
}
78+
}
79+
80+
#[test]
81+
fn test_empty_counter() {
82+
let counter = WordCounter::new();
83+
assert_eq!(counter.word_count("any"), 0);
84+
assert!(counter.most_frequent().is_empty());
85+
}
86+
87+
#[test]
88+
fn test_simple_text() {
89+
let mut counter = WordCounter::new();
90+
counter.count_words("Hello world, hello Rust!");
91+
assert_eq!(counter.word_count("hello"), 2);
92+
assert_eq!(counter.word_count("rust"), 1);
93+
assert_eq!(counter.word_count("world"), 1);
94+
}
95+
96+
#[test]
97+
fn test_case_insensitive() {
98+
let mut counter = WordCounter::new();
99+
counter.count_words("Hello HELLO hello");
100+
assert_eq!(counter.word_count("hello"), 3);
101+
assert_eq!(counter.word_count("HELLO"), 3);
102+
}
103+
104+
#[test]
105+
fn test_most_frequent() {
106+
let mut counter = WordCounter::new();
107+
counter.count_words("hello world hello rust hello");
108+
let most_frequent = counter.most_frequent();
109+
assert_eq!(most_frequent, vec![("hello", 3)]);
110+
}

0 commit comments

Comments
 (0)