在多个级别上同时合并哈希数组的复杂散列

时间:2016-02-04 20:02:27

标签: arrays ruby hash merge

我意识到有很多与这个问题有不同程度相似性的问题。我已经搜索了很长时间(使用: [ruby]合并哈希在键上的哈希数组)并且我已经尝试了每个答案的点点滴滴来尝试自己解决这个问题。在来到StackOverflow之前,我甚至与同样难过的同事分享了我的问题。这似乎是一个独特的问题,或者我们都只是盯着它看到一个明显的答案。

基本要求

  1. 解决方案必须与Ruby 1.8.7标准库(没有宝石)一起使用。请随意另外说明其他版本的Ruby的解决方案,但这样做不会自动使一个答案比另一个更好。
  2. 输入数据的结构不能由其提供者更改;整个数据结构按原样交付。如果需要临时重新排列数据以提供最有效的答案,只要输出与下面所需的样本匹配,那就完全没问题。此外,该解决方案不能对Hashes中排序键的位置做出任何假设。
  3. 源变量不能以任何方式改变;它在运行时是不可变的(这是检查的),因此必须将结果提供给新变量。
  4. 下面的示例数据是虚构的,但问题是真实的。还有其他级别的哈希数组也必须以相同的方式合并到其他键上;因此,最佳答案通常可以应用于任意级别的数据结构。
  5. 最佳解决方案将易于阅读,维护并应用于任意(尽管类似)数据结构。它不一定是单行,但如果你能满足一行Ruby代码中的所有要求,那就是你的荣誉。
  6. 示例数据

    如果我们将Apache Tomcat server.xml文件视为Ruby数据结构而不是XML,它可以为此问题提供非常好的模拟。进一步假设默认配置在上传之前合并 - 在交付给您之前 - 使用必须合并的数据,之后某些操作会消耗生成的数据结构。源数据看起来非常像这样:

    source = {
      :Server => {
        :'attribute.port'     => 8005,
        :'attribute.shutdown' => 'SHUTDOWN',
        :Listener             => [
          { :'attribute.className'  => 'org.apache.catalina.startup.VersionLoggerListener' },
          { :'attribute.className'  => 'org.apache.catalina.core.AprLifecycleListener',
            :'attribute.SSLEngine'  => 'off'},
          { :'attribute.className'  => 'org.apache.catalina.core.JasperListener' },
          { :'attribute.className'  => 'org.apache.catalina.core.JreMemoryLeakPreventionListener' },
          { :'attribute.className'  => 'org.apache.catalina.core.AprLifecycleListener',
            :'attribute.SSLEngine'  => 'on'}
        ],
        :Service              => [
          { :'attribute.name' => 'Catalina',
            :Connector        => [
              { :'attribute.port'     => 8080,
                :'attribute.protocol' => 'HTTP/1.1'},
              { :'attribute.port'     => 8009,
                :'attribute.protocol' => 'AJP/1.3'}
            ],
            :Engine           => {
              :'attribute.name'         => 'Catalina',
              :'attribute.defaultHost'  => 'localhost',
              :Realm                    => {
                :'attribute.className'  => 'org.apache.catalina.realm.LockOutRealm',
                :Realm                  => [
                  { :'attribute.className'    => 'org.apache.catalina.realm.UserDatabaseRealm',
                    :'attribute.resourceName' => 'UserDatabase'}
                ]
              },
              :Host                     => [
                { :'attribute.name'     => 'localhost',
                  :'attribute.appBase'  => 'webapps',
                  :Valve                => [
                    { :'attribute.className'  => 'org.apache.catalina.valves.AccessLogValve',
                      :'attribute.directory'  => 'logs'}
                  ]
                }
              ]
            }
          },
          { :'attribute.name' => 'Catalina',
            :Connector        => [
              { :'attribute.port'         => 8080,
                :'attribute.protocol'     => 'HTTP/1.1',
                :'attribute.secure'       => true,
                :'attribute.scheme'       => 'https',
                :'attribute.proxyPort'    => 443}
            ]
          },
          { :'attribute.name' => 'JSVCBridge',
            :Connector        => [
              { :'attribute.port'         => 8010,
                :'attribute.protocol'     => 'HTTP/2'}
            ]
          },
          { :'attribute.name' => 'Catalina',
            :Engine           => {
              :Host => [
                { :'attribute.name'     => 'localhost',
                  :Valve                => [
                    { :'attribute.className'                => 'org.apache.catalina.valves.RemoteIpValve',
                      :'attribute.internalProxies'          => '*',
                      :'attribute.remoteIpHeader'           => 'X-Forwarded-For',
                      :'attribute.protocolHeader'           => 'X-Forwarded-Proto',
                      :'attribute.protocolHeaderHttpsValue' => 'https'}
                  ]
                }
              ]
            }
          }
        ]
      }
    }
    

    挑战在于从中产生这样的结果:

    result = {
      :Server => {
        :'attribute.port'     => 8005,
        :'attribute.shutdown' => 'SHUTDOWN',
        :Listener             => [
          { :'attribute.className'  => 'org.apache.catalina.startup.VersionLoggerListener' },
          { :'attribute.className'  => 'org.apache.catalina.core.AprLifecycleListener',
            :'attribute.SSLEngine'  => 'on'},
          { :'attribute.className'  => 'org.apache.catalina.core.JasperListener' },
          { :'attribute.className'  => 'org.apache.catalina.core.JreMemoryLeakPreventionListener' },
        ],
        :Service              => [
          { :'attribute.name' => 'Catalina',
            :Connector        => [
              { :'attribute.port'         => 8080,
                :'attribute.protocol'     => 'HTTP/1.1',
                :'attribute.secure'       => true,
                :'attribute.scheme'       => 'https',
                :'attribute.proxyPort'    => 443},
              { :'attribute.port'     => 8009,
                :'attribute.protocol' => 'AJP/1.3'}
            ],
            :Engine           => {
              :'attribute.name'         => 'Catalina',
              :'attribute.defaultHost'  => 'localhost',
              :Realm                    => {
                :'attribute.className'  => 'org.apache.catalina.realm.LockOutRealm',
                :Realm                  => [
                  { :'attribute.className'    => 'org.apache.catalina.realm.UserDatabaseRealm',
                    :'attribute.resourceName' => 'UserDatabase'}
                ]
              },
              :Host                     => [
                { :'attribute.name'     => 'localhost',
                  :'attribute.appBase'  => 'webapps',
                  :Valve                => [
                    { :'attribute.className'  => 'org.apache.catalina.valves.AccessLogValve',
                      :'attribute.directory'  => 'logs'},
                    { :'attribute.className'                => 'org.apache.catalina.valves.RemoteIpValve',
                      :'attribute.internalProxies'          => '*',
                      :'attribute.remoteIpHeader'           => 'X-Forwarded-For',
                      :'attribute.protocolHeader'           => 'X-Forwarded-Proto',
                      :'attribute.protocolHeaderHttpsValue' => 'https'}
                  ]
                }
              ]
            }
          },
          { :'attribute.name' => 'JSVCBridge',
            :Connector        => [
              { :'attribute.port'         => 8010,
                :'attribute.protocol'     => 'HTTP/2'}
            ]
          }
        ]
      }
    }
    

    问题

    我们需要source成为result。为此,:Listenerattribute.className合并; :Service合并了attribute.name;生成的:Connector数组由attribute.port合并;等等。应该很容易地为解决方案提供数据结构中哈希数组的位置标识和每个要合并的密钥。

    这个问题的真正本质是找到可以应用于这样的复杂数据结构的多个任意级别的通用解决方案,通过提供的密钥合并Hashes的哈希,并在该组位置之后生成合并结果和密钥对。

    非常感谢你在这个问题上的时间和兴趣。

2 个答案:

答案 0 :(得分:1)

可能有更优雅的方法来缩小这段代码,但我终于找到了这个非常具有挑战性的问题的答案。虽然Wand Maker的答案很接近,但它基于一种不可靠的假设,即哈希中键的顺序是可预测和稳定的。由于这是一个Ruby 1.8.7问题,并且由于数据提供者没有这样的保证,我不得不采取不同的方式;我们必须通知合并引擎为每个哈希数组使用哪个密钥。

我的(非优化)解决方案需要三个函数和一个定义必要合并键的外部哈希:

  1. deepMergeHash遍历哈希,深入扫描数组
  2. deepMergeArrayOfHashes针对哈希数组执行所需的合并
  3. subMergeHelper以递归方式协助deepMergeArrayOfHashes
  4. 诀窍不仅是递归地对待哈希,而且要始终意识到"现在" Hash中的位置,以便可以知道必要的合并密钥。建立了一种确定位置,定义,查找和使用合并密钥的方法变得微不足道。

    解决方案

    def subMergeHelper(lhs, rhs, mergeKeys, crumbTrail)
      lhs.merge(rhs){|subKey, subLHS, subRHS|
        mergeTrail = crumbTrail + ':' + subKey.to_s
        case subLHS
        when Array
          deepMergeArrayOfHashes(subLHS + subRHS, mergeKeys, mergeTrail)
        when Hash
          subMergeHelper(subLHS, subRHS, mergeKeys, mergeTrail)
        else
          subRHS
        end
      }
    end
    
    def deepMergeArrayOfHashes(arrayOfHashes, mergeKeys, crumbTrail)
      mergedArray = arrayOfHashes
      if arrayOfHashes.all? {|e| e.class == Hash}
        if mergeKeys.has_key?(crumbTrail)
          mergeKey = mergeKeys[crumbTrail]
          mergedArray = arrayOfHashes.group_by{|evalHash| evalHash[mergeKey.to_sym]}.map{|groupID, groupArrayOfHashes|
            groupArrayOfHashes.reduce({}){|memoHash, evalHash|
              memoHash.merge(evalHash){|hashKey, lhs, rhs|
                deepTrail = crumbTrail + ':' + hashKey.to_s
                case lhs
                when Array
                  deepMergeArrayOfHashes(lhs + rhs, mergeKeys, deepTrail)
                when Hash
                  subMergeHelper(lhs, rhs, mergeKeys, deepTrail)
                else
                  rhs
                end
              }
            }
          }
        else
          $stderr.puts "[WARNING] deepMergeArrayOfHashes:  received an Array of Hashes without merge key at #{crumbTrail}."
        end
      else
        $stderr.puts "[WARNING] deepMergeArrayOfHashes:  received an Array containing non-Hashes at #{crumbTrail}?"
      end
      return mergedArray
    end
    
    def deepMergeHash(hashConfig, mergeKeys, crumbTrail = '')
      return hashConfig unless Hash == hashConfig.class
      mergedConfig = {}
      hashConfig.each{|nodeKey, nodeValue|
        nodeCrumb = nodeKey.to_s
        testTrail = crumbTrail + ':' + nodeCrumb
        case nodeValue
        when Hash
          mergedConfig[nodeKey] = deepMergeHash(nodeValue, mergeKeys, testTrail)
        when Array
          mergedConfig[nodeKey] = deepMergeArrayOfHashes(nodeValue, mergeKeys, testTrail)
        else
          mergedConfig[nodeKey] = nodeValue
        end
      }
      return mergedConfig
    end
    

    使用示例

    使用问题中的数据,我们现在可以:

    mergeKeys = {
      ':Server:Listener'                    => 'attribute.className',
      ':Server:Service'                     => 'attribute.name',
      ':Server:Service:Connector'           => 'attribute.port',
      ':Server:Service:Engine:Host'         => 'attribute.name',
      ':Server:Service:Engine:Host:Valve'   => 'attribute.className',
      ':Server:Service:Engine:Realm:Realm'  => 'attribute.className'
    }
    
    mergedConfig = deepMergeHash(source, mergeKeys)
    

    我似乎无法像(result == mergedConfig)那样执行成功的平等测试,但对mergedConfig的视觉检查表明它与result完全相同,除了某些顺序键变化。我怀疑这是使用Ruby 1.8.x的副作用,并且可以接受这个问题。

    快乐的编码,每个人都非常感谢你对这次讨论的兴趣。

答案 1 :(得分:0)

基于假设您基于给定散列数组中第一个键的值合并散列的解决方案如下:

def merge_ary(ary_hash)
  # Lets not process something that is not array of hash
  return ary_hash if not ary_hash.all? {|h| h.class == Hash }

  # If array of hash, lets group them by value of first key
  # Then, reduce the resultant group of hashes by merging them.
  c = ary_hash.group_by {|h| h.values.first}.map do |k,v| 
    v_reduced = v.reduce({}) do |memo_hash, h|
      memo_hash.merge(h) do |k, v1, v2|
        v1.class == Array ? merge_ary(v1 + v2) : v2
      end
    end
    [k, v_reduced]
  end
  return Hash[c].values
end

def merge_hash(hash)
  t = hash.map do |k,v|
    new_v = v
    if v.class == Hash
      new_v = merge_hash(v)
    elsif v.class == Array
      new_v = merge_ary(v)
    end
    [k,new_v]
  end
  return Hash[t]
end

# Test the output
merge_hash(source) == result
#=> true