We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 5 of 21
Case-insensitive search without allocating
Two small functions over a slice of strings, both case-insensitive, both expected to allocate essentially nothing.
pub fn find_all_ci(haystack: &[String], needle: &str) -> Vec<usize>
pub fn any_contains_ci(haystack: &[String], needle: &str) -> bool
-
find_all_cireturns the indices of the entries that equal the needle, ignoring ASCII case. Order ascending. -
any_contains_cireturns whether any entry contains the needle as a substring, ignoring ASCII case. The empty string is a substring of every string, so["ab"]with needle""istrue. An empty haystack with a non-empty needle isfalse— there is nothing there to contain it.
The hidden case runs both over a 200 000-string haystack and asserts
find_all_ci performs at most 2 heap allocations and any_contains_ci
performs zero.
The whole lesson in one number
The obvious way to compare two strings without caring about case is to
lowercase both and use ==. It is correct. It also allocates a fresh
String for every comparison, and to_lowercase has to walk the input
applying Unicode case-folding tables.
Measured over the same workload:
s.to_lowercase() == n.to_lowercase() 2.4550 ms
s.eq_ignore_ascii_case(n) 0.0755 ms <-- 32.5x
and the fast one allocates nothing, because it compares byte by byte in
place. That is the same shape as the needless-clone lesson: the cost is not
the comparison, it is the temporary you built in order to do the comparison.
Separately, for substring search: s.chars().collect::<String>().contains(n)
takes 3.49 ms where s.contains(n) takes 0.331 ms — 10.6×. Collecting a
string into a string to search it is a real thing people write.
But read the small print, because 32× is not free
eq_ignore_ascii_case does exactly what it says: it folds A–Z against
a–z and nothing else. Ä and ä are different strings to it. İ is
different from i̇. If your data is international text and you use it for a
user-facing “same name?” check, you have not optimised anything — you have
introduced a correctness bug that will be reported as “search doesn’t work in
Turkish”.
So the rule is not “always use the ASCII one”. The rule is: know which
question you are asking. Protocol tokens, file extensions, HTTP header
names, hex digits, identifiers in an ASCII grammar — ASCII folding is not
just faster there, it is more correct, because Content-Length should
match content-length and should not match some Unicode homoglyph.
One more trap worth carrying with you: to_lowercase() can change the number
of characters. '\u{130}' (Latin capital I with dot above) lowercases to
two chars. So byte offsets computed on a lowercased copy do not map back
onto the original — which is a second, quieter reason not to lowercase a
string just to search it.
::: question One of the test cases is ["straße", "STRASSE"] searched for "STRASSE". Why is the answer [1] and not [0, 1]?
Because ß uppercases to SS under full Unicode case folding, so a truly
locale-aware comparison would call those equal — but
eq_ignore_ascii_case only maps A–Z to a–z, and ß is not in that range.
The lengths do not even match (7 bytes versus 7… but 6 chars versus 7), so
the byte comparison fails immediately.
The test exists so that the semantics you are implementing are written down rather than assumed. “Case-insensitive” is not one operation; it is at least three, and the standard library makes you pick. :::
What clippy will and will not do for you
clippy has manual_ignore_case_cmp in the default set, and it is one of the
few default-on lints that teaches exactly the right thing:
error: manual case-insensitive ASCII comparison
help: consider using `.eq_ignore_ascii_case()` instead
Be precise about when it fires, though — verified against clippy 0.1.95:
a.to_ascii_lowercase() == b.to_ascii_lowercase() // lint fires
a.to_lowercase() == b.to_lowercase() // lint stays SILENT
It only recognises the ASCII spelling, because only there can it prove the rewrite preserves meaning. The Unicode spelling — the one most beginners actually write, the one that costs 32× — sails straight through the gate. That is why this problem measures allocations instead of trusting the lint.
Building the substring search
str::contains is case-sensitive, and there is no
contains_ignore_ascii_case in std. You need one comparison primitive that
does exist: <[u8]>::eq_ignore_ascii_case, which compares two byte slices
with ASCII folding and no allocation. Combine it with windows(n) over the
haystack’s bytes and the whole thing is three lines and zero allocations.
let n = needle.as_bytes();
s.as_bytes().windows(n.len()).any(|w| w.eq_ignore_ascii_case(n))
Two things to be careful about: windows(0) panics, so guard the empty
needle before you get there; and windows is a slice method, not an iterator
adapter — you will meet that boundary properly later in this track.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.