typist-rust 0.2.0

A simple typing tutor game.
Documentation
//! Simple typing game written in Rust.
extern crate generic_matrix;

use generic_matrix::Matrix;
use std::io;
use std::time::{Duration, Instant};

mod words;

/* Main methods for running the program */

pub fn run(num_rounds: u32, num_words: u32) {
    let mut mistake_count = 0;
    let mut char_count = 0;
    let mut word_count = 0;
    let mut elapsed_time: Duration = Duration::from_secs(0);

    for _ in 0..num_rounds {
        let result = play_one_round(num_words);
        mistake_count += result.mistakes;
        char_count += result.answer.chars().count();
        word_count += num_words;
        elapsed_time += result.time;

        if result.mistakes == 0 {
            println!("Correct!\n");
        } else {
            println!("Number of mistakes: {}\n", result.mistakes);
        }
    }

    print_results(mistake_count, char_count, word_count, elapsed_time);
}

/// Play one round (display a string and have the user type an answer), returning a RoundResult.
fn play_one_round(num_words: u32) -> RoundResult {
    let line = generate_string(num_words);

    println!("{}", line);
    let mut answer = String::new();

    let begin = Instant::now();
    io::stdin()
        .read_line(&mut answer)
        .expect("Failed to read from console.");
    let end = Instant::now();

    let answer = String::from(answer.trim());
    let mistakes = edit_distance(line.trim(), &answer);

    RoundResult {
        answer,
        mistakes,
        time: end - begin,
    }
}

/// Outcome of a single round.
struct RoundResult {
    answer: String,
    mistakes: u32,
    time: Duration,
}

fn print_results(mistake_count: u32, char_count: usize, word_count: u32, elapsed_time: Duration) {
    // Dodge possible division by zero.
    if word_count == 0 || elapsed_time.as_secs() == 0 {
        println!("No data to calculate.");
        return;
    }

    let secs = elapsed_time.as_secs() as f64;

    println!(
        "{:30}{:>.3}\n\
         {:30}{:>.3}\n",
        "Mistakes per word:",
        mistake_count as f64 / word_count as f64,
        "Characters per second:",
        char_count as f64 / secs,
    );
}

/* String generation and manipulation functions */

fn generate_string(num_words: u32) -> String {
    (0..num_words)
        .map(|_| words::get_word())
        .collect::<Vec<_>>()
        .join(" ")
}

/// Calculate the minimum number of edits (add, delete, replace) needed to transform `a` into `b`.
fn edit_distance(a: &str, b: &str) -> u32 {
    let a: Vec<char> = a.chars().collect();
    let b: Vec<char> = b.chars().collect();

    let mut distances = Matrix::from_fn(a.len() + 1, b.len() + 1, |_, _| 0);

    // Push initial conditions into the matrix
    for j in 1..=b.len() {
        distances[(0, j)] = j as u32;
    }

    for i in 1..=a.len() {
        distances[(i, 0)] = i as u32;
    }

    use std::cmp::min;

    for i in 1..=a.len() {
        for j in 1..=b.len() {
            let compare = if a[i - 1] == b[j - 1] { 0 } else { 1 };
            let distance = min(
                distances[(i, j - 1)] + 1,
                min(
                    distances[(i - 1, j)] + 1,
                    distances[(i - 1, j - 1)] + compare,
                ),
            );
            distances[(i, j)] = distance;
        }
    }

    distances[(a.len(), b.len())]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_edit_distance_short_cases() {
        assert_eq!(0, edit_distance("", ""));
        assert_eq!(0, edit_distance("a", "a"));
        assert_eq!(1, edit_distance("x", ""));
        assert_eq!(1, edit_distance("", "q"));
        assert_eq!(1, edit_distance("p", "q"));
        assert_eq!(1, edit_distance("ap", "p"));
        assert_eq!(2, edit_distance("ap", "b"));
    }

    #[test]
    fn test_edit_distance_normal_words() {
        assert_eq!(3, edit_distance("hector", "extort"));
        assert_eq!(4, edit_distance("abcd", "defg"));
    }

    #[test]
    fn test_non_ascii_characters() {
        assert_eq!(
            4,
            edit_distance("Добрый день", "Добрый вечер")
        );
    }

    #[test]
    fn test_edit_distance_big_difference() {
        assert_eq!(10, edit_distance("", "1234567890"));
        assert_eq!(10, edit_distance("abc", "1234567890abc"));
        assert_eq!(10, edit_distance("abc", "abc1234567890"));
        assert_eq!(13, edit_distance("abc", "abc1234567890abc"));

        assert_eq!(10, edit_distance("1234567890", ""));
        assert_eq!(10, edit_distance("1234567890abc", "abc"));
        assert_eq!(10, edit_distance("abc1234567890", "abc"));
        assert_eq!(13, edit_distance("abc1234567890abc", "abc"));
    }
}