26 Rustlings Conversions Solution

Type conversions

Rust offers a multitude of ways to convert a value of a given type into another type.

The simplest form of type conversion is a type cast expression. It is denoted with the binary operator as. For instance, println!("{}", 1 + 1.0); would not compile, since 1 is an integer while 1.0 is a float. However, println!("{}", 1 as f32 + 1.0) should compile. The exercise using_as tries to cover this.

Rust also offers traits that facilitate type conversions upon implementation. These traits can be found under the convert module. The traits are the following:

Furthermore, the std::str module offers a trait called FromStr which helps with converting strings into target types via the parse method on strings. If properly implemented for a given type Person, then let p: Person = "Mark,20".parse().unwrap() should both compile and run without panicking.

These should be the main ways within the standard library to convert data into your desired types.

Further information

These are not directly covered in the book, but the standard library has a great documentation for it.

Let's get started!

Using_as.rs

// Type casting in Rust is done via the usage of the `as` operator.
// Please note that the `as` operator is not only used when type casting.
// It also helps with renaming imports.
//
// The goal is to make sure that the division does not fail to compile
// and returns the proper type.
// Execute `rustlings hint using_as` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

fn average(values: &[f64]) -> f64 {
    let total = values.iter().sum::<f64>();
    total / values.len()
}

fn main() {
    let values = [3.5, 0.3, 13.0, 11.7];
    println!("{}", average(&values));
}

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

    #[test]
    fn returns_proper_type_and_value() {
        assert_eq!(average(&[3.5, 0.3, 13.0, 11.7]), 7.125);
    }
}

our instructions tell us we can use as in a couple of different ways but in this case we're trying to make sure that the division doesn't fail to compile. Let's look at the errors.

Using_as.rs Errors

error[E0277]: cannot divide `f64` by `usize`
  --> exercises/conversions/using_as.rs:13:11
   |
13 |     total / values.len()
   |           ^ no implementation for `f64 / usize`
   |
   = help: the trait `Div<usize>` is not implemented for `f64`
   = help: the following other types implement trait `Div<Rhs>`:
             <f64 as Div>
             <f64 as Div<&f64>>
             <&'a f64 as Div<f64>>
             <&f64 as Div<&f64>>

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.

Here we're told that the traitDiv<usize> is not implemented for f64, so I think I understand why this is happening and what the solution is but let's break it down a bit further.

Using_as.rs Solution

Rust is a strongly typed language, which means that it is very strict about how different types interact with each other. In this specific case, we have total, which is a f64, and values.len(), which is a usize. Rust does not implicitly convert between these types, as doing so can sometimes lead to unexpected behavior or loss of precision. Therefore, we need to explicitly convert types when they are not the same.

If we look again at values you might be thinking, but all the values in defined in the array are f64 so how does it end up a usize?

Well, when you call the .len() method on a slice (or an array), the returned type is usize, not f64. Let's break this down:

  1. What is usize?:

    • usize is a type in Rust that is primarily used for indexing and for sizes. Its size is determined by the architecture of the machine on which your program is running. On a 32-bit system, usize is 32 bits, and on a 64-bit system, it's 64 bits.
    • It's the type returned by the .len() method because the length of a slice or an array (or any collection, generally) is logically a count of items, which is an integer, not a floating point.
  2. Why .len() Returns usize:

    • The .len() method is designed to return the number of elements in a slice or an array. Since this is essentially a count of items and is used for indexing, it returns a usize. This is a design choice in Rust to ensure type safety in memory indexing and counting operations.

So, the solution is simple we simply add as f64 to values.len() to convert the usize as f64 and code should compile.

fn average(values: &[f64]) -> f64 {
    let total = values.iter().sum::<f64>();
    total / values.len() as f64 // add `as f64`
}

fn main() {
    let values = [3.5, 0.3, 13.0, 11.7];
    println!("{}", average(&values));
}

From_into.rs

// The From trait is used for value-to-value conversions.
// If From is implemented correctly for a type, the Into trait should work conversely.
// You can read more about it at https://doc.rust-lang.org/std/convert/trait.From.html
// Execute `rustlings hint from_into` or use the `hint` watch subcommand for a hint.

#[derive(Debug)]
struct Person {
    name: String,
    age: usize,
}

// We implement the Default trait to use it as a fallback
// when the provided string is not convertible into a Person object
impl Default for Person {
    fn default() -> Person {
        Person {
            name: String::from("John"),
            age: 30,
        }
    }
}

// Your task is to complete this implementation
// in order for the line `let p = Person::from("Mark,20")` to compile
// Please note that you'll need to parse the age component into a `usize`
// with something like `"4".parse::<usize>()`. The outcome of this needs to
// be handled appropriately.
//
// Steps:
// 1. If the length of the provided string is 0, then return the default of Person
// 2. Split the given string on the commas present in it
// 3. Extract the first element from the split operation and use it as the name
// 4. If the name is empty, then return the default of Person
// 5. Extract the other element from the split operation and parse it into a `usize` as the age
// If while parsing the age, something goes wrong, then return the default of Person
// Otherwise, then return an instantiated Person object with the results

// I AM NOT DONE

impl From<&str> for Person {
    fn from(s: &str) -> Person {
    }
}

fn main() {
    // Use the `from` function
    let p1 = Person::from("Mark,20");
    // Since From is implemented for Person, we should be able to use Into
    let p2: Person = "Gerald,70".into();
    println!("{:?}", p1);
    println!("{:?}", p2);
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_default() {
        // Test that the default person is 30 year old John
        let dp = Person::default();
        assert_eq!(dp.name, "John");
        assert_eq!(dp.age, 30);
    }
    #[test]
    fn test_bad_convert() {
        // Test that John is returned when bad string is provided
        let p = Person::from("");
        assert_eq!(p.name, "John");
        assert_eq!(p.age, 30);
    }
    #[test]
    fn test_good_convert() {
        // Test that "Mark,20" works
        let p = Person::from("Mark,20");
        assert_eq!(p.name, "Mark");
        assert_eq!(p.age, 20);
    }
    #[test]
    fn test_bad_age() {
        // Test that "Mark,twenty" will return the default person due to an error in parsing age
        let p = Person::from("Mark,twenty");
        assert_eq!(p.name, "John");
        assert_eq!(p.age, 30);
    }

    #[test]
    fn test_missing_comma_and_age() {
        let p: Person = Person::from("Mark");
        assert_eq!(p.name, "John");
        assert_eq!(p.age, 30);
    }

    #[test]
    fn test_missing_age() {
        let p: Person = Person::from("Mark,");
        assert_eq!(p.name, "John");
        assert_eq!(p.age, 30);
    }

    #[test]
    fn test_missing_name() {
        let p: Person = Person::from(",1");
        assert_eq!(p.name, "John");
        assert_eq!(p.age, 30);
    }

    #[test]
    fn test_missing_name_and_age() {
        let p: Person = Person::from(",");
        assert_eq!(p.name, "John");
        assert_eq!(p.age, 30);
    }

    #[test]
    fn test_missing_name_and_invalid_age() {
        let p: Person = Person::from(",one");
        assert_eq!(p.name, "John");
        assert_eq!(p.age, 30);
    }

    #[test]
    fn test_trailing_comma() {
        let p: Person = Person::from("Mike,32,");
        assert_eq!(p.name, "John");
        assert_eq!(p.age, 30);
    }

    #[test]
    fn test_trailing_comma_and_some_string() {
        let p: Person = Person::from("Mike,32,man");
        assert_eq!(p.name, "John");
        assert_eq!(p.age, 30);
    }
}

We have clear instructions in the comments that tell us to complete the implementation and we even have specific steps, we'll review those later, for now let's look at the errors, to make sure we're not missing anything.

From_into.rs Errors

⚠️  Compiling of exercises/conversions/from_into.rs failed! Please try again. Here's the output:
error[E0308]: mismatched types
  --> exercises/conversions/from_into.rs:41:25
   |
41 |     fn from(s: &str) -> Person {
   |        ----             ^^^^^^ expected `Person`, found `()`
   |        |
   |        implicitly returns `()` as its body has no tail or `return` expression

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

These errors are not providing much insight as we have a missing implementation

From_into.rs Solution

To solve this exercise, we need to implement the From<&str> trait for the Person struct. The trait implementation should convert a string in the format "Name,Age" into a Person object. Let's go through the steps required:

  1. Check if the provided string is empty. If it is, return the default Person.
  2. Split the string on commas.
  3. Extract the name and age from the split string.
  4. Handle any parsing errors for the age.

Here's the complete implementation:

impl From<&str> for Person {
    fn from(s: &str) -> Person {
        // Step 1: Check if the string is empty
        if s.is_empty() {
            return Person::default();
        }

        // Step 2: Split the string on the comma
        let parts: Vec<&str> = s.split(',').collect();

        // Check for the correct number of parts (name and age)
        if parts.len() != 2 {
            return Person::default();
        }

        // Step 3: Extract name and age
        let name = parts[0].to_string();
        let age_str = parts[1];

        // Step 4: Handle parsing errors for age
        if name.is_empty() || age_str.is_empty() {
            return Person::default();
        }

        match age_str.parse::<usize>() {
            Ok(age) => Person { name, age },
            Err(_) => Person::default(),
        }
    }
}

fn main() {
    let p1 = Person::from("Mark,20");
    let p2: Person = "Gerald,70".into();
    println!("{:?}", p1);
    println!("{:?}", p2);
}

In this implementation:

  • The string is split into two parts based on the comma.
  • If the number of parts isn't exactly 2 (name and age), or if either part is empty, the default Person is returned.
  • If the age part can't be parsed into a usize, the default Person is returned.
  • If all conditions are met, a new Person instance is created and returned.

With these changes our code compiles! On to the next one.

From_str.rs

// from_str.rs
// This is similar to from_into.rs, but this time we'll implement `FromStr`
// and return errors instead of falling back to a default value.
// Additionally, upon implementing FromStr, you can use the `parse` method
// on strings to generate an object of the implementor type.
// You can read more about it at https://doc.rust-lang.org/std/str/trait.FromStr.html
// Execute `rustlings hint from_str` or use the `hint` watch subcommand for a hint.

use std::num::ParseIntError;
use std::str::FromStr;

#[derive(Debug, PartialEq)]
struct Person {
    name: String,
    age: usize,
}

// We will use this error type for the `FromStr` implementation.
#[derive(Debug, PartialEq)]
enum ParsePersonError {
    // Empty input string
    Empty,
    // Incorrect number of fields
    BadLen,
    // Empty name field
    NoName,
    // Wrapped error from parse::<usize>()
    ParseInt(ParseIntError),
}

// I AM NOT DONE

// Steps:
// 1. If the length of the provided string is 0, an error should be returned
// 2. Split the given string on the commas present in it
// 3. Only 2 elements should be returned from the split, otherwise return an error
// 4. Extract the first element from the split operation and use it as the name
// 5. Extract the other element from the split operation and parse it into a `usize` as the age
//    with something like `"4".parse::<usize>()`
// 6. If while extracting the name and the age something goes wrong, an error should be returned
// If everything goes well, then return a Result of a Person object
//
// As an aside: `Box<dyn Error>` implements `From<&'_ str>`. This means that if you want to return a
// string error message, you can do so via just using return `Err("my error message".into())`.

impl FromStr for Person {
    type Err = ParsePersonError;
    fn from_str(s: &str) -> Result<Person, Self::Err> {
    }
}

fn main() {
    let p = "Mark,20".parse::<Person>().unwrap();
    println!("{:?}", p);
}

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

    #[test]
    fn empty_input() {
        assert_eq!("".parse::<Person>(), Err(ParsePersonError::Empty));
    }
    #[test]
    fn good_input() {
        let p = "John,32".parse::<Person>();
        assert!(p.is_ok());
        let p = p.unwrap();
        assert_eq!(p.name, "John");
        assert_eq!(p.age, 32);
    }
    #[test]
    fn missing_age() {
        assert!(matches!(
            "John,".parse::<Person>(),
            Err(ParsePersonError::ParseInt(_))
        ));
    }

    #[test]
    fn invalid_age() {
        assert!(matches!(
            "John,twenty".parse::<Person>(),
            Err(ParsePersonError::ParseInt(_))
        ));
    }

    #[test]
    fn missing_comma_and_age() {
        assert_eq!("John".parse::<Person>(), Err(ParsePersonError::BadLen));
    }

    #[test]
    fn missing_name() {
        assert_eq!(",1".parse::<Person>(), Err(ParsePersonError::NoName));
    }

    #[test]
    fn missing_name_and_age() {
        assert!(matches!(
            ",".parse::<Person>(),
            Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_))
        ));
    }

    #[test]
    fn missing_name_and_invalid_age() {
        assert!(matches!(
            ",one".parse::<Person>(),
            Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_))
        ));
    }

    #[test]
    fn trailing_comma() {
        assert_eq!("John,32,".parse::<Person>(), Err(ParsePersonError::BadLen));
    }

    #[test]
    fn trailing_comma_and_some_string() {
        assert_eq!(
            "John,32,man".parse::<Person>(),
            Err(ParsePersonError::BadLen)
        );
    }
}

We have a similar exercise as before except that instead of using from_into we'll be we'll be using FromStr, similarly we have the instructions in the comments that tell us exactly what we need to be doing, and again we'll look at those in detail after we look at the errors.

From_str.rs Errors

error[E0308]: mismatched types
  --> exercises/conversions/from_str.rs:48:29
   |
48 |     fn from_str(s: &str) -> Result<Person, Self::Err> {
   |        --------             ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<Person, ParsePersonError>`, found `()`
   |        |
   |        implicitly returns `()` as its body has no tail or `return` expression
   |
   = note:   expected enum `Result<Person, ParsePersonError>`
           found unit type `()`

error: aborting due to previous error

Here we see that the error is related to the compiler wanting an enum type or essentially a Result type but it found a () type because our solution isn't implemented yet. So, let's fix that.

From_str.rs Solution

Before we implement a solution let's make sure we're all on the same page here with the FromStr trait.

Understanding the FromStr Trait

Purpose of FromStr: The FromStr trait in Rust is used for converting a string into another type. In this case, we are converting a string into a Person struct.

Why Use FromStr: Implementing FromStr allows you to use the parse method on strings, which simplifies the conversion process. This is particularly useful when you want to turn user input (which is often in string format) into more complex data types.

Breaking Down the Implementation

Error Handling: The FromStr trait requires you to define an error type. In this example, ParsePersonError is used to represent different kinds of errors that might occur during parsing (like empty input, incorrect format, etc.). Using custom error types makes your code more robust and easier to debug.

Parsing Logic: The input string is split on commas, assuming the format name,age. The code then checks if the input is correctly formatted and handles each part (name and age) separately. The parse method is used to convert the age from a string to a usize. If this fails, the error is captured and returned.

Creating the Person Object: If all validations pass, a new Person object is created with the parsed name and age. This object is then returned as a Result<Person, ParsePersonError>, indicating a successful parsing or an error.

impl FromStr for Person {
    type Err = ParsePersonError;

    fn from_str(s: &str) -> Result<Person, Self::Err> {
        // Check if the input string is empty
        if s.is_empty() {
            return Err(ParsePersonError::Empty);
        }

        // Split the input string on the comma
        let parts: Vec<&str> = s.split(',').collect();
        // Ensure that there are exactly two parts (name and age)
        if parts.len() != 2 {
            return Err(ParsePersonError::BadLen);
        }

        // Extract the first part as the name
        let name = parts[0].to_string();
        // Ensure that the name is not empty
        if name.is_empty() {
            return Err(ParsePersonError::NoName);
        }

        // Parse the second part as age and handle any parsing errors
        let age = parts[1].parse::<usize>().map_err(ParsePersonError::ParseInt)?;

        // Return a Person object if all is well
        Ok(Person { name, age })
    }
}

fn main() {
    let p = "Mark,20".parse::<Person>().unwrap();
    println!("{:?}", p);
}

This code does the following:

  1. Error Checking: It checks for empty input, an incorrect number of fields, and parsing errors for the age field.
  2. String Splitting and Parsing: It splits the input string by a comma, extracting the name and age, and attempts to parse the age as an usize.
  3. Error Handling: It uses custom error types to handle different error scenarios such as empty input, no name, or parsing errors.
  4. Person Creation: If all checks pass, it creates and returns a Person object.
  5. Testing with Main: The main function demonstrates how to parse a string to create a Person object, using the implemented from_str method.

Additional Notes on this Exercise

  1. Real-world Application: Parsing user input is a common requirement in many applications. Understanding how to do it properly in Rust is crucial for writing robust software.
  2. Learning Rust’s Conventions: Rust emphasizes safety and clear error handling. Implementing FromStr with comprehensive error checking is a great exercise in learning these aspects of Rust.
  3. Hands-on Practice: By working through this example, beginners get practical experience with common Rust concepts like traits, error handling, and generics.

With the changes above our code is compiling and we can move on to our next exercise!

Try_from_into.rs

// try_from_into.rs
// TryFrom is a simple and safe type conversion that may fail in a controlled way under some circumstances.
// Basically, this is the same as From. The main difference is that this should return a Result type
// instead of the target type itself.
// You can read more about it at https://doc.rust-lang.org/std/convert/trait.TryFrom.html
// Execute `rustlings hint try_from_into` or use the `hint` watch subcommand for a hint.

use std::convert::{TryFrom, TryInto};

#[derive(Debug, PartialEq)]
struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

// We will use this error type for these `TryFrom` conversions.
#[derive(Debug, PartialEq)]
enum IntoColorError {
    // Incorrect length of slice
    BadLen,
    // Integer conversion error
    IntConversion,
}

// I AM NOT DONE

// Your task is to complete this implementation
// and return an Ok result of inner type Color.
// You need to create an implementation for a tuple of three integers,
// an array of three integers, and a slice of integers.
//
// Note that the implementation for tuple and array will be checked at compile time,
// but the slice implementation needs to check the slice length!
// Also note that correct RGB color values must be integers in the 0..=255 range.

// Tuple implementation
impl TryFrom<(i16, i16, i16)> for Color {
    type Error = IntoColorError;
    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
    }
}

// Array implementation
impl TryFrom<[i16; 3]> for Color {
    type Error = IntoColorError;
    fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
    }
}

// Slice implementation
impl TryFrom<&[i16]> for Color {
    type Error = IntoColorError;
    fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
    }
}

fn main() {
    // Use the `try_from` function
    let c1 = Color::try_from((183, 65, 14));
    println!("{:?}", c1);

    // Since TryFrom is implemented for Color, we should be able to use TryInto
    let c2: Result<Color, _> = [183, 65, 14].try_into();
    println!("{:?}", c2);

    let v = vec![183, 65, 14];
    // With slice we should use `try_from` function
    let c3 = Color::try_from(&v[..]);
    println!("{:?}", c3);
    // or take slice within round brackets and use TryInto
    let c4: Result<Color, _> = (&v[..]).try_into();
    println!("{:?}", c4);
}

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

    #[test]
    fn test_tuple_out_of_range_positive() {
        assert_eq!(
            Color::try_from((256, 1000, 10000)),
            Err(IntoColorError::IntConversion)
        );
    }
    #[test]
    fn test_tuple_out_of_range_negative() {
        assert_eq!(
            Color::try_from((-1, -10, -256)),
            Err(IntoColorError::IntConversion)
        );
    }
    #[test]
    fn test_tuple_sum() {
        assert_eq!(
            Color::try_from((-1, 255, 255)),
            Err(IntoColorError::IntConversion)
        );
    }
    #[test]
    fn test_tuple_correct() {
        let c: Result<Color, _> = (183, 65, 14).try_into();
        assert!(c.is_ok());
        assert_eq!(
            c.unwrap(),
            Color {
                red: 183,
                green: 65,
                blue: 14
            }
        );
    }
    #[test]
    fn test_array_out_of_range_positive() {
        let c: Result<Color, _> = [1000, 10000, 256].try_into();
        assert_eq!(c, Err(IntoColorError::IntConversion));
    }
    #[test]
    fn test_array_out_of_range_negative() {
        let c: Result<Color, _> = [-10, -256, -1].try_into();
        assert_eq!(c, Err(IntoColorError::IntConversion));
    }
    #[test]
    fn test_array_sum() {
        let c: Result<Color, _> = [-1, 255, 255].try_into();
        assert_eq!(c, Err(IntoColorError::IntConversion));
    }
    #[test]
    fn test_array_correct() {
        let c: Result<Color, _> = [183, 65, 14].try_into();
        assert!(c.is_ok());
        assert_eq!(
            c.unwrap(),
            Color {
                red: 183,
                green: 65,
                blue: 14
            }
        );
    }
    #[test]
    fn test_slice_out_of_range_positive() {
        let arr = [10000, 256, 1000];
        assert_eq!(
            Color::try_from(&arr[..]),
            Err(IntoColorError::IntConversion)
        );
    }
    #[test]
    fn test_slice_out_of_range_negative() {
        let arr = [-256, -1, -10];
        assert_eq!(
            Color::try_from(&arr[..]),
            Err(IntoColorError::IntConversion)
        );
    }
    #[test]
    fn test_slice_sum() {
        let arr = [-1, 255, 255];
        assert_eq!(
            Color::try_from(&arr[..]),
            Err(IntoColorError::IntConversion)
        );
    }
    #[test]
    fn test_slice_correct() {
        let v = vec![183, 65, 14];
        let c: Result<Color, _> = Color::try_from(&v[..]);
        assert!(c.is_ok());
        assert_eq!(
            c.unwrap(),
            Color {
                red: 183,
                green: 65,
                blue: 14
            }
        );
    }
    #[test]
    fn test_slice_excess_length() {
        let v = vec![0, 0, 0, 0];
        assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen));
    }
    #[test]
    fn test_slice_insufficient_length() {
        let v = vec![0, 0];
        assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen));
    }
}

In this exercise, our specific task is to implement the TryFrom trait for a Color struct, catering to different types of inputs: tuples, arrays, and slices of i16. Each of these inputs represents potential RGB color values. The challenge involves not only facilitating these conversions but also meticulously handling potential errors. This includes managing out-of-range color values and ensuring that inputs, especially slices, are of appropriate lengths. By doing so, we aim to leverage Rust's robust type conversion capabilities while adhering to its strict safety and error-handling principles.

Try_from_into Errors.rs

⚠️  Compiling of exercises/conversions/try_from_into.rs failed! Please try again. Here's the output:
error[E0308]: mismatched types
  --> exercises/conversions/try_from_into.rs:40:44
   |
40 |     fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
   |        --------                            ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<Color, IntoColorError>`, found `()`
   |        |
   |        implicitly returns `()` as its body has no tail or `return` expression
   |
   = note:   expected enum `Result<Color, IntoColorError>`
           found unit type `()`

error[E0308]: mismatched types
  --> exercises/conversions/try_from_into.rs:47:35
   |
47 |     fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
   |        --------                   ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<Color, IntoColorError>`, found `()`
   |        |
   |        implicitly returns `()` as its body has no tail or `return` expression
   |
   = note:   expected enum `Result<Color, IntoColorError>`
           found unit type `()`

error[E0308]: mismatched types
  --> exercises/conversions/try_from_into.rs:54:35
   |
54 |     fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
   |        --------                   ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<Color, IntoColorError>`, found `()`
   |        |
   |        implicitly returns `()` as its body has no tail or `return` expression
   |
   = note:   expected enum `Result<Color, IntoColorError>`
           found unit type `()`

error: aborting due to 3 previous errors

The compiler errors we're encountering are due to mismatched types in our try_from function implementations. These functions currently return the unit type (), but they are expected to return a Result<Color, IntoColorError>. The errors indicate that the body of each implementation is empty and lacks the necessary logic to perform the conversion and return the correct type.

Try_from_into Solution

To resolve these errors and fulfill the exercise's requirements, we'll need to implement the TryFrom trait for tuples (i16, i16, i16), arrays [i16; 3], and slices &[i16] with the following considerations:

  • Range Validation: Ensure that each color value (red, green, blue) is within the 0 to 255 range. If any value is outside this range, return an Err with IntoColorError::IntConversion.
  • Handling Slice Length: For the slice implementation, check that the length of the slice is exactly 3. If not, return an Err with IntoColorError::BadLen.
  • Type Conversion: Convert the valid i16 values to u8 for constructing the Color struct, and return Ok(Color).

Here's an overview of how the implementations should look:

// Tuple, array, and slice implementations for TryFrom
impl TryFrom<(i16, i16, i16)> for Color { /* ... */ }
impl TryFrom<[i16; 3]> for Color { /* ... */ }
impl TryFrom<&[i16]> for Color { /* ... */ }

In each case, we ensure proper error handling and validation, adhering to the TryFrom trait's requirements.

Complete Code

impl TryFrom<(i16, i16, i16)> for Color {
    type Error = IntoColorError;

    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
        let (red, green, blue) = tuple;
        if red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255 {
            return Err(IntoColorError::IntConversion);
        }
        Ok(Color {
            red: red as u8,
            green: green as u8,
            blue: blue as u8,
        })
    }
}

impl TryFrom<[i16; 3]> for Color {
    type Error = IntoColorError;

    fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
        let [red, green, blue] = arr;
        if red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255 {
            return Err(IntoColorError::IntConversion);
        }
        Ok(Color {
            red: red as u8,
            green: green as u8,
            blue: blue as u8,
        })
    }
}

impl TryFrom<&[i16]> for Color {
    type Error = IntoColorError;

    fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
        if slice.len() != 3 {
            return Err(IntoColorError::BadLen);
        }
        let [red, green, blue] = match slice.try_into() {
            Ok(arr) => arr,
            Err(_) => return Err(IntoColorError::IntConversion),
        };
        if red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255 {
            return Err(IntoColorError::IntConversion);
        }
        Ok(Color {
            red: red as u8,
            green: green as u8,
            blue: blue as u8,
        })
    }
}

This implementation includes:

  • Tuple, Array, and Slice Implementations: It implements TryFrom for a tuple of three i16s, an array of three i16s, and a slice of i16s.
  • Error Handling: It checks for valid RGB values (0 to 255) and handles errors accordingly using the IntoColorError enum.
  • Length Check for Slice: For the slice implementation, it checks if the length of the slice is exactly 3, which is crucial for correct color value extraction.
  • Type Conversions: The valid i16 values are safely converted to u8 for the Color struct fields.

This exercise demonstrates the importance and utility of the TryFrom trait in Rust for handling fallible conversions. By carefully implementing TryFrom, we can create robust and error-resilient code, especially when working with data that requires validation or has specific constraints, like our Color struct in this case.

With these updates our code is now compiling.

As_ref_mut.rs

// AsRef and AsMut allow for cheap reference-to-reference conversions.
// Read more about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html
// and https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
// Execute `rustlings hint as_ref_mut` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

// Obtain the number of bytes (not characters) in the given argument.
// TODO: Add the AsRef trait appropriately as a trait bound.
fn byte_counter<T>(arg: T) -> usize {
    arg.as_ref().as_bytes().len()
}

// Obtain the number of characters (not bytes) in the given argument.
// TODO: Add the AsRef trait appropriately as a trait bound.
fn char_counter<T>(arg: T) -> usize {
    arg.as_ref().chars().count()
}

// Squares a number using as_mut().
// TODO: Add the appropriate trait bound.
fn num_sq<T>(arg: &mut T) {
    // TODO: Implement the function body.
    ???
}

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

    #[test]
    fn different_counts() {
        let s = "Café au lait";
        assert_ne!(char_counter(s), byte_counter(s));
    }

    #[test]
    fn same_counts() {
        let s = "Cafe au lait";
        assert_eq!(char_counter(s), byte_counter(s));
    }

    #[test]
    fn different_counts_using_string() {
        let s = String::from("Café au lait");
        assert_ne!(char_counter(s.clone()), byte_counter(s));
    }

    #[test]
    fn same_counts_using_string() {
        let s = String::from("Cafe au lait");
        assert_eq!(char_counter(s.clone()), byte_counter(s));
    }

    #[test]
    fn mult_box() {
        let mut num: Box<u32> = Box::new(3);
        num_sq(&mut num);
        assert_eq!(*num, 9);
    }
}

In this exercise, the objective is to correctly implement the AsRef and AsMut traits. These traits are crucial for type conversion in Rust, allowing for flexible yet safe conversions between different types.

Exercise Tasks

  1. Implement byte_counter and char_counter Functions: These functions should work with any type that can be converted to a string slice (&str), thus requiring the implementation of the AsRef<str> trait.

  2. Implement num_sq Function: This function is intended to mutate a numeric value. It should work with any type that can provide a mutable reference to a u32, necessitating the use of the AsMut<u32> trait. Let's review the errors.

As_ref_mut.rs Errors

⚠️  Compiling of exercises/conversions/as_ref_mut.rs failed! Please try again. Here's the output:
error: expected expression, found `?`
  --> exercises/conversions/as_ref_mut.rs:24:5
   |
24 |     ???
   |     ^ expected expression

error[E0599]: no method named `as_ref` found for type parameter `T` in the current scope
  --> exercises/conversions/as_ref_mut.rs:11:9
   |
10 | fn byte_counter<T>(arg: T) -> usize {
   |                 - method `as_ref` not found for this type parameter
11 |     arg.as_ref().as_bytes().len()
   |         ^^^^^^ method not found in `T`
   |
   = help: items from traits can only be used if the type parameter is bounded by the trait
help: the following trait defines an item `as_ref`, perhaps you need to restrict type parameter `T` with it:
   |
10 | fn byte_counter<T: AsRef>(arg: T) -> usize {
   |                  +++++++

error[E0599]: no method named `as_ref` found for type parameter `T` in the current scope
  --> exercises/conversions/as_ref_mut.rs:17:9
   |
16 | fn char_counter<T>(arg: T) -> usize {
   |                 - method `as_ref` not found for this type parameter
17 |     arg.as_ref().chars().count()
   |         ^^^^^^ method not found in `T`
   |
   = help: items from traits can only be used if the type parameter is bounded by the trait
help: the following trait defines an item `as_ref`, perhaps you need to restrict type parameter `T` with it:
   |
16 | fn char_counter<T: AsRef>(arg: T) -> usize {
   |                  +++++++

error: aborting due to 3 previous errors

The errors encountered during compilation are primarily due to:

  1. Missing Trait Bounds: Functions byte_counter and char_counter are trying to use as_ref() without the type T being bounded by AsRef<str>. This results in the error that the method as_ref is not found for the generic type T.

  2. Incomplete Implementation: The num_sq function has an incomplete implementation, indicated by the placeholder ???.

As_ref_mut.rs Solution

Here's how to implement the AsRef and AsMut traits in this Rust exercise:

  1. byte_counter and char_counter Functions:
    • These functions are designed to work with any type T that can be referenced as a string slice (&str).
    • By specifying T: AsRef<str> as the trait bound, you're indicating that these functions can accept any type T that implements the AsRef<str> trait.
    • arg.as_ref() converts arg into a string slice, on which you can then call .as_bytes() or .chars() to get the length in bytes or characters, respectively.
  2. num_sq Function:
    • This function is supposed to mutate a numeric value.
    • By using T: AsMut<u32>, you're specifying that it accepts a mutable reference to any type T that can be mutated into a u32.
    • Inside the function, *arg.as_mut() gives you a mutable reference to the inner u32, which you then square.
use std::convert::AsRef;
use std::convert::AsMut;

fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
    arg.as_ref().as_bytes().len()
}

fn char_counter<T: AsRef<str>>(arg: T) -> usize {
    arg.as_ref().chars().count()
}

fn num_sq<T: AsMut<u32>>(arg: &mut T) {
    let value = arg.as_mut();
    *value *= *value;
}

And with that we are done we are compiling!

Conclusion

In conclusion, Rust's comprehensive type conversion system, including as, From/Into, TryFrom/TryInto, FromStr, AsRef, and AsMut, provides programmers with a versatile toolkit to handle various scenarios of type conversions safely and efficiently.

  • as Operator: Essential for basic type casting, especially when dealing with primitive data types. It's a straightforward way to convert between compatible types, ensuring that the programmer explicitly acknowledges and handles the conversion.

  • From/Into Traits: These traits offer a more Rust-idiomatic way of handling infallible conversions between types. They simplify code and reduce boilerplate, making conversions more readable and intuitive.

  • TryFrom/TryInto Traits: They extend the capabilities of From/Into to include fallible conversions, where there's a possibility of failure. This is particularly important in Rust, which emphasizes safety and error handling.

  • FromStr Trait: Tailored for parsing strings into other types, FromStr is crucial in scenarios where data comes as strings (like user input or file reading) and needs to be converted to more specific types.

  • AsRef and AsMut Traits: They facilitate cheap, non-owning conversions to references, allowing functions to accept a wide variety of arguments without sacrificing performance or flexibility. This is particularly useful in generic programming.

Together, these mechanisms contribute to Rust's robust type system, ensuring that type conversions are handled explicitly and safely, reducing the risk of errors and increasing the overall reliability of the code. By understanding and utilizing these tools effectively, you can write Rust code that is both efficient and elegant.

And with that we are DONE! I hope you've enjoyed going through these exercises with me.

🎉 All exercises completed! 🎉

+----------------------------------------------------+
|          You made it to the Fe-nish line!          |
+--------------------------  ------------------------+
                          \\/
     ▒▒          ▒▒▒▒▒▒▒▒      ▒▒▒▒▒▒▒▒          ▒▒
   ▒▒▒▒  ▒▒    ▒▒        ▒▒  ▒▒        ▒▒    ▒▒  ▒▒▒▒
   ▒▒▒▒  ▒▒  ▒▒            ▒▒            ▒▒  ▒▒  ▒▒▒▒
 ░░▒▒▒▒░░▒▒  ▒▒            ▒▒            ▒▒  ▒▒░░▒▒▒▒
   ▓▓▓▓▓▓▓▓  ▓▓      ▓▓██  ▓▓  ▓▓██      ▓▓  ▓▓▓▓▓▓▓▓
     ▒▒▒▒    ▒▒      ████  ▒▒  ████      ▒▒░░  ▒▒▒▒
       ▒▒  ▒▒▒▒▒▒        ▒▒▒▒▒▒        ▒▒▒▒▒▒  ▒▒
         ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒
           ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
             ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒
           ▒▒  ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒  ▒▒
         ▒▒    ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒    ▒▒
       ▒▒    ▒▒    ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒    ▒▒    ▒▒
       ▒▒  ▒▒    ▒▒                  ▒▒    ▒▒  ▒▒
           ▒▒  ▒▒                      ▒▒  ▒▒

We hope you enjoyed learning about the various aspects of Rust!
If you noticed any issues, please don't hesitate to report them to our repo.
You can also contribute your own exercises to help the greater community!