herophuong
herophuong

Reputation: 364

Deep merge Ruby hashes with no new keys

How can I deep merge two Ruby hash so that the second hash only override the first hash without adding any key to it?

Example:

Merge

{
  "dog" => {
    "ear" => "big",
    "hair" => "smooth"
  }
}

with

{
  "dog" => {
    "ear" => "small",
    "tail" => "curve"
  }
}

will have result

{
  "dog" => {
    "ear" => "small", # Only override key "ear"
    "hair" => "smooth"
    # Without adding key "tail"
  }
}

Upvotes: 1

Views: 716

Answers (5)

Малъ Скрылевъ
Малъ Скрылевъ

Reputation: 16505

There is the example here to deep merge hash with an other value, even it is not a hash.

class Hash
   def deep_merge other
      return self if other.nil? or other == {}
      
      other_hash = other.is_a?(Hash) && other || { nil => other }
      common_keys = self.keys & other_hash.keys
      base_hash = (other_hash.keys - common_keys).reduce({}) do |res, key|
         res[key] = other_hash[key]
         res
      end
      
      self.reduce(base_hash) do |res, (key, value)|
         new =
         if common_keys.include?(key)
            case value
            when Hash
               value.deep_merge(other_hash[key])
            when Array
               value.concat([ other_hash[key] ].compact.flatten(1))
            when NilClass
               other_hash[key]
            else
               [ value, other_hash[key] ].compact.flatten(1)
            end
         else
            value
         end
         
         res[key] = new
         res
      end
   end
end

It allows to correctly merge embedded values, when they are of incompatible types, like hash and array, array and non-array, etc.

Upvotes: 0

Windor C
Windor C

Reputation: 1120

I think your answer is easy-understanding enough.

To know if a hash includes some key, you can just use has_key?, key? or include? methods.

Here is my code.

Hash.send :define_method, :exclusive_merge do |other|
  other.each do |k, v|
    if self.has_key?(k)
      if self[k].is_a?(Hash) and v.is_a?(Hash)
        self[k].exclusive_merge v
      else
        self[k] = v
      end
    end
  end
  self
end

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110725

Recursively (and similar to @Tim's answer, which I hadn't seen when I started),

def merge_em(h1, h2)
  h1.each_key do |k|
    if h2.key?(k)
      if h1[k].is_a? Hash
        merge_em(h1[k], h2[k]) 
      else
        h1[k] = h2[k]
      end
    end
  end   
end           

h = Marshal.load(Marshal.dump(h1))

merge_em(h, h2)

I've used Marshal.load(Marshal.dump(h1)) to make a deep copy of h1. If it's OK to change h1, just

merge_em(h1, h2)

Upvotes: 0

Tim Morgan
Tim Morgan

Reputation: 1184

Seems like something I would ask in an interview. I hope you're not cheating ;-)

def my_merge(h1, h2)
  h1.inject({}) do |h, (k, v)|
    if Hash === v
      h[k] = my_merge(v, h2[k] || {})
    else
      h[k] = h2[k] || h1[k]
    end
    h
  end
end

And a test :-)

require 'minitest/autorun'

describe 'my_merge' do

  it "maintains same keys" do
    h1 = {
      "dog" => {
        "ear" => "big",
        "hair" => "smooth"
      }
    }

    h2 = {
      "dog" => {
        "ear" => "small",
        "tail" => "curve"
      }
    }

    expected = {
      "dog" => {
        "ear" => "small", # Only override key "ear"
        "hair" => "smooth"
        # Without adding key "tail"
      }
    }
    my_merge(h1, h2).must_equal(expected)
  end
end

Upvotes: 1

herophuong
herophuong

Reputation: 364

So I answer my own question with two versions:

  1. Procedural style

    def exclusive_deep_merge(merge_to, merge_from)
      merged = merge_to.clone
      merge_from.each do |key, value|
        # Only override existing key
        if merged.keys.include?(key)
          # Deep merge for nested hash
          if value.is_a?(Hash) && merged[key].is_a?(Hash)
            merged[key] = exclusive_deep_merge(merged[key], value)
          else
            merged[key] = value
          end
        end
      end
      merged
    end
    
  2. The monkey-patching

    class Hash
      def exclusive_deep_merge(other_hash)
        dup.exclusive_deep_merge!(other_hash)
      end
    
      def exclusive_deep_merge!(other_hash)
        other_hash.each_pair do |k,v|
          if self.keys.include? k
            self[k] = self[k].is_a?(Hash) && v.is_a?(Hash) ? self[k].exclusive_deep_merge(v) : v
          end
        end
        self
      end
    end
    

Any comments on improvement are warmly welcome ♥

Upvotes: 1

Related Questions