5

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?

2
  • 1
    Btw: you can do what you want more easily by using groups = scenes.group_by(&:number) instead of inject. Commented Mar 21, 2010 at 11:54
  • The new_hash variable is initialized with the injected value (keep in mind: it's a folding operation which has a start value in the accumulator) Commented Mar 21, 2010 at 12:27

5 Answers 5

10

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.

Sign up to request clarification or add additional context in comments.

2 Comments

@shingara: No, it doesn't fail. His code runs without error and returns the expected result.
Thank you sepp2k. I agree, "why was it downvoted?" It ran right, fixed the bug in his code and explained the problem.
6

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">
  ]
}

Comments

2

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.

Comments

0

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.

Comments

0

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 } }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.