0

I have a hash-

h1 = {"a"=>{"x"=>"1","y"=>"2"}, "b"=>"z"}

I have another hash h2 which basically has updated values for some keys in h1

h2 = {"a"=>{"x"=>"3"}, "b"=>"q"}

when I do a -

h1.merge(h2)

I am expecting it to return-

{"a"=>{"x"=>"3","y"=>"2"}, "b"=>"q"}

But it actually gives me- {"a"=>{"x"=>"3"}, "b"=>"q"}

What do I need to do to get {"a"=>{"x"=>"3","y"=>"2"}, "b"=>"q"} ?

I am not using rails. The ruby version is 2.6.0

lurker
  • 56,987
  • 9
  • 69
  • 103
Rahul Vig
  • 716
  • 1
  • 7
  • 24
  • If the key exists in both, then the key value of the merged-in hash replaces the the first one in all cases. What would you want it to do if you had, say, `h1 = {"a" => "x", "b" => {"a" => "4", "b" => "5"}` and `h2 = {"a" => {"f" => "1", "g" => "2"}, "b" => "y"}`? – lurker May 08 '20 at 15:54
  • 1
    if you can’t use deep_merge of Rails then maybe you want to implement it yourself. There is quite an elegant Ruby solution here: https://stackoverflow.com/a/32268934/1505529. However, as pointed by @lurker, in your case the values are not always hashes, you’ll have to add some logic – Olkin May 08 '20 at 16:00

1 Answers1

3

The way Hash#merge works is consistent: if the key exists in both, then the key value of the merged-in hash replaces the the first one in all cases. You'll need a recursive merge.

There is a deep_merge available in Rails. But if you're not using Rails, or if it doesn't suit your requirements, you can roll your own fairly easily.

Hash#merge does support a block which can help:

h1.merge(h2) { |k, old, new| (old.instance_of?(Hash) && new.instance_of?(Hash)) ?
                             old.merge(new) : new }

This will work if you just have one layer deep of embedded hashes. If you have an arbitrary depth of nested hashes, you can monkey patch Hash with a deep_merge:

class Hash
  def deep_merge(h)
    self.merge(h) { |k, old, new| (old.instance_of?(Hash) && new.instance_of?(Hash)) ?
                            old.deep_merge(new) : new }
  end
end

Or something like that... :) This will recursively merge if there is a hash in both cases, otherwise, it replaces as usual. You can modify it to your taste.

Trying this out for your case:

2.6.1 :008 > class Hash
2.6.1 :009?>   def deep_merge(h)
2.6.1 :010?>     self.merge(h) { |k, old, new| (old.instance_of?(Hash) && new.instance_of?(Hash)) ?
2.6.1 :011 >                                   old.deep_merge(new) : new }
2.6.1 :012?>   end
2.6.1 :013?> end
 => :deep_merge
2.6.1 :014 > h1 = {"a"=>{"x"=>"1","y"=>"2"}, "b"=>"z"}
 => {"a"=>{"x"=>"1", "y"=>"2"}, "b"=>"z"}
2.6.1 :015 > h2 = {"a"=>{"x"=>"3"}, "b"=>"q"}
 => {"a"=>{"x"=>"3"}, "b"=>"q"}
2.6.1 :016 > h1.deep_merge(h2)
 => {"a"=>{"x"=>"3", "y"=>"2"}, "b"=>"q"}
2.6.1 :017 >
lurker
  • 56,987
  • 9
  • 69
  • 103
  • You should use ‘&&’ instead of ‘and’ – Olkin May 08 '20 at 16:03
  • 1
    @Olkin yes, got it, thanks. With the higher precedence of `&&` (versus `and`) I needed to add some parentheses... – lurker May 08 '20 at 16:11
  • No you don't, parentheses are needed with the keyword `and` because `? :` has a higher precedence than `and`, so only the last expression will be evaluated for `and`. `&&` has a higher precedence than `? :`, so you don't need parentheses. The rule of thumb is use `and` for flow control and `&&` for boolean logic. – 3limin4t0r May 08 '20 at 16:19
  • @3limin4t0r my point was that after changing `and` to `&&` I needed the parentheses around the argument for `instance_of?`. That's a hard fact. Syntax errors occurred without them. I'm not complaining or saying it's good or bad or otherwise. I needed the parentheses around the argumen for `instance_of?`. This had nothing to do with the tertiary operator. I did not change those. – lurker May 08 '20 at 16:54
  • 1
    @lurker Ah, I thought you meant the parentheses around `(old.instance_of?(Hash) && new.instance_of?(Hash))`, those can be left out. – 3limin4t0r May 08 '20 at 16:59
  • 1
    Yeah the outer ones? I have a habit of always using those for clarity whether they are required or not. :) – lurker May 08 '20 at 16:59