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?
-
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
Kyle Kaitan : Will this handle the case of fruits_of_kind([:apple, 'banana'])? -
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 endThe '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 endIf 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) } endOR (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) } endKyle Kaitan : 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.Chris Doggett : 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.Greg Campbell : 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) -
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 linekinds = [] if kinds.nil?at the beginning.Chris Doggett : 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.
0 comments:
Post a Comment