145

I'd like to replace each value in a hash with value.some_method.

For example, for given a simple hash:

{"a" => "b", "c" => "d"}` 

every value should be .upcased, so it looks like:

{"a" => "B", "c" => "D"}

I tried #collect and #map but always just get arrays back. Is there an elegant way to do this?

UPDATE

Damn, I forgot: The hash is in an instance variable which should not be changed. I need a new hash with the changed values, but would prefer not to define that variable explicitly and then loop over the hash filling it. Something like:

new_hash = hash.magic{ ... }
potashin
  • 44,205
  • 11
  • 83
  • 107

12 Answers12

237
my_hash.each { |k, v| my_hash[k] = v.upcase } 

or, if you'd prefer to do it non-destructively, and return a new hash instead of modifying my_hash:

a_new_hash = my_hash.inject({}) { |h, (k, v)| h[k] = v.upcase; h } 

This last version has the added benefit that you could transform the keys too.

kch
  • 77,385
  • 46
  • 136
  • 148
  • 30
    The first suggestion is not appropriate; modifying an enumerable element while iterating leads to undefined behavior - this is valid in other languages also. Here's a reply from Matz (he replies to an example using an Array, but the answer is generic): http://j.mp/tPOh2U – Marcus Dec 06 '11 at 15:01
  • 4
    @Marcus I agree that in general such modification should be avoided. But in this case it's probably safe, because the modification doesn't change future iterations. Now if another key (that wasn't the one passed to the block) was assigned, then there would be trouble. – Kelvin Mar 20 '12 at 18:15
  • 37
    @Adam @kch `each_with_object` is a more convenient version of `inject` when you don't want to end the block with the hash: `my_hash.each_with_object({}) {|(k, v), h| h[k] = v.upcase }` – Kelvin Mar 20 '12 at 18:42
  • 6
    There is also a very neat syntax to convert a list of pairs into a hash, so you can write `a_new_hash = Hash[ my_hash.map { |k, v| [k, v.upcase] } ]` – rewritten Mar 31 '14 at 11:27
  • 2
    with Ruby 2 you can write `.to_h` – roman-roman Sep 12 '14 at 07:20
  • 2
    See @potashin 's answer below for a better, modern solution. – David Hempy May 30 '19 at 20:32
132

Since ruby 2.4.0 you can use native Hash#transform_values method:

hash = {"a" => "b", "c" => "d"}
new_hash = hash.transform_values(&:upcase)
# => {"a" => "B", "c" => "D"}

There is also destructive Hash#transform_values! version.

schmijos
  • 8,114
  • 3
  • 50
  • 58
potashin
  • 44,205
  • 11
  • 83
  • 107
  • 7
    great, it also comes with rails 4.2.1 https://apidock.com/rails/v4.2.1/Hash/transform_values – rj487 Aug 25 '17 at 07:44
36

You can collect the values, and convert it from Array to Hash again.

Like this:

config = Hash[ config.collect {|k,v| [k, v.upcase] } ]
MatzFan
  • 877
  • 8
  • 17
Endel Dreyer
  • 1,644
  • 18
  • 27
25

This will do it:

my_hash.each_with_object({}) { |(key, value), hash| hash[key] = value.upcase }

As opposed to inject the advantage is that you are in no need to return the hash again inside the block.

Konrad Reiche
  • 27,743
  • 15
  • 106
  • 143
12

There's a method for that in ActiveSupport v4.2.0. It's called transform_values and basically just executes a block for each key-value-pair.

Since they're doing it with a each I think there's no better way than to loop through.

hash = {sample: 'gach'}

result = {}
hash.each do |key, value|
  result[key] = do_stuff(value)
end

Update:

Since Ruby 2.4.0 you can natively use #transform_values and #transform_values!.

schmijos
  • 8,114
  • 3
  • 50
  • 58
10

Try this function:

h = {"a" => "b", "c" => "d"}
h.each{|i,j| j.upcase!} # now contains {"a" => "B", "c" => "D"}.
Somnath Muluk
  • 55,015
  • 38
  • 216
  • 226
Chris Doggett
  • 19,959
  • 4
  • 61
  • 86
  • 6
    that's good, but it should be noted that it only works for when j has a destructive method that acts unto itself. So it can't handle the general value.some_method case. – kch May 01 '09 at 18:24
  • Good point, and with his update, your second solution (non-destructive) is the best. – Chris Doggett May 01 '09 at 18:28
4

You may want to go a step further and do this on a nested hash. Certainly this happens a fair amount with Rails projects.

Here's some code to ensure a params hash is in UTF-8:

  def convert_hash hash
    hash.inject({}) do |h,(k,v)|
      if v.kind_of? String
        h[k] = to_utf8(v) 
      else
        h[k] = convert_hash(v)
      end
      h
    end      
  end    

  # Iconv UTF-8 helper
  # Converts strings into valid UTF-8
  #
  # @param [String] untrusted_string the string to convert to UTF-8
  # @return [String] your string in UTF-8
  def to_utf8 untrusted_string=""
    ic = Iconv.new('UTF-8//IGNORE', 'UTF-8')
    ic.iconv(untrusted_string + ' ')[0..-2]
  end  
Tokumine
  • 51
  • 1
1

Ruby has the tap method (1.8.7, 1.9.3 and 2.1.0) that's very useful for stuff like this.

original_hash = { :a => 'a', :b => 'b' }
original_hash.clone.tap{ |h| h.each{ |k,v| h[k] = v.upcase } }
# => {:a=>"A", :b=>"B"}
original_hash # => {:a=>"a", :b=>"b"}
Nate
  • 12,963
  • 4
  • 59
  • 80
1

Rails-specific

In case someone only needs to call to_s method to each of the values and is not using Rails 4.2 ( which includes transform_values method link), you can do the following:

original_hash = { :a => 'a', :b => BigDecimal('23.4') }
#=> {:a=>"a", :b=>#<BigDecimal:5c03a00,'0.234E2',18(18)>}
JSON(original_hash.to_json)
#=> {"a"=>"a", "b"=>"23.4"}

Note: The use of 'json' library is required.

Note 2: This will turn keys into strings as well

lllllll
  • 4,715
  • 6
  • 29
  • 42
1

If you know that the values are strings, you can call the replace method on them while in the cycle: this way you will change the value.

Altohugh this is limited to the case in which the values are strings and hence doesn't answer the question fully, I thought it can be useful to someone.

reallynice
  • 1,289
  • 2
  • 21
  • 41
1
new_hash = old_hash.merge(old_hash) do |_key, value, _value|
             value.upcase
           end

# old_hash = {"a" => "b", "c" => "d"}
# new_hash = {"a" => "B", "c" => "D"}
cnst
  • 11
  • 1
1

I do something like this:

new_hash = Hash[*original_hash.collect{|key,value| [key,value + 1]}.flatten]

This provides you with the facilities to transform the key or value via any expression also (and it's non-destructive, of course).