How to check whether a string contains a substring
Short answer: str::contains answers whether a substring appears anywhere in a string; starts_with and ends_with check the two ends. All three borrow the pattern instead of allocating, and all three are case-sensitive, so a case-insensitive search folds both sides to the same case first. Unicode case folding is not clean in either direction, which is the one place this gets subtle.
From a fresh project — cargo new contains-demo — replace src/main.rs with each block below and run it with cargo run. The project name only shows up in cargo’s own output; nothing else on this page depends on it.
The three questions
Section titled “The three questions”fn main() { let text = "Ada Lovelace"; println!("{}", text.contains("Grace")); println!("{}", text.starts_with("Ada")); println!("{}", text.starts_with("Lovelace")); println!("{}", text.ends_with("ace"));}falsetruefalsetruecontains scans the whole string; the other two look at one end. Each returns bool and borrows, so nothing is allocated and the haystack is untouched. If you also need where the match is, str::find hands back an Option<usize>: Some(4) for where "Lovelace" starts in the text above, None when the substring is absent.
What you can search for
Section titled “What you can search for”The second argument is anything implementing Pattern, which covers the &str you expected plus four more spellings:
fn main() { let text = "Ada 1843"; println!("{}", text.contains('1')); println!("{}", text.contains(['1', '9'])); println!("{}", "Ada".contains(['d', 'A'])); println!("{}", text.contains(['x', 'y'])); println!("{}", text.contains(|c: char| c.is_ascii_digit())); let needle = String::from("1843"); println!("{}", text.contains(&needle));}truetruetruefalsetruetrueAn array of chars means “any of these characters”, not that sequence: "Ada".contains(['d', 'A']) is true even though the string contains no dA anywhere, because both letters appear. The closure runs per character, so it cannot match a chunk longer than one. A String needle needs the &, because the trait is implemented for the borrowed forms:
fn main() { let text = "Ada Lovelace"; let needle = String::from("Lovelace"); println!("{}", text.contains(needle));}error[E0277]: the trait bound `String: Pattern` is not satisfied --> src/main.rs:4:34 |4 | println!("{}", text.contains(needle)); | -------- ^^^^^^ the trait `Pattern` is not implemented for `String` | | | required by a bound introduced by this call | = note: required for `String` to implement `Pattern`note: required by a bound in `core::str::<impl str>::contains` --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/core/src/str/mod.rs:1383:4help: consider borrowing here |4 | println!("{}", text.contains(&needle)); | +
For more information about this error, try `rustc --explain E0277`.To ask about several substrings at once, iterate: ["Grace", "Lovelace"].iter().any(|p| text.contains(p)).
One trap sits in the pattern itself. An empty needle is contained in everything, so "anything".contains("") is true, and so are starts_with("") and ends_with(""). When the needle came from a split or from user input, check needle.is_empty() before believing a true.
The case-insensitive search
Section titled “The case-insensitive search”std has no contains_ignore_case, and the compiler is blunt about it:
fn main() { let text = "Ada Lovelace"; println!("{}", text.contains_ignore_case("lovelace"));}error[E0599]: no method named `contains_ignore_case` found for reference `&str` in the current scope --> src/main.rs:3:25 |3 | println!("{}", text.contains_ignore_case("lovelace")); | ^^^^^^^^^^^^^^^^^^^^ method not found in `&str`
For more information about this error, try `rustc --explain E0599`.Fold both sides to the same case and search again:
fn main() { let text = "Ada Lovelace"; println!("{}", text.contains("lovelace")); println!("{}", text.to_lowercase().contains("lovelace")); println!("{}", "ADA LOVELACE".to_ascii_lowercase().contains("lovelace")); println!("{}", "ADA".eq_ignore_ascii_case("ada"));}falsetruetruetrueto_lowercase allocates a String for the haystack, so a search inside a loop over many lines pays that per line; to_ascii_lowercase is the cheaper ASCII-only spelling. eq_ignore_ascii_case compares two whole strings without allocating, but it cannot answer “contains”.
The two cases that do not fold cleanly
Section titled “The two cases that do not fold cleanly”Unicode case mapping is not a pair of clean inverses, and both directions lose something:
fn main() { println!("{}", "Straße".to_lowercase().contains("strasse")); println!("{}", "Straße".to_uppercase().contains("STRASSE")); println!("{:?}", "İstanbul".to_lowercase()); println!("{}", "İstanbul".to_lowercase().contains("istanbul")); println!("{}", "İstanbul".to_uppercase().contains(&"istanbul".to_uppercase()));}falsetrue"i\u{307}stanbul"falsefalseß uppercases to SS, so uppercasing both sides matches Straße against STRASSE where lowercasing both sides misses it. İ goes the other way: its lowercase is an i with a combining dot above, which is why the {:?} line shows the escape rather than a bare letter, and uppercasing does not rescue it either, because İ stays İ while i becomes I. Checking both folds covers ß; nothing in std covers İ. For text that mixes them, this is the point to stop folding by hand and use a crate that implements real case folding.
Related
Section titled “Related”- How to trim whitespace and strip characters — the other end of the same job, and the
\rthat survives a split - How to match and replace text with regex — when the thing you are looking for stops being a fixed string
- How to split a string — cutting where it matches, instead of asking whether it does
- Working with text — the rest of the chapter