Konstantin
Konstantin

Reputation: 3123

How to implement Ruby max_by to return all elements which have the maximum value?

Ruby max_by method finds the maximal element form an array. Sometimes the maximal elements are with multipicity, in this case max_by chooses only one of them, seemingly arbitrarily. When I need all of them, I use this approach currently, to find the maximal values in an array of arrays:

sorted=ary.sort_by{|a,b| b}.reverse
max_score=sorted.first[1]
t=sorted.take_while{|z| z[1]==max_score}

But how could I monkey-patch the Array class with a "maxes_by" method, which accept a block, similarly to max_by, and returns an array of the maximal values?

Upvotes: 2

Views: 2810

Answers (3)

spickermann
spickermann

Reputation: 106962

Without writing a new, optimized method that returns the expected output, you can combine max_by and select:

maximum = array.max_by { |element| element[1] }
t = array.select { |element| element[1] == maximum[1] }

Another option might be to group all elements by the value in question (with group_by) and then just pick the list with the max value.

lists = array.group_by { |element| element[1] }
lists[lists.keys.maximum]

Upvotes: 6

Cary Swoveland
Cary Swoveland

Reputation: 110725

It's worth a mention that the task could be done in a single pass through the array, or more generally, in a single pass through any collection whose class includes Enumerable.

module Enumerable
  def max_by_all
    return each unless block_given?
    last_yield = nil
    each_with_object([]) do |e,a|
      ye = yield(e)
      case last_yield.nil? ? -1 : last_yield <=> ye
      when -1
        a.replace([e])
        last_yield = ye
      when 0
        a << e
      end
    end
  end
end
arr = [2, 4, 3, 4, 1, 2, 5, 3, 5, 1]
arr.max_by_all(&:itself)
  #=> [5, 5]
arr =  ["style", "assets", "misty", "assist", "corgi", "bossy", "bosses", "chess"]
arr.max_by_all { |s| s.count('s') }
  #=> ["assets", "assist", "bosses"] 
h = { a: 1, b: 3, c: 2, d: 3, e: 1 }
h.max_by_all(&:last)
  #=> [[:b, 3], [:d, 3]] 
arr = [1, 2, 3]
arr.max_by_all.map { |n| 2*n }
  #=> [2, 4, 6] 

In the last example max_by_all has no block and therefore returns an enumerator which merely enumerates the elements of self. This behaviour may seem pointless but I've provided for it (the line return each unless block_given?) to mimic the behaviour of Enumerable#max_by when no block is provided.

Upvotes: 1

Victor Cordeiro Costa
Victor Cordeiro Costa

Reputation: 2194

Using Monkey-Patch

class Array
  def maxes_by
    maximal = max_by { |x| yield(x) }
    select { |x| yield(x) == yield(maximal) }
  end
end
  • Usage
> ['house', 'car', 'mouse'].maxes_by { |x| x.length }

=> ['house', 'mouse']

But I don't recommend to monkey patch the Array class, this practice is dangerous and can potentially lead to undesirable effects on your system.

For our good, ruby language provides a nice feature to overcome this problem, the Refinements, which is a safe way for monkey patching on ruby.

To simplify, with the Refinements you can monkey patch the Array class and the changes will only be available inside the scope of the class that is using the refinement! :)

You can use the refinement inside the class you are working on and you are ready to go.

Using Refinements

module MaxesByRefinement
  refine Array do
    def maxes_by
      maximal = max_by { |x| yield(x) }
      select { |x| yield(x) == yield(maximal) }
    end
  end
end

class MyClass
  using MaxesByRefinement

  def test
    a = %w(house car mouse)
    a.maxes_by { |x| x.length } # maxes_by is available here!
  end
end
  • Usage
> MyClass.new.test

=> ['house', 'mouse']

Upvotes: 0

Related Questions