Beginner Projects

Text Statistics Analyzer

A text statistics analyzer scans a document to compute structural metrics—line count, blank-line count, sentence count, word count, average word length, vocabulary richness (unique words), and top-$K$ most frequent words—in a single streaming pass.

Document Text (`&str`)
        |
        +-- Line Iteration (`.lines()`) ---> `total_lines`, `blank_lines`
        |
        +-- Sentence Punctuation (`.`, `!`, `?`) ---> `sentences`
        |
        +-- Word Normalization (split non-alphanumeric, lowercase)
                |
                +--> `total_words`, `total_word_chars`, `freq_map: HashMap<String, usize>`
                +--> Sort `(Reverse(count), word)` ---> Top-K Most Frequent Words!

Key Text Metrics Computed

Metric Definition & Edge-Case Handling
total_lines / blank_lines Counted via .lines(); a line is blank if line.trim().is_empty().
word_count / unique_words Alphanumeric word tokens normalized to lowercase; unique_words = freq_map.len().
avg_word_length total_word_chars as f64 / word_count as f64 (or 0.0 if word_count == 0).
sentence_count Count of non-empty sentence segments delimited by ., !, or ?.
top_words(k) Words sorted deterministically by (Reverse(frequency), alphabetical_word).

Complete Runnable Implementation

use std::cmp::Reverse;
use std::collections::HashMap;

#[derive(Debug)]
pub struct TextReport {
    pub total_lines: usize,
    pub blank_lines: usize,
    pub sentence_count: usize,
    pub word_count: usize,
    pub unique_words: usize,
    pub avg_word_length: f64,
    frequencies: HashMap<String, usize>,
}

impl TextReport {
    pub fn analyze(text: &str) -> Self {
        let mut total_lines = 0usize;
        let mut blank_lines = 0usize;

        for line in text.lines() {
            total_lines += 1;
            if line.trim().is_empty() {
                blank_lines += 1;
            }
        }

        let sentence_count = text
            .split(['.', '!', '?'])
            .filter(|seg| seg.chars().any(char::is_alphanumeric))
            .count();

        let mut word_count = 0usize;
        let mut total_word_chars = 0usize;
        let mut frequencies: HashMap<String, usize> = HashMap::new();

        for raw_word in text.split(|c: char| !c.is_alphanumeric() && c != '\'') {
            let word = raw_word.trim_matches('\'');
            if word.is_empty() {
                continue;
            }
            let normalized = word.to_lowercase();
            word_count += 1;
            total_word_chars += normalized.chars().count();
            *frequencies.entry(normalized).or_insert(0) += 1;
        }

        let unique_words = frequencies.len();
        let avg_word_length = if word_count > 0 {
            (total_word_chars as f64) / (word_count as f64)
        } else {
            0.0
        };

        Self {
            total_lines,
            blank_lines,
            sentence_count,
            word_count,
            unique_words,
            avg_word_length,
            frequencies,
        }
    }

    /// Returns the top `k` most frequent words sorted by frequency descending, then alphabetically.
    pub fn top_words(&self, k: usize) -> Vec<(&str, usize)> {
        let mut pairs: Vec<(&str, usize)> = self
            .frequencies
            .iter()
            .map(|(w, &count)| (w.as_str(), count))
            .collect();

        pairs.sort_unstable_by_key(|&(word, count)| (Reverse(count), word));
        pairs.truncate(k);
        pairs
    }
}

fn main() {
    let sample_doc = "\
Rust is fast, and Rust is memory-safe!
Memory safety without garbage collection makes Rust unique.

Does Rust have zero-cost abstractions? Yes, Rust does.";

    let report = TextReport::analyze(sample_doc);

    assert_eq!(report.total_lines, 4);
    assert_eq!(report.blank_lines, 1);
    assert_eq!(report.sentence_count, 4);
    assert_eq!(report.word_count, 23);

    println!(
        "Lines: {} (blank: {}), Sentences: {}, Words: {} (unique: {}), Avg len: {:.2}",
        report.total_lines,
        report.blank_lines,
        report.sentence_count,
        report.word_count,
        report.unique_words,
        report.avg_word_length
    );
    println!("Top 3 words: {:?}", report.top_words(3));
}
Lines: 4 (blank: 1), Sentences: 4, Words: 23 (unique: 18), Avg len: 5.17
Top 3 words: [("rust", 5), ("is", 2), ("memory", 2)]