#218 Jun 22, 2026

218. str::match_indices — Find Every Match and Its Position in One Pass

Hand-rolling a find loop to locate every occurrence of a substring means juggling a running offset and remembering to skip past each match. match_indices hands you each hit and its byte position as an iterator — no bookkeeping.

The classic way to collect every position of a needle is a loop over find, slicing the remainder each time and adding the offset back by hand:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let log = "ERROR x ERROR y WARN z ERROR";

let mut positions = Vec::new();
let mut start = 0;
while let Some(i) = log[start..].find("ERROR") {
    let idx = start + i;
    positions.push(idx);
    start = idx + "ERROR".len(); // easy to get this advance wrong
}

assert_eq!(positions, vec![0, 8, 23]);

It works, but the offset arithmetic is exactly the kind of thing you fumble at 5pm. match_indices walks the string for you and yields (byte_index, matched_str) pairs:

1
2
3
4
5
let log = "ERROR x ERROR y WARN z ERROR";

let positions: Vec<(usize, &str)> = log.match_indices("ERROR").collect();

assert_eq!(positions, vec![(0, "ERROR"), (8, "ERROR"), (23, "ERROR")]);

It’s lazy and composes like any other iterator, so you can map down to just the indices, or count without allocating at all:

1
2
3
4
let log = "ERROR x ERROR y WARN z ERROR";

let count = log.matches("ERROR").count();
assert_eq!(count, 3);

The pattern argument is the full Pattern family — a &str, a char, a closure, or a slice of chars — so you can find runs of a class of characters without a regex:

1
2
3
4
let s = "a1b22c333";

let digits: Vec<(usize, &str)> = s.match_indices(char::is_numeric).collect();
assert_eq!(digits, vec![(1, "1"), (3, "2"), (4, "2"), (6, "3"), (7, "3"), (8, "3")]);

Matches are non-overlapping and scanned left to right; when you need the last hit first, rmatch_indices walks right to left:

1
2
3
4
let s = "aXbXc";

let last = s.rmatch_indices('X').next();
assert_eq!(last, Some((3, "X")));

The byte indices are real &str offsets, so they slot straight into slicing and replacement logic without any conversion. When you only care about whether something matches, reach for contains; when you want each location, match_indices already did the bookkeeping.

← Previous 217. eq_ignore_ascii_case — Case-Insensitive Compare Without the Allocation