Ruby: merge nested hash

前端 未结 9 1843
渐次进展
渐次进展 2020-11-30 06:25

I would like to merge a nested hash.

a = {:book=>
    [{:title=>\"Hamlet\",
      :author=>\"William Shakespeare\"
      }]}

b = {:book=>
    [{         


        
相关标签:
9条回答
  • 2020-11-30 07:05

    I found a more generic deep-merge algorithm here, and used it like so:

    class ::Hash
        def deep_merge(second)
            merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
            self.merge(second, &merger)
        end
    end
    
    a.deep_merge(b)
    
    0 讨论(0)
  • 2020-11-30 07:07

    All answers look to me overcomplicated. Here's what I came up with eventually:

    # @param tgt [Hash] target hash that we will be **altering**
    # @param src [Hash] read from this source hash
    # @return the modified target hash
    # @note this one does not merge Arrays
    def self.deep_merge!(tgt_hash, src_hash)
      tgt_hash.merge!(src_hash) { |key, oldval, newval|
        if oldval.kind_of?(Hash) && newval.kind_of?(Hash)
          deep_merge!(oldval, newval)
        else
          newval
        end
      }
    end
    

    P.S. use as public, WTFPL or whatever license

    0 讨论(0)
  • 2020-11-30 07:08

    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 for simple usage is written at this answer. Here is an advanced example.

    The example in this question is bad because it got nothing to do with recursive merging. Following line would meet question's example:

    a.merge!(b) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
    

    Let me give you a better example to show the power of the code above. Imagine two rooms, each have one bookshelf in it. There are 3 rows on each bookshelf and each bookshelf currently have 2 books. Code:

    room1   =   {
        :shelf  =>  {
            :row1   =>  [
                {
                    :title  =>  "Hamlet",
                    :author =>  "William Shakespeare"
                }
            ],
            :row2   =>  [
                {
                    :title  =>  "Pride and Prejudice",
                    :author =>  "Jane Austen"
                }
            ]
        }
    }
    
    room2   =   {
        :shelf  =>  {
            :row2   =>  [
                {
                    :title  =>  "The Great Gatsby",
                    :author =>  "F. Scott Fitzgerald"
                }
            ],
            :row3   =>  [
                {
                    :title  =>  "Catastrophe Theory",
                    :author =>  "V. I. Arnol'd"
                }
            ]
        }
    }
    

    We are going to move books from the shelf in the second room to the same rows on the shelf in the first room. First we will do this without setting recursive flag, i.e. same as using unmodified Hash::merge!:

    room1.merge!(room2) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
    puts room1
    

    The output will tell us that the shelf in the first room would look like this:

    room1   =   {
        :shelf  =>  {
            :row2   =>  [
                {
                    :title  =>  "The Great Gatsby",
                    :author =>  "F. Scott Fitzgerald"
                }
            ],
            :row3   =>  [
                {
                    :title  =>  "Catastrophe Theory",
                    :author =>  "V. I. Arnol'd"
                }
            ]
        }
    }
    

    As you can see, not having recursive forced us to throw out our precious books.

    Now we will do the same thing but with setting recursive flag to true. You can pass as second argument either recursive=true or just true:

    room1.merge!(room2, true) {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
    puts room1
    

    Now the output will tell us that we actually moved our books:

    room1   =   {
        :shelf  =>  {
            :row1   =>  [
                {
                    :title  =>  "Hamlet",
                    :author =>  "William Shakespeare"
                }
            ],
            :row2   =>  [
                {
                    :title  =>  "Pride and Prejudice",
                    :author =>  "Jane Austen"
                },
                {
                    :title  =>  "The Great Gatsby",
                    :author =>  "F. Scott Fitzgerald"
                }
            ],
            :row3   =>  [
                {
                    :title  =>  "Catastrophe Theory",
                    :author =>  "V. I. Arnol'd"
                }
            ]
        }
    }
    

    That last execution could be rewritten as following:

    room1 = room1.merge(room2, recursive=true) do |k, v1, v2|
        if v1.is_a?(Array) && v2.is_a?(Array)
            v1+v2
        else
            v2
        end
    end
    puts room1
    

    or

    block = Proc.new {|k,v1,v2| [v1, v2].all? {|v| v.is_a?(Array)} ? v1+v2 : v2}
    room1.merge!(room2, recursive=true, &block)
    puts room1
    

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

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