Pangrams code - how to improve?

I'm in the early stages of learning Rust and thought I'd do something a little more interesting based on the structure of the Guessing Game example from The Book. Everyone needs a program to help them write pangrams, right? (A pangram is a piece of text that contains every letter of the alphabet at least once. "The quick brown fox jumps over a lazy dog" is probably the best known example).

Anyway, I'm looking for any advice on how I can improve this, make it more idiomatic, more concise etc. A specific part I was wondering about was:

unused = unused[0..unused.len() - 2].to_string();

I'm sure there must be a more elegant way to remove the last two characters from a string. The only other thing I thought of was:

unused.pop();
unused.pop();

But again, it seems fairly inelegant just to repeat the same method call twice. I'm sure there are other parts of my code that could be better, too.

The full program is as follows:

use std::io;

fn main() {
    println!("\nTry to type a pangram:");
    loop {
        let mut attempt = String::new();
        io::stdin()
            .read_line(&mut attempt)
            .expect("Failed to read line");

        attempt = attempt.trim().to_string();
        let attempt_lc = attempt.to_lowercase();

        if attempt_lc == "quit" {
            println!("\nGoodbye.\n");
            break;
        }

        let mut unused = String::new();
        for char in 'a'..='z' {
            if !attempt_lc.contains(char) {
                unused.push(char);
                unused.push_str(", ");
            }
        }

        if unused == "" {
            println!("\nWell done! \"{}\" is a pangram.\n", attempt);
            println!("Try another one, or type \"quit\" to exit this program:");
        } else {
            unused = unused[0..unused.len() - 2].to_string();
            println!("\nSorry, \"{}\" is not a pangram.", attempt);
            println!("The following letters were not used:");
            println!("{}\n", unused);
            println!("Try again, or type \"quit\" to exit this program:");
        }
    }
}

At least here your question is not "remove the last 2 char"

it is, something like ", ".join(list_of_chars) in python.

although

let mut s = String::from("hello");

s.truncate(2);

assert_eq!("he", s);

might be what you want, things could be better by using

let unused=('a'..='z')
    .filter_map(|ch|if attempt_lc.contains(char){Some(ch.to_string())}else{None})
    .collect::<Vec<_>>()
    .join(", ");
1 Like

The standard library's join method is surprisingly limited. (Only works with slices of strings.) Fortunately itertools helps out in this case:

let unused = ('a'..='z').filter(|&c| !attempt_lc.contains(c)).join(", ");
// no need for removing the final 2 characters anymore

Also, following a clippy suggestion (make sure to use clippy when learning Rust), we can replace unused == "" by unused.is_empty().

The attempt = attempt.trim().to_string(); line can be improved, no need to clone the whole string, you can just save the trimmed input as a &str in a new variable, e.g. by shadowing the old one:

let attempt = attempt.trim();

If you would like to, you can also be ahead of your time and use syntax like

println!("\nWell done! \"{attempt}\" is a pangram.\n");

instead of

println!("\nWell done! \"{}\" is a pangram.\n", attempt);

This is already supported on nightly and beta compiler and will become available on stable on January 13. No "need" to learn the old ways, I guess :wink:

Also, it might be slightly more readable if you just use additional println!() for empty lines; though that's probably a matter of personal taste. You could also try to make use of multi-line string literals, but it's a bit hard without messing up the indentation. So after all these changes, the code might look as follows

use itertools::Itertools;
use std::io;

fn main() {
    println!();
    println!("Try to type a pangram:");
    loop {
        let mut attempt = String::new();
        io::stdin()
            .read_line(&mut attempt)
            .expect("Failed to read line");

        let attempt = attempt.trim();
        let attempt_lc = attempt.to_lowercase();

        if attempt_lc == "quit" {
            println!();
            println!("Goodbye.");
            println!();
            break;
        }

        let unused = ('a'..='z').filter(|&c| !attempt_lc.contains(c)).join(", ");

        if unused.is_empty() {
            println!();
            println!("Well done! \"{attempt}\" is a pangram.");
            println!();
            println!("Try another one, or type \"quit\" to exit this program:");
        } else {
            println!();
            println!("Sorry, \"{attempt}\" is not a pangram.");
            println!("The following letters were not used:");
            println!("{unused}");
            println!();
            println!("Try again, or type \"quit\" to exit this program:");
        }
    }
}

or, taking the printlns to the "other extreme", it could also look like

use itertools::Itertools;
use std::io;

fn main() {
    print!("\
        \n\
        Try to type a pangram:\n\
    ");
    loop {
        let mut attempt = String::new();
        io::stdin()
            .read_line(&mut attempt)
            .expect("Failed to read line");

        let attempt = attempt.trim();
        let attempt_lc = attempt.to_lowercase();

        if attempt_lc == "quit" {
            print!("\
                \n\
                Goodbye.\n\
                \n\
            ");
            break;
        }

        let unused = ('a'..='z').filter(|&c| !attempt_lc.contains(c)).join(", ");

        if unused.is_empty() {
            print!("\
                \n\
                Well done! \"{attempt}\" is a pangram.\n\
                \n\
                Try another one, or type \"quit\" to exit this program:\n\
            ");
        } else {
            print!("\
                \n\
                Sorry, \"{attempt}\" is not a pangram.\n\
                The following letters were not used:\n\
                {unused}\n\
                \n\
                Try again, or type \"quit\" to exit this program:\n\
            ");
        }
    }
}
1 Like

I didn't recognize the meaning of itertools helps out.

impl a trait Join<String> for char might be the way for solving the problem.

Sorry for my poor experience.


actually you do not read the reason why I am using filter_map:

error[E0599]: no method named `join` found for struct `Filter` in the current scope
 --> src/main.rs:3:57
  |
3 |     let unused = ('a'..='z').filter(|&c| c as u8 %2==1).join(", ");
  |                                                         ^^^^ method not found in `Filter<RangeInclusive<char>, [closure@src/main.rs:3:37: 3:55]>`

As mentioned in the sentence right before that code example, this join method comes from the itertools crate. You need that dependency and then bring the trait into scope with use itertools::Itertools;.

2 Likes

These are some great tips. Thanks!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.