Getting Started with Rust Using Rustlings — Part 8: Strings

Jesse Verbruggen
5 min readFeb 21, 2023
Part 8: Strings

In the previous part, we learned about Enums in Rust. These behaved very differently from many other programming languages and were quite interesting.

This is part eight of my miniseries on learning Rust. If you want to read the previous article, you can get to it by following the link below.

In this series, I am going over my process of learning the syntax of Rust with Rustlings. In Part 1, I explain how to install Rust and Rustlings. So if you haven’t yet done that, please navigate here to follow along.

Strings

Yes, you heard it right. This part will be about strings. You might think you already know everything there is to know about strings, but there are some behaviours in Rust that you should know about.

What is a string? Simply put, a string is a vector of bytes. This means that a string can grow or shrink in size like a vector. There are two representations of this in Rust. String and &str. The first is an actual vector of bytes represented as UTF-8 encoded text. The second one, called a string slice, has fewer capabilities than the first. Both of these representations store UTF-8 encoded strings.

You can follow this link to learn more if you want to see how Strings work in Rust. I’ve summarized what I think is important in a cheat sheet below.

The String type cheat sheet

// Creates an empty String
let mut s = String::new();

// Creates a reference str (&str)
let data = "initial contents";

// Converts this &str to a new String value
let s = data.to_string();

// the method also works on a literal directly:
let s = "initial contents".to_string();

// Creates a string with initial content
let s = String::from("initial contents");

// Strings have full UTF-8 support
// This allows us to use any language or symbol supported by UTF-8
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");


// We can append to a string with push_str
let mut s = String::from("foo");
s.push_str("bar");

// We can append a single character using push
let mut s = String::from("lo");
s.push('l');

// We can combine multiple strings together with format!
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{s1}-{s2}-{s3}");

// We can use the + operator to add two strings
// But this will take ownership of the first argument
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used

// We can take a slice from our string
// But as each character of this alphabet takes up two bytes
// This will return "Зд"
let hello = "Здравствуйте";

let s = &hello[0..4];

// We can use .chars to get a vector of characters
// We can get the first 4 characters and create a new string
// This will return "Здра"
let mut chars = "Здравствуйте".chars();
let mut short = String::new();
for _ in 0..4 {
short.push(chars.next().unwrap());
}
println!("{}", short);

Exercises

strings1.rs

fn main() {
let answer = current_favorite_color();
println!("My current favorite color is {}", answer);
}

fn current_favorite_color() -> String {
"blue"
}

Make me compile without changing the function signature!

The function is returning “blue”, which is a string slice. Using the built-in to_string method. We can cast this to a String and solve our issue.

fn main() {
let answer = current_favorite_color();
println!("My current favorite color is {}", answer);
}

fn current_favorite_color() -> String {
"blue".to_string()
}

strings2.rs

fn main() {
let word = String::from("green"); // Try not changing this line :)
if is_a_color_word(word) {
println!("That is a color word I know!");
} else {
println!("That is not a color word I know.");
}
}

fn is_a_color_word(attempt: &str) -> bool {
attempt == "green" || attempt == "blue" || attempt == "red"
}

Make me compile without changing the function signature!

Now we see the same scenario the other way around. We can pass word as a reference &word, and no other changes are required.


fn main() {
let word = String::from("green"); // Try not changing this line :)
if is_a_color_word(&word) {
println!("That is a color word I know!");
} else {
println!("That is not a color word I know.");
}
}

fn is_a_color_word(attempt: &str) -> bool {
attempt == "green" || attempt == "blue" || attempt == "red"
}

strings3.rs

fn trim_me(input: &str) -> String {
// TODO: Remove whitespace from both ends of a string!
???
}

fn compose_me(input: &str) -> String {
// TODO: Add " world!" to the string! There's multiple ways to do this!
???
}

fn replace_me(input: &str) -> String {
// TODO: Replace "cars" in the string with "balloons"!
???
}

In this exercise, we will make use of trim, format!, and replace to solve each of these problems. Trim will remove all whitespace before and at the end of our string. Format will join two strings together to create a new one. Replace will replace the first string argument with the second one.

fn trim_me(input: &str) -> String {
input.trim().to_string()
}

fn compose_me(input: &str) -> String {
format!("{} world!", input)
}

fn replace_me(input: &str) -> String {
input.replace("cars", "balloons")
}

strings4.rs

fn string_slice(arg: &str) {
println!("{}", arg);
}
fn string(arg: String) {
println!("{}", arg);
}

fn main() {
???("blue");
???("red".to_string());
???(String::from("hi"));
???("rust is fun!".to_owned());
???("nice weather".into());
???(format!("Interpolation {}", "Station"));
???(&String::from("abc")[0..1]);
???(" hello there ".trim());
???("Happy Monday!".to_string().replace("Mon", "Tues"));
???("mY sHiFt KeY iS sTiCkY".to_lowercase());
}

Ok, here are a bunch of values — some are String, some are &str. Your task is to call one of these two functions on each value depending on what you think each value is. That is, add either string_slice or string before the parentheses on each line. If you’re right, it will compile!

I’m leaving this final exercise up to you. It will be better for you to mess around and make mistakes to learn the difference than for me to give you the answer here. Good luck!

In the next part, I explain how the module system works in Rust and how we can use multiple files to separate our logic. The link below will take you there.

If I helped you learn more about Rust or you found this interesting, please leave a clap 👏 to help more people find these articles.

Let’s help each other learn and grow! Peace out✌️

--

--