We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Closures and Iterators step 18 of 28
any, all, find, position: audit a list of records
Answer four questions about a list of "key=value" records. Each question
should be answered by one call to an iterator method that stops as soon as it
knows the answer.
pub fn audit(records: Vec<String>) -> (bool, bool, Option<usize>, Option<String>)
Splitting a record
Split each record at its first =. The text before it is the key and
everything after it is the value, so "a=b=c" has key "a" and value
"b=c". A record with no = at all uses the whole string as its key and has
an empty value. Records can be any strings, including empty strings and
non-ASCII text, and the list can be empty.
The four answers
| Element | Question |
|---|---|
.0 |
Does any record have an empty value (zero characters)? |
.1 |
Do all keys avoid the ASCII uppercase letters A to Z? Other characters, including non-ASCII uppercase letters such as É, do not count. Values are ignored. |
.2 |
The zero-based index of the first record whose key is exactly equal (case-sensitive, byte for byte) to the key of an earlier record, or None. |
.3 |
The first value, in record order, that str::parse::<i64> accepts and that is below zero, returned as the original value string, or None. |
str::parse::<i64> accepts an optional leading + or - followed by one or
more ASCII digits, and nothing else: no spaces, no underscores, and nothing
outside the i64 range. So "-007" counts and is returned as "-007",
while "- 7", "-1.5" and "-9223372036854775809" do not count. "-0"
parses as 0, which is not below zero.
[] -> (false, true, None, None)
["a=1", "b=2", "a=3"] -> (false, true, Some(2), None)
["a=5", "b=-7", "c=-9"] -> (false, true, None, Some("-7"))
["a=1", "B=2"] -> (false, false, None, None)
["noequals"] -> (true, true, None, None)
In the second example, record 2 has key "a", which record 0 already used.
In the third, "-7" is the first negative value, so "-9" is never
examined. In the fourth, key "B" contains an uppercase letter. In the last,
the record has no =, so its value is empty.
The test harness turns your tuple into a JSON object with the fields
any_empty, all_lower, first_dup and first_negative, using null for
None.
Empty input
Look at the first example. On an empty input:
any()returnsfalse. No element satisfies the predicate, because there are no elements.all()returnstrue. There is no element that fails the predicate, so the claim "every element passes" holds. Logicians call this vacuously true.
This is the standard logic for "there exists" and "for all" over an empty
collection, and it is not specific to Rust: Python's any([]) and
all([]) give the same answers. It can still surprise a caller. "All users
are verified" is true when there are no users, which may not be what the
code that relies on it intended. When that case matters, check for emptiness
separately.
Five short-circuiting methods
A method short-circuits when it stops pulling items from the iterator as
soon as the answer is known. any stops at the first true, all at the
first false, and find, position and find_map at the first match.
Their signatures in std::iter::Iterator, with the where Self: Sized
bounds left out, are:
fn any<F: FnMut(Self::Item) -> bool>(&mut self, f: F) -> bool
fn all<F: FnMut(Self::Item) -> bool>(&mut self, f: F) -> bool
fn find<P: FnMut(&Self::Item) -> bool>(&mut self, predicate: P) -> Option<Self::Item>
fn position<P: FnMut(Self::Item) -> bool>(&mut self, predicate: P) -> Option<usize>
fn find_map<B, F: FnMut(Self::Item) -> Option<B>>(&mut self, f: F) -> Option<B>
Two details in those signatures matter.
They take &mut self, not self. The iterator is borrowed, not used up,
so after a search you can keep reading from where it stopped. This also means
a second position call on the same iterator counts from the item after the
first match, not from the original start:
let mut it = [1, 0, 2, 0].into_iter();
assert_eq!(it.position(|x| x == 0), Some(1));
assert_eq!(it.position(|x| x == 0), Some(1)); // index 3 overall
To get absolute indices from repeated searches, add the offsets yourself, or
use enumerate() so each item carries its original index.
find's predicate takes &Self::Item; any, all and position take
Self::Item. find must return the matching item, so it only lends each
item to the predicate and keeps ownership. filter takes &Self::Item for
the same reason. In this exercise parts.iter() yields &(&str, &str), so a
find predicate receives &&(&str, &str). A tuple pattern such as
|(_, v)| still works there, because patterns can match through references.
find_map is the short-circuiting relative of filter_map: it returns the
first Some the closure produces and stops.
The stepper below shows the same stopping rule on the chain
.map(|x| x * 10).filter(|x| x % 20 == 0).take(2) over 1..=6. Each step
moves one item through every stage before the next item is pulled. Once
take(2) has yielded two items it stops asking for more, so the remaining
source items never reach map. any, all and the find family stop in
the same way when they have their answer.
Loading visualization…
The starter passes the tests but fails the Clippy gate
The starter answers three of the questions with filter(..).next(), which
Clippy's filter_next lint reports. That lint is on by default, and the grader
runs Clippy with -D warnings, which turns every warning into an error:
error: called `filter(..).next()` on an `Iterator`
help: use `.find(..)` instead
Rewriting filter(p).next().is_some() as find(p).is_some() is only half the
fix. That expression asks "is there a match?", which is what any(p) asks
directly. Clippy's search_is_some lint suggests any for
find(..).is_some() and !any(..) for find(..).is_none(), but it is not on
by default. The starter turns it on with a crate-level attribute:
#![deny(clippy::search_is_some)]
Keep that line. An inner attribute (#![...]) that applies to the whole file
must come before every item such as use or fn; comments above it are
fine. With it in place, find(..).is_some() is reported as:
error: called `is_some()` after searching an `Iterator` with `find`
For the .1 question, negating any (is there any key with an uppercase
letter?) also gives the right answer, but all states the question as asked.
Finding the first duplicate
position with a HashSet answers .2 in one call. HashSet::insert
returns false when the value was already present, so !seen.insert(k)
means "this key is a repeat", and position returns the index of the first
record for which that is true.
A predicate that changes outside state deserves a second look, but this one
is correct: position calls it on the items in iterator order, once each,
and stops at the first true. When it is called for record i, the set
holds exactly the keys of records 0 to i - 1.
The grade is: the code compiles, every test passes, and clippy -D warnings
reports nothing.
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.