Fellow Stranger
Fellow Stranger

Reputation: 34023

Return object in collection that has the highest (but not higher) or equal attribute value compared to given value

I have a collection of five objects.

The have a number attribute day with a unique value ranging 0-6.

If compared to a value, let's say 5 or 0, how could I find the object with equal attribute value OR the highest value in collection but not exceeding the given?

collection.map(&:day)
=> [0, 2, 3, 4, 6]

The desired return when comparing collection with value 5 should return the object with day value 4.

The desired return when comparing collection with value 0 should return the object with day value 0.

Upvotes: 1

Views: 99

Answers (4)

Cary Swoveland
Cary Swoveland

Reputation: 110685

Let's see how the methods compare in performance. I only compare methods that do not require the array to be pre-sorted. I will edit to add other methods that are suggested.

Methods compared

module Methods
  def awendt(collection, value)
    collection_sorted_desc = collection.sort_by{|num| -num}
    collection_sorted_desc.detect{|num| num <= value }
  end

  def awendt_rev(collection, value)
    collection_sorted_desc = collection.sort.reverse
    collection_sorted_desc.detect{|num| num <= value }
  end

  def cary1(a,v)
    a.select { |e| e <= v }.max
  end

  def cary2(a,v)
    enum = a.each
    mx = - Float::INFINITY
    loop do
     x = enum.next
     mx = x if x <= v && x > mx
    end
    mx
  end

  def cary3(a,v)
    a.sort.take_while { |e| e <= v }.last
  end
end

include Methods
methods = Methods.instance_methods
  #=> [:awendt, :cary1, :cary2, :cary3]

Test data

def test_data(n,m)
  [[*(0..n)].shuffle, m.times.with_object([]) { |_,a| a << rand(n) }]
end

arr, cutoffs = test_data(8,3)
  #=> [[1, 5, 2, 0, 3, 8, 4, 6, 7], [5, 1, 7]]

Confirm methods return the same values

arr, cutoffs = test_data(1000,100)
results = cutoffs.each_with_object([]) { |c,a|
  a << send(methods.first, arr, c) }
puts methods[1..-1].all? { |m|
  cutoffs.zip(results).all? { |c,r| r == send(m, arr, c) } }
  #=> true

Benchmark

require 'benchmark'

arr, cutoffs = test_data(200_000, 20)

Benchmark.bm(methods.map { |m| m.to_s.size }.max) do |bm|
  methods.each do |m|
    bm.report m.to_s do
      cutoffs.each { |c| send(m, arr, c) }
    end
  end
end

                 user     system      total        real
awendt       5.140000   0.040000   5.180000 (  5.188094)
awendt_rev   1.030000   0.010000   1.040000 (  1.050535)
cary1        0.490000   0.010000   0.500000 (  0.489371)
cary2        6.740000   0.010000   6.750000 (  6.782410)
cary3        0.910000   0.010000   0.920000 (  0.919538)

Edit: I added a method :awendt_rev that replaces collection.sort_by{|num| -num} in :awendt with collection.sort.reverse.

Upvotes: 3

Cary Swoveland
Cary Swoveland

Reputation: 110685

Here are three ways you could do it. The benchmark I added below shows that #1 and #3 are more efficient than #2.

#1

a = [0,2,3,4,6]

def not_exceed(a,v)
  a.select { |e| e <= v }.max
end

not_exceed(a,5) #=> 4
not_exceed(a,0) #=> 0

#2

def not_exceed(a,v)
  enum = a.each
  mx = - Float::INFINITY
  loop do
   x = enum.next
   mx = x if x <= v && x > mx
  end
  mx
end

not_exceed(a,5) #=> 4
not_exceed(a,0) #=> 0

#3

def not_exceed(a,v)
  a.sort.take_while { |e| e <= v }.last
end

not_exceed(a,5) #=> 4
not_exceed(a,0) #=> 0

Upvotes: 2

Mohammad AbuShady
Mohammad AbuShady

Reputation: 42799

Assuming there's only sorted numbers like you mentioned in your comment, and that all numbers are integers, you can easily use the detect function without any other needs

[1,2,3,4,5,6].detect {|x| x >= minimum}

This will return the first number that is either equal or greater than the minimum number.

EDIT: ok i just noticed you need the opposite, in this case you'll need to reverse the array

[1,2,3,4,5].reverse.detect{ |x| x <= maximum }

Upvotes: 1

awendt
awendt

Reputation: 13673

If the input is not sorted, you could use this:

def highest_or_equal(value, collection)
  collection_sorted_desc = collection.sort_by{|num| -num}
  collection_sorted_desc.detect{|num| num <= value }
end

And here's the test for it:

require 'minitest'
require 'minitest/autorun'

class Test < Minitest::Test
  def test_returns_lower
    assert_equal highest_or_equal(5, [0, 2, 3, 4, 6]), 4
  end

  def test_returns_exact_match
    assert_equal highest_or_equal(0, [0, 2, 3, 4, 6]), 0
  end
end

Upvotes: 2

Related Questions