RustArena

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

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