Ruby 3 supports transforming hash keys using a hash argument

Yedhin Kizhakkethara

By Yedhin Kizhakkethara

on December 15, 2020

This blog is part of our  Ruby 3.0 series.

From Ruby 3 onwards, the Hash#transform_keys method accepts a hash argument for transforming existing keys to new keys as specified in the argument.

Usage before Ruby 3

The following example shows how we used to apply transform_keys:

1# 1. Declare address hash
2irb(main)> address = {House: 'Kizhakkethara', house_no: 123, locality: 'India'}
3=> {:House=>"Kizhakkethara", :house_no=>123, :locality=>"India"}
4
5# 2. Lowercase all the keys
6irb(main)> address.transform_keys(&:downcase)
7=> {:house=>"Kizhakkethara", :house_no=>123, :locality=>"India"}
8
9# 3. Replace a particular key with a new key along with lowercasing
10irb(main)* address.transform_keys do |key|
11irb(main)*   new_key = key
12irb(main)*   if key == :locality
13irb(main)*     new_key = :country
14irb(main)*   end
15irb(main)*   new_key.to_s.downcase.to_sym
16irb(main)> end
17=> {:house=>"Kizhakkethara", :house_no=>123, :country=>"India"}

Although the changes required are trivial, we ended up writing a block to do the job. But what happens when the number of keys that needs to be transformed increases? Do we need to write n-number of conditions within a block? Not anymore!

Introducing Hash#transform_keys with hash argument

Let's take the same example and provide a hash, which will be used for the transformation:

1# 1. Declare address hash
2irb(main)> address = {House: 'Kizhakkethara', house_no: 123, locality: 'India'}
3=> {:House=>"Kizhakkethara", :house_no=>123, :locality=>"India"}
4
5# 2. Provide hash with transform_keys
6irb(main)> address.transform_keys({House: :house, locality: :country})
7=> {:house=>"Kizhakkethara", :house_no=>123, :country=>"India"}

That does the job. But let's try to improve this code. Ultimately what happens when we invoke that method is that it goes through each of the keys in our variable and maps the existing keys to the new keys. The transform_keys method accepts a block as a parameter. Thus let's pass in the downcase method as a Proc argument:

1# 1. Passing in block parameters
2irb(main)> address.transform_keys({locality: :country}, &:downcase)
3=> {:house=>"Kizhakkethara", :house_no=>123, :country=>"India"}

An important point to be noted about the block parameter is that, it's only applied to keys which are not specified in the hash argument.

Other common use cases

Transforming params received in the Rails controller
1# 1. Declare params
2irb(rails)> params = ActionController::Parameters.new({"firstName"=>"oliver", "lastName"=>"smith", "email"=>"oliver@bigbinary.com"})
3=> <ActionController::Parameters {"firstName"=>"oliver", "lastName"=>"smith", "email"=>"oliver@bigbinary.com"} permitted: false>
4
5# 2. Convert camelCase to snake_case using block parameter
6irb(rails)> params.permit(:firstName, :lastName, :email).transform_keys(&:underscore)
7=> <ActionController::Parameters {"first_name"=>"oliver", "last_name"=>"smith", "email"=>"oliver@bigbinary.com"} permitted: true>
8
9# 3. Or using hash argument
10irb(rails)> params.permit(:firstName, :lastName, :email).transform_keys({firstName: 'first_name', lastName: 'last_name'})
11=> <ActionController::Parameters {"first_name"=>"oliver", "last_name"=>"smith", "email"=>"oliver@bigbinary.com"} permitted: true>
Slicing hash along with key transformation
1irb(main)> address.transform_keys({locality: :country}).slice(:house_no, :country)
2=> {:house_no=>123, :country=>"India"}
Transforming keys in place using bang counterpart
1irb(main)> address.transform_keys!({locality: :country}, &:downcase)
2irb(main)> address
3=> {:house=>"Kizhakkethara", :house_no=>123, :country=>"India"}
References
  • Discussions regarding this feature can be found here.
  • Commit for this feature can be found here.

Stay up to date with our blogs. Sign up for our newsletter.

We write about Ruby on Rails, ReactJS, React Native, remote work,open source, engineering & design.