diff --git a/.rustlings-state.txt b/.rustlings-state.txt index dac5f44..c3678cb 100644 --- a/.rustlings-state.txt +++ b/.rustlings-state.txt @@ -1,6 +1,6 @@ DON'T EDIT THIS FILE! -errors1 +generics1 intro1 intro2 @@ -51,4 +51,10 @@ hashmaps3 quiz2 options1 options2 -options3 \ No newline at end of file +options3 +errors1 +errors2 +errors3 +errors4 +errors5 +errors6 \ No newline at end of file diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs index e07fddc..0cced95 100644 --- a/exercises/13_error_handling/errors1.rs +++ b/exercises/13_error_handling/errors1.rs @@ -4,12 +4,12 @@ // construct to `Option` that can be used to express error conditions. Change // the function signature and body to return `Result` instead // of `Option`. -fn generate_nametag_text(name: String) -> Option { +fn generate_nametag_text(name: String) -> Result { if name.is_empty() { // Empty names aren't allowed - None + Err("Empty names aren't allowed".to_string()) } else { - Some(format!("Hi! My name is {name}")) + Ok(format!("Hi! My name is {name}")) } } diff --git a/exercises/13_error_handling/errors2.rs b/exercises/13_error_handling/errors2.rs index defe359..988733b 100644 --- a/exercises/13_error_handling/errors2.rs +++ b/exercises/13_error_handling/errors2.rs @@ -23,7 +23,16 @@ fn total_cost(item_quantity: &str) -> Result { // TODO: Handle the error case as described above. let qty = item_quantity.parse::(); - Ok(qty * cost_per_item + processing_fee) + match qty { + Ok(value) => { + Ok(value * cost_per_item + processing_fee) + } + Err(err) => { + Err(err) + } + } + + } fn main() { diff --git a/exercises/13_error_handling/errors3.rs b/exercises/13_error_handling/errors3.rs index 8e8c38a..18f32df 100644 --- a/exercises/13_error_handling/errors3.rs +++ b/exercises/13_error_handling/errors3.rs @@ -15,7 +15,7 @@ fn total_cost(item_quantity: &str) -> Result { // TODO: Fix the compiler error by changing the signature and body of the // `main` function. -fn main() { +fn main() -> Result<(), Box> { let mut tokens = 100; let pretend_user_input = "8"; @@ -28,4 +28,5 @@ fn main() { tokens -= cost; println!("You now have {tokens} tokens."); } + Ok(()) } diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs index ba01e54..3b40650 100644 --- a/exercises/13_error_handling/errors4.rs +++ b/exercises/13_error_handling/errors4.rs @@ -9,7 +9,12 @@ struct PositiveNonzeroInteger(u64); impl PositiveNonzeroInteger { fn new(value: i64) -> Result { - // TODO: This function shouldn't always return an `Ok`. + if value < 0 { + return Err(CreationError::Negative); + } + if value == 0 { + return Err(CreationError::Zero); + } Ok(Self(value as u64)) } } diff --git a/exercises/13_error_handling/errors5.rs b/exercises/13_error_handling/errors5.rs index 5721835..7b9019a 100644 --- a/exercises/13_error_handling/errors5.rs +++ b/exercises/13_error_handling/errors5.rs @@ -48,7 +48,7 @@ impl PositiveNonzeroInteger { // TODO: Add the correct return type `Result<(), Box>`. What can we // use to describe both errors? Is there a trait which both errors implement? -fn main() { +fn main() -> Result<(), Box> { let pretend_user_input = "42"; let x: i64 = pretend_user_input.parse()?; println!("output={:?}", PositiveNonzeroInteger::new(x)?); diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs index b1995e0..e92bd5e 100644 --- a/exercises/13_error_handling/errors6.rs +++ b/exercises/13_error_handling/errors6.rs @@ -24,8 +24,9 @@ impl ParsePosNonzeroError { Self::Creation(err) } - // TODO: Add another error conversion function here. - // fn from_parse_int(???) -> Self { ??? } + fn from_parse_int(err: ParseIntError) -> Self { + Self::ParseInt(err) + } } #[derive(PartialEq, Debug)] @@ -41,9 +42,9 @@ impl PositiveNonzeroInteger { } fn parse(s: &str) -> Result { - // TODO: change this to return an appropriate error instead of panicking - // when `parse()` returns an error. - let x: i64 = s.parse().unwrap(); + let x: i64 = s.parse().map_err(|e| { + ParsePosNonzeroError::from_parse_int(e) + })?; Self::new(x).map_err(ParsePosNonzeroError::from_creation) } } diff --git a/solutions/13_error_handling/errors1.rs b/solutions/13_error_handling/errors1.rs index dcf2377..f552ca7 100644 --- a/solutions/13_error_handling/errors1.rs +++ b/solutions/13_error_handling/errors1.rs @@ -1,4 +1,37 @@ -fn main() { - // DON'T EDIT THIS SOLUTION FILE! - // It will be automatically filled after you finish the exercise. +fn generate_nametag_text(name: String) -> Result { + // ^^^^^^ ^^^^^^ + if name.is_empty() { + // `Err(String)` instead of `None`. + Err("Empty names aren't allowed".to_string()) + } else { + // `Ok` instead of `Some`. + Ok(format!("Hi! My name is {name}")) + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generates_nametag_text_for_a_nonempty_name() { + assert_eq!( + generate_nametag_text("Beyoncé".to_string()).as_deref(), + Ok("Hi! My name is Beyoncé"), + ); + } + + #[test] + fn explains_why_generating_nametag_text_fails() { + assert_eq!( + generate_nametag_text(String::new()) + .as_ref() + .map_err(|e| e.as_str()), + Err("Empty names aren't allowed"), + ); + } } diff --git a/solutions/13_error_handling/errors2.rs b/solutions/13_error_handling/errors2.rs index dcf2377..0597c8c 100644 --- a/solutions/13_error_handling/errors2.rs +++ b/solutions/13_error_handling/errors2.rs @@ -1,4 +1,58 @@ -fn main() { - // DON'T EDIT THIS SOLUTION FILE! - // It will be automatically filled after you finish the exercise. +// Say we're writing a game where you can buy items with tokens. All items cost +// 5 tokens, and whenever you purchase items there is a processing fee of 1 +// token. A player of the game will type in how many items they want to buy, and +// the `total_cost` function will calculate the total cost of the items. Since +// the player typed in the quantity, we get it as a string. They might have +// typed anything, not just numbers! +// +// Right now, this function isn't handling the error case at all. What we want +// to do is: If we call the `total_cost` function on a string that is not a +// number, that function will return a `ParseIntError`. In that case, we want to +// immediately return that error from our function and not try to multiply and +// add. +// +// There are at least two ways to implement this that are both correct. But one +// is a lot shorter! + +use std::num::ParseIntError; + +#[allow(unused_variables)] +fn total_cost(item_quantity: &str) -> Result { + let processing_fee = 1; + let cost_per_item = 5; + + // Added `?` to propagate the error. + let qty = item_quantity.parse::()?; + // ^ added + + // Equivalent to this verbose version: + let qty = match item_quantity.parse::() { + Ok(v) => v, + Err(e) => return Err(e), + }; + + Ok(qty * cost_per_item + processing_fee) +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + use std::num::IntErrorKind; + + #[test] + fn item_quantity_is_a_valid_number() { + assert_eq!(total_cost("34"), Ok(171)); + } + + #[test] + fn item_quantity_is_an_invalid_number() { + assert_eq!( + total_cost("beep boop").unwrap_err().kind(), + &IntErrorKind::InvalidDigit, + ); + } } diff --git a/solutions/13_error_handling/errors3.rs b/solutions/13_error_handling/errors3.rs index dcf2377..63f4aba 100644 --- a/solutions/13_error_handling/errors3.rs +++ b/solutions/13_error_handling/errors3.rs @@ -1,4 +1,32 @@ -fn main() { - // DON'T EDIT THIS SOLUTION FILE! - // It will be automatically filled after you finish the exercise. +// This is a program that is trying to use a completed version of the +// `total_cost` function from the previous exercise. It's not working though! +// Why not? What should we do to fix it? + +use std::num::ParseIntError; + +// Don't change this function. +fn total_cost(item_quantity: &str) -> Result { + let processing_fee = 1; + let cost_per_item = 5; + let qty = item_quantity.parse::()?; + + Ok(qty * cost_per_item + processing_fee) +} + +fn main() -> Result<(), ParseIntError> { + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ added + let mut tokens = 100; + let pretend_user_input = "8"; + + let cost = total_cost(pretend_user_input)?; + + if cost > tokens { + println!("You can't afford that many!"); + } else { + tokens -= cost; + println!("You now have {tokens} tokens."); + } + + // Added this line to return the `Ok` variant of the expected `Result`. + Ok(()) } diff --git a/solutions/13_error_handling/errors4.rs b/solutions/13_error_handling/errors4.rs index dcf2377..70c5f1c 100644 --- a/solutions/13_error_handling/errors4.rs +++ b/solutions/13_error_handling/errors4.rs @@ -1,4 +1,42 @@ -fn main() { - // DON'T EDIT THIS SOLUTION FILE! - // It will be automatically filled after you finish the exercise. +use std::cmp::Ordering; + +#[derive(PartialEq, Debug)] +enum CreationError { + Negative, + Zero, +} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + match value.cmp(&0) { + Ordering::Less => Err(CreationError::Negative), + Ordering::Equal => Err(CreationError::Zero), + Ordering::Greater => Ok(Self(value as u64)), + } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_creation() { + assert_eq!( + PositiveNonzeroInteger::new(10), + Ok(PositiveNonzeroInteger(10)), + ); + assert_eq!( + PositiveNonzeroInteger::new(-10), + Err(CreationError::Negative), + ); + assert_eq!(PositiveNonzeroInteger::new(0), Err(CreationError::Zero)); + } } diff --git a/solutions/13_error_handling/errors5.rs b/solutions/13_error_handling/errors5.rs index dcf2377..c1424ee 100644 --- a/solutions/13_error_handling/errors5.rs +++ b/solutions/13_error_handling/errors5.rs @@ -1,4 +1,54 @@ -fn main() { - // DON'T EDIT THIS SOLUTION FILE! - // It will be automatically filled after you finish the exercise. +// This exercise is an altered version of the `errors4` exercise. It uses some +// concepts that we won't get to until later in the course, like `Box` and the +// `From` trait. It's not important to understand them in detail right now, but +// you can read ahead if you like. For now, think of the `Box` type as +// an "I want anything that does ???" type. +// +// In short, this particular use case for boxes is for when you want to own a +// value and you care only that it is a type which implements a particular +// trait. To do so, The `Box` is declared as of type `Box` where +// `Trait` is the trait the compiler looks for on any value used in that +// context. For this exercise, that context is the potential errors which +// can be returned in a `Result`. + +use std::error::Error; +use std::fmt; + +#[derive(PartialEq, Debug)] +enum CreationError { + Negative, + Zero, +} + +// This is required so that `CreationError` can implement `Error`. +impl fmt::Display for CreationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let description = match *self { + CreationError::Negative => "number is negative", + CreationError::Zero => "number is zero", + }; + f.write_str(description) + } +} + +impl Error for CreationError {} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + match value { + x if x < 0 => Err(CreationError::Negative), + 0 => Err(CreationError::Zero), + x => Ok(PositiveNonzeroInteger(x as u64)), + } + } +} + +fn main() -> Result<(), Box> { + let pretend_user_input = "42"; + let x: i64 = pretend_user_input.parse()?; + println!("output={:?}", PositiveNonzeroInteger::new(x)?); + Ok(()) } diff --git a/solutions/13_error_handling/errors6.rs b/solutions/13_error_handling/errors6.rs index dcf2377..8679361 100644 --- a/solutions/13_error_handling/errors6.rs +++ b/solutions/13_error_handling/errors6.rs @@ -1,4 +1,91 @@ -fn main() { - // DON'T EDIT THIS SOLUTION FILE! - // It will be automatically filled after you finish the exercise. +// Using catch-all error types like `Box` isn't recommended for +// library code where callers might want to make decisions based on the error +// content instead of printing it out or propagating it further. Here, we define +// a custom error type to make it possible for callers to decide what to do next +// when our function returns an error. + +use std::num::ParseIntError; + +#[derive(PartialEq, Debug)] +enum CreationError { + Negative, + Zero, +} + +// A custom error type that we will be using in `PositiveNonzeroInteger::parse`. +#[derive(PartialEq, Debug)] +enum ParsePosNonzeroError { + Creation(CreationError), + ParseInt(ParseIntError), +} + +impl ParsePosNonzeroError { + fn from_creation(err: CreationError) -> Self { + Self::Creation(err) + } + + fn from_parse_int(err: ParseIntError) -> Self { + Self::ParseInt(err) + } +} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + match value { + x if x < 0 => Err(CreationError::Negative), + 0 => Err(CreationError::Zero), + x => Ok(Self(x as u64)), + } + } + + fn parse(s: &str) -> Result { + // Return an appropriate error instead of panicking when `parse()` + // returns an error. + let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?; + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Self::new(x).map_err(ParsePosNonzeroError::from_creation) + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_error() { + assert!(matches!( + PositiveNonzeroInteger::parse("not a number"), + Err(ParsePosNonzeroError::ParseInt(_)), + )); + } + + #[test] + fn test_negative() { + assert_eq!( + PositiveNonzeroInteger::parse("-555"), + Err(ParsePosNonzeroError::Creation(CreationError::Negative)), + ); + } + + #[test] + fn test_zero() { + assert_eq!( + PositiveNonzeroInteger::parse("0"), + Err(ParsePosNonzeroError::Creation(CreationError::Zero)), + ); + } + + #[test] + fn test_positive() { + let x = PositiveNonzeroInteger::new(42).unwrap(); + assert_eq!(x.0, 42); + assert_eq!(PositiveNonzeroInteger::parse("42"), Ok(x)); + } }