11

Python has the Counter class in the collections module. It is a class for counting hashable objects. E.g.:

cnt = Counter()
cnt['Anna'] += 3
cnt['John'] += 2
cnt['Anna'] += 4
print(cnt)
=> Counter({'Anna': 7, 'John': 2})
print(cnt['Mario'])
=> 0

What is the equivalent of Counter in Ruby?

EDIT:

The Counter class provides also the following mathematical operations and helper methods:

c = Counter(a=3, b=1)
d = Counter(a=1, b=2)
c + d 
=> Counter({'a': 4, 'b': 3})
c - d
=> Counter({'a': 2})
c.most_common(1)
=> ['a']
Marco
  • 3,053
  • 5
  • 27
  • 29
  • 2
    Note: you haven't really picked a good example, a `defaultdict` would work here just as well. `Counter` is nice because the constructor allows iterables and counts the elements. – Karoly Horvath Nov 20 '15 at 14:00
  • 1
    Possible duplicate of [How to count identical string elements in a Ruby array](http://stackoverflow.com/questions/5128200/how-to-count-identical-string-elements-in-a-ruby-array) – Karoly Horvath Nov 20 '15 at 14:08
  • @KarolyHorvath thanks, I added a better example of its features. – Marco Nov 21 '15 at 10:14

3 Answers3

6
cnt = Hash.new(0)
cnt["Anna"] += 3
cnt["John"] += 2
cnt["Anna"] += 4
cnt # => {"Anna" => 7, "John" => 2}
cnt["Mario"] #=> 0

c = {"a" => 3, "b" => 1}
d = {"a" => 1, "b" => 2}
c.merge(d){|_, c, d| c + d} # => {"a" => 4, "b" => 3}
c.merge(d){|_, c, d| c - d}.select{|_, v| v > 0} # => {"a" => 2}
c.max(1).map(&:first) # => ["a"]
sawa
  • 165,429
  • 45
  • 277
  • 381
4

Here's a small implementation:

class Counter < Hash
  def initialize(other = nil)
    super(0)
    if other.is_a? Array 
      other.each { |e| self[e] += 1 }
    end
    if other.is_a? Hash
      other.each { |k,v| self[k] = v }
    end
    if other.is_a? String
      other.each_char { |e| self[e] += 1 }
    end
  end
  def +(rhs)
     raise TypeError, "cannot add #{rhs.class} to a Counter" if ! rhs.is_a? Counter  
     result = Counter.new(self)
     rhs.each { |k, v| result[k] += v }
     result
  end
  def -(rhs)
     raise TypeError, "cannot subtract #{rhs.class} to a Counter" if ! rhs.is_a? Counter  
     result = Counter.new(self)
     rhs.each { |k, v| result[k] -= v }
     result
  end
  def most_common(n = nil)
     s = sort_by {|k, v| -v}
     return n ? s.take(n) : s
  end
  def to_s
     "Counter(#{super.to_s})"
  end
  def inspect
     to_s
  end
end

It supports

  • construction from strings, arrays and hashes
  • accessing and modifying counts
  • addition and substraction for two Counters
  • access for the most_common element(s)

c1 = Counter.new([1,0,1])   #=> Counter({1=>2, 0=>1})
c1[2] = 1                   #=> 1 
c1                          #=> Counter({1=>2, 0=>1, 2=>1})
c2 = Counter.new([3,1])     #=> Counter({3=>1, 1=>1})
c1 + c2                     #=> {1=>3, 0=>1, 2=>1, 3=>1}
c3 = Counter.new("abraca")  #=> {"a"=>3, "b"=>1, "r"=>1, "c"=>1}
c3.most_common(2)           #=> [["a", 3], ["b", 1]]
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
Karoly Horvath
  • 94,607
  • 11
  • 117
  • 176
  • Works the same, but Python's counter is a heapq underneath. So, this isn't the same if you're intending to use this in any way that performance matters. – jayreed1 May 04 '21 at 15:13
-1

Counter is a really strange and awfully misleading name for this. Everybody else calls it a Multiset. There is no multiset implementation in the Ruby core or standard libraries, but there are some implementations floating around on the web:

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • 1
    Just curious: why do you think it's misleading? What would you expect `Counter([1, 2, 1, 3])` to return? – Eric Duminil Jan 10 '18 at 18:31
  • 1
    I wouldn't expect a counter to take an argument, and I would expect it to return a monotonically strictly increasing sequence of numbers where each subsequent call returns the result of the previous call + 1. – Jörg W Mittag Jan 11 '18 at 00:57