Add the methods range_by and range_by_mut to BTreeMap, and
add the method range_by to BTreeSet.
Motivation
BTreeMap and BTreeSet have a handy range function, which provide
iterators over a range of elements. They are efficient, requiring only O(log n)
time, meaning that we can quickly iterate over a small range even when the map
or set is very large. The range functions are useful for things other than
iteration; for example, btree_set.range(x..).next() is the fastest way to
find the first element of btree_set that is larger than or equal to x.
One annoyance of BTreeSet::range is that in order to call it you need to be
able to produce values of some type Q where T: Borrow<Q> (here T is the
element type of the BTreeSet). There are situations where you want to search
in a set, but it is difficult or impossible to produce such values. Here are
two examples:
- your set is of type
BTreeSet<(i32, LargeType)>, you have access toa(of typei32) and&b(of type&LargeType). You want to find the first element of your set that is larger than(a, b). IfLargeType: Clone, this can be done withset.range(&(a, b.clone()))but might be expensive. - your set is of type
BTreeSet<(i32, SomeWeirdType)>and you want to iterate over all pairs whose first coordinate is at least5. If you could construct a valuebthat is smaller than every other value of typeSomeWeirdType, you could do this withset.range(&(5, b)). But constructing such abmight be annoying, particularly ifSomeWeirdTypeis a type parameter.
This RFC proposes adding a method
to BTreeSet<T>, along with similar methods for BTreeMap. By passing a
callback function instead of a value of the appropriate type, we avoid the need
to construct problematic objects.
Similar methods already exist on the slice type. For example, [T] has
binary_search (taking an element) and binary_search_by (taking a callback).
RFC 2351 proposes adding both is_sorted (taking no parameters) and
is_sorted_by (taking a comparison callback) to [T].
Guide-level explanation
Possible documentation of the new method on BTreeSet:
Constructs a double-ended iterator over a sub-range of elements in the set, namely those elements for which
freturnsOrdering::Equal. The functionfis required to be a non-decreasing function; for example, iff(x)returnsOrdering::Equalandy >= xthenf(y)must returnOrdering::EqualorOrdering::Greater.Panics
May panic if
ffails to be non-decreasing. The exact circumstances under which a panic occurs are implementation-dependent.Example
use BTreeSet; use *; let mut set = new; set.insert; set.insert; set.insert; // See if there's an element whose first coordinate is 5. println!; // Iterate over all elements whose first coordinate is between 4 and 7 inclusive. let f = ; for pair in set.range_by
Possible documentation of the new methods on BTreeMap:
Constructs a double-ended iterator over a sub-range of elements in the map, namely those elements for which
freturnsOrdering::Equalwhen applied to the key. The functionfis required to be a non-decreasing function; for example, iff(x)returnsOrdering::Equalandy >= xthenf(y)must returnOrdering::EqualorOrdering::Greater.Panics
May panic if
ffails to be non-decreasing. The exact circumstances under which a panic occurs are implementation-dependent.Examples
use BTreeMap; use *; let mut map = new; map.insert; map.insert; map.insert; // See if there's an element whose key is at least 5. // The call to `range_by` here is just a more complicated way of writing // `map.range(5..)`. println!; // Iterate over all items for which the square of the key is between 20 and // 30 (inclusive). let f = ; for in map.range_byConstructs a mutable double-ended iterator over a sub-range of elements in the map, namely those elements for which
freturnsOrdering::Equalwhen applied to the key. The functionfis required to be a non-decreasing function; for example, iff(x)returnsOrdering::Equalandy >= xthenf(y)must returnOrdering::EqualorOrdering::Greater.Panics
May panic if
ffails to be non-decreasing. The exact circumstances under which a panic occurs are implementation-dependent.Examples
use BTreeMap; use *; let mut map = new; map.insert; map.insert; map.insert; let f = ; // This will print all the values for with the first element of the key is // between 5 and 8 inclusive. for in map.range_by
Reference-level explanation
This RFC proposes to add the following three methods:
Each of these methods requires the function f to be non-decreasing. The
semantics are that the returned iterator iterates over all elements for which
f returns Ordering::Equal. The performance is the same as that of the
range method. That is, O(log n) to construct the iterator, and then amortized
O(1) for each iterate.
If the desired range is empty (that is, if f does not return
Ordering::Equal for andy element of the set or map), the returned iterator
will be an empty iterator. If the function f fails to be non-decreasing then
the methods are allowed to panic, and they are also allowed to return valid
iterators over some arbitrary sub-range of the map or set.
The implementation of these methods is expected to be a simple modification of
the implementation of range, in which comparisons against the range bounds
are replaced by calls to the comparison function. If it doesn't result in a
performance penalty, range would probably be reimplemented in terms of range_by.
Drawbacks
It increases the size of the standard library.
Rationale and alternatives
There are other possibilities for the interface of these functions. For
example, instead of passing a single function it would be possible to pass two
functions returning bools: the first would be a non-decreasing function and the
second would be a non-increasing function, and the range being iterated over
would be those values for which both functions returned true. That is, the
signature of range_by for BTreeSet would become
This formulation (and its requirements on the functions) is a bit more
complicated, but it would be more convenient to use in certain circumstances.
For example, set.range(a..b) could be expressed as set.range_by(|x| x >= a, |y| y < b). On the other hand, expressing set.range(a..b) in terms of the
main proposal requires some clunky if/then or match expressions.
Another possibility would be to add a function range_by_key, either instead of
or in addition to range_by. The signature would be something like
and the semantics would be that the returned iterator iterates over all
elements x for which f(x) belongs to range. (f would need to be a
non-decreasing function.) I think this function would fit all the use cases of
the motivation, and the binary_search_by_key function on slices provides
some precedent. The large number of generic parameters is a slight turn-off, though.
Unresolved questions
None so far.