Using Ruby's 2.7 new #tally method
Ruby 2.7 introduced the Enumberable#tally
method. It allows to easily count elements’ occurrences in a given collection. In other words, it literally tallies them up. :)
The use case
Say we have a collection of words, and we’d like to count the occurrences of each word within it.
words = %w(the be to of and a in that have I it for not on with he as you do at
the not for it be we her so up and a to for on with)
# some of the most common English words based on https://en.wikipedia.org/wiki/Most_common_words_in_English
Pre Ruby 2.7
In previous versions of Ruby, whenever we encountered a logic counting occurrences of collection elements (here: words in the array), we would most likely see a code similar to this:
word_counts = {}
words.each do |word|
if word_counts[word]
word_counts[word] += 1
else
word_counts[word] = 1
end
end
word_counts
# => {"the"=>2, "be"=>2, "to"=>2, "of"=>1, "and"=>2, "a"=>2, "in"=>1, "that"=>1, "have"=>1, "I"=>1, "it"=>2, "for"=>3, "not"=>2, "on"=>2, "with"=>2, "he"=>1, "as"=>1, "you"=>1, "do"=>1, "at"=>1, "we"=>1, "her"=>1, "so"=>1, "up"=>1}
This could be simplified by initializing the Hash with a default value of 0
(which makes total sense in this case):
word_counts = Hash.new(0)
words.each { |word| word_counts[word] += 1 }
word_counts
# => {"the"=>2, "be"=>2, "to"=>2, "of"=>1, "and"=>2, "a"=>2, "in"=>1, "that"=>1, "have"=>1, "I"=>1, "it"=>2, "for"=>3, "not"=>2, "on"=>2, "with"=>2, "he"=>1, "as"=>1, "you"=>1, "do"=>1, "at"=>1, "we"=>1, "her"=>1, "so"=>1, "up"=>1}
We could even transform it further to a one-liner using #each_with_object
:
words.each_with_object(Hash.new(0)) { |word, word_counts| word_counts[word] += 1 }
# => {"the"=>2, "be"=>2, "to"=>2, "of"=>1, "and"=>2, "a"=>2, "in"=>1, "that"=>1, "have"=>1, "I"=>1, "it"=>2, "for"=>3, "not"=>2, "on"=>2, "with"=>2, "he"=>1, "as"=>1, "you"=>1, "do"=>1, "at"=>1, "we"=>1, "her"=>1, "so"=>1, "up"=>1}
I personally like the second solution the most. I think it strikes the best balance between simplicity and ease of understanding.
As we can see, all of the above approaches rely on iterating through the collection to tally up its members.
There is also another approach that could be chosen: relying on Enumerable#group_by
. If I were to use this one, the code I would write would probably look something like this:
words.group_by(&:itself). # this produces a structure like: {"the"=>["the", "the"], "be"=>["be", "be"], ... }
map { |word, occurrences| [word, occurrences.count] }.
to_h
# => {"the"=>2, "be"=>2, "to"=>2, "of"=>1, "and"=>2, "a"=>2, "in"=>1, "that"=>1, "have"=>1, "I"=>1, "it"=>2, "for"=>3, "not"=>2, "on"=>2, "with"=>2, "he"=>1, "as"=>1, "you"=>1, "do"=>1, "at"=>1, "we"=>1, "her"=>1, "so"=>1, "up"=>1}
The new way
Since Ruby 2.7, we could get the same result (a Hash containing numbers of occurrences of each element) by simply invoking the #tally
method on the collection.
words.tally
# => {"the"=>2, "be"=>2, "to"=>2, "of"=>1, "and"=>2, "a"=>2, "in"=>1, "that"=>1, "have"=>1, "I"=>1, "it"=>2, "for"=>3, "not"=>2, "on"=>2, "with"=>2, "he"=>1, "as"=>1, "you"=>1, "do"=>1, "at"=>1, "we"=>1, "her"=>1, "so"=>1, "up"=>1}
It’s not only a nice shortcut, it also expresses the intent in a clear way - something many of us love in Ruby code.
Summary
Have you had a chance to use the Enumerable#tally
method in your production code yet? Please share in the comments below!
Do you use continuous integration in your Ruby project? Consider using Knapsack Pro to increase your team’s productivity by shortening build times on your CI server!