I have an array of Item
objects that each have a string particular
and a numeric amount
. I want to put all particular
s (separated by newlines) into a single string and the same for all amount
s.
Here's what I came up with:
particulars = []
amounts = []
items.each do |item|
particulars << item.particular
amounts << item.amount
end
particulars_string = particulars.join("\n")
amounts_string = amounts.join("\n")
So if
item1.particular = "food"
item2.particular = "drink"
item1.amount = 1000
item2.amount = 2000
then running the code above gives
particulars_string # "food\ndrink"
amounts_string # "1000\n2000"
which is correct. However, I feel that the above code can be done better. In particular, I want all the code in one loop, not the three (each
and two join
s) I have now. What's a better way to do this?
2 Answers 2
You can get by with just two lines:
particulars_string = items.map(&:particular).join("\n")
amounts_string = items.map(&:amount).join("\n")
Enumerable#map
, which is mixed into Array
, creates a new array from an existing array by running a block on each element and storing the result. In this case though you don't need a full block, since you just need to call a single method (particular
or amount
). So it basically extracts those into new arrays and then joins those arrays.
As a rule of thumb, you almost never have to "manually" map stuff from one array to another in Ruby using each
and <<
. Be sure to read the docs for Array
and Enumerable
as they're full of good stuff.
-
\$\begingroup\$ Exactly what I was looking for! \$\endgroup\$jcm– jcm2014年01月15日 07:50:48 +00:00Commented Jan 15, 2014 at 7:50
-
\$\begingroup\$ I don't really like this approach — you're still doing 4 enumerations of the length of the items array, and there's duplication on 2 local variables, 2 methods, and an argument. \$\endgroup\$coreyward– coreyward2014年01月20日 20:40:05 +00:00Commented Jan 20, 2014 at 20:40
-
\$\begingroup\$ @coreyward True. Your use of
transpose
is more elegant than the duplication above. \$\endgroup\$Flambino– Flambino2014年01月20日 21:34:49 +00:00Commented Jan 20, 2014 at 21:34 -
\$\begingroup\$ @Flambino, you may be mixing me up with coreyward. I think your solution is just fine. +1 \$\endgroup\$Cary Swoveland– Cary Swoveland2014年01月22日 07:29:15 +00:00Commented Jan 22, 2014 at 7:29
-
\$\begingroup\$ @CarySwoveland Argh, so sorry. I was mixing you up - my mistake. But, then, your use of
transpose
is more elegant :) \$\endgroup\$Flambino– Flambino2014年01月22日 10:40:06 +00:00Commented Jan 22, 2014 at 10:40
If you had several attributes (particular, amount, ...), you might consider doing it this way:
class Items
attr_accessor :attributes
def initialize(*attributes)
@attributes = attributes
end
end
items = [Items.new('food', 1000), Items.new('drink', 2000)]
attributes_strings = items.map(&:attributes).transpose.map {|e| e.join('\n')}
puts attribute_strings
food\ndrink
1000\n2000
-
\$\begingroup\$ Clever. I didn't know about transpose. \$\endgroup\$jcm– jcm2014年01月16日 02:55:41 +00:00Commented Jan 16, 2014 at 2:55
-
\$\begingroup\$ As mentioned: Nicer than my answer. However, you're assuming/adding some stuff compared to the original question where the item objects; it's not given that the objects in
items
respond to, or can feasibly be made to respond to,attributes
. Luckily, it's not necessary. I'd shorten it to simply:particulars_string, amounts_string = items.map {|item| [item.particular, item.amount]}.transpose.map {|a|a.join("\n")}
. Same input/result as the original with no assumptions about or additions to code outside of that in the original question. \$\endgroup\$Flambino– Flambino2014年01月20日 21:44:50 +00:00Commented Jan 20, 2014 at 21:44 -
\$\begingroup\$ @Flambino, I'm not sure if I understand your point about
items
. The contents ofitems
is code, not data. \$\endgroup\$Cary Swoveland– Cary Swoveland2014年01月22日 07:27:49 +00:00Commented Jan 22, 2014 at 7:27 -
\$\begingroup\$ @CarySwoveland I just consider the objects in
items
"out of scope". All we know is that there's an array of items that each respond toparticular
andamount
. You define your ownItem
class that responds toattributes
, which may or may not be possible - we don't know enough about the context of OP's code. Of course you're more than welcome to suggest different approaches, but it's not necessary in this case, where it's a simple "given this array, produce two strings". And the core of your answer (map + transpose + map) is great for that even without assuming anything about the items \$\endgroup\$Flambino– Flambino2014年01月22日 10:57:17 +00:00Commented Jan 22, 2014 at 10:57