3
\$\begingroup\$

I'm new to Rust and would like to know if my code is using the language well, if there are big improvements and if the code in general is rust-like. At the moment it feels very C. I haven't yet developed good feeling on how to incorporate the match function or how to check in a range of chars. The flushing and input part feels bulky as well. Here the description and link:

  1. Your program must prompt the user for a string of text (using get_string).
  2. Your program should count the number of letters, words, and sentences in the text. You may assume that a letter is any lowercase character from a to z or any uppercase character from A to Z, any sequence of characters separated by spaces should count as a word, and that any occurrence of a period, exclamation point, or question mark indicates the end of a sentence.
  3. Your program should print as output "Grade X" where X is the grade level computed by the Coleman-Liau formula, rounded to the nearest integer.
  4. If the resulting index number is 16 or higher (equivalent to or greater than a senior undergraduate reading level), your program should output "Grade 16+" instead of giving the exact index number. If the index number is less than 1, your program should output "Before Grade 1".
// Rust implementation of C problem from: https://cs50.harvard.edu/x/2020/psets/2/readability/
use std::io;

fn main() {
    print!("Text: ");
    io::Write::flush(&mut io::stdout()).expect("flush failed!");
    let mut line = String::new();
    match io::stdin().read_line(&mut line) {
        Ok(_) => (),
        Err(err) => println!("Could not parse input: {}", err),
    }
    let bytes = line.into_bytes();
    let mut l = 0.0;
    let mut w = 1.0;
    let mut s = 0.0;
    let sentmarker = ['?', '.', '!'];

    for b in bytes {
        if b >= 65 && b <= 90 || b >= 97 && b <= 122 { // check if character in [a-zA-Z]
            l += 1.0;
        } else if sentmarker.contains(&(b as char)) {
            s += 1.0;
        } else if (b as char) == ' ' {
            w += 1.0;
        }
    }
    let average_letters = l / w * 100.0; // the average number of letters per 100 words in the text
    let average_sentences = s / w * 100.0; // the average number of sentences per 100 words in the text

    let index = 0.0588 * average_letters - 0.296 * average_sentences - 15.8; // Coleman-Liau index

    let indexresult = if index > 16.0 {
        String::from("Grade 16+")
    } else if index < 1.0 {
        String::from("Before Grade 1")
    } else {
        format!("Grade {:.0}", index)
    };

    println!("{}", indexresult);
}

\$\endgroup\$
  • 1
    \$\begingroup\$ Welcome to Code Review, if you aim to receive more detailed answers adding the description of the problem to your question can help you. \$\endgroup\$ – dariosicily May 14 at 14:52
  • \$\begingroup\$ Okay, thanks. Was wondering what the MO is over here. \$\endgroup\$ – vaeng May 14 at 17:39
4
\$\begingroup\$
match io::stdin().read_line(&mut line) {
    Ok(_) => (),
    Err(err) => println!("Could not parse input: {}", err),
}

Instead of continuing, the program should panic if it fails to read input. Use expect instead:

io::stdin()
    .read_line(&mut input)
    .expect("Failed to read input");
let mut l = 0.0;
let mut w = 1.0;
let mut s = 0.0;
let sentmarker = ['?', '.', '!'];

for b in bytes {
    if b >= 65 && b <= 90 || b >= 97 && b <= 122 { // check if character in [a-zA-Z]
        l += 1.0;
    } else if sentmarker.contains(&(b as char)) {
        s += 1.0;
    } else if (b as char) == ' ' {
        w += 1.0;
    }
}

This parse can be simplified using filter and count. Also, use is_ascii_alphabetic instead of hard-coding the values.

It is probably clearer to create a dedicated struct to hold the parameters.


My version of the code: (takes advantage of the Unicode definition of letters and whitespace)

use std::io::{self, prelude::*};

fn main() {
    println!("Text: ");
    io::stdout().flush().expect("Failed to flush stdout");

    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("Failed to read input");

    let analysis = Analysis::new(&input);
    match analysis.index() {
        0 => println!("Before Grade 1"),
        index @ 1..=16 => println!("Grade {}", index),
        _ => println!("Grade 16+"),
    }
}

struct Analysis {
    n_letters: usize,
    n_words: usize,
    n_sentences: usize,
}

impl Analysis {
    fn new(text: &str) -> Analysis {
        Analysis {
            n_letters: text.chars().filter(|c| c.is_alphabetic()).count(),
            n_words: text.split_whitespace().count(),
            n_sentences: text.split_terminator(|c| ".!?".contains(c)).count(),
        }
    }
    fn index(&self) -> usize {
        let n_letters_per_100_words = self.n_letters as f64 / self.n_words as f64 * 100.0;
        let n_sentences_per_100_words = self.n_sentences as f64 / self.n_words as f64 * 100.0;

        let index = 0.0588 * n_letters_per_100_words - 0.296 * n_sentences_per_100_words - 15.8;
        index.round() as usize
    }
}

(playground, with some tests)

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thank you @L.F., I looked into this a few times and learned a lot from it. It#s easy to read and very condensed. The part that I have not yet though enough about are the || functions and all the idea about the preludes \$\endgroup\$ – vaeng May 21 at 19:28
  • 1
    \$\begingroup\$ @vaeng You're welcome. The || functions are called closures, and importing std::io::prelude is a convenient way to import some common I/O functionalities (flush in this case). \$\endgroup\$ – L. F. May 22 at 9:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.