Compare commits

...

18 commits

Author SHA1 Message Date
mo8it 2f8fa469ac Small writing changes 2024-07-02 16:26:59 +02:00
mo8it d3a0c26999 Improve the placement of TODO comments 2024-07-02 16:26:28 +02:00
mo8it 95f10c8068 Update CHANGELOG 2024-07-02 16:10:42 +02:00
mo8it 9bb174e96e Add a guide for third-party exercises 2024-07-02 16:09:05 +02:00
mo8it 4c5573b09f Update CONTRIBUTING 2024-07-02 14:45:25 +02:00
mo8it 43eb014026 Update README 2024-07-02 14:45:19 +02:00
mo8it 2d792651ea chore: Release 2024-07-02 14:29:07 +02:00
mo8it 6cf75d569b Fix typos 2024-07-02 14:28:08 +02:00
mo8it 67ce9b9e56 Underline "next" 2024-07-02 01:50:05 +02:00
mo8it bcebbb9df6 Update deps 2024-07-02 01:45:55 +02:00
mo8it bdd76cdf77 Merge branch 'run-sols-in-parallel' 2024-07-02 01:43:22 +02:00
mo8it 825637f32c as_ref_mut solution 2024-07-02 01:35:38 +02:00
mo8it 8ef5d10da2 Import the error variants in the tests 2024-07-02 01:29:30 +02:00
mo8it 5217cdc5e2 try_from_into solution 2024-07-02 01:26:09 +02:00
mo8it e3c8c457ba from_str solution 2024-07-02 01:03:55 +02:00
mo8it cddaf4881e from_into solution 2024-07-01 22:09:48 +02:00
mo8it 428d64ffa0 using_as solution 2024-07-01 21:41:22 +02:00
mo8it 42a3503906 Run solutions in parallel 2024-06-10 17:42:11 +02:00
24 changed files with 867 additions and 233 deletions

View file

@ -1,3 +1,69 @@
<a name="6.0.0"></a>
## 6.0.0 (2024-07-02)
This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨
The most important changes are highlighted below.
### Installation
The installation has been simplified a lot!
To install Rustlings after installing Rust, all what you need to do now is running the following command:
```bash
cargo install rustlings
```
Yes, this means that Rustlings is now on [crates.io](https://crates.io/crates/rustlings) 🎉
You can read about the motivations of this change in [this issue](https://github.com/rust-lang/rustlings/issues/1919).
### UI/UX
- The UI is now responsive when the terminal is resized.
- The progress bar was moved to the bottom so that you can always see your progress and the current exercise to work on.
- The current exercise path is now a terminal link. It will open the exercise file in your default editor when you click on it.
- A small prompt is now always shown at the bottom. It allows you to choose an action by entering a character. For example, entering `h` will show you the hint of the current exercise.
- The comment "I AM NOT DONE!" doesn't exist anymore. Instead of needing to remove it to go to the next exercise, you need to enter `n` in the terminal.
### List mode
A list mode was added using [`Ratatui`](https://ratatui.rs).
You can enter it by entering `l` in the watch mode.
It offers the following features:
- Browse all exercises and see their state (pending/done).
- Filter exercises based on their state (pending/done).
- Continue at another exercise. This allows you to skip some exercises or go back to previous ones.
- Reset an exercise so you can start over and revert your changes.
### Solutions
After finishing an exercise, a solution file will be available and Rustlings will show you its path in green.
This allows you to compare your solution with an idiomatic solution and maybe learn about other ways to solve a problem.
While writing the solutions, all exercises have been polished 🌟
For example, every exercise now contains `TODO` comments to highlight what the user needs to change and where.
### LSP support out of the box
Instead of creating a `project.json` file using `rustlings lsp`, Rustlings now works with a `Cargo.toml` file out of the box.
No actions are needed to activate the language server `rust-analyzer`.
This should avoid issues related to the language server or to running exercises, especially the ones with Clippy.
### Clippy
Clippy lints are now shown on all exercises, not only the Clippy exercises 📎
Make Clippy your friend from early on 🥰
### Third party exercises
Rustlings now supports third-party exercises!
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
Or do you want to translate the original Rustlings exercises?
Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)!
<a name="5.6.1"></a>
## 5.6.1 (2023-09-18)
@ -30,7 +96,7 @@
- Swapped the order of threads and smart pointer exercises.
- Rewrote the CLI to use `clap` - it's matured much since we switched to `argh` :)
- `structs3`: Switched from i32 to u32.
- `move_semantics`: Switched 1-4 to tests, and rewrote them to be way simpler, while still teaching about the same
- `move_semantics`: Switched 1-4 to tests, and rewrote them to be way simpler, while still teaching about the same
concepts.
#### Fixed

View file

@ -32,11 +32,14 @@ If you need any help with it or face any Git related problems, don't hesitate to
It may take time to review your pull request.
Please be patient 😇
When updating an exercise, check if its solution needs to be updated.
## Adding An Exercise
- Name the file `exercises/yourTopic/yourTopicN.rs`.
- Make sure to put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`.
- Add a (possible) solution at `solutions/yourTopic/yourTopicN.rs` with comments and links explaining it.
- Make sure to put in some helpful links, and link to sections of The Book in `exercises/yourTopic/README.md`.
- In the exercise, add a `// TODO: …` comment where user changes are required.
- Add a solution at `solutions/yourTopic/yourTopicN.rs` with comments explaining it.
- Add the [metadata for your exercise](#exercise-metadata) in the `rustlings-macros/info.toml` file.
- Make sure your exercise runs with `rustlings run yourTopicN`.
- [Open a pull request](#pull-requests).
@ -49,7 +52,9 @@ The exercise metadata should contain the following:
[[exercises]]
name = "yourTopicN"
dir = "yourTopic"
hint = """A useful (multi-line) hint for your exercise."""
hint = """
A useful (multi-line) hint for your exercise.
Include links to a section in The Book or a documentation page."""
```
If your exercise doesn't contain any test, add `test = false` to the exercise metadata.

24
Cargo.lock generated
View file

@ -151,9 +151,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.7"
version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d"
dependencies = [
"clap_builder",
"clap_derive",
@ -161,9 +161,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.7"
version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708"
dependencies = [
"anstream",
"anstyle",
@ -173,9 +173,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.5"
version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
dependencies = [
"heck",
"proc-macro2",
@ -416,9 +416,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.21"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lru"
@ -654,7 +654,7 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "rustlings"
version = "6.0.0-beta.9"
version = "6.0.0-beta.10"
dependencies = [
"anyhow",
"assert_cmd",
@ -673,7 +673,7 @@ dependencies = [
[[package]]
name = "rustlings-macros"
version = "6.0.0-beta.9"
version = "6.0.0-beta.10"
dependencies = [
"quote",
"serde",
@ -729,9 +729,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.118"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
dependencies = [
"itoa",
"ryu",

View file

@ -8,7 +8,7 @@ exclude = [
]
[workspace.package]
version = "6.0.0-beta.9"
version = "6.0.0-beta.10"
authors = [
"Liv <mokou@fastmail.com>",
"Mo Bitar <mo8it@proton.me>",
@ -47,14 +47,14 @@ include = [
[dependencies]
anyhow = "1.0.86"
clap = { version = "4.5.7", features = ["derive"] }
clap = { version = "4.5.8", features = ["derive"] }
crossterm = "0.27.0"
hashbrown = "0.14.5"
notify-debouncer-mini = { version = "0.4.1", default-features = false }
os_pipe = "1.2.0"
ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] }
rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.9" }
serde_json = "1.0.118"
rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.10" }
serde_json = "1.0.120"
serde.workspace = true
toml_edit.workspace = true

View file

@ -32,19 +32,15 @@ This'll also install _Cargo_, Rust's package/project manager.
The following command will download and compile Rustlings:
<!-- TODO: Remove @6.0.0-beta.x -->
```bash
cargo install rustlings@6.0.0-beta.9
cargo install rustlings
```
<details>
<summary><strong>If the installation fails…</strong> (<em>click to expand</em>)</summary>
<!-- TODO: Remove @6.0.0-beta.x -->
- Make sure you have the latest Rust version by running `rustup update`
- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.9 --locked`
- Try adding the `--locked` flag: `cargo install rustlings --locked`
- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
</details>
@ -88,6 +84,9 @@ We highly recommend that you have a look at them before you start 📚️
Most exercises contain an error that keeps them from compiling, and it's up to you to fix it!
Some exercises contain tests that need to pass for the exercise to be done ✅
Search for `TODO` and `todo!()` to find out what you need to change.
Ask for hints by entering `h` in the _watch mode_ 💡
### Watch Mode
After [initialization](#initialization), Rustlings can be launched by simply running the command `rustlings`.
@ -118,11 +117,15 @@ See the footer of the list for all possible keys.
## Continuing On
<!-- TODO: Mention third-party exercises -->
Once you've completed Rustlings, put your new knowledge to good use!
Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to.
## Third-Party Exercises
Do you want to create your own set of Rustlings exercises to focus on some specific topic?
Or do you want to translate the original Rustlings exercises?
Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)!
## Uninstalling Rustlings
If you want to remove Rustlings from your system, run the following command:

53
THIRD_PARTY_EXERCISES.md Normal file
View file

@ -0,0 +1,53 @@
# Third-Party Exercises
The support of Rustlings for third-party exercises allows you to create your own set of Rustlings exercises to focus on some specific topic.
You could also offer a translatation of the original Rustlings exercises as third-party exercises.
## Getting started
To create third-party exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`.
This command will, similar to `cargo new PROJECT_NAME`, create a template directory called `PROJECT_NAME` with all what you need to get started.
Read the comments in the generated `info.toml` file to understand its format.
It allows you to set a custom welcome and final message and specify the metadata of every exercise.
## Create an exercise
Here is an example of the metadata of one file:
```toml
[[exercises]]
name = "intro1"
hint = """
To finish this exercise, you need to …
This link might help you …"""
```
After entering this in `info.toml`, create the file `intro1.rs` in the `exercises/` directory.
The exercise needs to contain a `main` function, but it can be empty.
Adding tests is recommended.
Look at the official Rustlings exercises for inspiration.
You can optionally add a solution file `intro1.rs` to the `solutions/` directory.
Now, run `rustlings dev check`.
It will tell you about any issues with your exercises.
For example, it will tell you to run `rustlings dev update` to update the `Cargo.toml` file to include the new exercise `intro1`.
`rustlings dev check` will also run your solutions (if you have any) to make sure that they run successfully.
That's it!
You finished your first exercise 🎉
## Publish
Now, add more exercises and publish them as a Git repository.
Users just have to clone that repository and run `rustlings` in it to start working on your set of exercises just like the official ones.
One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise.
But you can trust the users to not look at the solution too early 😉
## Share
After publishing your set of exercises, open an issue or a pull request in the official Rustlings repository to link to your project in the README 😃

View file

@ -1,5 +1,5 @@
// We sometimes encourage you to keep trying things on a given exercise, even
// after you already figured it out. If you got everything working and feel
// TODO: We sometimes encourage you to keep trying things on a given exercise,
// even after you already figured it out. If you got everything working and feel
// ready for the next exercise, enter `n` in the terminal.
//
// The exercise file will be reloaded when you change one of the lines below!

View file

@ -1,5 +1,4 @@
// TODO: Fix the code to print "Hello world!".
fn main() {
// TODO: Fix the code to print "Hello world!".
printline!("Hello world!");
}

View file

@ -1,5 +1,4 @@
// TODO: Fix the compiler error.
fn main() {
let x = 3;
println!("Number {x}");

View file

@ -3,7 +3,7 @@ trait AppendBar {
}
// TODO: Implement the trait `AppendBar` for a vector of strings.
// `appned_bar` should push the string "Bar" into the vector.
// `append_bar` should push the string "Bar" into the vector.
fn main() {
// You can optionally experiment here.

View file

@ -18,7 +18,7 @@ impl Queue {
fn send_tx(q: Queue, tx: mpsc::Sender<u32>) {
// TODO: We want to send `tx` to both threads. But currently, it is moved
// into the frist thread. How could you solve this problem?
// into the first thread. How could you solve this problem?
thread::spawn(move || {
for val in q.first_half {
println!("Sending {val:?}");

View file

@ -3,22 +3,21 @@
// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively.
// Obtain the number of bytes (not characters) in the given argument.
// TODO: Add the AsRef trait appropriately as a trait bound.
// 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.
// 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().
// Squares a number using `as_mut()`.
// TODO: Add the appropriate trait bound.
fn num_sq<T>(arg: &mut T) {
// TODO: Implement the function body.
???
}
fn main() {

View file

@ -1,81 +1,79 @@
// 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
// The `From` trait is used for value-to-value conversions. If `From` is
// 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
#[derive(Debug)]
struct Person {
name: String,
age: usize,
age: u8,
}
// We implement the Default trait to use it as a fallback
// when the provided string is not convertible into a Person object
// 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 {
fn default() -> Self {
Self {
name: String::from("John"),
age: 30,
}
}
}
// Your task is to complete this implementation in order for the line `let p1 =
// 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.
// 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::<u8>()`.
//
// 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
// 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) -> Person {}
fn from(s: &str) -> Self {}
}
fn main() {
// Use the `from` function
// Use the `from` function.
let p1 = Person::from("Mark,20");
// Since From is implemented for Person, we should be able to use Into
println!("{p1:?}");
// Since `From` is implemented for Person, we are able to use `Into`.
let p2: Person = "Gerald,70".into();
println!("{:?}", p1);
println!("{:?}", p2);
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);

View file

@ -1,7 +1,8 @@
// 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
// This is similar to the previous `from_into` exercise. 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 in the documentation:
// https://doc.rust-lang.org/std/str/trait.FromStr.html
use std::num::ParseIntError;
@ -10,53 +11,54 @@ use std::str::FromStr;
#[derive(Debug, PartialEq)]
struct Person {
name: String,
age: usize,
age: u8,
}
// 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>()
// Wrapped error from parse::<u8>()
ParseInt(ParseIntError),
}
// 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::<u8>()`.
//
// 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
// 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
// error `ParsePersonError::BadLen`.
// 3. Use the first element from the split operation as the name.
// 4. If the name is empty, return the error `ParsePersonError::NoName`.
// 5. Parse the second element from the split operation into a `u8` as the age.
// 6. If parsing the age fails, return the error `ParsePersonError::ParseInt`.
impl FromStr for Person {
type Err = ParsePersonError;
fn from_str(s: &str) -> Result<Person, Self::Err> {
}
fn from_str(s: &str) -> Result<Self, Self::Err> {}
}
fn main() {
let p = "Mark,20".parse::<Person>().unwrap();
println!("{:?}", p);
let p = "Mark,20".parse::<Person>();
println!("{p:?}");
}
#[cfg(test)]
mod tests {
use super::*;
use ParsePersonError::*;
#[test]
fn empty_input() {
assert_eq!("".parse::<Person>(), Err(ParsePersonError::Empty));
assert_eq!("".parse::<Person>(), Err(BadLen));
}
#[test]
fn good_input() {
let p = "John,32".parse::<Person>();
@ -65,58 +67,47 @@ mod tests {
assert_eq!(p.name, "John");
assert_eq!(p.age, 32);
}
#[test]
fn missing_age() {
assert!(matches!(
"John,".parse::<Person>(),
Err(ParsePersonError::ParseInt(_))
));
assert!(matches!("John,".parse::<Person>(), Err(ParseInt(_))));
}
#[test]
fn invalid_age() {
assert!(matches!(
"John,twenty".parse::<Person>(),
Err(ParsePersonError::ParseInt(_))
));
assert!(matches!("John,twenty".parse::<Person>(), Err(ParseInt(_))));
}
#[test]
fn missing_comma_and_age() {
assert_eq!("John".parse::<Person>(), Err(ParsePersonError::BadLen));
assert_eq!("John".parse::<Person>(), Err(BadLen));
}
#[test]
fn missing_name() {
assert_eq!(",1".parse::<Person>(), Err(ParsePersonError::NoName));
assert_eq!(",1".parse::<Person>(), Err(NoName));
}
#[test]
fn missing_name_and_age() {
assert!(matches!(
",".parse::<Person>(),
Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_))
));
assert!(matches!(",".parse::<Person>(), Err(NoName | ParseInt(_))));
}
#[test]
fn missing_name_and_invalid_age() {
assert!(matches!(
",one".parse::<Person>(),
Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_))
Err(NoName | ParseInt(_)),
));
}
#[test]
fn trailing_comma() {
assert_eq!("John,32,".parse::<Person>(), Err(ParsePersonError::BadLen));
assert_eq!("John,32,".parse::<Person>(), Err(BadLen));
}
#[test]
fn trailing_comma_and_some_string() {
assert_eq!(
"John,32,man".parse::<Person>(),
Err(ParsePersonError::BadLen)
);
assert_eq!("John,32,man".parse::<Person>(), Err(BadLen));
}
}

View file

@ -1,9 +1,10 @@
// 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
// `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 in the documentation:
// https://doc.rust-lang.org/std/convert/trait.TryFrom.html
#![allow(clippy::useless_vec)]
use std::convert::{TryFrom, TryInto};
#[derive(Debug, PartialEq)]
@ -13,7 +14,7 @@ struct Color {
blue: u8,
}
// We will use this error type for these `TryFrom` conversions.
// We will use this error type for the `TryFrom` conversions.
#[derive(Debug, PartialEq)]
enum IntoColorError {
// Incorrect length of slice
@ -22,78 +23,67 @@ enum IntoColorError {
IntConversion,
}
// 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
// TODO: Tuple implementation.
// Correct RGB color values must be integers in the 0..=255 range.
impl TryFrom<(i16, i16, i16)> for Color {
type Error = IntoColorError;
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
}
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {}
}
// Array implementation
// TODO: Array implementation.
impl TryFrom<[i16; 3]> for Color {
type Error = IntoColorError;
fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
}
fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {}
}
// Slice implementation
// TODO: Slice implementation.
// This implementation needs to check the slice length.
impl TryFrom<&[i16]> for Color {
type Error = IntoColorError;
fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
}
fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {}
}
fn main() {
// Use the `try_from` function
// Using the `try_from` function.
let c1 = Color::try_from((183, 65, 14));
println!("{:?}", c1);
println!("{c1:?}");
// Since TryFrom is implemented for Color, we should be able to use TryInto
// Since `TryFrom` is implemented for `Color`, we can use `TryInto`.
let c2: Result<Color, _> = [183, 65, 14].try_into();
println!("{:?}", c2);
println!("{c2:?}");
let v = vec![183, 65, 14];
// With slice we should use `try_from` function
// With slice we should use the `try_from` function
let c3 = Color::try_from(&v[..]);
println!("{:?}", c3);
// or take slice within round brackets and use TryInto
println!("{c3:?}");
// or put the slice within round brackets and use `try_into`.
let c4: Result<Color, _> = (&v[..]).try_into();
println!("{:?}", c4);
println!("{c4:?}");
}
#[cfg(test)]
mod tests {
use super::*;
use IntoColorError::*;
#[test]
fn test_tuple_out_of_range_positive() {
assert_eq!(
Color::try_from((256, 1000, 10000)),
Err(IntoColorError::IntConversion)
);
assert_eq!(Color::try_from((256, 1000, 10000)), Err(IntConversion));
}
#[test]
fn test_tuple_out_of_range_negative() {
assert_eq!(
Color::try_from((-1, -10, -256)),
Err(IntoColorError::IntConversion)
);
assert_eq!(Color::try_from((-1, -10, -256)), Err(IntConversion));
}
#[test]
fn test_tuple_sum() {
assert_eq!(
Color::try_from((-1, 255, 255)),
Err(IntoColorError::IntConversion)
);
assert_eq!(Color::try_from((-1, 255, 255)), Err(IntConversion));
}
#[test]
fn test_tuple_correct() {
let c: Result<Color, _> = (183, 65, 14).try_into();
@ -103,25 +93,29 @@ mod tests {
Color {
red: 183,
green: 65,
blue: 14
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));
assert_eq!(c, Err(IntConversion));
}
#[test]
fn test_array_out_of_range_negative() {
let c: Result<Color, _> = [-10, -256, -1].try_into();
assert_eq!(c, Err(IntoColorError::IntConversion));
assert_eq!(c, Err(IntConversion));
}
#[test]
fn test_array_sum() {
let c: Result<Color, _> = [-1, 255, 255].try_into();
assert_eq!(c, Err(IntoColorError::IntConversion));
assert_eq!(c, Err(IntConversion));
}
#[test]
fn test_array_correct() {
let c: Result<Color, _> = [183, 65, 14].try_into();
@ -135,30 +129,25 @@ mod tests {
}
);
}
#[test]
fn test_slice_out_of_range_positive() {
let arr = [10000, 256, 1000];
assert_eq!(
Color::try_from(&arr[..]),
Err(IntoColorError::IntConversion)
);
assert_eq!(Color::try_from(&arr[..]), Err(IntConversion));
}
#[test]
fn test_slice_out_of_range_negative() {
let arr = [-256, -1, -10];
assert_eq!(
Color::try_from(&arr[..]),
Err(IntoColorError::IntConversion)
);
assert_eq!(Color::try_from(&arr[..]), Err(IntConversion));
}
#[test]
fn test_slice_sum() {
let arr = [-1, 255, 255];
assert_eq!(
Color::try_from(&arr[..]),
Err(IntoColorError::IntConversion)
);
assert_eq!(Color::try_from(&arr[..]), Err(IntConversion));
}
#[test]
fn test_slice_correct() {
let v = vec![183, 65, 14];
@ -169,18 +158,20 @@ mod tests {
Color {
red: 183,
green: 65,
blue: 14
blue: 14,
}
);
}
#[test]
fn test_slice_excess_length() {
let v = vec![0, 0, 0, 0];
assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen));
assert_eq!(Color::try_from(&v[..]), Err(BadLen));
}
#[test]
fn test_slice_insufficient_length() {
let v = vec![0, 0];
assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen));
assert_eq!(Color::try_from(&v[..]), Err(BadLen));
}
}

View file

@ -1,12 +1,10 @@
// 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.
// Type casting in Rust is done via the usage of the `as` operator.
// Note that the `as` operator is not only used when type casting. It also helps
// with renaming imports.
fn average(values: &[f64]) -> f64 {
let total = values.iter().sum::<f64>();
// TODO: Make a conversion before dividing.
total / values.len()
}

View file

@ -1174,7 +1174,7 @@ Use the `as` operator to cast one of the operands in the last line of the
name = "from_into"
dir = "23_conversions"
hint = """
Follow the steps provided right before the `From` implementation"""
Follow the steps provided right before the `From` implementation."""
[[exercises]]
name = "from_str"
@ -1183,13 +1183,11 @@ hint = """
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.
This is almost like the previous `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.
Another hint: You can use the `map_err` method of `Result` with a function
or a closure to wrap the error from `parse::<usize>`.
Another hint: You can use the `map_err` method of `Result` with a function or a
closure to wrap the error from `parse::<u8>`.
Yet another hint: If you would like to propagate errors by using the `?`
operator in your solution, you might want to look at
@ -1199,21 +1197,8 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen
name = "try_from_into"
dir = "23_conversions"
hint = """
Follow the steps provided right before the `TryFrom` implementation.
You can also use the example at
https://doc.rust-lang.org/std/convert/trait.TryFrom.html
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?
Another hint: Look at the test cases to see which error variants to return.
Yet another hint: You can use the `map_err` or `or` methods of `Result` to
convert errors.
Yet another hint: If you would like to propagate errors by using the `?`
operator in your solution, you might want to look at
https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html
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?
Challenge: Can you make the `TryFrom` implementations generic over many integer types?"""

View file

@ -1 +1,59 @@
// Solutions will be available before the stable release. Thank you for testing the beta version 🥰
// 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.
// Obtain the number of bytes (not characters) in the given argument.
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.
fn char_counter<T: AsRef<str>>(arg: T) -> usize {
arg.as_ref().chars().count()
}
// Squares a number using `as_mut()`.
fn num_sq<T: AsMut<u32>>(arg: &mut T) {
let arg = arg.as_mut();
*arg = *arg * *arg;
}
fn main() {
// You can optionally experiment here.
}
#[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 mut_box() {
let mut num: Box<u32> = Box::new(3);
num_sq(&mut num);
assert_eq!(*num, 9);
}
}

View file

@ -1 +1,136 @@
// Solutions will be available before the stable release. Thank you for testing the beta version 🥰
// The `From` trait is used for value-to-value conversions. If `From` is
// 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
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
// 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,
}
}
}
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,
}
}
}
fn main() {
// Use the `from` function.
let p1 = Person::from("Mark,20");
println!("{p1:?}");
// Since `From` is implemented for Person, we are able to use `Into`.
let p2: Person = "Gerald,70".into();
println!("{p2:?}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default() {
let dp = Person::default();
assert_eq!(dp.name, "John");
assert_eq!(dp.age, 30);
}
#[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);
}
}

View file

@ -1 +1,117 @@
// Solutions will be available before the stable release. Thank you for testing the beta version 🥰
// This is similar to the previous `from_into` exercise. 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 in the documentation:
// https://doc.rust-lang.org/std/str/trait.FromStr.html
use std::num::ParseIntError;
use std::str::FromStr;
#[derive(Debug, PartialEq)]
struct Person {
name: String,
age: u8,
}
// We will use this error type for the `FromStr` implementation.
#[derive(Debug, PartialEq)]
enum ParsePersonError {
// Incorrect number of fields
BadLen,
// Empty name field
NoName,
// Wrapped error from parse::<u8>()
ParseInt(ParseIntError),
}
impl FromStr for Person {
type Err = ParsePersonError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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 Err(ParsePersonError::BadLen);
};
if name.is_empty() {
return Err(ParsePersonError::NoName);
}
let age = age.parse().map_err(ParsePersonError::ParseInt)?;
Ok(Self {
name: name.into(),
age,
})
}
}
fn main() {
let p = "Mark,20".parse::<Person>();
println!("{p:?}");
}
#[cfg(test)]
mod tests {
use super::*;
use ParsePersonError::*;
#[test]
fn empty_input() {
assert_eq!("".parse::<Person>(), Err(BadLen));
}
#[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(ParseInt(_))));
}
#[test]
fn invalid_age() {
assert!(matches!("John,twenty".parse::<Person>(), Err(ParseInt(_))));
}
#[test]
fn missing_comma_and_age() {
assert_eq!("John".parse::<Person>(), Err(BadLen));
}
#[test]
fn missing_name() {
assert_eq!(",1".parse::<Person>(), Err(NoName));
}
#[test]
fn missing_name_and_age() {
assert!(matches!(",".parse::<Person>(), Err(NoName | ParseInt(_))));
}
#[test]
fn missing_name_and_invalid_age() {
assert!(matches!(
",one".parse::<Person>(),
Err(NoName | ParseInt(_)),
));
}
#[test]
fn trailing_comma() {
assert_eq!("John,32,".parse::<Person>(), Err(BadLen));
}
#[test]
fn trailing_comma_and_some_string() {
assert_eq!("John,32,man".parse::<Person>(), Err(BadLen));
}
}

View file

@ -1 +1,192 @@
// Solutions will be available before the stable release. Thank you for testing the beta version 🥰
// `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 in the documentation:
// https://doc.rust-lang.org/std/convert/trait.TryFrom.html
use std::convert::{TryFrom, TryInto};
#[derive(Debug, PartialEq)]
struct Color {
red: u8,
green: u8,
blue: u8,
}
// We will use this error type for the `TryFrom` conversions.
#[derive(Debug, PartialEq)]
enum IntoColorError {
// Incorrect length of slice
BadLen,
// Integer conversion error
IntConversion,
}
impl TryFrom<(i16, i16, i16)> for Color {
type Error = IntoColorError;
fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
let (Ok(red), Ok(green), Ok(blue)) = (
u8::try_from(tuple.0),
u8::try_from(tuple.1),
u8::try_from(tuple.2),
) else {
return Err(IntoColorError::IntConversion);
};
Ok(Self { red, green, blue })
}
}
impl TryFrom<[i16; 3]> for Color {
type Error = IntoColorError;
fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
// Reuse the implementation for a tuple.
Self::try_from((arr[0], arr[1], arr[2]))
}
}
impl TryFrom<&[i16]> for Color {
type Error = IntoColorError;
fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
// Check the length.
if slice.len() != 3 {
return Err(IntoColorError::BadLen);
}
// Reuse the implementation for a tuple.
Self::try_from((slice[0], slice[1], slice[2]))
}
}
fn main() {
// Using the `try_from` function.
let c1 = Color::try_from((183, 65, 14));
println!("{c1:?}");
// Since `TryFrom` is implemented for `Color`, we can use `TryInto`.
let c2: Result<Color, _> = [183, 65, 14].try_into();
println!("{c2:?}");
let v = vec![183, 65, 14];
// With slice we should use the `try_from` function
let c3 = Color::try_from(&v[..]);
println!("{c3:?}");
// or put the slice within round brackets and use `try_into`.
let c4: Result<Color, _> = (&v[..]).try_into();
println!("{c4:?}");
}
#[cfg(test)]
mod tests {
use super::*;
use IntoColorError::*;
#[test]
fn test_tuple_out_of_range_positive() {
assert_eq!(Color::try_from((256, 1000, 10000)), Err(IntConversion));
}
#[test]
fn test_tuple_out_of_range_negative() {
assert_eq!(Color::try_from((-1, -10, -256)), Err(IntConversion));
}
#[test]
fn test_tuple_sum() {
assert_eq!(Color::try_from((-1, 255, 255)), Err(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(IntConversion));
}
#[test]
fn test_array_out_of_range_negative() {
let c: Result<Color, _> = [-10, -256, -1].try_into();
assert_eq!(c, Err(IntConversion));
}
#[test]
fn test_array_sum() {
let c: Result<Color, _> = [-1, 255, 255].try_into();
assert_eq!(c, Err(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(IntConversion));
}
#[test]
fn test_slice_out_of_range_negative() {
let arr = [-256, -1, -10];
assert_eq!(Color::try_from(&arr[..]), Err(IntConversion));
}
#[test]
fn test_slice_sum() {
let arr = [-1, 255, 255];
assert_eq!(Color::try_from(&arr[..]), Err(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(BadLen));
}
#[test]
fn test_slice_insufficient_length() {
let v = vec![0, 0];
assert_eq!(Color::try_from(&v[..]), Err(BadLen));
}
}

View file

@ -1 +1,24 @@
// Solutions will be available before the stable release. Thank you for testing the beta version 🥰
// Type casting in Rust is done via the usage of the `as` operator.
// Note that the `as` operator is not only used when type casting. It also helps
// with renaming imports.
fn average(values: &[f64]) -> f64 {
let total = values.iter().sum::<f64>();
total / values.len() as f64
// ^^^^^^
}
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);
}
}

View file

@ -4,6 +4,11 @@ use std::{
fs::{self, read_dir, OpenOptions},
io::{self, Read, Write},
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
Mutex,
},
thread,
};
use crate::{
@ -167,36 +172,52 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> {
}
fn check_solutions(require_solutions: bool, info_file: &InfoFile) -> Result<()> {
let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len());
let target_dir = parse_target_dir()?;
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len()));
let error_occurred = AtomicBool::new(false);
for exercise_info in &info_file.exercises {
let path = exercise_info.sol_path();
if !Path::new(&path).exists() {
if require_solutions {
bail!("Exercise {} is missing a solution", exercise_info.name);
}
println!("Running all solutions. This may take a while...\n");
thread::scope(|s| {
for exercise_info in &info_file.exercises {
s.spawn(|| {
let error = |e| {
let mut stderr = io::stderr().lock();
stderr.write_all(e).unwrap();
stderr
.write_all(b"\nFailed to run the solution of the exercise ")
.unwrap();
stderr.write_all(exercise_info.name.as_bytes()).unwrap();
stderr.write_all(SEPARATOR).unwrap();
error_occurred.store(true, atomic::Ordering::Relaxed);
};
// No solution to check.
continue;
let path = exercise_info.sol_path();
if !Path::new(&path).exists() {
if require_solutions {
error(b"Solution missing");
}
// No solution to check.
return;
}
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
match exercise_info.run_solution(&mut output, &target_dir) {
Ok(true) => {
paths.lock().unwrap().insert(PathBuf::from(path));
}
Ok(false) => error(&output),
Err(e) => error(e.to_string().as_bytes()),
}
});
}
});
println!("Running the solution of {}", exercise_info.name);
let success = exercise_info.run_solution(&mut output, &target_dir)?;
if !success {
io::stderr().write_all(&output)?;
bail!(
"Failed to run the solution of the exercise {}",
exercise_info.name,
);
}
paths.insert(PathBuf::from(path));
if error_occurred.load(atomic::Ordering::Relaxed) {
bail!("At least one solution failed. See the output above.");
}
check_unexpected_files("solutions", &paths)?;
check_unexpected_files("solutions", &paths.into_inner().unwrap())?;
Ok(())
}
@ -224,3 +245,6 @@ pub fn check(require_solutions: bool) -> Result<()> {
Ok(())
}
const SEPARATOR: &[u8] =
b"\n========================================================================================\n";

View file

@ -101,7 +101,7 @@ impl<'a> WatchState<'a> {
}
if self.done_status != DoneStatus::Pending {
write!(self.writer, "{}:next / ", 'n'.bold())?;
write!(self.writer, "{}:{} / ", 'n'.bold(), "next".underlined())?;
}
if !self.show_hint {