Step 4 of 7
Trimming and searching
Input is full of stray spaces, prefixes, brackets and markers. These methods clean it up and find things in it. None of them change the string: they return a new &str (a view of part of the original) or an answer.
fn main() {
println!("{:?}", " padded\t\n".trim());
println!("{:?}", " padded ".trim_start());
println!("{:?}", "007".trim_start_matches('0'));
println!("{:?}", "--name--".trim_matches('-'));
println!("{:?}", "$12.50".strip_prefix('#39;));
println!("{:?}", "12.50".strip_prefix('#39;));
println!("{:?}", "report.pdf".strip_suffix(".pdf"));
println!("{:?} {:?}", "banana".find("an"), "banana".rfind("an"));
println!("{:?}", "banana".find('z'));
println!("{}", "Error: disk".to_lowercase().starts_with("error"));
}
"padded"
"padded "
"7"
"name"
Some("12.50")
None
Some("report")
Some(1) Some(3)
None
true
| Method | Returns |
|---|---|
trim(), trim_start(), trim_end() |
the text without whitespace at both ends / the start / the end |
trim_matches(p), trim_start_matches(p), trim_end_matches(p) |
the text with every repeat of p removed from those ends |
strip_prefix(p), strip_suffix(p) |
Some(rest) with one p removed, or None if it isn't there |
starts_with(p), ends_with(p), contains(p) |
bool |
find(p), rfind(p) |
Option<usize>: the byte position of the first / last match |
The pattern p can be a char, a &str, an array of chars, or a closure: "abc123".trim_end_matches(|c: char| c.is_ascii_digit()) gives "abc".
strip_prefix is the one to reach for when you require a prefix: its None tells you the input was malformed, and it fits let ... else perfectly.
Putting them together: parsing log lines
fn main() {
let lines = [
" [WARN] disk 91% full ",
"[INFO] user ana logged in",
"# a comment",
"[ERROR] payment failed: card declined",
"garbage",
];
for raw in lines {
let line = raw.trim();
if line.starts_with('#') {
continue;
}
let Some(rest) = line.strip_prefix('[') else {
continue;
};
let Some(end) = rest.find(']') else {
continue;
};
let level = &rest[..end];
let message = rest[end + 1..].trim_start();
println!("{level} | {message}");
}
}
WARN | disk 91% full
INFO | user ana logged in
ERROR | payment failed: card declined
Slicing with positions from find is safe: find returns the byte where the match starts, always a char boundary, and ] is one byte long, so end + 1 is a boundary too.
Common mistakes
- Ignoring the result.
input.trim();on its own line does nothing: it returns the trimmed text and you threw it away. The compiler warnsunused return value of ... that must be used. Writelet input = input.trim();. - Treating
find's result as a character count. It's a byte position, which is exactly what slicing needs, but not "the 3rd letter". - Case.
line.contains("error")missesERROR. Lowercase first when case doesn't matter. trim_start_matcheswhere you meantstrip_prefix."$5".trim_start_matches('#39;)removes both dollars;strip_prefixremoves exactly one and tells you whether it was there.
Your task
Write fn parse_price(text: &str) -> Option<u32> that turns a price like "$12.50" into cents (1250). Rules: surrounding spaces are allowed; the price must start with $ (check it with strip_prefix); the dollars must be a whole number; the cents part is optional but, if there's a ., it must be exactly two digits. "$3" is 300, "12.50" (no $) and "$1.5" are None.
Challenge 2
Hashtags arrive in every shape. Write fn clean_tag(raw: &str) -> String that trims whitespace, removes all leading # characters and lowercases the rest: " #RustLang " gives "rustlang", "##Crab" gives "crab".
Challenge 3
Read log lines and report the errors. A line is an error if, after trimming, it starts with [ERROR]. For each error print its message (the text after [ERROR], trimmed), then a final line N errors. Other lines are ignored.
Previous: Splitting: lines, split, splitn, split_once Next: Building strings