From db5ad7f42f8b59dc8b2c7cf6ff26203d447a6f73 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 3 May 2026 13:59:32 +0200 Subject: [PATCH 1/3] Use infallible conversion to teach From trait --- CHANGELOG.md | 1 + exercises/23_conversions/conversions2.rs | 134 +++++----------------- rustlings-macros/info.toml | 4 +- solutions/23_conversions/conversions2.rs | 138 +++++------------------ 4 files changed, 60 insertions(+), 217 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2acf6c6..807f7d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - `structs3`: Rewrote the exercise to make users type method syntax themselves. - Rename the exercises for smart pointers and conversions so they're sorted alphabetically. [@foxfromworld](https://github.com/foxfromworld) - `vecs1`: Remove array literal. Some learners assumed their task is to convert it to a vector. +- `conversions2`: Redesign the context such that infallible conversion makes sense. ## 6.5.0 (2025-08-21) diff --git a/exercises/23_conversions/conversions2.rs b/exercises/23_conversions/conversions2.rs index bc2783a3..f6df67f2 100644 --- a/exercises/23_conversions/conversions2.rs +++ b/exercises/23_conversions/conversions2.rs @@ -2,49 +2,36 @@ // implemented, an implementation of `Into` is automatically provided. // You can read more about it in the documentation: // https://doc.rust-lang.org/std/convert/trait.From.html +// +// Frank the fairy would like to buy some truffles from Grace the gnome, a +// world-renowned chocolatier. The truffles are priced in GnomeCoin though, and +// Frank only has FairyCredit. Help Frank by providing a `From` implementation +// to convert his FairyCredit to GnomeCoin. At the current exchange rate, one +// FairyCredit is valued at 100 GnomeCoin. #[derive(Debug)] -struct Person { - name: String, - age: u8, +struct FairyCredit(u32); + +#[derive(Debug, PartialEq)] +struct GnomeCoin(u64); + +impl From for GnomeCoin { + // TODO: implement From for GnomeCoin } -// 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() -> Self { - Self { - name: String::from("John"), - age: 30, - } - } -} - -// TODO: Complete this `From` implementation to be able to parse a `Person` -// out of a string in the form of "Mark,20". -// Note that you'll need to parse the age component into a `u8` with something -// like `"4".parse::()`. -// -// Steps: -// 1. Split the given string on the commas present in it. -// 2. If the split operation returns less or more than 2 elements, return the -// default of `Person`. -// 3. Use the first element from the split operation as the name. -// 4. If the name is empty, return the default of `Person`. -// 5. Parse the second element from the split operation into a `u8` as the age. -// 6. If parsing the age fails, return the default of `Person`. -impl From<&str> for Person { - fn from(s: &str) -> Self {} -} +// Note that we shouldn't provide the opposite conversion: from GnomeCoin to +// FairyCredits. That's because less than 100 GnomeCoins cannot be represented +// as FairyCredits, which would make the conversion lossy. The `From` trait is +// only appropriate for infallible and lossless conversions. fn main() { // Use the `from` function. - let p1 = Person::from("Mark,20"); - println!("{p1:?}"); + let g1 = GnomeCoin::from(FairyCredit(12)); + println!("{g1:?}"); - // Since `From` is implemented for Person, we are able to use `Into`. - let p2: Person = "Gerald,70".into(); - println!("{p2:?}"); + // Since `From` is implemented for GnomeCoin, we are able to use `Into`. + let g2: GnomeCoin = FairyCredit(9).into(); + println!("{g2:?}"); } #[cfg(test)] @@ -52,79 +39,14 @@ mod tests { use super::*; #[test] - fn test_default() { - let dp = Person::default(); - assert_eq!(dp.name, "John"); - assert_eq!(dp.age, 30); + fn test_from() { + let g = GnomeCoin::from(FairyCredit(12)); + assert_eq!(g, GnomeCoin(1200)); } #[test] - fn test_bad_convert() { - let p = Person::from(""); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_good_convert() { - let p = Person::from("Mark,20"); - assert_eq!(p.name, "Mark"); - assert_eq!(p.age, 20); - } - - #[test] - fn test_bad_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,dog"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); + fn test_into() { + let g: GnomeCoin = FairyCredit(9).into(); + assert_eq!(g, GnomeCoin(900)); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 637283d2..054da99b 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1171,7 +1171,9 @@ Use the `as` operator to cast one of the operands in the last line of the name = "conversions2" dir = "23_conversions" hint = """ -Follow the steps provided right before the `From` implementation.""" +Implement From for GnomeCoin. Check the documentation of `From` to +learn about its required items: +https://doc.rust-lang.org/std/convert/trait.From.html""" [[exercises]] name = "conversions3" diff --git a/solutions/23_conversions/conversions2.rs b/solutions/23_conversions/conversions2.rs index cec23cb4..b8b2372e 100644 --- a/solutions/23_conversions/conversions2.rs +++ b/solutions/23_conversions/conversions2.rs @@ -2,55 +2,38 @@ // implemented, an implementation of `Into` is automatically provided. // You can read more about it in the documentation: // https://doc.rust-lang.org/std/convert/trait.From.html +// +// Frank the fairy would like to buy some truffles from Grace the gnome, a +// world-renowned chocolatier. The truffles are priced in GnomeCoin though, and +// Frank only has FairyCredit. Help Frank by providing a `From` implementation +// to convert his FairyCredit to GnomeCoin. At the current exchange rate, one +// FairyCredit is valued at 100 GnomeCoin. #[derive(Debug)] -struct Person { - name: String, - age: u8, -} +struct FairyCredit(u32); -// 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() -> Self { - Self { - name: String::from("John"), - age: 30, - } +#[derive(Debug, PartialEq)] +struct GnomeCoin(u64); + +impl From for GnomeCoin { + fn from(value: FairyCredit) -> Self { + Self(value.0 as u64 * 100) } } -impl From<&str> for Person { - fn from(s: &str) -> Self { - let mut split = s.split(','); - let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else { - // ^^^^ there should be no third element - return Self::default(); - }; - - if name.is_empty() { - return Self::default(); - } - - let Ok(age) = age.parse() else { - return Self::default(); - }; - - Self { - name: name.into(), - age, - } - } -} +// Note that we shouldn't provide the opposite conversion: from GnomeCoin to +// FairyCredits. That's because less than 100 GnomeCoins cannot be represented +// as FairyCredits, which would make the conversion lossy. The `From` trait is +// only appropriate for infallible and lossless conversions. fn main() { // Use the `from` function. - let p1 = Person::from("Mark,20"); - println!("{p1:?}"); + let g1 = GnomeCoin::from(FairyCredit(12)); + println!("{g1:?}"); - // Since `From` is implemented for Person, we are able to use `Into`. - let p2: Person = "Gerald,70".into(); - println!("{p2:?}"); + // Since `From` is implemented for GnomeCoin, we are able to use `Into`. + let g2: GnomeCoin = FairyCredit(9).into(); + println!("{g2:?}"); } #[cfg(test)] @@ -58,79 +41,14 @@ mod tests { use super::*; #[test] - fn test_default() { - let dp = Person::default(); - assert_eq!(dp.name, "John"); - assert_eq!(dp.age, 30); + fn test_from() { + let g = GnomeCoin::from(FairyCredit(12)); + assert_eq!(g, GnomeCoin(1200)); } #[test] - fn test_bad_convert() { - let p = Person::from(""); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); - } - - #[test] - fn test_good_convert() { - let p = Person::from("Mark,20"); - assert_eq!(p.name, "Mark"); - assert_eq!(p.age, 20); - } - - #[test] - fn test_bad_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,dog"); - assert_eq!(p.name, "John"); - assert_eq!(p.age, 30); + fn test_into() { + let g: GnomeCoin = FairyCredit(9).into(); + assert_eq!(g, GnomeCoin(900)); } } From 360344ab6c5323e4289589ca0c1b6c2d23927b47 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 15 May 2026 01:42:30 +0200 Subject: [PATCH 2/3] Simplify story and increase difficulty Conversion between Celsius and Fahrenheit should be understandable to most. Inverting the formula is still not very hard, but a little harder than only multiplying by 100. --- exercises/23_conversions/conversions2.rs | 58 +++++++++++----------- rustlings-macros/info.toml | 5 +- solutions/23_conversions/conversions2.rs | 61 +++++++++++++----------- 3 files changed, 64 insertions(+), 60 deletions(-) diff --git a/exercises/23_conversions/conversions2.rs b/exercises/23_conversions/conversions2.rs index f6df67f2..491b8073 100644 --- a/exercises/23_conversions/conversions2.rs +++ b/exercises/23_conversions/conversions2.rs @@ -3,50 +3,52 @@ // You can read more about it in the documentation: // https://doc.rust-lang.org/std/convert/trait.From.html // -// Frank the fairy would like to buy some truffles from Grace the gnome, a -// world-renowned chocolatier. The truffles are priced in GnomeCoin though, and -// Frank only has FairyCredit. Help Frank by providing a `From` implementation -// to convert his FairyCredit to GnomeCoin. At the current exchange rate, one -// FairyCredit is valued at 100 GnomeCoin. +// Representing units of measurements with separate types is a common practice. +// It avoids accidentally mixing up values of different units of measurement. -#[derive(Debug)] -struct FairyCredit(u32); +struct Celsius(f64); -#[derive(Debug, PartialEq)] -struct GnomeCoin(u64); +struct Fahrenheit(f64); -impl From for GnomeCoin { - // TODO: implement From for GnomeCoin +impl From for Fahrenheit { + // TODO: Convert Celsius to Fahrenheit. Don't worry about floating-point + // precision. The formula is: F = C * 1.8 + 32 } -// Note that we shouldn't provide the opposite conversion: from GnomeCoin to -// FairyCredits. That's because less than 100 GnomeCoins cannot be represented -// as FairyCredits, which would make the conversion lossy. The `From` trait is -// only appropriate for infallible and lossless conversions. +impl From for Celsius { + // TODO: Convert Fahrenheit to Celsius. +} fn main() { - // Use the `from` function. - let g1 = GnomeCoin::from(FairyCredit(12)); - println!("{g1:?}"); - - // Since `From` is implemented for GnomeCoin, we are able to use `Into`. - let g2: GnomeCoin = FairyCredit(9).into(); - println!("{g2:?}"); + // You can optionally experiment here. } #[cfg(test)] mod tests { use super::*; + const CASES: [(f64, f64); 6] = [ + (-50.0, -58.0), + (0.0, 32.0), + (20.0, 68.0), + (100.0, 212.0), + (400.0, 752.0), + (1000.0, 1832.0), + ]; + #[test] - fn test_from() { - let g = GnomeCoin::from(FairyCredit(12)); - assert_eq!(g, GnomeCoin(1200)); + fn celsius_to_fahrenheit() { + for (celsius, fahrenheit) in CASES { + let Fahrenheit(actual) = Celsius(celsius).into(); + assert_eq!(actual.round(), fahrenheit); + } } #[test] - fn test_into() { - let g: GnomeCoin = FairyCredit(9).into(); - assert_eq!(g, GnomeCoin(900)); + fn fahrenheit_to_celsius() { + for (celsius, fahrenheit) in CASES { + let Celsius(actual) = Fahrenheit(fahrenheit).into(); + assert_eq!(actual.round(), celsius); + } } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 054da99b..08f83e5f 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1171,9 +1171,8 @@ Use the `as` operator to cast one of the operands in the last line of the name = "conversions2" dir = "23_conversions" hint = """ -Implement From for GnomeCoin. Check the documentation of `From` to -learn about its required items: -https://doc.rust-lang.org/std/convert/trait.From.html""" +For the conversion from Fahrenheit to Celsius, you have to determine the formula +yourself. Don't forget the order of operations!""" [[exercises]] name = "conversions3" diff --git a/solutions/23_conversions/conversions2.rs b/solutions/23_conversions/conversions2.rs index b8b2372e..c8580f6b 100644 --- a/solutions/23_conversions/conversions2.rs +++ b/solutions/23_conversions/conversions2.rs @@ -3,52 +3,55 @@ // You can read more about it in the documentation: // https://doc.rust-lang.org/std/convert/trait.From.html // -// Frank the fairy would like to buy some truffles from Grace the gnome, a -// world-renowned chocolatier. The truffles are priced in GnomeCoin though, and -// Frank only has FairyCredit. Help Frank by providing a `From` implementation -// to convert his FairyCredit to GnomeCoin. At the current exchange rate, one -// FairyCredit is valued at 100 GnomeCoin. +// Representing units of measurements with separate types is a common practice. +// It avoids accidentally mixing up values of different units of measurement. -#[derive(Debug)] -struct FairyCredit(u32); +struct Celsius(f64); -#[derive(Debug, PartialEq)] -struct GnomeCoin(u64); +struct Fahrenheit(f64); -impl From for GnomeCoin { - fn from(value: FairyCredit) -> Self { - Self(value.0 as u64 * 100) +impl From for Fahrenheit { + fn from(Celsius(celsius): Celsius) -> Self { + Fahrenheit(celsius * 1.8 + 32.0) } } -// Note that we shouldn't provide the opposite conversion: from GnomeCoin to -// FairyCredits. That's because less than 100 GnomeCoins cannot be represented -// as FairyCredits, which would make the conversion lossy. The `From` trait is -// only appropriate for infallible and lossless conversions. +impl From for Celsius { + fn from(Fahrenheit(fahrenheit): Fahrenheit) -> Self { + Celsius((fahrenheit - 32.0) / 1.8) + } +} fn main() { - // Use the `from` function. - let g1 = GnomeCoin::from(FairyCredit(12)); - println!("{g1:?}"); - - // Since `From` is implemented for GnomeCoin, we are able to use `Into`. - let g2: GnomeCoin = FairyCredit(9).into(); - println!("{g2:?}"); + // You can optionally experiment here. } #[cfg(test)] mod tests { use super::*; + const CASES: [(f64, f64); 6] = [ + (-50.0, -58.0), + (0.0, 32.0), + (20.0, 68.0), + (100.0, 212.0), + (400.0, 752.0), + (1000.0, 1832.0), + ]; + #[test] - fn test_from() { - let g = GnomeCoin::from(FairyCredit(12)); - assert_eq!(g, GnomeCoin(1200)); + fn celsius_to_fahrenheit() { + for (celsius, fahrenheit) in CASES { + let Fahrenheit(actual) = Celsius(celsius).into(); + assert_eq!(actual.round(), fahrenheit); + } } #[test] - fn test_into() { - let g: GnomeCoin = FairyCredit(9).into(); - assert_eq!(g, GnomeCoin(900)); + fn fahrenheit_to_celsius() { + for (celsius, fahrenheit) in CASES { + let Celsius(actual) = Fahrenheit(fahrenheit).into(); + assert_eq!(actual.round(), celsius); + } } } From 361a7f501e0b8b40d0eac819b5efeb6f2667574f Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 15 May 2026 10:33:41 +0200 Subject: [PATCH 3/3] Add inverted formula and its derivation to hint --- rustlings-macros/info.toml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 08f83e5f..6b6af3a1 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1171,8 +1171,13 @@ Use the `as` operator to cast one of the operands in the last line of the name = "conversions2" dir = "23_conversions" hint = """ -For the conversion from Fahrenheit to Celsius, you have to determine the formula -yourself. Don't forget the order of operations!""" +The formula for converting from Fahrenheit to Celsius is: C = (F - 32) / 1.8 +This can be derived from the first formula: + + F = C * 1.8 + 32 // now subtract 32 on both sides + F - 32 = C * 1.8 // then divide by 1.8 +(F - 32) / 1.8 = C +""" [[exercises]] name = "conversions3"