tags:

views:

140

answers:

3

I've got a Ruby method like the following:

# Retrieve all fruits from basket that are of the specified kind.
def fruits_of_kind(kind)
  basket.select { |f| f.fruit_type == kind.to_s }
end

Right now, you can call this like:

fruits_of_kind(:apple)    # => all apples in basket
fruits_of_kind('banana')  # => all bananas in basket

and so on.

How do I change the method so that it will correctly handle iterable inputs as well as no inputs and nil inputs? For example, I'd like to be able to support:

fruits_of_kind(nil) # => nil
fruits_of_kind(:apple, :banana)    # => all apples and bananas in basket
fruits_of_kind([:apple, 'banana']) # => likewise

Is this possible to do idiomatically? If so, what's the best way to write methods so that they can accept zero, one, or many inputs?

+2  A: 

Use the VARARGS feature of Ruby.

# Retrieve all fruits from basket that are of the specified kind.
# notice the * prefix used for method parameter
def fruits_of_kind(*kind)
  kind.each do |x|
    puts x
  end
end

fruits_of_kind(:apple, :orange)
fruits_of_kind()
fruits_of_kind(nil)

-sasuke

sasuke
Will this handle the case of fruits_of_kind([:apple, 'banana'])?
Kyle Kaitan
+2  A: 
def fruits_of_kind(kind)
    return nil if kind.nil?

    result = []

    ([] << kind).flatten.each{|k| result << basket.select{|f| f.fruit_type == k.to_s }}

    result
end

The 'splat' operator is probably the best way to go, but there are two things to watch out for: passing in nil or lists. To modify Pesto's solution for the input/output you'd like, you should do something like this:

def fruits_of_kind(*kinds)
  return nil if kinds.compact.empty? 

  basket.select do |fruit|
    kinds.flatten.each do |kind|
      break true if fruit.fruit_type == kind.to_s
    end == true #if no match is found, each returns the whole array, so == true returns false
  end
end

If you pass in nil, the * converts it to [nil]. If you want to return nil instead of an empty list, you have to compact it (remove nulls) to [], then return nil if it's empty.

If you pass in a list, like [:apple, 'banana'], the * converts it to [[:apple, 'banana']]. It's a subtle difference, but it's a one-element list containing another list, so you need to flatten kinds before doing the "each" loop. Flattening will convert it to [:apple, 'banana'], like you expect, and give you the results you're looking for.

EDIT: Even better, thanks to Greg Campbell:

   def fruits_of_kind(basket, kind)
       return nil if kind.nil?

       kind_list = ([] << kind).flatten.map{|kind| kind.to_s}

       basket.select{|fruit| kind_list.include?(fruit) } 
   end

OR (using splat)

   def fruits_of_kind(*kinds)
       return nil if kinds.compact.empty?

       kind_list = kinds.flatten.map{|kind| kind.to_s}

       basket.select{|fruit| kind_list.include?(fruit.fruit_type) } 
   end
Chris Doggett
I think that's almost what I want, except that I need "result +=", not "result <<".
Kyle Kaitan
This handles all of the cases I described above correctly and it seems the most idiomatic.
Kyle Kaitan
The "<<" just appends the items to the array. It's the same as "+=". I'd honestly either use the second one I wrote (modified from Pesto's), and possibly change your output expectations to expect an empty list ([]) instead of nil. Still need to flatten it, though.
Chris Doggett
You could make this more efficient (and readable, imo) by doing the following: kind_list = kinds.flatten.map {|kind| kind.to_s}; basket.select {|fruit| kind_list.include?(fruit.fruit_type)} (apologies for the formatting, we really need to be able to format code in comments)
Greg Campbell
+3  A: 

You need to use the Ruby splat operator, which wraps all remaining arguments into an Array and passes them in:

def foo (a, b, *c)
  #do stuff
end

foo(1, 2) # a = 1, b = 2, c = []
foo(1, 2, 3, 4, 5) #a = 1, b = 2, c = [3, 4, 5]

In your case, something like this should work:

def fruits_of_kind(*kinds)
  kinds.flatten!
  basket.select do |fruit|
    kinds.each do |kind|
      break true if fruit.fruit_type == kind.to_s
    end == true #if no match is found, each returns the whole array, so == true returns false
  end
end


EDIT

I changed the code to flatten kinds so that you can send in a list. This code will handle entering no kinds at all, but if you want to expressly input nil, add the line kinds = [] if kinds.nil? at the beginning.

Pesto
Doesn't look like it handles the case where he passes in a list, so you may need to flatten kinds. Otherwise, from your first example, foo(1,2,[3,4,5]) will have c == [[3,4,5]], and the each won't work.
Chris Doggett
Doesn't handle nil, either.
Chris Doggett