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"=>"[email protected]"}) 3=> <ActionController::Parameters {"firstName"=>"oliver", "lastName"=>"smith", "email"=>"[email protected]"} 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"=>"[email protected]"} 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"=>"[email protected]"} 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"}