Enumerable#tally
The tally method counts the occurrences of each element in a collection and returns the results as a hash. Each unique element becomes a key, and its count becomes the corresponding value. This is a straightforward way to analyze the distribution of values in your data.
It is especially useful when you want a quick frequency map without writing a manual counter loop. The output keeps the categories and counts together, which makes the result easy to inspect or pass into another step.
basic usage
Count all elements in an array:
fruits = ["apple", "banana", "apple", "orange", "banana", "apple"]
fruits.tally
# => {"apple"=>3, "banana"=>2, "orange"=>1}
The method iterates through the collection once and builds the hash of counts.
That single pass is the main appeal: you can look at the input once and keep the counts in step with the data as it moves through the enumerable.
with numbers
dice_rolls = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
dice_rolls.tally
# => {3=>2, 1=>2, 4=>1, 5=>3, 9=>1, 2=>1, 6=>1}
Numeric tallies are handy when you want to see how often a value repeats. The result can be compared directly with other frequency maps, which makes it easy to spot the most common entries.
with ranges
Ranges are enumerables, so tally works on them directly:
(1..6).tally
# => {1=>1, 2=>1, 3=>1, 4=>1, 5=>1, 6=>1}
# Count even vs odd in a range
(1..10).tally { |n| n.even? ? "even" : "odd" }
# => {"odd"=>5, "even"=>5}
The block form transforms each element before counting, giving you flexibility in how you categorize.
That block can collapse many values into just a few buckets. It is a simple way to turn a raw series into a smaller summary without building a separate grouping structure first.
custom counting with hash argument
You can provide an existing hash to tally to customize how counts are accumulated:
votes = ["yes", "no", "yes", "yes", "abstain", "no", "yes"]
# Pre-initialize all expected keys
tally = votes.tally({ "yes" => 0, "no" => 0, "abstain" => 0, "absent" => 0 })
# => {"yes"=>3, "no"=>2, "abstain"=>1, "absent"=>0}
This is useful when you need all possible categories represented in the result, even if some have zero counts.
Preloading the hash is a practical way to keep the output shape stable. Even if some categories do not appear in the source data, the final result still includes them with a zero count.
use case: survey with all options
survey_responses = ["satisfied", "neutral", "satisfied", "dissatisfied", "satisfied"]
# Ensure all rating levels appear in results
all_options = ["very_dissatisfied", "dissatisfied", "neutral", "satisfied", "very_satisfied"]
tally = survey_responses.tally(Hash.new(0).merge!(all_options.map { |k| [k, 0] }.to_h))
# Or more simply, start with the hash you need:
tally = survey_responses.tally(all_options.product([0]).to_h)
# => {"satisfied"=>3, "neutral"=>1, "dissatisfied"=>1}
That pattern can be helpful in reports where every option should show up, even when nobody selected it. It keeps the summary predictable for a chart or table consumer.
comparison to group_by + count
Before tally, you would often combine group_by with count to achieve similar results:
words = ["cat", "dog", "fish", "cat", "dog", "cat"]
# Old approach with group_by + count
words.group_by(&:itself).transform_values(&:count)
# => {"cat"=>3, "dog"=>2, "fish"=>1}
# Modern approach with tally
words.tally
# => {"cat"=>3, "dog"=>2, "fish"=>1}
The tally method is more concise and performs the same operation in a single pass, making it both cleaner and slightly more efficient.
It also reads more directly when the only thing you need is counts. group_by still has a place, but tally is the shorter path when the groups themselves do not matter.
when group_by still shines
group_by remains useful when you need the actual grouped elements rather than just counts:
words = ["cat", "dog", "fish", "cat", "dog", "cat"]
# Get the actual elements grouped
words.group_by(&:itself)
# => {"cat"=>["cat", "cat", "cat"], "dog"=>["dog", "dog"], "fish"=>["fish"]}
# tally only gives you counts, not the elements
words.tally
# => {"cat"=>3, "dog"=>2, "fish"=>1}
That distinction matters when the actual grouped values will be used later. If you only need counts, tally is simpler; if you need the source items grouped together, group_by keeps that richer shape intact.
with custom enumerables
tally works with any object that includes the Enumerable module:
# Via to_enum on a Hash
stock = { apples: 50, bananas: 30, oranges: 25, apples: 20 }
stock.to_enum.tally
# Note: Hash keys are unique, so this counts key occurrences
# Fibonacci as an Enumerable
fib = Enumerator.new do |y|
a, b = 0, 1
loop do
y << a
a, b = b, a + b
end
end
fib.take(10).tally
# => {0=>1, 1=>3, 2=>1, 3=>1, 4=>1, 5=>1, 6=>1, 8=>1}
Any enumerable that yields repeatable values can use the same method. That makes tally useful for streams, generated sequences, or collections that are not plain arrays.
return value
tally always returns a hash. The keys are the unique elements from the collection, and the values are integers representing how many times each element appeared.
[].tally
# => {}
[1, 1, 1].tally
# => {1=>3}
The return value is always a hash, so the result is easy to feed into another step that expects key-value counts. That keeps the output shape consistent across the different examples above.
practical examples
The practical examples below show how tally converts raw categories into frequency maps for log analysis, word counting, and inventory tracking. In each case, the method replaces a manual counter loop with a single expressive call.
Analyzing log files
log_levels = ["INFO", "DEBUG", "INFO", "ERROR", "INFO", "DEBUG", "ERROR", "INFO"]
log_levels.tally
# => {"INFO"=>4, "DEBUG"=>2, "ERROR"=>2}
This is a common production use case because log levels are already categorical. Counting them gives you a quick summary without needing a separate reporting pass.
This is a common production use case because log levels are already categorical. Counting them gives you a quick summary without needing a separate reporting pass. Tally works equally well on tokenized text, where each word becomes a key and the value tracks how often that word appears in the source string.
Word frequency analysis
sentence = "the quick brown fox jumps over the lazy dog the fox"
sentence.split.tally
# => {"the"=>2, "quick"=>1, "brown"=>1, "fox"=>2, "jumps"=>1, "over"=>1, "lazy"=>1, "dog"=>1}
Word frequency is another case where the method is a natural fit. Split the string, tally the tokens, and you have a compact summary you can sort or display right away. Symbols and other types work the same way, making the method a quick choice for any categorical data that repeats.
Split the string, tally the tokens, and you have a compact summary that you can sort or display right away. Symbols and other types work the same way, making the method a quick choice for any categorical data that repeats.
Color distribution in inventory
inventory = [:red, :blue, :red, :green, :blue, :red, :blue, :blue]
inventory.tally
# => {:red=>3, :blue=>4, :green=>1}
The same idea works for symbols, strings, or any other repeated values. If you just need to know how often something appears, tally keeps the code short and direct. The result is always a clean hash that pairs each category with its count, ready for display or further processing.
See Also
Enumerable#group_by— groups elements into categoriesEnumerable#count— counts elements matching a conditionEnumerable#reduce— accumulates values into a single result