Ruby 3 supports transforming hash keys using a hash argument

Yedhin Kizhakkethara

Yedhin Kizhakkethara

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. Declare address hash
irb(main)> address = {House: 'Kizhakkethara', house_no: 123, locality: 'India'}
=> {:House=>"Kizhakkethara", :house_no=>123, :locality=>"India"}

# 2. Lowercase all the keys
irb(main)> address.transform_keys(&:downcase)
=> {:house=>"Kizhakkethara", :house_no=>123, :locality=>"India"}

# 3. Replace a particular key with a new key along with lowercasing
irb(main)* address.transform_keys do |key|
irb(main)*   new_key = key
irb(main)*   if key == :locality
irb(main)*     new_key = :country
irb(main)*   end
irb(main)*   new_key.to_s.downcase.to_sym
irb(main)> end
=> {: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. Declare address hash
irb(main)> address = {House: 'Kizhakkethara', house_no: 123, locality: 'India'}
=> {:House=>"Kizhakkethara", :house_no=>123, :locality=>"India"}

# 2. Provide hash with transform_keys
irb(main)> address.transform_keys({House: :house, locality: :country})
=> {: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. Passing in block parameters
irb(main)> address.transform_keys({locality: :country}, &:downcase)
=> {: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. Declare params
irb(rails)> params = ActionController::Parameters.new({"firstName"=>"oliver", "lastName"=>"smith", "email"=>"[email protected]"})
=> <ActionController::Parameters {"firstName"=>"oliver", "lastName"=>"smith", "email"=>"[email protected]"} permitted: false>

# 2. Convert camelCase to snake_case using block parameter
irb(rails)> params.permit(:firstName, :lastName, :email).transform_keys(&:underscore)
=> <ActionController::Parameters {"first_name"=>"oliver", "last_name"=>"smith", "email"=>"[email protected]"} permitted: true>

# 3. Or using hash argument
irb(rails)> params.permit(:firstName, :lastName, :email).transform_keys({firstName: 'first_name', lastName: 'last_name'})
=> <ActionController::Parameters {"first_name"=>"oliver", "last_name"=>"smith", "email"=>"[email protected]"} permitted: true>
Slicing hash along with key transformation
irb(main)> address.transform_keys({locality: :country}).slice(:house_no, :country)
=> {:house_no=>123, :country=>"India"}
Transforming keys in place using bang counterpart
irb(main)> address.transform_keys!({locality: :country}, &:downcase)
irb(main)> address
=> {:house=>"Kizhakkethara", :house_no=>123, :country=>"India"}
References
  • Discussions regarding this feature can be found here.
  • Commit for this feature can be found here.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.