Rust string manipulations

Rust string manipulations

Strings are the building blocks of text data, and it is crucial to know how to manipulate them to suit your needs efficiently. 

In this article, we’ll explore the different ways to manipulate strings in Rust with examples.

There are two types of strings in Rust: string slice and String

  • A string slice (str) is a reference to a string or part of a string. It is stored in the program’s executable and is immutable
  • A String is a mutable string type in Rust and it’s stored on the heap.

Let’s start with concatenation…

Concatenation

Try running this code in Rust Playground:

fn main() {
    let new_string = "Hello" + ", world!";
     println!("{}", new_string);
}

You’ll get an error like this one:

While this is a common method to concatenate strings in other languages, Rust is surprisingly different. You can’t use the above method to concatenate the above string because it’s a type of string slice known as a string literal.

The + operator (provided by the Add trait) isn’t implemented for string slices, so you can’t directly concatenate them. However, you can achieve the same result with this approach:

let new_string = String::from("Hello") + ", world!";

As a quick aside, in Rust, the :: syntax is called the “path separator” and is used to access items within a module, struct, or enum. In this case, String::from("Hello") uses :: to call the from function associated with the String type.

This works because the Add trait is implemented for String in a way that allows string literals on the right-hand side to be appended to a String on the left-hand side.

Below is the relevant part of the implementation of the Add trait in the source code:

#[cfg(not(no_global_oom_handling))]
#[stable(feature = "rust1", since = "1.0.0")]

impl Add<&str> for String {

    type Output = String;

    #[inline]

    fn add(mut self, other: &str) -> String {

        self.push_str(other);

        self

    }

}

As you can see, the Add trait implementation only allows appending a string slice (&str) to a String. Attempting to concatenate two String types directly will result in an error:

That being said, if you reverse the order of the concatenation in the way it is written below, it won’t work; it will throw an error instead:

fn main() {

   let new_string = "Hello" + ", world!".to_string();

    println!("{}", new_string);

}

Converting String Slices to Owned Strings for Concatenation

When working with string literals (which are &str types) in Rust, you often need to convert them to owned String types for concatenation. This section explores different methods to achieve this.

Using to_owned()

fn main() { 
    //Convert the first literal to an owned String, then concatenate 
    let new_string: String = "Hello".to_owned() + ", world!"; 
    
    println!("{}", new_string); // Output: Hello, world! 
  
  }

Using into():

fn main() { 
    // Explicitly convert the first literal to a String using Into trait 
    
    let new_string: String = <&str as Into<String>>::into("Hello") + ", world!"; 
    
    println!("{}", new_string); // Output: Hello, world! 
}

Note: While to_owned() and to_string() both create owned Strings, to_owned() is generally preferred for its slightly better performance when working with large string slices. The performance difference will be negligible in practice.

Other ways to concatenate a string

If you need more flexibility, here are some alternative methods:

Using the format! macro:

let result = format!("{}{}", "Hello", ", world!");

Using the concat! macro:

let result = concat!("Hello", ", world!");

Using push_str method like so:

let mut s = String::from("Hello");

s.push_str(",world!");

String slicing

String slicing allows you to extract a portion of a string. For example, to extract “Hello” from “Hello, world,” you might want to use slicing. However, due to Rust’s handling of UTF-8 encoded strings, direct indexing (like s[0]) is not allowed.

Rust strings store characters using UTF-8 encoding. UTF-8 uses a variable number of bytes to represent a single character. This means a single index might not necessarily correspond to a single character.

For example, the symbol in UTF-8 encoding takes up 3 bytes. If you indexed s[0] on a string "€17", you might only get the first byte, which wouldn’t represent the entire character ‘‘ which is why indexing a string this way s[0] is forbidden in Rust:

fn main() {
    let s = "Hello, world!";
    let slice = &s[0]; 
    println!("{}", slice);
}
Using indexing to slice a string

string[start_index..end_index] This extracts a slice from the start_index (inclusive) up to, but not including, the end_index as shown in the example below:

fn main() {
    let s = "Hello, world!";
    let slice = &s[0..5]; 
    println!("{}", slice); // "Hello"
}

There is also another way to index a string by splitting its characters and returning the nth index value:

fn main() {
    let string  = "Hello, world";
    println!("{:?}", string.chars().nth(0).unwrap()); // 'H'
}
Using skip, and take to slice a string

Use the chars method to iterate over the string and use skip to define the start index of the substring and take to define the end index of the substring as shown below:


fn main() {
    let s = "Hello, world";
    let slice: String = s.chars().skip(7).take(12).collect();
    println!("{}", slice); // "world"
}
Using the get method to slice a string

You can also use the .get method to slice a string and get part of it as shown below. .get returns a result, so you might want to unwrap it to get the actual value.


fn main() {
    let s = "Hello, world";
    let slice = s.get(7..12);
    println!("{}", slice.unwrap()); // world
}

Adding and removing a character from the end of a string

In a string, the push and pop methods are used to add a single character to the end of a string and remove a character from the end of a string, respectively.

.push method

The .push method doesn’t return the pushed in character.

fn main() {
    let mut string  = "Hello, world".to_string();
    string.push('r');
    println!("{}", string); // Hello, worldr
}
.Pop method

The pop method returns a Result that contains the popped slice of the string.

fn main() {
    let mut string  = "Hello, world".to_string();
    let popped = string.pop();
    println!("{}", popped.unwrap()); // d
}

Insert and Remove a character from the beginning of the string

Unlike pop and push that focuses on the last characters of a string, the .insert and .remove focuses on the beginning part of a string. .insert pushes a character to the beginning of a string while the .remove method removes a character from the beginning of a string.

fn main() {
    let mut s = String::from("ello");
    s.insert(0, 'H'); // inserts "H" at the beginning
    println!("{}", s); // "Hello"

    s.remove(0); // removes the first character
    println!("{}", s); // "ello"
}

Removing whitespaces from the beginning of a string

The .trim method helps with removing whitespaces from the beginning and end of a string. Here is an example:


fn main() {
    let s = String::from("          hello world     ");
    println!("{}", s.trim()); // "hello world"
}

Convert a string to upper case and lower case

Use the .to_lowercase method to convert a string to lower case letters and the .to_uppercase to make the conversions.

fn main() {
    let s = String::from("HELLO");
    println!("{}", s.to_lowercase()); // "hello"

    let t = String::from("hello");
    println!("{}", t.to_uppercase()); // "HELLO"
}

Splitting a string into substrings

We can use the .split() method to split a string into parts and join them back together using the .join() method. Here is an example:

fn main() {

    let s = "hello,world,rust";
    let substrings: Vec<&str> = s.split(",").collect(); // splits into substrings
    println!("{:?}", substrings); // ["hello", "world", "rust"]

    let joined = substrings.join(","); // joins substrings back together
    println!("{}", joined); // "hello,world,rust"
}

Check a substring exists in a string

We can use the .contains to check if a string contains a specific substring. Use the .starts_with() to check that a string starts with a specific substring. And also use the ends_with() to check that a string ends with a specific substring.




fn main() {

    let s = "hello, world";
    if s.contains("world") { // checks if "world" is in the string
        println!("'world' is in the string");
    }

    if s.starts_with("hello") { // checks if the string starts with "hello"
        println!("the string starts with 'hello'");
    }

    if s.ends_with("world") { // checks if the string ends with "world"
        println!("the string ends with 'world'");
    }
}

Replacing a string

The .replace method is used to replace a string with another string

fn main() {
    let s = String::from("hello world");
    println!("{}", s.replace("world", "rust")); // "hello rust"
}

Conclusion

Rust’s string manipulation capabilities might seem tricky at first due to the distinction between String and &str, but with practice, they offer powerful and flexible tools for working with text

Happy hacking

Buy Me A Coffee

Published by Eze Sunday Eze

Hi, welcome to my blog. I am Software Engineer and Technical Writer. And in this blog, I focus on sharing my views on the tech and tools I use. If you love my content and wish to stay in the loop, then by all means share this page and bookmark this website.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.