Beginner Projects
Word and Character Counter
A word and character counter (modeled after the Unix wc utility) inspects one or more byte streams or UTF-8 text streams to compute newline counts (-l), whitespace-delimited word counts (-w), Unicode scalar character counts (-m), raw byte counts (-c), and maximum line width (-L), aggregating a grand total row when multiple inputs are supplied.
Single-Pass State-Machine Counter (`wc -l -w -m -c -L`):
Input Stream ---> For each `char` `c`:
- `bytes += c.len_utf8()`
- `chars += 1`
- If `c == '\n'`: `lines += 1`, update `max_line_chars`, reset `cur_line_chars = 0`
- If `c.is_whitespace()`: `in_word = false`
- Else if `!in_word`: `words += 1`, `in_word = true`
Distinguishing Bytes (-c) vs Unicode Characters (-m)
In UTF-8 text containing non-ASCII characters (such as 'é', '🦀', or '৳'), the byte count and character count differ:
| Metric | wc Flag |
How Rust Computes It | Value on "Rust 🦀\n" |
|---|---|---|---|
| Lines | -l |
Number of newline bytes b'\n' |
1 |
| Words | -w |
Transitions from whitespace to non-whitespace (char::is_whitespace) |
2 ("Rust" and "🦀") |
| Characters | -m |
Count of Unicode scalar values (char) |
7 ('R','u','s','t',' ','🦀','\n') |
| Bytes | -c |
Exact byte length (len()) |
10 ('🦀' is 4 UTF-8 bytes) |
| Max Line Length | -L |
Maximum character count of any line (excluding \r and \n) |
6 |
Complete Runnable Implementation
Implementing std::ops::AddAssign on WcCounts makes accumulating totals across multiple files effortless:
use std::fmt;
use std::ops::AddAssign;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct WcCounts {
pub lines: usize,
pub words: usize,
pub chars: usize,
pub bytes: usize,
pub max_line_width: usize,
}
impl AddAssign for WcCounts {
fn add_assign(&mut self, rhs: Self) {
self.lines += rhs.lines;
self.words += rhs.words;
self.chars += rhs.chars;
self.bytes += rhs.bytes;
self.max_line_width = self.max_line_width.max(rhs.max_line_width);
}
}
impl WcCounts {
/// Computes all five `wc` metrics in a single O(n) character pass with 0 allocations!
pub fn count_str(text: &str) -> Self {
let mut stats = Self {
bytes: text.len(),
..Self::default()
};
let mut in_word = false;
let mut current_line_width = 0usize;
for ch in text.chars() {
stats.chars += 1;
if ch == '\n' {
stats.lines += 1;
stats.max_line_width = stats.max_line_width.max(current_line_width);
current_line_width = 0;
} else if ch != '\r' {
current_line_width += 1;
}
if ch.is_whitespace() {
in_word = false;
} else if !in_word {
in_word = true;
stats.words += 1;
}
}
// Account for a final line that does not end with '\n':
stats.max_line_width = stats.max_line_width.max(current_line_width);
stats
}
}
impl fmt::Display for WcCounts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:>6} {:>6} {:>6} {:>6} (max width {:>3})",
self.lines, self.words, self.chars, self.bytes, self.max_line_width
)
}
}
fn main() {
let files = [
("README.md", "# Project\nFast Rust CLI tools \u{1F980}\n"),
("src/main.rs", "fn main() {\n println!(\"ok\");\n}\n"),
];
let mut total = WcCounts::default();
println!(" lines words chars bytes");
for (name, content) in files {
let counts = WcCounts::count_str(content);
total += counts;
println!("{counts} {name}");
}
println!("{total} total");
// Verify UTF-8 multi-byte distinction on `README.md` ('\u{1F980}' is 1 char, 4 bytes):
let readme_stats = WcCounts::count_str(files[0].1);
assert_eq!(readme_stats.bytes - readme_stats.chars, 3);
}
lines words chars bytes
2 6 32 35 (max width 21) README.md
3 4 34 34 (max width 19) src/main.rs
5 10 66 69 (max width 21) total