8

I built this method to find the longest word in an array, but I'm wondering if there's a better way to have done it. I'm pretty new to Ruby, and just did this as an exercise for learning the inject method.

It returns either the longest word in an array, or an array of the equal longest words.

class Array
  def longest_word
    # Convert array elements to strings in the event that they're not.
    test_array = self.collect { |e| e.to_s }
    test_array.inject() do |word, comparison|
      if word.kind_of?(Array) then
        if word[0].length == comparison.length then
          word << comparison
        else
          word[0].length > comparison.length ? word : comparison
        end
      else
        # If words are equal, they are pushed into an array
        if word.length == comparison.length then
          the_words = Array.new
          the_words << word
          the_words << comparison
        else
          word.length > comparison.length ? word : comparison
        end
      end
    end
  end
end
clem
  • 3,524
  • 3
  • 25
  • 41

6 Answers6

29

I would do

class Array
  def longest_word
    group_by(&:size).max.last
  end
end
Marc-André Lafortune
  • 78,216
  • 16
  • 166
  • 166
steenslag
  • 79,051
  • 16
  • 138
  • 171
  • 1
    if you are sure you have only strings, you can write it as `self.group_by(&length).max.last` else you'd need an additional `to_s` call like `self.group_by{|el|el.to_s.size}.max.last`. And finally, to return a single string when you have only one element, you can do `longest_word.size > 1 ? longest_word : longest_word.first` before returning. – Holger Just Mar 06 '11 at 20:20
  • Nice! I took the liberty of making the code more concise, but please revert if you don't like the style – Marc-André Lafortune Mar 06 '11 at 20:32
  • The OP said in the question that he did this in order to learn `inject`, and it's not quite clear whether he was looking for a better way using `inject` or just a better way period. Yours is a pretty nice answer to the second interpretation and @Mladen Jablanović's to the first. Both get my +1. – Jörg W Mittag Mar 06 '11 at 20:39
  • It started with me toying around with `inject`, but seeing that there are shorter ways to do it without that method is interesting. Thanks! – clem Mar 06 '11 at 20:51
  • @Marc-André Lafortune It is much more readable this way, thanks. – steenslag Mar 06 '11 at 20:51
  • You might want to check this too [link](http://stackoverflow.com/a/22438653/2125520) – Islam Azab Sep 30 '14 at 12:52
  • @IslamAzab `min_by` (and `max_by`) returns just one value, this one returns an array with one or more values. – steenslag Sep 30 '14 at 20:19
  • @steenslag Since I have seen you getting only the last value, I thought mentioning the other answer is worth it. Thanks for the hint. – Islam Azab Oct 01 '14 at 02:10
7

Ruby has a standard method for returning an element in a list with the maximum of a value.

anArray.max{|a, b| a.length <=> b.length}

or you can use the max_by method

anArray.max_by(&:length)

to get all the elements with the maximum length

max_length = anArray.max_by(&:length).length
all_with_max_length = anArray.find_all{|x| x.length = max_length}
David Nehme
  • 21,379
  • 8
  • 78
  • 117
4

Here's one using inject (doesn't work for an empty array):

words.inject(['']){|a,w|
  case w.length <=> a.last.length
  when -1
    a
  when 0
    a << w
  when 1
    [w]
  end
}

which can be shortened to

words.inject(['']){|a,w|
  [a + [w], [w], a][w.length <=> a.last.length]
}

for those who like golf.

Mladen Jablanović
  • 43,461
  • 10
  • 90
  • 113
  • Actually, this *should* work for an empty array, that's (one) of the point(s) of explicitly supplying an initial value for the accumulator. – Jörg W Mittag Mar 06 '11 at 20:33
  • In my version, I somehow thought it would be much cleaner to thread the word lengths implicitly through the accumulator, but it hasn't exactly cleaned it up much :-) Yours looks much nicer. – Jörg W Mittag Mar 06 '11 at 20:35
  • In order to work for empty arrays, one would have to kickstart `inject` with an empty array (rather than an array containing an empty string as I do), and complicate the branching inside a bit more. But for purpose of learning `inject`, I thought this was enough. – Mladen Jablanović Mar 06 '11 at 20:38
  • Ah, yes, it will return a wrong result. I was thinking more about Haskell, where `foldl1` will throw an error while `foldl` won't, or my solution which calls `first` on the result of the `inject`, which would be `nil` for an empty array without an explicit initial accumulator and thus raise a `NoMethodError`. (I should learn to not answer SO questions or leave comments in a rush, with my jacket on, literally halfway out the door :-) ) – Jörg W Mittag Mar 06 '11 at 20:43
2

A two liner:

vc = ['asd','s','1234','1235'].sort{|a,b| b.size <=> a.size}
vc.delete_if{|a| a.size < vc.first.size} 


#Output
["1235", "1234"]

or if you want use inject, this use your idea, but its more short.

test_array.inject{ |ret,word|
   ret = [ret] unless ret.kind_of?(Array)

   ret << word  if word.size == ret.first.size
   ret = [word] if word.size > ret.first.size
   ret
}
Gareve
  • 3,372
  • 2
  • 21
  • 23
  • This method is not idempotent when applied directly on an array. Probably not what you'd expect. You could use a `clone` here. But the solution is okay, and probably not even the slowest (without knowing implementation details of the `group_by` built-in method) – Holger Just Mar 06 '11 at 20:24
  • 1
    @Holger: `group_by` uses array lookups, so it's basically `O(n)`, while the `sort` is slower. – Marc-André Lafortune Mar 06 '11 at 20:56
  • Instead of `delete_if`, you could use `take_while` which would stop as soon as it reaches a smaller word. – Marc-André Lafortune Mar 06 '11 at 20:58
1
module Enumerable
  def longest_word
    (strings = map(&:to_s)).
      zip(strings.map(&:length)).
      inject([[''],0]) {|(wws, ll), (w, l)|
        case  l <=> ll
        when -1 then [wws, ll] 
        when  1 then [[w], l]
        else         [wws + [w], ll]
        end
      }.first
  end
end

This method only depends on generic Enumerable methods, there's nothing Array specific about it, therefore we can pull it up into the Enumerable module, where it will also be available for Sets or Enumerators, not just Arrays.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • This returns just one element, not an array of words with equal sizes. – steenslag Mar 06 '11 at 20:15
  • @steenslag: Yeah, thanks for noticing. The method name threw me off a little, I would have expected a method which returns multiple words to be called `longest_words` (plural). – Jörg W Mittag Mar 06 '11 at 20:30
0

This solution uses the inject method to accumulate the longest strings in an array, then picks the ones with the highest length.

animals = ["mouse", "cat", "bird", "bear", "moose"]

animals.inject(Hash.new{|h,k| h[k] = []}) { |acc, e| acc[e.size] << e; acc }.sort.last[1]

This returns: ["mouse", "mouse"]

Glenn
  • 436
  • 1
  • 3
  • 12