๐Ÿฆ€ Functional Rust

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: This is like Python's `re.search()` for simple cases, but without the overhead of compiling a regex. `find()` returns a byte position (`usize`). Use it for slicing. `rfind()` searches from the right โ€” useful for finding file extensions, last path component, etc.

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

Key Differences

ConceptOCamlRust
Substring checkCustom `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 withManual slice check`s.ends_with(pat)`
Pattern typesChar only`char`, `&str`, `&[char]`, closure
Not found value`None` (via `_opt`)`None` โ€” `Option<usize>`
All occurrencesManual 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->())