rubyguides

Array#tally

The .tally method counts how many times each element appears in an array. It returns a hash where keys are the unique elements and values are their occurrence counts.

Ruby 2.7 introduced tally as an Enumerable method, and Array gained it in Ruby 3.0. The optional hash parameter was added in Ruby 3.1.

tally is a good fit any time you need a frequency table, a histogram, or a quick summary of repeated values. Instead of building the hash by hand, tally turns the intent into one direct call. That keeps the code short and makes it obvious that the question is “how many of each?” rather than “how do I count this myself?”

Skimming the code later, the method name is a strong hint that the result will be a count map, so the reader does not need to infer the shape from a longer loop.

Syntax

arr.tally -> hash
arr.tally(hash) -> hash (Ruby 3.1+)

.tally takes no required parameters. In Ruby 3.1+, you can pass an existing hash to accumulate counts into.

Parameters

ParameterTypeDefaultDescription
hashHashnilOptional (Ruby 3.1+). A hash to accumulate counts into.

Examples

Basic string counting

fruits = ["apple", "banana", "apple", "orange", "banana", "apple"]
fruits.tally
# => {"apple"=>3, "banana"=>2, "orange"=>1}

The return value is a plain hash, so you can use any hash method on the result — keys, values, select, or sort_by — without an extra conversion step. This makes tally a natural fit for pipelines where the count map is just one step in a larger data transformation.

Counting integers

scores = [95, 87, 95, 92, 87, 90, 95]
scores.tally
# => {95=>3, 87=>2, 92=>1, 90=>1}

Integer counts work the same way as string counts — tally does not care about the element type, only about equality. That gives you a consistent hash shape whether the input is numbers, symbols, or any objects that respond to == and hash.

Using with a block (Ruby 3.0+)

words = ["hello", "world", "HELLO", "ruby", "WORLD"]
words.tally(&:upcase)
# => {"HELLO"=>2, "WORLD"=>2, "RUBY"=>1}

Passing a block to tally normalizes the elements before counting, which is useful when case differences, whitespace, or formatting variations would otherwise split the same logical value across multiple keys. The block is applied to each element first, and the normalized result is what gets counted.

Using hash accumulator (Ruby 3.1+)

existing_counts = { "ruby" => 5 }
["ruby", "python", "ruby"].tally(existing_counts)
# => {"ruby"=>7, "python"=>1}

The hash accumulator is a nice way to combine counts from separate sources without merging hashes afterward. Pass an existing hash and tally adds to the counts that are already there, which keeps the code compact when counts come from multiple passes over the data.

Common use cases

Finding the most frequent element

votes = ["Alice", "Bob", "Alice", "Charlie", "Alice", "Bob"]
tally = votes.tally
most_voted = tally.max_by { |_, count| count }
# => ["Alice", 3]

Once you have the tally hash, methods like max_by and min_by let you ask questions about the distribution without writing a loop. The resulting array pair makes it easy to report the dominant element and its count in a single expression.

Filtering by frequency

letters = ["a", "b", "a", "c", "a", "b", "d"]
letters.tally.select { |_, count| count > 1 }
# => {"a"=>3, "b"=>2}

Chaining select right after tally is a quick way to turn raw counts into a filtered summary. It is common in reports where you only care about values that appear more than once.

Performance

.tally has O(n) time complexity since it iterates through the array once. It builds a hash internally, so space complexity is O(u) where u is the number of unique elements.

This is significantly faster than the manual approach using .each_with_object(Hash.new(0)):

# Slower manual approach
arr.each_with_object(Hash.new(0)) { |k, h| h[k] += 1 }

# Faster native method
arr.tally

Beyond raw speed, the native tally method avoids the boilerplate of setting up a default-value hash and the extra block syntax. That makes the code shorter and the intent clearer, especially in scripts where the count map is only one small step in a larger processing chain.

When you are counting elements for reporting or validation, the biggest win is readability. The method name tells the next reader that the code is building a count map, and the implementation stays compact even when the input grows.

The practical benefit is that you get a small, readable call that already matches the question you are asking of the data. For counting tasks, the built-in method usually reads better than a custom accumulator.

See Also

  • arr-count — Count elements matching a condition or value
  • arr-uniq — Return unique elements from an array
  • arr-filter — Select elements matching a condition