【问题标题】:Merge Complex Hash of Arrays of Hashes Simultaneously on Multiple Levels在多个级别上同时合并哈希数组的复杂哈希
【发布时间】:2016-05-14 15:01:15
【问题描述】:

我意识到有许多问题与这个问题有不同程度的相似性。我已经对它们进行了详细搜索(使用:[ruby] 键上的哈希值合并数组),并且我尝试了每个答案的点点滴滴,试图自己解决这个问题。在来到 StackOverflow 之前,我什至与同样被难住的同事分享了我的问题。这似乎是一个独特的问题,或者我们都只是盯着它看太近而看不到其他明显的答案。

基本要求

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

样本数据

如果我们将 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 合并; :Serviceattribute.name 合并; :Connector 的结果数组由 attribute.port 合并;等等。数据结构中 Arrays-of-Hashes 的位置以及每个要合并的键的标识应该很容易提供给解决方案。

这个问题的真正本质是找到一个通用的解决方案,它可以应用于像这样的复杂数据结构的多个任意级别,通过提供的键合并 Arrays-of-Hashes,并在设置位置后产生合并的结果并提供密钥对。

非常感谢大家对这个问题的时间和兴趣。

【问题讨论】:

  • 什么规则决定了 :Listener 的变化?仅仅是在散列数组中,如果键重复,则该键的最新实例是唯一剩下的吗?如果是这样,为什么“attribute.className sill 在结果中出现两次?
  • 对于这个问题,所有 Arrays-of-Hashes 必须在 key 上合并,以便 Array 中后面的 Hashes 中的值覆盖早期 Hashes 中的值。 :Listener Array-of-Hashes 在attribute.className 上合并,因此结果中attribute.SSLEngineon for org.apache.catalina.core.AprLifecycleListenerattribute.className 的值在 :Listeners 的两个幸存元素之间有所不同;只有org.apache.catalina.core.AprLifecycleListener 需要合并。
  • 您能否添加一个您可能应用该方法的更复杂结构的示例?
  • @ian 我扩展了数据样本以添加更多 :Listener 元素并进一步说明额外的复杂性和预期行为。

标签: arrays ruby hash merge


【解决方案1】:

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

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

【讨论】:

  • 这是一个有趣的方法,我会玩弄。不幸的是,它不可行,因为我们正在处理包含哈希数组的哈希。无法保证键的顺序。我为这个问题手动编写了源数据和结果数据,因此所需的键总是在我的样本中排在第一位的事实只是因为我希望读者在查看数据时很容易看到这些键(因为我在问题)。鉴于此,merge_ary 需要被告知要在哪个键上合并,因此,我们需要一种方法来为每个数组提供这些键名。
  • 除非我搞砸了我的 Ruby 环境,否则to_h 似乎在您使用它的 Ruby 1.8.7 标准库中可能不可用:NoMethodError: undefined method 'to_h'
  • to_h 在 1.8.7 中不存在,请查看 stackoverflow.com/a/20831486/794242 以获取替代方案
  • 感谢您的推荐,但我们不能使用 Ruby > 1.8.7,也不能使用 gems 来解决这个特殊问题。
  • @seWilliam 我已经更新了不使用to_h 的答案,但它不会处理您最近对问题的更新 - 测试因您当前问题的数据而失败
【解决方案2】:

可能有更优雅的方式来压缩此代码,但我最终为这个非常具有挑战性的问题找到了答案。虽然 Wand Maker 的回答很接近,但它基于一个站不住脚的假设,即哈希中的键顺序是可预测且稳定的。由于这是 Ruby 1.8.7 的问题,而且数据提供者没有做出这样的保证,我不得不采取不同的方式;我们必须通知合并引擎为每个 Array-of-Hashes 使用哪个键。

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

  1. deepMergeHash 遍历哈希,深度扫描数组
  2. deepMergeArrayOfHashes 对 Array-of-Hashes 执行所需的合并
  3. subMergeHelper 递归辅助 deepMergeArrayOfHashes

诀窍不仅在于递归处理哈希,而且始终注意哈希中的“当前”位置,以便知道必要的合并键。建立确定位置的方法后,定义、查找和使用合并键变得很简单。

解决方案

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 的副作用,对于这个问题是可以接受的。

祝大家编码愉快,非常感谢您对本次讨论的关注。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-02-03
    • 2020-07-24
    • 2016-05-28
    • 1970-01-01
    • 2016-10-07
    • 1970-01-01
    相关资源
    最近更新 更多