Merging multi-dimensional hashes in Ruby

前端 未结 6 2126
轮回少年
轮回少年 2020-12-07 02:38

I have two hashes which have a structure something similar to this:

hash_a = { :a => { :b => { :c => \"d\" } } }
hash_b = { :a => { :b => { :x         


        
相关标签:
6条回答
  • 2020-12-07 02:38

    Ruby's existing Hash#merge allows a block form for resolving duplicates, making this rather simple. I've added functionality for merging multiple conflicting values at the 'leaves' of your tree into an array; you could choose to pick one or the other instead.

    hash_a = { :a => { :b => { :c => "d", :z => 'foo' } } }
    hash_b = { :a => { :b => { :x => "y", :z => 'bar' } } }
    
    def recurse_merge(a,b)
      a.merge(b) do |_,x,y|
        (x.is_a?(Hash) && y.is_a?(Hash)) ? recurse_merge(x,y) : [*x,*y]
      end
    end
    
    p recurse_merge( hash_a, hash_b )
    #=> {:a=>{:b=>{:c=>"d", :z=>["foo", "bar"], :x=>"y"}}}
    

    Or, as a clean monkey-patch:

    class Hash
      def merge_recursive(o)
        merge(o) do |_,x,y|
          if x.respond_to?(:merge_recursive) && y.is_a?(Hash)
            x.merge_recursive(y)
          else
            [*x,*y]
          end
        end
      end
    end
    
    p hash_a.merge_recursive hash_b
    #=> {:a=>{:b=>{:c=>"d", :z=>["foo", "bar"], :x=>"y"}}}
    
    0 讨论(0)
  • 2020-12-07 02:44

    If you change the first line of recursive_merge to

    merged_hash = merge_to.clone
    

    it works as expected:

    recursive_merge(hash_a, hash_b)    
    ->    {:a=>{:b=>{:c=>"d", :x=>"y"}}}
    

    Changing the hash as you move through it is troublesome, you need a "work area" to accumulate your results.

    0 讨论(0)
  • 2020-12-07 02:45

    Try this monkey-patching solution:

    class Hash
      def recursive_merge(hash = nil)
        return self unless hash.is_a?(Hash)
        base = self
        hash.each do |key, v|
          if base[key].is_a?(Hash) && hash[key].is_a?(Hash)
            base[key].recursive_merge(hash[key])
          else
            base[key]= hash[key]
          end
        end
        base
      end
    end
    
    0 讨论(0)
  • 2020-12-07 02:56

    You can do it in one line :

    merged_hash = hash_a.merge(hash_b){|k,hha,hhb| hha.merge(hhb){|l,hhha,hhhb| hhha.merge(hhhb)}}
    

    If you want to imediatly merge the result into hash_a, just replace the method merge by the method merge!

    If you are using rails 3 or rails 4 framework, it is even easier :

    merged_hash = hash_a.deep_merge(hash_b)
    

    or

    hash_a.deep_merge!(hash_b)
    
    0 讨论(0)
  • 2020-12-07 02:57

    In order to merge one into the other as the ticket suggested, you could modify @Phrogz function

    def recurse_merge( merge_from, merge_to )
      merge_from.merge(merge_to) do |_,x,y|
        (x.is_a?(Hash) && y.is_a?(Hash)) ? recurse_merge(x,y) : x
      end
    end
    

    In case there is duplicate key, it will only use the content of merge_from hash

    0 讨论(0)
  • 2020-12-07 03:04

    Here is even better solution for recursive merging that uses refinements and has bang method alongside with block support. This code does work on pure Ruby.

    module HashRecursive
        refine Hash do
            def merge(other_hash, recursive=false, &block)
                if recursive
                    block_actual = Proc.new {|key, oldval, newval|
                        newval = block.call(key, oldval, newval) if block_given?
                        [oldval, newval].all? {|v| v.is_a?(Hash)} ? oldval.merge(newval, &block_actual) : newval
                    }   
                    self.merge(other_hash, &block_actual)
                else
                    super(other_hash, &block)
                end
            end
            def merge!(other_hash, recursive=false, &block)
                if recursive
                    self.replace(self.merge(other_hash, recursive, &block))
                else
                    super(other_hash, &block)
                end
            end
        end
    end
    
    using HashRecursive
    

    After using HashRecursive was executed you can use default Hash::merge and Hash::merge! as if they haven't been modified. You can use blocks with these methods as before.

    The new thing is that you can pass boolean recursive (second argument) to these modified methods and they will merge hashes recursively.


    Example usage for answering the question. It's extremely easy:

    hash_a  =   { :a => { :b => { :c => "d" } } }
    hash_b  =   { :a => { :b => { :x => "y" } } }
    
    puts hash_a.merge(hash_b)                                   # Won't override hash_a
    # output:   { :a => { :b => { :x => "y" } } }
    
    puts hash_a                                                 # hash_a is unchanged
    # output:   { :a => { :b => { :c => "d" } } }
    
    hash_a.merge!(hash_b, recursive=true)                       # Will override hash_a
    
    puts hash_a                                                 # hash_a was changed
    # output:   { :a => { :b => { :c => "d", :x => "y" } } }
    

    For advanced example take a look at this answer.

    Also take a look at my recursive version of Hash::each(Hash::each_pair) here.

    0 讨论(0)
提交回复
热议问题