478: contains(), find(), starts_with()
Difficulty: 1 Level: Beginner Search strings for substrings, chars, or patterns โ with flexible pattern matching.The Problem This Solves
Python has `in`, `.find()`, `.index()`, `.startswith()`, `.endswith()`. JavaScript has `.includes()`, `.indexOf()`, `.startsWith()`, `.endsWith()`. These work on literal strings only. If you want to search by character category โ "find the first digit" โ you need a regex or manual loop. Rust's string search methods accept patterns โ not just string literals, but also single chars, slices of chars, and closures. `s.find(|c: char| c.is_ascii_digit())` finds the first digit without importing any regex crate. The same API works for `contains`, `starts_with`, `ends_with`, `find`, `rfind`, `split`, `trim`, and more. And unlike Python's `.find()` which returns `-1` on failure (a classic footgun), Rust's `.find()` returns `Option<usize>` โ `None` when not found. You're forced to handle the "not found" case. The byte position you get back is also valid for slicing, since `.find()` always returns a char boundary.The Intuition
The pattern system is Rust's key insight here. Anything that can match a char or substring is a pattern:- `"World"` โ substring match
- `'!'` โ single char match
- `|c: char| c.is_uppercase()` โ closure match
- `['a', 'e', 'i', 'o', 'u'].as_ref()` โ any-of match
How It Works in Rust
let s = "Hello, World! Hello, Rust!";
// Boolean checks โ O(n) scan
s.contains("World") // true
s.starts_with("Hello") // true
s.ends_with("Rust!") // true
// find โ returns byte position of FIRST match
s.find("World") // Some(7)
s.find(',') // Some(5)
s.find('z') // None
// rfind โ returns byte position of LAST match
s.rfind("Hello") // Some(14)
// Pattern as closure โ "first digit"
let t = "abc123";
t.find(|c: char| c.is_ascii_digit()) // Some(3)
// match_indices โ iterator over all (position, match) pairs
for (pos, sub) in s.match_indices("Hello") {
println!("'{}' at byte {}", sub, pos);
}
// 'Hello' at byte 0
// 'Hello' at byte 14
// matches โ count occurrences
s.matches("Hello").count() // 2
// Pattern with char slice โ find first vowel
"Hello".find(['a', 'e', 'i', 'o', 'u'].as_ref()) // Some(1) โ 'e'
What This Unlocks
- Input validation โ check for forbidden characters using closure patterns without a regex crate.
- Parsing structured text โ `rfind('.')` to locate file extension, `find(':')` to split host:port.
- Text analysis โ count occurrences with `.matches(pat).count()`, locate all positions with `.match_indices()`.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Substring check | Custom `contains` function | `s.contains(pat)` |
| Find position | `String.index_opt s c` (char only) | `s.find(pat)` โ `Option<usize>` |
| Find from right | `String.rindex_opt s c` | `s.rfind(pat)` |
| Starts with | `String.sub s 0 n = prefix` | `s.starts_with(pat)` |
| Ends with | Manual slice check | `s.ends_with(pat)` |
| Pattern types | Char only | `char`, `&str`, `&[char]`, closure |
| Not found value | `None` (via `_opt`) | `None` โ `Option<usize>` |
| All occurrences | Manual loop | `.match_indices(pat)` iterator |
// 478. contains(), find(), starts_with()
fn main() {
let s = "Hello, World! Hello, Rust!";
println!("contains World: {}", s.contains("World"));
println!("starts_with Hello: {}", s.starts_with("Hello"));
println!("ends_with Rust!: {}", s.ends_with("Rust!"));
println!("find World: {:?}", s.find("World"));
println!("rfind Hello: {:?}", s.rfind("Hello"));
println!("find ',': {:?}", s.find(','));
// Pattern closure โ first digit
let t = "abc123"; println!("first digit: {:?}", t.find(|c:char| c.is_ascii_digit()));
// match_indices โ all occurrences
for (pos, sub) in s.match_indices("Hello") { println!(" '{}' at {}", sub, pos); }
println!("count: {}", s.matches("Hello").count());
}
#[cfg(test)]
mod tests {
#[test] fn test_contains() { assert!("hello world".contains("world")); assert!(!"hello".contains("xyz")); }
#[test] fn test_starts() { assert!("hello".starts_with("hel")); assert!(!"hello".ends_with("hel")); }
#[test] fn test_find() { assert_eq!("hello".find('l'),Some(2)); assert_eq!("hello".find('z'),None); }
#[test] fn test_rfind() { assert_eq!("hello".rfind('l'),Some(3)); }
#[test] fn test_matches() { assert_eq!("aaabaa".matches('a').count(),5); }
}
(* 478. String searching โ OCaml *)
let contains s sub =
let ls=String.length s and lp=String.length sub in
if lp=0 then true
else let found=ref false in
for i=0 to ls-lp do
if String.sub s i lp = sub then found:=true done;
!found
let () =
let s = "Hello, World! Hello, OCaml!" in
Printf.printf "contains World: %b\n" (contains s "World");
Printf.printf "starts Hello: %b\n" (String.length s >= 5 && String.sub s 0 5 = "Hello");
Printf.printf "ends !: %b\n" (s.[String.length s - 1] = '!');
(match String.index_opt s ',' with
| Some i -> Printf.printf "comma at %d\n" i | None->())