248

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.

Graham John
  • 170
  • 12
Manish Das
  • 3,785
  • 2
  • 21
  • 34

13 Answers13

825
hash[:new_key] = hash.delete :old_key
gayavat
  • 18,910
  • 11
  • 45
  • 55
  • 9
    Saved me a couple LOC, love it! – nicohvi Jun 15 '14 at 21:41
  • 13
    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. – Lucas Nov 13 '14 at 18:55
  • 3
    This should indeed be the accepted answer! Easy, clean and straight to the point! – parreirat Apr 02 '15 at 15:33
  • 2
    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. – Dsel Aug 21 '15 at 06:11
  • @RicardoOtero I would have! Check when this was answered compared to the accepted answer :) – Manish Das Sep 15 '15 at 21:51
  • @ManishDas, see my variant of solution for exact your problem http://stackoverflow.com/a/32651181/1007043 – gayavat Sep 18 '15 at 13:47
  • hash.transform_keys{ |key| key.to_s.upcase } http://api.rubyonrails.org/classes/Hash.html#method-i-transform_keys – gayavat Oct 09 '15 at 10:44
  • 3
    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 – Bart C Oct 16 '15 at 10:44
  • This worked for me, but I'm not sure how the new key replaces the old key.. When I read it I think set the value of a hash with a new key to delete an old key from the hash which just sounds strange. – Gwater17 Apr 29 '16 at 21:20
  • @MJGwater, I found that rails code looks the same for this action https://github.com/rails/rails/blob/64ae9a58ae54d7d036e930e75fac3e4a1ab0052e/activesupport/lib/active_support/core_ext/hash/keys.rb#L23 – gayavat Oct 05 '16 at 07:52
  • @huanson: As noted above, this doesn't actually answer the question because "I don't know what are the keys in the hash, they are random which comes with an "_" prefix for every key and I want no underscores". – mu is too short Oct 18 '16 at 06:08
  • @muistooshort you know how SO works, right? you google a problem, you then check the answers and try them out. if they don't work, then you start reading the questions ;) – Tim Kretschmer Oct 18 '16 at 06:36
  • For people who don't care about monkey-patching a base class of Ruby: `class Hash; def rename_key(old, new); self[new] = self.delete(old); end; end` – djvs Mar 27 '17 at 03:25
  • What if the key is not there? you'll add the new key with nil value :P – M.Prabha karan May 05 '17 at 08:57
  • 1
    This is the ruby way answer. – timlentse Jun 02 '17 at 06:35
  • 1
    The most Rubyish answer. Example with array of hashes: `hash=[{a:1},{a:2},{a:3}] hash.each do |h| h[:a_id]=h.delete :a end` – matiss Sep 11 '17 at 12:14
  • how is this the most upvotes?, OP said the key is random, so he don't know the value of `:new_key` and `:old_key` – buncis Jan 06 '22 at 05:25
  • I always refer back to this question somehow, so this is a ruby hack which adding a new key meanwhile deleting the old key at the same time in ruby when you deleting the key it also returning the value thus you could do this 2 task (creating a new key and deleting a key) in one line – buncis May 06 '23 at 06:38
161

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

gayavat
  • 18,910
  • 11
  • 45
  • 55
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.

mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • yours answer worked but after ward i found few hash like this – Manish Das Jun 02 '11 at 05:20
  • 3
    {"_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"} – Manish Das Jun 02 '11 at 05:22
  • 2
    {"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 Das Jun 02 '11 at 05:23
  • @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. – mu is too short Jun 02 '11 at 05:28
  • h.inject({ }) { |x, (k,v)| x[k.sub(/\A_/, '')] = v; x } worked perfectly ........thank you very much – Manish Das Jun 02 '11 at 05:32
  • 2
    @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". – mu is too short Jun 02 '11 at 05:36
  • @mu: h.keys.each do |k| if(k[0,1] == '_') h[k[1, k.length - 1]] = v h.delete(k) end end but v in this code shows error – Manish Das Jun 02 '11 at 05:39
  • @Manish: That "v" is for "mu is too confused and typing the wrong things", it should have been (and now is) "h[k]". – mu is too short Jun 02 '11 at 05:46
  • @BlairAnderson: No it isn't, you might want to read the question again: "I don't know what are the keys in the hash, they are random which comes with an "_" prefix for every key and I want no underscores". – mu is too short Jun 30 '16 at 16:33
  • @TasawarHussain Works fine in 2.3 as near as I can tell. Which one doesn't work for you? What's the error? Which version of Ruby? – mu is too short May 30 '17 at 07:09
  • @muistooshort i am using ruby 2.3 and get this error RuntimeError: can't add a new key into hash during iteration – Tasawar Hussain May 30 '17 at 12:13
  • @muistooshort give a look at this answer https://stackoverflow.com/a/27643890/6919038 – Tasawar Hussain May 30 '17 at 12:15
  • @TasawarHussain But which one in my answer is modifying the hash while iterating over it? I went through this to check when you tried to edit an erroneous warning into my answer and everything worked fine. – mu is too short May 30 '17 at 17:25
  • @muistooshort sorry for that, but i got the error when i ran this code h.each{ |k,v| h[k.to_s+"_"+v.to_s] = h[k]; h.delete(k) } appending value to the key with "_" – Tasawar Hussain May 31 '17 at 04:45
  • @TasawarHussain But you'll notice that there is no `h.each` in my answer specifically because iterating over something that you're modifying is never a good idea, there is an `h.keys.each` though and that should be safe. – mu is too short May 31 '17 at 05:42
  • @muistooshort in your case you'r just traversing keys of hash and modifying them, and in my case i am also modifying keys but traversing complete hash, coz i need values too to modify keys. I think both are similar cases So what should i do? – Tasawar Hussain May 31 '17 at 07:13
  • @TasawarHussain Hard to say without knowing what specifically you're doing. Creating a new hash is generally a better/easier idea than trying to modify something in-place. – mu is too short May 31 '17 at 17:28
  • @muistooshort i just wanted to append value to the key with "_". At the end i created a new hash , but question still remains https://stackoverflow.com/q/6210572/6919038 , dont mind – Tasawar Hussain Jun 02 '17 at 09:24
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')

Swapnil Chincholkar
  • 2,869
  • 3
  • 22
  • 22
  • 1
    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. – Manish Das Dec 05 '13 at 04:54
  • 4
    I came to this via a google search and wanted @Swapnil's answer. Thanks – toobulkeh Oct 21 '15 at 02:37
14

you can do

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

This should work for your case!

Sadiksha Gautam
  • 5,032
  • 6
  • 40
  • 71
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"}
Yurii Verbytskyi
  • 1,962
  • 3
  • 19
  • 27
10
h.inject({}) { |m, (k,v)| m[k.sub(/^_/,'')] = v; m }
DigitalRoss
  • 143,651
  • 25
  • 248
  • 329
  • 4
    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](http://homakov.blogspot.nl/2012/05/saferweb-injects-in-various-ruby.html) for an explanation. Hope you don't mind spreading the awareness. – Jorn van de Beek Sep 12 '12 at 14:47
3

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

B-M
  • 1,248
  • 9
  • 19
  • This wasn't included in ruby – MaicolBen Jun 12 '23 at 21:03
  • @MaicolBen, `tranform_keys` is available on Ruby 2.8+. See documentation: https://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} ``` – B-M Jun 14 '23 at 17:25
  • 1
    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 – MaicolBen Jun 14 '23 at 20:57
2

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.

2
hash.each {|k,v| hash.delete(k) && hash[k[1..-1]]=v if k[0,1] == '_'}
maerics
  • 151,642
  • 46
  • 269
  • 291
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"}
#   ]
# }

CTS_AE
  • 12,987
  • 8
  • 62
  • 63
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

Repolês
  • 1,563
  • 12
  • 14
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
stcho
  • 1,909
  • 3
  • 28
  • 45