We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 5 of 21
ASCII search without lowercase copies
Search strings while ignoring the case of ASCII letters. Implement two functions with different matching rules:
pub fn find_all_ci(haystack: &[String], needle: &str) -> Vec<usize>
pub fn any_contains_ci(haystack: &[String], needle: &str) -> bool
find_all_ci returns the indices of entries equal to needle, ignoring
ASCII case. Preserve ascending index order and include every matching entry.
any_contains_ci returns whether at least one entry contains needle as a
substring under the same comparison rule.
| Haystack | Needle | Indices | Any substring match? |
|---|---|---|---|
["Alpha", "ALPHA", "xxalphaxx"] |
"alpha" |
[0, 1] |
true |
["xxalphaxx"] |
"alpha" |
[] |
true |
["ab", ""] |
"" |
[1] |
true |
[] |
"" |
[] |
false |
["Ä"] |
"ä" |
[] |
false |
An empty needle occurs in every string, including an empty string. An empty
haystack contains no strings, so any_contains_ci returns false for every
needle. Neither function should change its inputs.
Compare without making lowercase copies
Use str::eq_ignore_ascii_case for equality. It compares without allocating
a lowercase String. Only ASCII letters A through Z are matched to their
lowercase equivalents; all other bytes must match exactly.
The starter's to_lowercase() comparisons have two problems: they create
temporary strings, and they use different semantics. For example, Unicode
lowercasing makes "Ä" and "ä" equal, while this exercise requires them to
remain different. Replacing those calls with to_ascii_lowercase() fixes
that semantic difference but still creates unnecessary copies.
ASCII matching is useful when a format defines ASCII names or tokens. It
is not a general solution for comparing human names or international text.
Unicode lowercasing, Unicode case folding and locale-sensitive matching are
different operations. Rust's to_lowercase() performs lowercasing, not full
case folding or locale-specific comparison.
Check your understanding Why does searching ["straße", "STRASSE"] for "STRASSE" return only index 1? Show answer Hide answer
ASCII matching leaves ß unchanged, so straße does not equal STRASSE.
Both strings occupy seven UTF-8 bytes, but their bytes differ even after
folding ASCII letters. Full Unicode case folding can map ß to ss; that
is outside this exercise's contract.
Lowercasing can also change a string's length: İ lowercases to i followed
by a combining dot. Offsets in a transformed copy therefore need not identify
the same positions in the original text.
Scan byte windows for substring matches
str::contains is case-sensitive. For this exercise, compare each byte window
of the needle's length using the byte-slice eq_ignore_ascii_case method.
Handle an empty needle first because windows(0) panics.
Here is a complete helper for a single string:
fn contains_ascii_case(text: &str, needle: &str) -> bool {
let needle = needle.as_bytes();
needle.is_empty()
|| text.as_bytes().windows(needle.len())
.any(|window| window.eq_ignore_ascii_case(needle))
}
fn main() {
assert!(contains_ascii_case("aBc", "BC"));
assert!(contains_ascii_case("", ""));
assert!(!contains_ascii_case("Ä", "ä"));
}
Apply the comparison to haystack entries with iter().any(...). The single
string's empty-needle rule then naturally gives false for an empty haystack.
A needle longer than an entry has no matching window.
Byte slices can start inside a UTF-8 character without panicking. This helper
never constructs a str from those slices. A valid UTF-8 needle cannot match
a window starting at a continuation byte, and ASCII folding does not change
non-ASCII bytes. It therefore preserves the required substring semantics.
This direct scan can compare up to the needle's length at each candidate position. Avoiding allocations does not make it a linear-time search algorithm for every input or guarantee a particular speedup.
The allocation budget
Do not allocate transformed copies of the strings. find_all_ci may allocate
storage for its returned indices; any_contains_ci needs no heap allocation.
One generated fixture has 200,000 strings and exactly three equality matches.
Around the two function calls, the harness counts allocator calls, including
reallocations. It permits at most two for find_all_ci and zero for
any_contains_ci. Building the input and serializing the result happen outside
these measurements. This is a budget for that workload, not a universal bound
on growing a result vector containing arbitrarily many matches.
Clippy's manual_ignore_case_cmp can suggest eq_ignore_ascii_case for
comparisons built from ASCII case-conversion methods. Unicode lowercasing has
different semantics, so do not rely on that lint to catch the starter's
allocation problem. Functional tests and allocation checks serve different
purposes here.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.