207

I want to change every value in a hash so as to add '%' before and after the value so

{ :a=>'a' , :b=>'b' }

must be changed to

{ :a=>'%a%' , :b=>'%b%' }

What's the best way to do this?

1
  • 1
    Please clarify if you want to mutate the original string objects, just mutate the original has, or mutate nothing. Commented Mar 4, 2011 at 3:14

11 Answers 11

292

In Ruby 2.1 and higher you can do

{ a: 'a', b: 'b' }.map { |k, str| [k, "%#{str}%"] }.to_h
Sign up to request clarification or add additional context in comments.

8 Comments

Actually, this isn't available until Ruby v2.1
This is, though, very slow and very RAM hungry. The input Hash is iterated over to produce an intermediate set of nested Arrays which are then converted into a new Hash. Ignoring the RAM peak usage, run time is much worse - benchmarking this versus the modify-in-place solutions in another answer show 2.5s versus 1.5s over the same number of iterations. Since Ruby is a comparatively slow language, avoiding slow bits of the slow language makes a lot of sense :-)
@AndrewHodgkinson while in general I agree and am not advocating not paying attention to runtime performance, doesn't keeping track of all these performance pitfalls begin to become a pain and go against the "developer productivity first" philosophy of ruby? I guess this is less of a comment to you, and more of a general comment on the eventual paradox this brings us to, using ruby.
The conundrum being: well, we're already giving up performance in our decision to even use ruby, so what difference does "this other little bit" make? It's a slippery slope, ain't it? For the record, I do prefer this solution to the accepted answer, from a readability perspective.
If you use Ruby 2.4+, it's even easier to use #transform_values! as pointed out by sschmeck (stackoverflow.com/a/41508214/6451879).
|
193

If you want the actual strings themselves to mutate in place (possibly and desirably affecting other references to the same string objects):

# Two ways to achieve the same result (any Ruby version)
my_hash.each{ |_,str| str.gsub! /^|$/, '%' }
my_hash.each{ |_,str| str.replace "%#{str}%" }

If you want the hash to change in place, but you don't want to affect the strings (you want it to get new strings):

# Two ways to achieve the same result (any Ruby version)
my_hash.each{ |key,str| my_hash[key] = "%#{str}%" }
my_hash.inject(my_hash){ |h,(k,str)| h[k]="%#{str}%"; h }

If you want a new hash:

# Ruby 1.8.6+
new_hash = Hash[*my_hash.map{|k,str| [k,"%#{str}%"] }.flatten]

# Ruby 1.8.7+
new_hash = Hash[my_hash.map{|k,str| [k,"%#{str}%"] } ]

11 Comments

@Andrew Marshall Right you are, thanks. In Ruby 1.8, Hash.[] doesn't accept an array of array pairs, it requires an even number of direct arguments (hence the splat up front).
Actually, Hash.[key_value_pairs] was introduced in 1.8.7, so only Ruby 1.8.6 doesn't needs the splat & flatten.
@Aupajo Hash#each yields both the key and the value to the block. In this case, I didn't care about the key, and so I didn't name it anything useful. Variable names may begin with an underscore, and in fact may be just an underscore. There is no performance benefit of doing this, it's just a subtle self-documenting note that I'm not doing anything with that first block value.
I think you mean my_hash.inject(my_hash){ |h,(k,str)| h[k]="%#{str}%"; h }, have to return the hash from the block
Alternately, you might use the each_value method, which is a little easier to understand than using an underscore for the unused key value.
|
184

Ruby 2.4 introduced the method Hash#transform_values!, which you could use.

{ :a=>'a' , :b=>'b' }.transform_values! { |v| "%#{v}%" }
# => {:a=>"%a%", :b=>"%b%"} 

2 Comments

Of course there is also Hash#transform_values (without the bang), which doesn't modify the receiver. Otherwise a great answer, thanks!
This will really reduce my use of reduce :-p
92

The best way to modify a Hash's values in place is

hash.update(hash){ |_,v| "%#{v}%" }

Less code and clear intent. Also faster because no new objects are allocated beyond the values that must be changed.

4 Comments

Not exactly true: new strings are allocated. Still, an interesting solution that is effective. +1
@Phrogz good point; I updated the answer. The value allocation cannot be avoided in general because not all value transforms can be expressed as mutators such as gsub!.
Same as my answer but with another synonym, I agree that update conveys the intention better than merge!. I think this is the best answer.
If you don't use k, use _ instead.
28

A bit more readable one, map it to an array of single-element hashes and reduce that with merge

the_hash.map{ |key,value| {key => "%#{value}%"} }.reduce(:merge)

3 Comments

Clearer to use Hash[the_hash.map { |key,value| [key, "%#{value}%"] }]
This is an extremely inefficient way to update values. For every value pair it first creates a pair Array (for map) then a Hash. Then, each step of the reduce operation will duplicate the "memo" Hash and add the new key-value pair to it. At least use :merge! in reduce to modify the final Hash in place. And in the end, you are not modifying the values of the existing object but creating a new object, which is not what the question asked.
it returns nil if the_hash is empty
19

There is a new 'Rails way' method for this task :) http://api.rubyonrails.org/classes/Hash.html#method-i-transform_values

3 Comments

Also Ruby 2.4.0+ contains Hash#transform_values. This should be the way to go from now on.
In a nutshell, available for Rails 5+ or Ruby 2.4+
17

One method that doesn't introduce side-effects to the original:

h = {:a => 'a', :b => 'b'}
h2 = Hash[h.map {|k,v| [k, '%' + v + '%']}]

Hash#map may also be an interesting read as it explains why the Hash.map doesn't return a Hash (which is why the resultant Array of [key,value] pairs is converted into a new Hash) and provides alternative approaches to the same general pattern.

Happy coding.

[Disclaimer: I am not sure if Hash.map semantics change in Ruby 2.x]

2 Comments

Does Matz even know if Hash.map semantics change in Ruby 2.x?
The Hash#[] method is so useful, but so ugly. Is there a prettier method of converting arrays to hashes in the same way?
16
my_hash.each do |key, value|
  my_hash[key] = "%#{value}%"
end

2 Comments

I don't like side-effects, but +1 for the approach :) There is each_with_object in Ruby 1.9 (IIRC) which avoids needing to access the name directly and Map#merge may also work. Not sure how the intricate details differ.
The initial hash is modified -- this is okay if the behavior is anticipated but can cause subtle issues if "forgotten". I prefer to reduce object mutability, but it may not always be practical. (Ruby is hardly a "side-effect-free" language ;-)
8

Hash.merge! is the cleanest solution

o = { a: 'a', b: 'b' }
o.merge!(o) { |key, value| "%#{ value }%" }

puts o.inspect
> { :a => "%a%", :b => "%b%" }

Comments

5

After testing it with RSpec like this:

describe Hash do
  describe :map_values do
    it 'should map the values' do
      expect({:a => 2, :b => 3}.map_values { |x| x ** 2 }).to eq({:a => 4, :b => 9})
    end
  end
end

You could implement Hash#map_values as follows:

class Hash
  def map_values
    Hash[map { |k, v| [k, yield(v)] }]
  end
end

The function then can be used like this:

{:a=>'a' , :b=>'b'}.map_values { |v| "%#{v}%" }
# {:a=>"%a%", :b=>"%b%"}

Comments

3

If you are curious which inplace variant is the fastest here it is:

Calculating -------------------------------------
inplace transform_values! 1.265k (± 0.7%) i/s -      6.426k in   5.080305s
      inplace update      1.300k (± 2.7%) i/s -      6.579k in   5.065925s
  inplace map reduce    281.367  (± 1.1%) i/s -      1.431k in   5.086477s
      inplace merge!      1.305k (± 0.4%) i/s -      6.630k in   5.080751s
        inplace each      1.073k (± 0.7%) i/s -      5.457k in   5.084044s
      inplace inject    697.178  (± 0.9%) i/s -      3.519k in   5.047857s

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.