Type conversions

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

  1. as: 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.

  1. from/into/AsRef: Rust also offers traits that facilitate type conversions upon implementation.

These traits can be found under the convert module.

The traits are the following:

  1. from_str: 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.

Rustlings

using as: type casting

  • 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.

using_as

// 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));
    // convert unit tests to here to run in the rust playground
    fn returns_proper_type_and_value() {
        assert_eq!(average(&[3.5, 0.3, 13.0, 11.7]), 7.125);
    }
    returns_proper_type_and_value();
}

#[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);
    }
}

Hint

Use the as operator to cast one of the operands in the last line of the average function into the expected return type.

solution

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

from_into: value-to-value

  • The From trait is used for value-to-value conversions.

If From is implemented correctly for a type, the Into trait should work conversely.

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
  6. If while parsing the age, something goes wrong, then return the default of Person
  7. Otherwise, then return an instantiated Person object with the results

🌟from_into: We implement the Default trait to use it as a fallback when the provided string is not convertible into a Person object

// 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);
    // convert unit tests to here to run in the rust playground
    // 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 that John is returned when bad string is provided
    let p = Person::from("");
    assert_eq!(p.name, "John");
    assert_eq!(p.age, 30);
    // Test that "Mark,20" works
    let p = Person::from("Mark,20");
    assert_eq!(p.name, "Mark");
    assert_eq!(p.age, 20);
    // 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);

    let p: Person = Person::from("Mark");
    assert_eq!(p.name, "John");
    assert_eq!(p.age, 30);

    let p: Person = Person::from("Mark,");
    assert_eq!(p.name, "John");
    assert_eq!(p.age, 30);

    let p: Person = Person::from(",1");
    assert_eq!(p.name, "John");
    assert_eq!(p.age, 30);

    let p: Person = Person::from(",");
    assert_eq!(p.name, "John");
    assert_eq!(p.age, 30);

    let p: Person = Person::from(",one");
    assert_eq!(p.name, "John");
    assert_eq!(p.age, 30);

    let p: Person = Person::from("Mike,32,");
    assert_eq!(p.name, "John");
    assert_eq!(p.age, 30);

    let p: Person = Person::from("Mike,32,man");
    assert_eq!(p.name, "John");
    assert_eq!(p.age, 30);
}

#[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);
    }
}

Hint

Follow the steps provided right before the From implementation

solution: match -> split_once -> parse:: -> into() or default()

impl From<&str> for Person {
    fn from(s: &str) -> Person { // 2. Split the given string on the commas present in it
        match s.split_once(',') {
            Some((first, second)) => {
                if first.is_empty() { // 4. If the name is empty, then return the default of Person
                    Person::default()
                } else if let Ok(a) = second.parse::<usize>() { // 5. Extract the other element from the split operation and parse it into a `usize` as the age
                    Person {
                        name: first.into(), // 3. Extract the first element from the split operation and use it as the name
                        age: a, // 6. If while parsing the age, something goes wrong, then return the default of Person 
                    }
                } else {
                    Person::default() // 7. Otherwise, then return an instantiated Person object with the results
                }
            },
            _ => Person::default(), // 1. If the length of the provided string is 0, then return the default of Person
        }
    }
}

try_from_into: try value-to-value

  • 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.

Your task is to complete this implementation:

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

⚡️try_from_into: with challenge

// 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);
    // convert unit tests to here to run in the rust playground
    fn test_tuple_out_of_range_positive() {
        assert_eq!(
            Color::try_from((256, 1000, 10000)),
            Err(IntoColorError::IntConversion)
        );
    }
    fn test_tuple_out_of_range_negative() {
        assert_eq!(
            Color::try_from((-1, -10, -256)),
            Err(IntoColorError::IntConversion)
        );
    }
    fn test_tuple_sum() {
        assert_eq!(
            Color::try_from((-1, 255, 255)),
            Err(IntoColorError::IntConversion)
        );
    }
    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
            }
        );
    }
    fn test_array_out_of_range_positive() {
        let c: Result<Color, _> = [1000, 10000, 256].try_into();
        assert_eq!(c, Err(IntoColorError::IntConversion));
    }
    fn test_array_out_of_range_negative() {
        let c: Result<Color, _> = [-10, -256, -1].try_into();
        assert_eq!(c, Err(IntoColorError::IntConversion));
    }
    fn test_array_sum() {
        let c: Result<Color, _> = [-1, 255, 255].try_into();
        assert_eq!(c, Err(IntoColorError::IntConversion));
    }
    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
            }
        );
    }
    fn test_slice_out_of_range_positive() {
        let arr = [10000, 256, 1000];
        assert_eq!(
            Color::try_from(&arr[..]),
            Err(IntoColorError::IntConversion)
        );
    }
    fn test_slice_out_of_range_negative() {
        let arr = [-256, -1, -10];
        assert_eq!(
            Color::try_from(&arr[..]),
            Err(IntoColorError::IntConversion)
        );
    }
    fn test_slice_sum() {
        let arr = [-1, 255, 255];
        assert_eq!(
            Color::try_from(&arr[..]),
            Err(IntoColorError::IntConversion)
        );
    }
    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
            }
        );
    }
    fn test_slice_excess_length() {
        let v = vec![0, 0, 0, 0];
        assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen));
    }
    fn test_slice_insufficient_length() {
        let v = vec![0, 0];
        assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen));
    }
    test_tuple_out_of_range_positive();
    test_tuple_out_of_range_negative();
    test_tuple_sum();
    test_tuple_correct();
    test_array_out_of_range_positive();
    test_array_out_of_range_negative();
    test_array_sum();
    test_array_correct();
    test_slice_out_of_range_positive();
    test_slice_out_of_range_negative();
    test_slice_sum();
    test_slice_correct();
    test_slice_excess_length();
    test_slice_insufficient_length();
}

#[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));
    }
}

Hint

  1. Follow the steps provided right before the TryFrom implementation. You can also use the example at: TryFrom in std::convert - Rust

Is there an implementation of TryFrom in the standard library that can both do the required integer conversion and check the range of the input?

  1. Another hint: Look at the test cases to see which error variants to return.

  2. Yet another hint: You can use the map_err or or methods of Result to convert errors.

  3. Yet another hint: If you would like to propagate errors by using the ? operator in your solution, you might want to look at: Other uses of ? - The Rust Programming Language

solution: essentially return to run the implementation of tuple

// Tuple implementation
impl TryFrom<(i16, i16, i16)> for Color {
    type Error = IntoColorError;
    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
        for i in [tuple.0, tuple.1, tuple.2] {
            if i < 0 || i > 255 {
                return Err(IntoColorError::IntConversion)
            }
        }
        // return an Ok result of inner type Color.
        Ok(Color { red: tuple.0 as u8, green: tuple.1 as u8, blue: tuple.2 as u8 })
    }
}

// Array implementation
impl TryFrom<[i16; 3]> for Color {
    type Error = IntoColorError;
    fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
        // return to run the implementation of tuple above
        Self::try_from((arr[0], arr[1], arr[2]))
    }
}

// Slice implementation
impl TryFrom<&[i16]> for Color {
    type Error = IntoColorError;
    fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
        if slice.len() != 3 {
            Err(IntoColorError::BadLen)
        } else {
            // return to run the implementation of tuple above
            Self::try_from((slice[0], slice[1], slice[2]))
        }
    }
}

challenge!

Challenge: Can you make the TryFrom implementations generic over many integer types?

as_ref_mut: reference-to-reference

AsRef and AsMut allow for cheap reference-to-reference conversions.

Read more about them at AsRef in std::convert - Rust and AsMut in std::convert - Rust, respectively.

as_ref_mut: Three ways to add appropriate trait bound

// 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);
    }
}

// convert the unit test to main
fn main() {
    // different_counts()
    let s = "Café au lait";
    assert_ne!(char_counter(s), byte_counter(s));
    // same_counts
    let s = "Cafe au lait";
    assert_eq!(char_counter(s), byte_counter(s));
    // different_counts_using_string
    let s = String::from("Café au lait");
    assert_ne!(char_counter(s.clone()), byte_counter(s));
    // same_counts_using_string
    let s = String::from("Cafe au lait");
    assert_eq!(char_counter(s.clone()), byte_counter(s));
    // mult_box
    let mut num: Box<u32> = Box::new(3);
    num_sq(&mut num);
    assert_eq!(*num, 9);
}

Hint

Add AsRef as a trait bound to the functions.

compare to traits4

solution1: generic trait bounds

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

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

// Squares a number using AsMut. Add the trait bound as is appropriate and
// implement the function body.
fn num_sq<T: AsMut<u32>>(arg: &mut T) {
    *arg.as_mut() *= *arg.as_mut()
}

solution2: impl trait bounds

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

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

// Squares a number using AsMut. Add the trait bound as is appropriate and
// implement the function body.
fn num_sq(arg: &mut impl AsRef<str>) {
    *arg.as_mut() *= *arg.as_mut()
}

solution3: where clause

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

from_str: convert str to target type

  • 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 FromStr in std::str - Rust

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
  7. 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()).

from_str

// 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);
    // convert unit tests to here to run in the rust playground
    assert_eq!("".parse::<Person>(), Err(ParsePersonError::Empty));
    let p = "John,32".parse::<Person>();
    assert!(p.is_ok());
    let p = p.unwrap();
    assert_eq!(p.name, "John");
    assert_eq!(p.age, 32);
    assert!(matches!(
            "John,".parse::<Person>(),
            Err(ParsePersonError::ParseInt(_))
        ));

    assert!(matches!(
            "John,twenty".parse::<Person>(),
            Err(ParsePersonError::ParseInt(_))
        ));

    assert_eq!("John".parse::<Person>(), Err(ParsePersonError::BadLen));

    assert_eq!(",1".parse::<Person>(), Err(ParsePersonError::NoName));

    assert!(matches!(
            ",".parse::<Person>(),
            Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_))
        ));

    assert!(matches!(
            ",one".parse::<Person>(),
            Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_))
        ));

    assert_eq!("John,32,".parse::<Person>(), Err(ParsePersonError::BadLen));

    assert_eq!(
        "John,32,man".parse::<Person>(),
        Err(ParsePersonError::BadLen)
    );
}

#[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)
        );
    }
}

Hint

  1. The implementation of FromStr should return an Ok with a Person object, or an Err with an error if the string is not valid.

This is almost like the from_into exercise, but returning errors instead of falling back to a default value.

Look at the test cases to see which error variants to return.

  1. Another hint: You can use the map_err method of Result with a function or a closure to wrap the error from parse::<usize>.

  2. Yet another hint: If you would like to propagate errors by using the ? operator in your solution, you might want to look at Other uses of ? - The Rust Programming Language

solution1

// Using ParsePersonError::{Empty, BadLen, NoName, ParseInt} above
impl FromStr for Person {
    type Err = ParsePersonError;
    fn from_str(s: &str) -> Result<Person, Self::Err> {
        if s.is_empty() {
            Err(Empty) // i.e., Err(ParsePersonError::Empty)
        } else {
            let p: Vec<&str> = s.split(',').collect();
            if p.len() != 2 {
                Err(BadLen)
            } else if p[0].len() == 0 {
                Err(NoName)
            } else { 
                match p[1].parse::<usize>() {
                    Ok(a) => Ok(Person { name: p[0].to_string(), age: a }),
                    Err(a) => Err(ParseInt(a)),
                }
            }
        }
    }
}

solution2: Err::Empty -> Err is a variant, not a module

impl FromStr for Person {
    type Err = ParsePersonError;
    fn from_str(s: &str) -> Result<Person, Self::Err> {
        if s.is_empty() {
            Err::Empty // i.e., Err(ParsePersonError::Empty)
        } else {
            let p: Vec<&str> = s.split(',').collect();
            if p.len() != 2 {
                Err::BadLen
            } else if p[0].len() == 0 {
                Err::NoName
            } else { 
                match p[1].parse::<usize>() {
                    Ok(a) => Ok(Person { name: p[0].to_string(), age: a }),
                    Err(a) => Err::ParseInt(a),
                }
            }
        }
    }
}

solution3: Err.Empty()

impl FromStr for Person {
    type Err = ParsePersonError;
    fn from_str(s: &str) -> Result<Person, Self::Err> {
        if s.is_empty() {
            Err.Empty // i.e., Err(ParsePersonError::Empty)
        } else {
            let p: Vec<&str> = s.split(',').collect();
            if p.len() != 2 {
                Err.BadLen
            } else if p[0].len() == 0 {
                Err.NoName
            } else { 
                match p[1].parse::<usize>() {
                    Ok(a) => Ok(Person { name: p[0].to_string(), age: a }),
                    Err(a) => Err.ParseInt(a),
                }
            }
        }
    }
}

solution4: ParsePersonError::{ErrorType}

// Using ParsePersonError::{Empty, BadLen, NoName, ParseInt} above
impl FromStr for Person {
    type Err = ParsePersonError;
    fn from_str(s: &str) -> Result<Person, Self::Err> {
        if s.is_empty() { // 1. If the length of the provided string is 0, an error should be returned
            Err(ParsePersonError::Empty) // i.e., Err(ParsePersonError::Empty)
        } else {
            let p: Vec<&str> = s.split(',').collect(); // 2. Split the given string on the commas present in it
            if p.len() != 2 { // 3. Only 2 elements should be returned from the split, otherwise return an error
                Err(ParsePersonError::BadLen)
            } else if p[0].len() == 0 { // 4. Extract the first element from the split operation and use it as the name
                Err(ParsePersonError::NoName)
            } else { 
                match p[1].parse::<usize>() { //  5. Extract the other element from the split operation and parse it into a `usize` as the age with something like `"4".parse::<usize>()`
                    Ok(a) => Ok(Person { name: p[0].to_string(), age: a }), // 7. If everything goes well, then return a Result of a Person object
                    Err(a) => Err(ParsePersonError::ParseInt(a)), // 6. If while extracting the name and the age something goes wrong, an error should be returned
                }
            }
        }
    }
}