jdantonio / functional-ruby

A gem for adding functional programming tools to Ruby. Inspired by Erlang, Clojure, Haskell, and Functional Java.

Home Page:http://www.functional-ruby.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

using built-in Array#bsearch for collection bisect_left; bisect_right

404pnf opened this issue · comments

commented

please ignore the following function definiton. They are wrong!

Array#bsearch return nil in find minimum mode. And one has to compare x with a[0] and a[-1] to decide what to return as the result. Not worth it.

The original solution is correct and simpler.

def my_bisect_left(a, x, opts={})
  return nil if a.nil?
  return 0 if a.empty?

  lo = (opts[:lo] || opts[:low]).to_i
  hi = opts[:hi] || opts[:high] || a.length

  idx = if block_given?
          a[lo..hi].bsearch { |e| yield(x) <= yield(e) }
        else
          a[lo..hi].bsearch { |e| x <= e }
        end
  # if non is found, bsearch return nil
  if idx
    idx - 1
  else
    0 # insert at the beginning since all elements are bigger than us
  end

end
# ==> nil

my_bisect_left (1..10000000).to_a, 56789
# ==> 56788

Benchmark.bmbm do |x|
  x.report('orig') { bisect_left (1..10000000).to_a, 56789 }
  x.report('new') { my_bisect_left (1..10000000).to_a, 56789 }
end
# =Rehearsal ----------------------------------------
# =orig   0.450000   0.030000   0.480000 (  0.488831)
# =new    0.460000   0.030000   0.490000 (  0.492856)
# =------------------------------- total: 0.970000sec
# =           user     system      total        real
# =orig   0.460000   0.030000   0.490000 (  0.488632)
# =new    0.460000   0.030000   0.490000 (  0.493830)
# ==> [#<Benchmark::Tms:0x007f8c1a80f428 @label="orig", @real=0.488632, @cstime=0.0, @cutime=0.0, @stime=0.03, @utime=0.4600000000000002, @total=0.4900000000000002>, #<Benchmark::Tms:0x007f8c1a80fe50 @label="new", @real=0.49383, @cstime=0.0, @cutime=0.0, @stime=0.03, @utime=0.45999999999999996, @total=0.49>]

Similiarly, bisect_right

  if idx
    idx + 1
  else
    -1
  end
commented

The doc says:

- (Object) bisect_left(a, x, opts = {})

Return the index where to insert item x in list a, assuming a is sorted.

It would be more appropriate to say "assuming a is in monotonically non-descending order."

We could check the argument a before call the method, or we could get wrong result

bisect_left (1..10000000).to_a, 56789
# ==> 56788
bisect_left (1..10000000).to_a.reverse, 1
# ==> 0
## THIS IS WRONG, BEAUSE THE COLLECTION GIVEN IS IN DESCENDING ORDER

Therefore I propose 4 predicts: ascending?, descending?, non_descending? non_ascending?

Then we can check input

def my_bisect_left(a, x, opts={})
  return nil if a.nil?
  return 0 if a.empty?
  rasie ArgumentError unless non_descending?(a)
  .... 
end

Please feel free to create a Pull Request. I'm happy to have the help.

commented

bisect_left and similar functions all take a opt arguments: (a, x, opts={}). I am afraid by using it casually will cause the result collection not sorted.

e.g

col = [ 1, 2, 3, 4, 5, 6]

bisect_left col,  1, { low: 3 }
=> 3

Then, insort_left! will insert 1 at index 3.

    [1, 2, 3, 1, 4, 5, 6]  # now col isn't sorted

I don't know if I understand it correctly. If so, I would suggest removing opt={} from bisect__, insort__ functions.

I'm comfortable with your suggestion. I have a bad habit of adding options hashes to method signatures then not using them. Please feel free to remove any you see.

Also, I've added you as a contributor to this repo. Feel free to merge your changes directly. Later this evening I'll start a new Issue where we discuss a roadmap for the next release. There are a few updates I'd like to make.

Thank you again for your help!

commented

I am still not confident to merge anything to master without you green light it. I will publish to dev branch and send PR instead.

Thank you for your trust.