18 Rustlings Quiz 3 Solution

Quiz3.rs

It's time to revisit what we've learned so far. In this case, we're focusing on generics and traits in Rust. Let's dive into the exercise.

// quiz3.rs
// This quiz tests:
// - Generics
// - Traits
// An imaginary magical school has a new report card generation system written in Rust!
// Currently the system only supports creating report cards where the student's grade
// is represented numerically (e.g. 1.0 -> 5.5).
// However, the school also issues alphabetical grades (A+ -> F-) and needs
// to be able to print both types of report card!

// Make the necessary code changes in the struct ReportCard and the impl block
// to support alphabetical report cards. Change the Grade in the second test to "A+"
// to show that your changes allow alphabetical grades.

// Execute `rustlings hint quiz3` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

pub struct ReportCard {
    pub grade: f32,
    pub student_name: String,
    pub student_age: u8,
}

impl ReportCard {
    pub fn print(&self) -> String {
        format!("{} ({}) - achieved a grade of {}",
            &self.student_name, &self.student_age, &self.grade)
    }
}

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

    #[test]
    fn generate_numeric_report_card() {
        let report_card = ReportCard {
            grade: 2.1,
            student_name: "Tom Wriggle".to_string(),
            student_age: 12,
        };
        assert_eq!(
            report_card.print(),
            "Tom Wriggle (12) - achieved a grade of 2.1"
        );
    }

    #[test]
    fn generate_alphabetic_report_card() {
        // TODO: Make sure to change the grade here after you finish the exercise.
        let report_card = ReportCard {
            grade: 2.1,
            student_name: "Gary Plotter".to_string(),
            student_age: 11,
        };
        assert_eq!(
            report_card.print(),
            "Gary Plotter (11) - achieved a grade of A+"
        );
    }
}

Our task here is to allow both numeric and alphabetical grades, currently we can only print numeric grades correctly. So we how can we fix this. Generics of course!

Quiz3.rs errors

When we run the tests, we find that one test passes while the other fails. Here's the output:

⚠️  Testing of exercises/quiz3.rs failed! Please try again. Here's the output:

running 2 tests
test tests::generate_numeric_report_card ... ok
test tests::generate_alphabetic_report_card ... FAILED

successes:

successes:
    tests::generate_numeric_report_card

failures:

---- tests::generate_alphabetic_report_card stdout ----
thread 'tests::generate_alphabetic_report_card' panicked at 'assertion failed: `(left == right)`
  left: `"Gary Plotter (11) - achieved a grade of 2.1"`,
 right: `"Gary Plotter (11) - achieved a grade of A+"`', exercises/quiz3.rs:57:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::generate_alphabetic_report_card

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Quiz3.rs solution

The issue here is that our ReportCard struct is not designed to handle both numerical and alphabetical grades. So let's solve this problem by implementing generics.

Our original ReportCard struct looks like this:

pub struct ReportCard {
    pub grade: f32,
    pub student_name: String,
    pub student_age: u8,
}

Updating the Struct to use Generics

Since the student_name will always be a String and the student_age will always be a u8, we only need to make the grade field generic:

pub struct ReportCard<T> {
	pub grade: T,
	pub student_name: String,
	pub student_age: u8,
}

This should do the trick for the struct but now we have to take a look at our impl block

impl ReportCard {
    pub fn print(&self) -> String {
        format!("{} ({}) - achieved a grade of {}",
            &self.student_name, &self.student_age, &self.grade)
    }
}

Updating the Implementation Block

Next, we update the impl block to accommodate the generic type T:

impl<T> ReportCard<T> {
 // rest of code
}

This let's the compiler know that we are using generics in this block of code. Alright let's save the changes and see what happens!

Uh oh. Problems

So it seems like we are not quite done yet. Since we are now using generics in our impl block the compiler is telling us that we cannot format the grade portion of this code on line 29 at : &self.grade, but luckily it has a suggestion. The compiler suggests restricting T to types that implement Display:

⚠️  Compiling of exercises/quiz3.rs failed! Please try again. Here's the output:
error[E0277]: `T` doesn't implement `std::fmt::Display`
  --> exercises/quiz3.rs:29:52
   |
29 |             &self.student_name, &self.student_age, &self.grade
   |                                                    ^^^^^^^^^^^ `T` cannot be formatted with the default formatter
   |
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
   = note: this error originates in the macro `$crate::__export::format_args` which comes from the expansion of the macro `format` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider restricting type parameter `T`
   |
25 | impl<T: std::fmt::Display> ReportCard<T> {
   |       +++++++++++++++++++

error: aborting due to previous error

Okay so let's fix that our impl block now looks like this:

impl<T> ReportCard<T> {
    pub fn print(&self) -> String {
        format!(
            "{} ({}) - achieved a grade of {}",
            &self.student_name, &self.student_age, &self.grade
        )
    }
}

If we save now we should be..doh. Our test fails.

running 2 tests
test tests::generate_numeric_report_card ... ok
test tests::generate_alphabetic_report_card ... FAILED

successes:

successes:
    tests::generate_numeric_report_card

failures:

---- tests::generate_alphabetic_report_card stdout ----
thread 'tests::generate_alphabetic_report_card' panicked at 'assertion failed: `(left == right)`
  left: `"Gary Plotter (11) - achieved a grade of 2.1"`,
 right: `"Gary Plotter (11) - achieved a grade of A+"`', exercises/quiz3.rs:59:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::generate_alphabetic_report_card

Resolving Test Failures

After making these changes, the test for alphabetical grades still fails. The issue is that we forgot to update the grade in the generate_alphabetic_report_card() test function from 2.1 to A+.

Final Working Code

Here's the complete, working code for reference:

pub struct ReportCard<T> {
    pub grade: T,
    pub student_name: String,
    pub student_age: u8,
}

impl<T: std::fmt::Display> ReportCard<T> {
    pub fn print(&self) -> String {
        format!(
            "{} ({}) - achieved a grade of {}",
            &self.student_name, &self.student_age, &self.grade
        )
    }
}

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

    #[test]
    fn generate_numeric_report_card() {
        let report_card = ReportCard {
            grade: 2.1,
            student_name: "Tom Wriggle".to_string(),
            student_age: 12,
        };
        assert_eq!(
            report_card.print(),
            "Tom Wriggle (12) - achieved a grade of 2.1"
        );
    }

    #[test]
    fn generate_alphabetic_report_card() {

        let report_card = ReportCard {
            grade: "A+",
            student_name: "Gary Plotter".to_string(),
            student_age: 11,
        };
        assert_eq!(
            report_card.print(),
            "Gary Plotter (11) - achieved a grade of A+"
        );
    }
}

Notes

I used the code below, in Rust playgrounds essentially letting me print the report card which then allowed me to have errors when trying to compile and better figure out what I needed to be doing by seeing the printout instead of just having the tests as the compiler gave me the error that T<: std::fmt::Debug> was missing from the impl signature.

Note this is different that the T<: std::fmt::Display> used in solving the exercise.

#[derive(Debug)]
pub struct ReportCard<T> {
    pub grade: T,
    pub student_name: String,
    pub student_age: u8,
}

impl<T: std::fmt::Display> ReportCard<T> {
    pub fn print(&self) -> String {
        format!("{} ({}) - achieved a grade of {:?}",
            &self.student_name, &self.student_age, &self.grade)
    }
}

fn main() {
    fn generate_numeric_report_card() {
        let report_card = ReportCard {
            grade: 2.1,
            student_name: "Tom Wriggle".to_string(),
            student_age: 12,
        };
        println!("{:?}", report_card);
    }
    generate_numeric_report_card();

    fn generate_alphabetic_report_card() {
        let report_card = ReportCard {
            grade: "A+",
            student_name: "Gary Plotter".to_string(),
            student_age: 12,
        };
        println!("{:?}", report_card);
    }
    generate_alphabetic_report_card();
}

Conclusion

In this exercise, we learned how to use generics to make our ReportCard struct more flexible, allowing it to handle both numerical and alphabetical grades. We also saw how to constrain our generic types to those that implement specific traits, in this case, the Display trait. This ensures that we can format the grade regardless of its type.