Rust - String Methods
Overview
Estimated time: 35–45 minutes
Master essential string manipulation methods in Rust. Learn how to search, modify, format, and transform strings using Rust's rich string API. Understand the difference between methods that work on &str
vs String
.
Learning Objectives
- Use common string methods for searching and manipulation.
- Transform strings with case conversion and formatting.
- Split and join strings with various delimiters.
- Handle Unicode correctly in string operations.
- Choose appropriate methods for
&str
vsString
.
Prerequisites
String Information Methods
Length and Capacity
Get information about string size and capacity:
fn main() {
let s = String::from("Hello, 世界!");
let str_slice = "Hello, world!";
// Length in bytes (not characters!)
println!("String length: {}", s.len()); // 13 (bytes)
println!("Str slice length: {}", str_slice.len()); // 13 (bytes)
// Character count (Unicode-aware)
println!("Character count: {}", s.chars().count()); // 9 characters
// String capacity (only for String, not &str)
println!("String capacity: {}", s.capacity()); // >= 13
// Check if empty
println!("Is empty: {}", s.is_empty()); // false
println!("Empty string: {}", "".is_empty()); // true
}
Checking String Content
Test string content with various predicates:
fn main() {
let text = "Hello, Rust Programming!";
// Check prefixes and suffixes
println!("Starts with 'Hello': {}", text.starts_with("Hello")); // true
println!("Ends with '!': {}", text.ends_with("!")); // true
println!("Ends with 'Rust': {}", text.ends_with("Rust")); // false
// Check if string contains substring
println!("Contains 'Rust': {}", text.contains("Rust")); // true
println!("Contains 'Python': {}", text.contains("Python")); // false
// Character-based checks
let code = "fn main() { println!(\"Hello\"); }";
println!("Contains alphabetic: {}", code.chars().any(|c| c.is_alphabetic())); // true
println!("Contains numeric: {}", code.chars().any(|c| c.is_numeric())); // false
println!("All ASCII: {}", code.is_ascii()); // true
}
String Searching
Finding Substrings
Search for patterns within strings:
fn main() {
let text = "The quick brown fox jumps over the lazy dog";
// Find first occurrence
if let Some(pos) = text.find("fox") {
println!("Found 'fox' at position: {}", pos); // 16
}
// Find last occurrence
if let Some(pos) = text.rfind("the") {
println!("Last 'the' at position: {}", pos); // 31
}
// Find with closures
if let Some(pos) = text.find(|c: char| c.is_uppercase()) {
println!("First uppercase at: {}", pos); // 0 (T)
}
// Multiple searches
for (i, word) in text.split_whitespace().enumerate() {
if word.contains('o') {
println!("Word {} contains 'o': {}", i, word);
}
}
// Word 2 contains 'o': brown
// Word 3 contains 'o': fox
// Word 6 contains 'o': over
// Word 9 contains 'o': dog
}
Pattern Matching
Use different types of patterns for searching:
fn main() {
let text = "[email protected], [email protected]";
// Character patterns
let at_positions: Vec<_> = text.match_indices('@').collect();
println!("@ positions: {:?}", at_positions); // [(4, "@"), (17, "@")]
// String patterns
let domains: Vec<_> = text.matches(|c: char| c.is_alphabetic()).collect();
println!("First few letters: {:?}", domains.take(5).collect::>());
// Custom patterns
let emails: Vec<&str> = text.split(", ").collect();
for email in emails {
if let Some(domain_start) = email.find('@') {
let domain = &email[domain_start + 1..];
println!("Domain: {}", domain);
}
}
// Domain: example.com
// Domain: test.org
}
String Modification
Case Conversion
Convert string case with Unicode awareness:
fn main() {
let text = "Hello, Wörld! 你好世界";
// Basic case conversion
println!("Uppercase: {}", text.to_uppercase()); // HELLO, WÖRLD! 你好世界
println!("Lowercase: {}", text.to_lowercase()); // hello, wörld! 你好世界
// ASCII-only case conversion (faster)
let ascii_text = "Hello, World!";
println!("ASCII upper: {}", ascii_text.to_ascii_uppercase()); // HELLO, WORLD!
println!("ASCII lower: {}", ascii_text.to_ascii_lowercase()); // hello, world!
// Title case (manual implementation)
fn to_title_case(s: &str) -> String {
s.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars.as_str().to_lowercase().chars()).collect(),
}
})
.collect::>()
.join(" ")
}
println!("Title case: {}", to_title_case("hello world from rust"));
// Title case: Hello World From Rust
}
Trimming Whitespace
Remove whitespace and other characters:
fn main() {
let messy_text = " \n\t Hello, World! \n\t ";
// Basic trimming
println!("Trimmed: '{}'", messy_text.trim()); // 'Hello, World!'
println!("Trim start: '{}'", messy_text.trim_start()); // 'Hello, World! \n\t '
println!("Trim end: '{}'", messy_text.trim_end()); // ' \n\t Hello, World!'
// Custom trimming
let text_with_dots = "...Hello, World...";
println!("Trim dots: '{}'", text_with_dots.trim_matches('.')); // 'Hello, World'
let text_with_chars = "abcHello, Worldcba";
println!("Trim abc: '{}'", text_with_chars.trim_matches(&['a', 'b', 'c'][..]));
// 'Hello, World'
// Trim with predicates
let text_with_nums = "123Hello, World456";
println!("Trim digits: '{}'", text_with_nums.trim_matches(|c: char| c.is_numeric()));
// 'Hello, World'
}
String Splitting and Joining
Splitting Strings
Break strings into parts using various delimiters:
fn main() {
let csv_data = "John,25,Engineer,New York";
let text = "The quick brown fox jumps over the lazy dog";
// Basic splitting
let fields: Vec<&str> = csv_data.split(',').collect();
println!("CSV fields: {:?}", fields);
// ["John", "25", "Engineer", "New York"]
// Split whitespace
let words: Vec<&str> = text.split_whitespace().collect();
println!("Words: {:?}", words);
// ["The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]
// Split with limit
let limited: Vec<&str> = csv_data.splitn(3, ',').collect();
println!("Limited split: {:?}", limited);
// ["John", "25", "Engineer,New York"]
// Split by predicate
let numbers_and_letters = "abc123def456ghi";
let parts: Vec<&str> = numbers_and_letters.split(|c: char| c.is_numeric()).collect();
println!("Split by numbers: {:?}", parts);
// ["abc", "", "", "def", "", "", "ghi"]
// Split lines
let multiline = "Line 1\nLine 2\r\nLine 3\n";
let lines: Vec<&str> = multiline.lines().collect();
println!("Lines: {:?}", lines);
// ["Line 1", "Line 2", "Line 3"]
}
Joining Strings
Combine string parts with separators:
fn main() {
let words = vec!["Hello", "beautiful", "world"];
let numbers = vec![1, 2, 3, 4, 5];
// Join with separator
let sentence = words.join(" ");
println!("Joined: {}", sentence); // Hello beautiful world
let csv = words.join(",");
println!("CSV: {}", csv); // Hello,beautiful,world
// Join numbers (convert to strings first)
let number_string = numbers.iter()
.map(|n| n.to_string())
.collect::>()
.join("-");
println!("Numbers: {}", number_string); // 1-2-3-4-5
// Join with custom formatting
let formatted = words.iter()
.enumerate()
.map(|(i, word)| format!("{}. {}", i + 1, word))
.collect::>()
.join("\n");
println!("Formatted:\n{}", formatted);
// 1. Hello
// 2. beautiful
// 3. world
}
String Replacement
Simple Replacement
Replace parts of strings with new content:
fn main() {
let text = "Hello world, wonderful world!";
// Replace first occurrence
let replaced_once = text.replacen("world", "Rust", 1);
println!("Replace once: {}", replaced_once); // Hello Rust, wonderful world!
// Replace all occurrences
let replaced_all = text.replace("world", "Rust");
println!("Replace all: {}", replaced_all); // Hello Rust, wonderful Rust!
// Replace with closure (for more complex logic)
let text_with_numbers = "I have 5 cats and 3 dogs";
let result = text_with_numbers.split_whitespace()
.map(|word| {
if word.chars().all(|c| c.is_numeric()) {
format!("[{}]", word)
} else {
word.to_string()
}
})
.collect::>()
.join(" ");
println!("Numbers in brackets: {}", result);
// I have [5] cats and [3] dogs
}
String Parsing and Conversion
Parsing from Strings
Convert strings to other types:
fn main() {
let number_str = "42";
let float_str = "3.14159";
let bool_str = "true";
// Parse to numbers
match number_str.parse::() {
Ok(num) => println!("Parsed number: {}", num), // 42
Err(e) => println!("Parse error: {}", e),
}
// Parse to float
if let Ok(pi) = float_str.parse::() {
println!("Pi approximately: {:.2}", pi); // 3.14
}
// Parse to boolean
if let Ok(flag) = bool_str.parse::() {
println!("Boolean value: {}", flag); // true
}
// Parse with error handling
fn safe_parse(s: &str) -> Option {
s.parse().ok()
}
println!("Safe parse: {:?}", safe_parse::("123")); // Some(123)
println!("Safe parse: {:?}", safe_parse::("abc")); // None
}
Unicode and Character Handling
Working with Unicode
Handle Unicode characters properly in string operations:
fn main() {
let unicode_text = "Hello, 世界! 🦀 Rust";
// Iterate over characters (Unicode-aware)
println!("Characters:");
for (i, ch) in unicode_text.chars().enumerate() {
println!("{}: {} ({})", i, ch, ch as u32);
}
// Iterate over bytes
println!("\nBytes:");
for (i, byte) in unicode_text.bytes().enumerate() {
println!("{}: {} (0x{:02x})", i, byte, byte);
}
// Character indices (byte positions)
println!("\nCharacter indices:");
for (byte_idx, ch) in unicode_text.char_indices() {
println!("Byte {}: {}", byte_idx, ch);
}
// Safe substring extraction
fn safe_substring(s: &str, start: usize, len: usize) -> Option<&str> {
let start_byte = s.char_indices().nth(start)?.0;
let end_byte = s.char_indices().nth(start + len)
.map(|(idx, _)| idx)
.unwrap_or(s.len());
Some(&s[start_byte..end_byte])
}
if let Some(sub) = safe_substring(unicode_text, 7, 2) {
println!("Substring: '{}'", sub); // '世界'
}
}
String Building and Formatting
Efficient String Building
Build strings efficiently for performance:
fn main() {
// Using String::with_capacity for efficiency
let mut builder = String::with_capacity(100);
for i in 1..=10 {
builder.push_str(&format!("Item {}\n", i));
}
println!("Built string:\n{}", builder);
// Using format! macro
let name = "Alice";
let age = 30;
let formatted = format!("Name: {}, Age: {}", name, age);
println!("{}", formatted);
// Using collect for joining
let items = vec!["apple", "banana", "cherry"];
let list = items.iter()
.map(|item| format!("- {}", item))
.collect::>()
.join("\n");
println!("List:\n{}", list);
}
Common Pitfalls
Mistakes to Avoid
- Confusing bytes and characters:
len()
returns bytes, not character count - Invalid string slicing: Slicing must occur at character boundaries
- Inefficient string building: Use
String::with_capacity()
for large strings - Ignoring Unicode: Use
chars()
instead of bytes for text processing
Checks for Understanding
- What's the difference between
len()
andchars().count()
? - How do you safely extract a substring from a Unicode string?
- When should you use
split()
vssplit_whitespace()
? - How can you replace all digits in a string with asterisks?
Answers
len()
returns byte count;chars().count()
returns Unicode character count- Use
char_indices()
to find byte positions and slice at character boundaries - Use
split()
for specific delimiters;split_whitespace()
handles all whitespace types and consecutive spaces - Use
chars().map(|c| if c.is_numeric() { '*' } else { c }).collect()