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