259

I have a condition that gets a hash.

  hash = {"_id"=>"4de7140772f8be03da000018", .....}

Yet, I want to rename the key of that hash as follows.

  hash = {"id"=>"4de7140772f8be03da000018", ......}

P.S. I don't know what keys are in the hash; they are random. Some keys are prefixed with an underscore that I would like to remove.

4

13 Answers 13

844
hash[:new_key] = hash.delete :old_key
Sign up to request clarification or add additional context in comments.

19 Comments

Saved me a couple LOC, love it!
I often don't like "smart" ruby code because it takes some time to tell what it is really doing. Your solution is in other hand simple and descriptive.
This should indeed be the accepted answer! Easy, clean and straight to the point!
This answer is elegant but it doesn't actually answer the question. The post states that the keys which need replacing are unknown... We only know that they begin with an underscore, we don't know what the keys actually are.
so this creates a new key/value pair where you specify the new key and get the value from what hash.delete :old_key returns and the delete uses the old key. WOW, I want it tattooed somewhere :-D Thanks
|
165

rails Hash has standard method for it:

hash.transform_keys{ |key| key.to_s.upcase }

http://api.rubyonrails.org/classes/Hash.html#method-i-transform_keys

UPD: ruby 2.5 method

8 Comments

It's Rails method, not standard. Good answer though.
Also, this method can't operate with hash keys recursively.
deep_transform_keys can be used for it :) apidock.com/rails/v4.2.1/Hash/deep_transform_keys
At last! This is exactly what I searched!
This is a standard part of the language as of Ruby 2.5: docs.ruby-lang.org/en/trunk/Hash.html#method-i-transform_keys
|
39

If all the keys are strings and all of them have the underscore prefix, then you can patch up the hash in place with this:

h.keys.each { |k| h[k[1, k.length - 1]] = h[k]; h.delete(k) }

The k[1, k.length - 1] bit grabs all of k except the first character. If you want a copy, then:

new_h = Hash[h.map { |k, v| [k[1, k.length - 1], v] }]

Or

new_h = h.inject({ }) { |x, (k,v)| x[k[1, k.length - 1]] = v; x }

You could also use sub if you don't like the k[] notation for extracting a substring:

h.keys.each { |k| h[k.sub(/\A_/, '')] = h[k]; h.delete(k) }
Hash[h.map { |k, v| [k.sub(/\A_/, ''), v] }]
h.inject({ }) { |x, (k,v)| x[k.sub(/\A_/, '')] = v; x }

And, if only some of the keys have the underscore prefix:

h.keys.each do |k|
  if(k[0,1] == '_')
    h[k[1, k.length - 1]] = h[k]
    h.delete(k)
  end
end

Similar modifications can be done to all the other variants above but these two:

Hash[h.map { |k, v| [k.sub(/\A_/, ''), v] }]
h.inject({ }) { |x, (k,v)| x[k.sub(/\A_/, '')] = v; x }

should be okay with keys that don't have underscore prefixes without extra modifications.

18 Comments

yours answer worked but after ward i found few hash like this
{"_id"=>"4de7140772f8be03da000018", "_type"=>"WorkStation", "created_at"=>"2011-06-02T10:24:35+05:45", "input_header_ids"=>[], "line_id"=>"4de7140472f8be03da000017", "updated_at"=>"2011-06-02T10:24:35+05:45"}
{"id"=>"4de7140772f8be03da000018", "type"=>"WorkStation", "reated_at"=>"2011-06-02T10:24:35+05:45", "nput_header_ids"=>[], "ine_id"=>"4de7140472f8be03da000017", "pdated_at"=>"2011-06-02T10:24:35+05:45"}
@Manish: I did say "If all the keys are strings and all of them have the underscore prefix". I included an example approach for your "keys without underscore prefixes" in an update.
@Manish: "k" is for "key", "v" is for "value", "x" is for "I don't know what to call it but I was trained as a mathematician so I call it x".
|
14

you can do

hash.inject({}){|option, (k,v) | option["id"] = v if k == "_id"; option}

This should work for your case!

Comments

14

If we want to rename a specific key in hash then we can do it as follows:
Suppose my hash is my_hash = {'test' => 'ruby hash demo'}
Now I want to replace 'test' by 'message', then:
my_hash['message'] = my_hash.delete('test')

2 Comments

How is your answer is then solution for my problem? If you thought this was helpful, then you could have added in the comment under the question. My question wasn't to replace a key with another key, the solution you gave is very basic Hash property. in my case it's not : hash[:new_key] = has[:old_key], instead it's : hash[:dynamic_key] = hash[:_dynamic_key], it was clear question on regex and not simple hash replace.
I came to this via a google search and wanted @Swapnil's answer. Thanks
13

For Ruby 2.5 or newer with transform_keys and delete_prefix / delete_suffix methods:

hash1 = { '_id' => 'random1' }
hash2 = { 'old_first' => '123456', 'old_second' => '234567' }
hash3 = { 'first_com' => 'google.com', 'second_com' => 'amazon.com' }

hash1.transform_keys { |key| key.delete_prefix('_') }
# => {"id"=>"random1"}
hash2.transform_keys { |key| key.delete_prefix('old_') }
# => {"first"=>"123456", "second"=>"234567"}
hash3.transform_keys { |key| key.delete_suffix('_com') }
# => {"first"=>"google.com", "second"=>"amazon.com"}

1 Comment

Much Appreciated!
10
h.inject({}) { |m, (k,v)| m[k.sub(/^_/,'')] = v; m }

1 Comment

I like that you tried to use a regex to filter out the underscores properly, but you should be aware that in ruby, unlike javascript and others, /^/ means 'start of string OR LINE', and /$/ means 'end of string OR LINE'. It's unlikely that the keys have newlines in them in this case, but you should be aware that using those two operators in ruby is not only error prone but also very dangerous when used wrong in validations against injections. See here for an explanation. Hope you don't mind spreading the awareness.
7

If you only want to change only one key, there is a straightforward way to do it in Ruby 2.8+ using the transform_keys method. In this example, if you want to change _id to id, then you can:

 hash.transform_keys({_id: :id})

Ruby docs: https://ruby-doc.org/core-3.0.0/Hash.html#method-i-transform_keys

Reference: https://bugs.ruby-lang.org/issues/16274

3 Comments

This wasn't included in ruby
@MaicolBen, tranform_keys is available on Ruby 2.8+. See documentation: ruby-doc.org/core-3.0.0/Hash.html#method-i-transform_keys. Example: ruby h = {foo: 0, bar: 1, baz: 2} h.transform_keys(foo: :bar, bar: :foo) #=> {bar: 0, foo: 1, baz: 2} h.transform_keys(foo: :hello, &:to_s) #=> {:hello=>0, "bar"=>1, "baz"=>2}
Sorry, I must have done something wrong when I tried the first time, you are right. I should have checked the doc since it says it can receive a hash
3

Previous answers are good enough, but they might update original data. In case if you don't want the original data to be affected, you can try my code.

 newhash=hash.reject{|k| k=='_id'}.merge({id:hash['_id']})

First it will ignore the key '_id' then merge with the updated one.

Comments

2
hash.each {|k,v| hash.delete(k) && hash[k[1..-1]]=v if k[0,1] == '_'}

Comments

1

I went overkill and came up with the following. My motivation behind this was to append to hash keys to avoid scope conflicts when merging together/flattening hashes.

Examples

Extend Hash Class

Adds rekey method to Hash instances.

# Adds additional methods to Hash
class ::Hash
  # Changes the keys on a hash
  # Takes a block that passes the current key
  # Whatever the block returns becomes the new key
  # If a hash is returned for the key it will merge the current hash 
  # with the returned hash from the block. This allows for nested rekeying.
  def rekey
    self.each_with_object({}) do |(key, value), previous|
      new_key = yield(key, value)
      if new_key.is_a?(Hash)
        previous.merge!(new_key)
      else
        previous[new_key] = value
      end
    end
  end
end

Prepend Example

my_feelings_about_icecreams = {
  vanilla: 'Delicious',
  chocolate: 'Too Chocolatey',
  strawberry: 'It Is Alright...'
}

my_feelings_about_icecreams.rekey { |key| "#{key}_icecream".to_sym }
# => {:vanilla_icecream=>"Delicious", :chocolate_icecream=>"Too Chocolatey", :strawberry_icecream=>"It Is Alright..."}

Trim Example

{ _id: 1, ___something_: 'what?!' }.rekey do |key|
  trimmed = key.to_s.tr('_', '')
  trimmed.to_sym
end
# => {:id=>1, :something=>"what?!"}

Flattening and Appending a "Scope"

If you pass a hash back to rekey it will merge the hash which allows you to flatten collections. This allows us to add scope to our keys when flattening a hash to avoid overwriting a key upon merging.

people = {
  bob: {
    name: 'Bob',
    toys: [
      { what: 'car', color: 'red' },
      { what: 'ball', color: 'blue' }
    ]
  },
  tom: {
    name: 'Tom',
    toys: [
      { what: 'house', color: 'blue; da ba dee da ba die' },
      { what: 'nerf gun', color: 'metallic' }
    ]
  }
}

people.rekey do |person, person_info|
  person_info.rekey do |key|
    "#{person}_#{key}".to_sym
  end
end

# =>
# {
#   :bob_name=>"Bob",
#   :bob_toys=>[
#     {:what=>"car", :color=>"red"},
#     {:what=>"ball", :color=>"blue"}
#   ],
#   :tom_name=>"Tom",
#   :tom_toys=>[
#     {:what=>"house", :color=>"blue; da ba dee da ba die"},
#     {:what=>"nerf gun", :color=>"metallic"}
#   ]
# }

Comments

1

Answering exactly what was asked:

hash = {"_id"=>"4de7140772f8be03da000018"}
hash.transform_keys { |key| key[1..] }
# => {"id"=>"4de7140772f8be03da000018"}

The method transform_keys exists in the Hash class since Ruby version 2.5.

https://blog.bigbinary.com/2018/01/09/ruby-2-5-adds-hash-transform_keys-method.html

Comments

1

If you had a hash inside a hash, something like

hash = {
  "object" => {
     "_id"=>"4de7140772f8be03da000018"
  }
}

and if you wanted to change "_id" to something like"token"

you can use deep_transform_keys here and do it like so

hash.deep_transform_keys do |key|
  key = "token" if key == "_id"
  key
end

which results in

{
  "object" => {
     "token"=>"4de7140772f8be03da000018"
  }
}

Even if you had a symbol key hash instead to start with, something like

hash = {
  object: {
     id: "4de7140772f8be03da000018"
  }
}

you can combine all of these concepts to convert them into a string key hash

hash.deep_transform_keys do |key|
  key = "token" if key == :id
  key.to_s
end

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.