How to count duplicate elements in a Ruby array

Viewed 71772

I have a sorted array:

[
  'FATAL <error title="Request timed out.">',
  'FATAL <error title="Request timed out.">',
  'FATAL <error title="There is insufficient system memory to run this query.">'
]

I would like to get something like this but it does not have to be a hash:

[
  {:error => 'FATAL <error title="Request timed out.">', :count => 2},
  {:error => 'FATAL <error title="There is insufficient system memory to run this query.">', :count => 1}
]
14 Answers

The following code prints what you asked for. I'll let you decide on how to actually use to generate the hash you are looking for:

# sample array
a=["aa","bb","cc","bb","bb","cc"]

# make the hash default to 0 so that += will work correctly
b = Hash.new(0)

# iterate over the array, counting duplicate entries
a.each do |v|
  b[v] += 1
end

b.each do |k, v|
  puts "#{k} appears #{v} times"
end

Note: I just noticed you said the array is already sorted. The above code does not require sorting. Using that property may produce faster code.

You can do this very succinctly (one line) by using inject:

a = ['FATAL <error title="Request timed out.">',
      'FATAL <error title="Request timed out.">',
      'FATAL <error title="There is insufficient ...">']

b = a.inject(Hash.new(0)) {|h,i| h[i] += 1; h }

b.to_a.each {|error,count| puts "#{count}: #{error}" }

Will produce:

1: FATAL <error title="There is insufficient ...">
2: FATAL <error title="Request timed out.">

Using Enumerable#tally

["a", "b", "c", "b"].tally 

#=> { "a" => 1, "b" => 2, "c" => 1 }

Note: Only for Ruby versions >= 2.7

From Ruby >= 2.2 you can use itself: array.group_by(&:itself).transform_values(&:count)

With some more detail:

array = [
  'FATAL <error title="Request timed out.">',
  'FATAL <error title="Request timed out.">',
  'FATAL <error title="There is insufficient system memory to run this query.">'
];

array.group_by(&:itself).transform_values(&:count)
 => { "FATAL <error title=\"Request timed out.\">"=>2,
      "FATAL <error title=\"There is insufficient system memory to run this query.\">"=>1 }
a = [1,1,1,2,2,3]
a.uniq.inject([]){|r, i| r << { :error => i, :count => a.select{ |b| b == i }.size } }
=> [{:count=>3, :error=>1}, {:count=>2, :error=>2}, {:count=>1, :error=>3}]

If you want to use this often I suggest to do this:

# lib/core_extensions/array/duplicates_counter
module CoreExtensions
  module Array
    module DuplicatesCounter
      def count_duplicates
        self.each_with_object(Hash.new(0)) { |element, counter| counter[element] += 1 }.sort_by{|k,v| -v}.to_h
      end
    end
  end
end

Load it with

Array.include CoreExtensions::Array::DuplicatesCounter

And then use from anywhere with just:

the_ar = %w(a a a a a a a  chao chao chao hola hola mundo hola chao cachacho hola)
the_ar.duplicates_counter
{
           "a" => 7,
        "chao" => 4,
        "hola" => 4,
       "mundo" => 1,
    "cachacho" => 1
}

Since #tally is for 2.7 and up, and I'm not there yet, it's easy to use the #count method on the array. Use #uniq on the array to get one copy of each member of the array, and then find #count for that member in the array:

counts=Hash.new
arr.uniq.each {|name| counts[name]=arr.count(name) }

Example:

arr = [ 1, 2, 2, 3, 3, 3, 3, 3, 4, 4, 5]
arr.uniq => [1, 2, 3, 4, 5]
counts=Hash.new; arr.uniq.each {|name| counts[name]=arr.count(name) }

gives us

counts => {1=>1, 2=>2, 3=>5, 4=>2, 5=>1} 

Simple implementation:

(errors_hash = {}).default = 0
array_of_errors.each { |error| errors_hash[error] += 1 }
def find_most_occurred_item(arr)
    return 'Array has unique elements already' if arr.uniq == arr
    m = arr.inject(Hash.new(0)) { |h,v| h[v] += 1; h }
    m.each do |k, v|
        a = arr.max_by { |v| m[v] }
        if v > a
            puts "#{k} appears #{v} times"
        elsif v == a
            puts "#{k} appears #{v} times"
        end 
    end
end

puts find_most_occurred_item([1, 2, 3,4,4,4,3,3])
Related