Feature #19075
closedBinary searching for the last element
Description
My latest proposal is https://bugs.ruby-lang.org/issues/19075#note-6.
I will leave the initial proposal below.
PR: https://github.com/ruby/ruby/pull/6611
(I'm going to talk about Array
here, but the same argument can be made for Range
. If Array#bsearch_last
is acceptable, I will work also for Range
.)
Ruby's bsearch returns the first element which satisfies the given block.
# Search the first element greater than 18
array = [10, 15, 20, 25]
array.bsearch { |x| x > 18 } # => 20
If we want the last element, we need to invert the condition and step backward.
# Search the last element less than 18
array = [10, 15, 20, 25]
index = array.bsearch_index { |x| !(x < 18) }
array[index-1] # => 15
Of course, we need to consider nil
and the boundary.
# Search the last element less than 100
index = array.bsearch_index { |x| !(x < 100) } # => nil
if index.nil?
array.last # => 25
else
array[index-1]
end
# Search the last element less than 0
index = array.bsearch_index { |x| !(x < 0) } # => 0
if index.nil?
array.last
elsif index == 0
nil
else
array[index-1]
end
This is where mistakes can easily be made, so I propose Array#bsearch_last
and Array#bsearch_last_index
.
Array#bsearch_last
returns the last element which satisfies the given block.
Array#bsearch
requires that all false-evaluating elements precede all true-evaluating elements. As is clear from the meaning of the method, conversely to bsearch
, bsearch_last
requires that all true-evaluating elements precede all false-evaluating elements. (If bsearch_last
is acceptable, the name "find-minimum mode" should be changed.)
array = [10, 15, 20, 25]
array.bsearch_last { |x| x < 18 } # => 15
array.bsearch_last { |x| x < 100 } # => 25
array.bsearch_last { |x| x < 0 } # => nil
There are several possible options for find-any mode.
(1) bsearch_last
does not support find-any mode.
A block for bsearch_last
must return true
, false
or nil
.
[1, 2, 3].bsearch_last { 0 } # => TypeError
My pull request tentatively includes this implementation.
(2) bsearch_last
supports find-any mode and it behaves like bsearch
.
bsearch
with find-any mode returns an element, for which the block returns zero.
If multiple elements satisfy the condition, it is not determined which of them will be returned.
It is conceivable that bsearch_last
behaves in the same way as bsearch
.
# current behavior
# It is not specified whether `:b`, `:c`, or `:d` is returned.
[[1,:a], [2, :b], [2, :c], [2, :d], [3, :e]].bsearch { |a, b| 2 <=> a } # => [2, :c]
(3) bsearch_last
supports find-any mode and returns the last element. Make bsearch
return the first element.
Change the behavior of bsearch
to return the first element for which the block returns zero.
bsearch_last
returns the last element for which the block returns zero.
# Change it like this:
[[1,:a], [2, :b], [2, :c], [2, :d], [3, :e]].bsearch { |a, b| 2 <=> a } # => [2, :b]
[[1,:a], [2, :b], [2, :c], [2, :d], [3, :e]].bsearch_last { |a, b| 2 <=> a } # => [2, :d]
(If this option is adopted, the name "find-any mode" should be renamed.)