Search code examples
ruby-on-railsrubyruby-on-rails-3mibf5

Ruby hash get from dynamic key and value (MIB SNMP)


I'm a newbie on ruby, I have hash data like below.

person = {
  "PoolName.11.22.33":"pool_a",
  "PoolMemberName.11.22.33.11":"member_pool_a1",
  "PoolMemberScore.11.22.33.11":0,
  "PoolName.11.22.44":"pool_b",
  "PoolMemberName.11.22.44.11":"member_pool_b1",
  "PoolMemberName.11.22.44.12":"member_pool_b2",
  "PoolMemberScore.11.22.44.11":2,
  "PoolMemberScore.11.22.44.12":3,
  "PoolName.11.22.55":"pool_c",
  "PoolMemberName.11.22.55.11":"member_pool_c1",
  "PolMemberName.11.22.55.12":"member_pool_c2",
  "PoolMemberName.11.22.55.13":"member_pool_c3",
  "PoolMemberScore.11.22.55.11":11,
  "PoolMemberScore.11.22.55.12":22,
  "PoolMemberScore.11.22.55.13":33
}

Can i get results like following below:

"pool_a.member_pool_a1" : 0,
"pool_b.member_pool_b1" : 2,
"pool_b.member_pool_b2" : 3,
"pool_c.member_pool_c1" : 11,
"pool_c.member_pool_c2" : 22,
"pool_c.member_pool_c3" : 33

Solution

  • person.
      slice_before { |_,v| v.is_a?(String) &&
        v.match?(/\Apool_\p{Lower}{,2}\z/) }.
      each_with_object({}) do |((_, pool), *rest),h|
        pool_str = pool + ".member_pool_"
        rest.group_by { |k,_| k[/(?:\.\d+)+\z/] }.
             each do |_,((_,s),(_,n))|
               (s,n = n,s) if n.is_a?(String)
               h[(pool_str + s[/[^_]+\z/]).to_sym] = n
             end  
      end
      #=> {:"pool_a.member_pool_a1"=>0,
      #    :"pool_b.member_pool_b1"=>2,
      #    :"pool_b.member_pool_b2"=>3,
      #    :"pool_c.member_pool_c1"=>11,
      #    :"pool_c.member_pool_c2"=>22,
      #    :"pool_c.member_pool_c3"=>33} 
    

    The steps are as follows1.

    c = person.slice_before { |_,v| v.is_a?(String) &&
          v.match?(/\Apool_\p{Lower}{,2}\z/) }
      #=> #<Enumerator: #<Enumerator::Generator:0x00005d500f652950>:each> 
    

    We can see the values that will be generated by this enumerator by converting it to an array.

    c.to_a
      #=> [[[:"PoolName.11.22.33", "pool_a"],
      #     [:"PoolMemberName.11.22.33.11", "member_pool_a1"],
      #     [:"PoolMemberScore.11.22.33.11", 0]],
      #    [[:"PoolName.11.22.44", "pool_b"],
      #     [:"PoolMemberName.11.22.44.11", "member_pool_b1"], 
      #     [:"PoolMemberName.11.22.44.12", "member_pool_b2"],
      #     [:"PoolMemberScore.11.22.44.11", 2],
      #     [:"PoolMemberScore.11.22.44.12", 3]],
      #    [[:"PoolName.11.22.55", "pool_c"],
      #     [:"PoolMemberName.11.22.55.11", "member_pool_c1"],
      #     [:"PolMemberName.11.22.55.12", "member_pool_c2"],
      #     [:"PoolMemberName.11.22.55.13", "member_pool_c3"],
      #     [:"PoolMemberScore.11.22.55.11", 11],
      #     [:"PoolMemberScore.11.22.55.12", 22],
      #     [:"PoolMemberScore.11.22.55.13", 33]]]
    

    As seen, c generates 3 arrays, one for each of the three pools, "a", "b" and "c". See Enumerable#slice_before. Continuing,

    d = c.each_with_object({})
      #=> #<Enumerator: #<Enumerator:
      # #<Enumerator::Generator:0x00005d500f652950>:each>
      #   :each_with_object({})> 
    

    d can be thought of as a compound enumerator, though Ruby has no such concept. See Enumerable#each_with_object. The first element is generated and passed to the block, and the block variables are assigned to that value.

    ((_, pool), *rest),h = d.next
      #=> [[[:"PoolName.11.22.33", "pool_a"],
      #     [:"PoolMemberName.11.22.33.11", "member_pool_a1"], 
      #     [:"PoolMemberScore.11.22.33.11", 0]], {}]
    

    We see the block variables now have the following values.

    pool
      #=> "pool_a" 
    rest
      #=> [[:"PoolMemberName.11.22.33.11", "member_pool_a1"],
      #    [:"PoolMemberScore.11.22.33.11", 0]]
    h #=> {} 
    

    The breaking down of arrays into their components of interest is called "array decomposition" (search for "Array Decomposition"). It's also known as array disambiguation. It's a very powerful and useful technique. Note that _ is a valid, but somewhat special, local variable. The main reason for using an underscore (or a variable name beginning with an underscore) is to inform the reader that that variable is not used in subsequent calculations. Continuing,

    pool_str = pool + ".member_pool_"
      #=> "pool_a.member_pool_" 
    e = rest.group_by { |k,_| k[/(?:\.\d+)+\z/] }
      #=> {".11.22.33.11"=>[
      #     [:"PoolMemberName.11.22.33.11", "member_pool_a1"],
      #     [:"PoolMemberScore.11.22.33.11", 0]]} 
    

    See Enumerable#group_by. Continuing,

    g = e.each
      #=> #<Enumerator:
      #    {".11.22.33.11"=>[
      #      [:"PoolMemberName.11.22.33.11", "member_pool_a1"],
      #      [:"PoolMemberScore.11.22.33.11", 0]]}:each> 
    

    Lots of enumerators, eh? Again, the first element of the enumerator is generated and passed to the block and the block variables are assigned values:

    p,((q,s),(r,n)) = g.next
      #=> [".11.22.33.11",
      #   [[:"PoolMemberName.11.22.33.11", "member_pool_a1"],
      #    [:"PoolMemberScore.11.22.33.11", 0]]]
    p #=> ".11.22.33.11"
    q #=> :"PoolMemberName.11.22.33.11"
    s #=> "member_pool_a1"
    r #=> :"PoolMemberScore.11.22.33.11"
    n #=> 0
    

    p, q and r are not used in the block calculations. For that reason I used _ for all three of those variables:

    each do |_,((_,s),(_,n))| ...
    

    This array decomposition may look extremely complex, but it really isn't. Just compare the locations of the parentheses in the line immediately above with the locations of brackets in the return value of g.next shown above.

    The block calculation is now performed. We cannot be sure the two elements of g.next are in the right order (though here they are), so we reverse them if needed.

    (s,n = n,s) if n.is_a?(String)
      #=> nil 
    s #=> "member_pool_a1" 
    n #=> 0 
    
    i = s[/[^_]+\z/]
      #=> "a1" 
    j = pool_str + i
      #=> "pool_a.member_pool_a1" 
    k = j.to_sym
      #=> :"pool_a.member_pool_a1" 
    h[k] = n
      #=> 0 
    h #=> {:"pool_a.member_pool_a1"=>0} 
    

    As g.size #=> 1 we are finished with g. The next step is therefore to generate the next element of d (pool "b"):

    ((_, pool), *rest),h = d.next
      #=> [[[:"PoolName.11.22.44", "pool_b"],
      #     [:"PoolMemberName.11.22.44.11", "member_pool_b1"],
      #     [:"PoolMemberName.11.22.44.12", "member_pool_b2"],
      #     [:"PoolMemberScore.11.22.44.11", 2], 
      #     [:"PoolMemberScore.11.22.44.12", 3]],
      #    {:"pool_a.member_pool_a1"=>0}] 
    pool
      #=> "pool_b"
    rest
      #=> [[:"PoolMemberName.11.22.44.11", "member_pool_b1"],
      #    [:"PoolMemberName.11.22.44.12", "member_pool_b2"],
      #    [:"PoolMemberScore.11.22.44.11", 2],
      #    [:"PoolMemberScore.11.22.44.12", 3]]
    h #=> {:"pool_a.member_pool_a1"=>0}
    

    Notice how the block variable h has been updated. The remaining calculations are similar.

    1 Experienced Rubiests: gory-detail alert!