Ruby 3.1 Enumerable#tally now accepts an optional hash to count occurrences


Enumeration refers to traversing over objects. In Ruby, we have Enumerable classes like Array, Hash and Range which get their enumeration features by including the Enumerable module.

This Enumerable module provides various methods #include?, #count, #map, #select and #uniq which we frequently use, and of course #tally about which this blog is. #tally counts the number of occurrences of an element in a collection and returns a hash containing the count for all elements.

To know more about Enumerable#tally that got introduced in Ruby 2.7, check out our previous blog.

Example

Consider the example of a stationery store. The shopkeeper sells his/her goods on a per-unit basis.

This type of problem is a perfect candidate to leverage our #tally method to calculate the total quantity of items sold in a week.

Consider the customers add items one by one to place an order as follows:

order_one = %i{ pen pencil eraser sharpener pen pen }
order_two = %i{ sharpener eraser eraser eraser eraser sharpener sharpener sharpener sharpener }

Before

Calculating the quantity of each item

tally_one = order_one.tally
# => { pen: 3, pencil: 1, eraser: 1, sharpener: 1 }

tally_two = order_two.tally
# => { sharpener: 5, eraser: 4 }
weekly_tally = (order_one + order_two).tally # order_one, order_two, ..., order_seven
# => { pen: 3, pencil: 1, eraser: 5, sharpener: 6 }

As we can see in the above example, the quantity of erasers and sharpeners has increased.

We have concatenated order_one and order_two arrays and then called #tally to get the combined count of the items ordered.

After

To calculate the total at runtime, Ruby 3.1 Enumerable#tally now accepts an optional hash to count occurrences.

In this case, we can store the running tally of the number of items and pass it to the #tally method.

order_one = %i{ pen pencil eraser sharpener pen pen }
order_two = %i{ sharpener eraser eraser eraser eraser sharpener sharpener sharpener sharpener }

weekly_tally = {}

weekly_tally = order_one.tally(weekly_tally)
# => {pen: 3, pencil: 1, eraser: 1, sharpener: 1}

weekly_tally = order_two.tally(weekly_tally)
# => {pen: 3, pencil: 1, eraser: 5, sharpener: 6}

# And so on

As we can see in the above example in Ruby 3.1, we have passed the weekly_tally hash as an argument to the #tally method that stores the count of items.

The count of erasers and sharpeners in order_two got added to the weekly_tally hash, returning the combined quantity for each item.

Note

  • A hash with a default value can be passed to the #tally method. The keys which are present in the array will ignore the default hash value and keys not present will return the default value instead of nil.
order = %i{ pen pencil eraser sharpener pen pen }
tally = order.tally(Hash.new 5)
# => { pen: 3, pencil: 1, eraser: 1, sharpener: 1 }

tally[:ruler]
# => 5
  • Similarly, Proc can be passed to the #tally method. If the key is present in the array Proc will be ignored. For keys not present in the array, the Proc gets executed.
order = %i{ pen pencil eraser sharpener pen pen }
tally = order.tally(Hash.new { puts "Proc called" })
# => { pen: 3, pencil: 1, eraser: 1, sharpener: 1 }

tally[:ruler]
Proc called
# => nil
  • Keys that are not present in the array are merged with the resulting hash. For eg., first_name and last_name that are not present in the order array got merged as it is.
order = %i{ pen pencil eraser sharpener pen pen }
tally = order.tally({ first_name: "Sam", last_name: "Example" }) # Allowed
# => { first_name: "Sam", last_name: "Example", pen: 3, pencil: 1, eraser: 1, sharpener: 1 }
  • Keys that are present in the array should only take integer value else it will raise a TypeError.
order = %i{ pen pencil eraser sharpener pen pen }
tally = order.tally({ pencil: "colored" }) # Not allowed
# => TypeError: wrong argument type String (expected Integer)