17

I have an array [1,2,4,5,4,7] and I want to find the frequency of each number and store it in a hash. I have this code, but it returns NoMethodError: undefined method '+' for nil:NilClass

def score( array )
  hash = {}
  array.each{|key| hash[key] += 1}
end

Desired output is

{1 => 1, 2 => 1, 4 => 2, 5 => 1, 7 => 1 }
stevenspiel
  • 5,775
  • 13
  • 60
  • 89
  • 3
    mr, some suggest using inject (aka reduce), which is fine, but all you have to do is change `hash = {}` to `hash = Hash.new(0)`. That tells Ruby that if it encounters `hash[key]` in a context where it must have a value (such as `hash[key] += 1` or `v = hash[key]`) and the hash does not contain the key `key`, it is to add `key=>0` to the hash before taking further action (such as `hash[key] += 1`). On the other hand, `if hash[key] == 7` will evaluate to `if nil == 7` if `key` is not in the hash; `key=>0` will not be added to the hash. – Cary Swoveland Nov 13 '13 at 20:45
  • @CarySwoveland Nice teaching...!! Liked it.. *+1*. – Arup Rakshit Nov 13 '13 at 20:47
  • 2
    @tokland, the problem may be the same, but not the question. Here, mr. wants to know why he got a particular error, which seems to me quite ligit. – Cary Swoveland Nov 13 '13 at 21:49
  • fair enough, then let's say it's related :) http://stackoverflow.com/questions/9480852/array-to-hash-words-count – tokland Nov 13 '13 at 22:08

8 Answers8

25

In Ruby 2.4+:

def score(array)
  array.group_by(&:itself).transform_values!(&:size)
end
mwp
  • 8,217
  • 20
  • 26
23

Do as below :

def score( array )
  hash = Hash.new(0)
  array.each{|key| hash[key] += 1}
  hash
end
score([1,2,4,5,4,7]) # => {1=>1, 2=>1, 4=>2, 5=>1, 7=>1}

Or more Rubyish using Enumerable#each_with_object:

def score( array )
  array.each_with_object(Hash.new(0)){|key,hash| hash[key] += 1}
end
score([1,2,4,5,4,7]) # => {1=>1, 2=>1, 4=>2, 5=>1, 7=>1}

The reason of why NoMethodError: undefined method '+' for nil:NilClass ?

hash = {} is an empty has,with default value as nil.nil is an instance of Nilclass,and NilClass doesn't have any instance method called #+. So you got NoMethodError.

Look at the Hash::new documentation :

new → new_hash
new(obj) → new_hash

Returns a new, empty hash. If this hash is subsequently accessed by a key that doesn’t correspond to a hash entry, the value returned depends on the style of new used to create the hash. In the first form, the access returns nil. If obj is specified, this single object will be used for all default values. If a block is specified, it will be called with the hash object and the key, and should return the default value. It is the block’s responsibility to store the value in the hash if required.

Arup Rakshit
  • 116,827
  • 30
  • 260
  • 317
19

Ruby 2.7 onwards will have the Enumerable#tally method that will solve this.

From the trunk documentation:

Tallys the collection. Returns a hash where the keys are the elements and the values are numbers of elements in the collection that correspond to the key.

["a", "b", "c", "b"].tally #=> {"a"=>1, "b"=>2, "c"=>1}
Pawan
  • 1,480
  • 2
  • 18
  • 27
13

Just use inject. This type of application is exactly what it is meant for. Something like:

a.inject(Hash.new(0)) {|hash,word| hash[word] += 1; hash }
Josh
  • 5,631
  • 1
  • 28
  • 54
2

Love me some inject:

results = array.inject(Hash.new(0)) {|hash, arr_element| hash[arr_element] += 1; hash }

1.9.3p448 :082 > array = [1,2,4,5,4,7]
 => [1, 2, 4, 5, 4, 7] 
1.9.3p448 :083 > results = array.inject(Hash.new(0)) {|hash, arr_element| hash[arr_element] += 1; hash }
 => {1=>1, 2=>1, 4=>2, 5=>1, 7=>1} 
CDub
  • 13,146
  • 4
  • 51
  • 68
2

Here is a short option that uses the Hash array initializer

Hash[arr.uniq.map {|v| [v, arr.count(v)] }]
Alex.Bullard
  • 5,533
  • 2
  • 25
  • 32
1

The point here is that hash[1] doesn't exist (nil) when it first sees 1 in the array.

You need to initialize it somehow, and hash = Hash.new(0) is the easiest way. 0 is the initial value you want in this case.

Arup Rakshit
  • 116,827
  • 30
  • 260
  • 317
jaeheung
  • 1,208
  • 6
  • 7
0

Or use the group by method:

arr = [1,2,4,5,4,7]

Hash[arr.group_by{|x|x}.map{|num,arr| [num, arr.size] }]
hirolau
  • 13,451
  • 8
  • 35
  • 47