Ruby longest word in array
Asked Answered
U

6

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
Unfailing answered 6/3, 2011 at 19:55 Comment(0)
S
29

I would do

class Array
  def longest_word
    group_by(&:size).max.last
  end
end
Slaty answered 6/3, 2011 at 20:12 Comment(8)
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.Gelatinous
Nice! I took the liberty of making the code more concise, but please revert if you don't like the styleBrede
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.Zaneta
It started with me toying around with inject, but seeing that there are shorter ways to do it without that method is interesting. Thanks!Unfailing
@Marc-André Lafortune It is much more readable this way, thanks.Slaty
You might want to check this too linkTresatrescha
@IslamAzab min_by (and max_by) returns just one value, this one returns an array with one or more values.Slaty
@Slaty Since I have seen you getting only the last value, I thought mentioning the other answer is worth it. Thanks for the hint.Tresatrescha
M
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}
Musjid answered 6/3, 2011 at 20:7 Comment(2)
That's nice, but the OP "just did this as an exercise for learning the inject method."Respondent
Both methods only return a single element, not an array of some equally length strings.Gelatinous
S
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.

Squire answered 6/3, 2011 at 20:21 Comment(4)
Actually, this should work for an empty array, that's (one) of the point(s) of explicitly supplying an initial value for the accumulator.Zaneta
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.Zaneta
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.Razo
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 :-) )Zaneta
G
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
}
Guib answered 6/3, 2011 at 20:9 Comment(3)
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)Gelatinous
@Holger: group_by uses array lookups, so it's basically O(n), while the sort is slower.Brede
Instead of delete_if, you could use take_while which would stop as soon as it reaches a smaller word.Brede
P
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.

Priester answered 6/3, 2011 at 20:10 Comment(2)
This returns just one element, not an array of words with equal sizes.Slaty
@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).Zaneta
O
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"]

Obsequies answered 18/9, 2016 at 12:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.