Reputation: 45295
I have code:
class Scene
def initialize(number)
@number = number
end
attr_reader :number
end
scenes = [Scene.new("one"), Scene.new("one"), Scene.new("two"), Scene.new("one")]
groups = scenes.inject({}) do |new_hash, scene|
new_hash[scene.number] = [] if new_hash[scene.number].nil?
new_hash[scene.number] << scene
end
When I'm lauching it I get error:
freq.rb:11:in `[]': can't convert String into Integer (TypeError)
from freq.rb:11:in `block in <main>'
from freq.rb:10:in `each'
from freq.rb:10:in `inject'
from freq.rb:10:in `<main>'
If I change scenes to:
scenes = [Scene.new(1), Scene.new(1), Scene.new(2), Scene.new(1)]
the problem dissapear.
Why I get error message in the first case? Why Ruby decide to convert scene.number from String to Integer?
And one additional question about the 'inject' method. When Ruby initialize the 'new_hash' variable and how can Ruby know the type of this variable?
Upvotes: 5
Views: 24593
Reputation: 64363
I know an answer is accepted for this question, but I can't help but post my answer.
groups = scenes.inject({}) { |nh, s| nh.tap {|h| (h[s.number] ||= []) << s } }
Upvotes: 0
Reputation: 5931
Why not use group_by which is probably exactly what you try to accomblish?
groups = scenes.group_by(&:number)
# => {"two"=>[#<Scene:0xb728ade0 @number="two">],
# "one"=>
# [#<Scene:0xb728ae30 @number="one">,
# #<Scene:0xb728ae08 @number="one">,
# #<Scene:0xb728ada4 @number="one">]}
inject
is a folding operation and not exactly what you want. At least it's cumbersome to use in this way. merge
with a block would probably be appropriate if you want to apply some algorithm during merging or grouping.
Upvotes: 2
Reputation: 96
Z.E.D.'s right. See Jay Fields' Thoughts: Ruby: inject for a good explanation of inject
by example.
As presented, your block returns an array. So the new_hash
in |new_hash, scene|
ends up being that array. When Ruby tries to find the array index 'one', it throws the error because 'one' is a String, not an Integer.
All you need to do is return new_hash
as Z.E.D. showed, and you'll get something like this:
{
"two" => [
#<Scene:0x101836470 @number="two">
],
"one" => [
#<Scene:0x101836510 @number="one">,
#<Scene:0x1018364c0 @number="one">,
#<Scene:0x101836420 @number="one">
]
}
Upvotes: 6
Reputation: 17876
Also, to explain 'how can Ruby know the type of this variable' and why it tries to 'convert String into Integer' you might want to revise: Ruby variables and dynamic typing.
Upvotes: 0
Reputation: 160551
try:
groups = scenes.inject({}) do |new_hash, scene| new_hash[scene.number] = [] if new_hash[scene.number].nil? new_hash[scene.number] << scene new_hash end
Ruby takes the empty hash passed into inject() and sets new_hash to that. When the block ends the return value gets used to initialize new_hash the next time through, i.e., new_hash keeps accumulating the result of the block.
In your original code you were not returning the hash but an array (new_hash[scene.number] is an array) and the next loop through Ruby complained because new_hash[scene.number] was trying to do a lookup into the array with a string value, hence the error you got.
Upvotes: 10