Konstantin Konstantin - 4 years ago 106
JSON Question

How to convert hash tables to embedded (nested) hash tables with Ruby?

I have such hash tables:

h={"c4"=>1, "c8"=>2, "ec"=>3, "a"=>4, "e4"=>5, "1"=>6, "8"=>7}


I can access value 2 as:
h["c8"]


I would like to convert the hash table to an embedded hash table like this:

h={"c"=>{"4"=>1, "8"=>2}, "e"=>{"c"=>3, "4"=>5}, "a"=>4, "1"=>6, "8"=>7}


So I could access value 2 as:
h["c"]["8"]
and all the other values too in a similar manner respectively.

All in all instead of:

h["c8"]


I would prefer to use:

h["c"]["8"]


Because I would like to recognize strings in javascript. So I would like to build a very large embedded hash table with Ruby, dump it to JSON and load into javascript. Such embedded hash tables are more easier to look up than the original ones. Keys come from MD5 hashing some original values which were filenames, and then finding minimal slices from the beginning of the MD5 hashed keys which are still uniq.

Another longer example:

h={"c4"=>1,
"c8"=>2,
"ec"=>3,
"a8"=>4,
"e4"=>5,
"1"=>6,
"8"=>7,
"c9"=>8,
"4"=>9,
"d"=>10,
"6"=>11,
"c2"=>12,
"c5"=>13,
"aa"=>14}


Would be:

h={"c"=>{"4"=>1, "8"=>2, "9"=>8, "2"=>12, "5"=>13},
"e"=>{"c"=>3, "4"=>5},
"a"=>{"8"=>4, "a"=>14},
"1"=>6,
"8"=>7,
"4"=>9,
"d"=>10,
"6"=>11}


Even longer example:

h={"c4"=>1, "c8"=>2, "ec"=>3, "a8"=>4, "e4"=>5, "16"=>6, "8f"=>7, "c9"=>8, "45"=>9, "d3"=>10, "65"=>11, "c2"=>12, "c5"=>13, "aa"=>14, "9b"=>15, "c7"=>16, "7"=>17, "6f"=>18, "1f0"=>19, "98"=>20, "3c"=>21, "b"=>22, "37"=>23, "1ff"=>24, "8e"=>25, "4e"=>26, "0"=>27, "33"=>28, "6e"=>29, "3417"=>30, "c1"=>31, "63"=>32, "18"=>33, "e3"=>34, "1c"=>35, "19"=>36, "a5b"=>37, "a57"=>38, "d67"=>39, "d64"=>40, "3416"=>41, "a1"=>42}


Would be:

h={"c"=>{"4"=>1, "8"=>2, "9"=>8, "2"=>12, "5"=>13, "7"=>16, "1"=>31},
"e"=>{"c"=>3, "4"=>5, "3"=>34},
"a"=>{"8"=>4, "a"=>14, "5"=>{"b"=>37, "7"=>38}, "1"=>42},
"1"=>{"6"=>6, "f"=>{"0"=>19, "f"=>24}, "8"=>33, "c"=>35, "9"=>36},
"8"=>{"f"=>7, "e"=>25},
"4"=>{"5"=>9, "e"=>26},
"d"=>{"3"=>10, "6"=>{"7"=>39, "4"=>40}},
"6"=>{"5"=>11, "f"=>18, "e"=>29, "3"=>32},
"9"=>{"b"=>15, "8"=>20},
"7"=>17,
"3"=>{"c"=>21, "7"=>23, "3"=>28, "4"=>{"1"=>{"7"=>30, "6"=>41}}},
"b"=>22,
"0"=>27}


My attempt to solve this problem is a bit ugly and uses "eval", "h" is the original hash:

nested_hash={}
h.keys.each{|k|
k.split(//).each_with_index{|b,i|

if nested_hash.dig(*k[0..i].split(//))==nil then
eval("nested_hash"+k[0..i].split(//).map{|z| "[\"#{z}\"]"}.join+"={}")
end
if i==k.size-1 then
eval("nested_hash"+k[0..i].split(//).map{|z| "[\"#{z}\"]"}.join+"=h[k]")
end
};
};

Answer Source

Based on a comment on the question posted by the OP, I've assumed that there are no keys k1 and k2, k2.size > k1.size, for which k2[0, ki.size] == k1.

Code

def splat_hash(h)
  h.select { |k,_| k.size > 1 }.
    group_by { |k,_| k[0] }.
    map { |k0,a| [k0, splat_hash(a.map { |k,v| [k[1..-1],v] }.to_h)] }.
    to_h.
    merge(h.select{ |k,_| k.size == 1 })
end

Examples

#1

h = {"c4"=>1, "c8"=>2, "ec"=>3, "a"=>4, "e4"=>5, "1"=>6, "8"=>7}
splat_hash h
  #=> {"c"=>{"4"=>1, "8"=>2}, "e"=>{"c"=>3, "4"=>5}, "a"=>4, "1"=>6, "8"=>7} 

#2

h = { "c4"=>1, "c8"=>2, "ec"=>3, "a8"=>4, "e4"=>5, "16"=>6, "8f"=>7, "c9"=>8,
      "45"=>9, "d3"=>10, "65"=>11, "c2"=>12, "c5"=>13, "aa"=>14, "9b"=>15,
      "c7"=>16, "7"=>17, "6f"=>18, "1f0"=>19, "98"=>20, "3c"=>21, "b"=>22,
      "37"=>23, "1ff"=>24, "8e"=>25, "4e"=>26, "0"=>27, "33"=>28, "6e"=>29,
      "3417"=>30, "c1"=>31, "63"=>32, "18"=>33, "e3"=>34, "1c"=>35, "19"=>36,
      "a5b"=>37, "a57"=>38, "d67"=>39, "d64"=>40, "3416"=>41, "a1"=>42 }
splat_hash h
  #=> {"c"=>{"4"=>1, "8"=>2, "9"=>8, "2"=>12, "5"=>13, "7"=>16, "1"=>31},
  #    "e"=>{"c"=>3, "4"=>5, "3"=>34},
  #    "a"=>{"5"=>{"b"=>37, "7"=>38}, "8"=>4, "a"=>14, "1"=>42},
  #    "1"=>{"f"=>{"0"=>19, "f"=>24}, "6"=>6, "8"=>33, "c"=>35, "9"=>36},
  #    "8"=>{"f"=>7, "e"=>25},
  #    "4"=>{"5"=>9, "e"=>26},
  #    "d"=>{"6"=>{"7"=>39, "4"=>40}, "3"=>10},
  #    "6"=>{"5"=>11, "f"=>18, "e"=>29, "3"=>32},
  #    "9"=>{"b"=>15, "8"=>20},
  #    "3"=>{"4"=>{"1"=>{"7"=>30, "6"=>41}}, "c"=>21, "7"=>23, "3"=>28},
  #    "7"=>17,
  #    "b"=>22,
  #    "0"=>27} 

#3

h = { "a"=>1, "ba"=>2, "bb"=>3, "caa"=>4, "cab"=>5, "daba"=>6, "dabb"=>7, "dabcde"=>8 }
splat_hash h
  #=> {"b"=>{"a"=>2, "b"=>3},
  #    "c"=>{"a"=>{"a"=>4, "b"=>5}},
  #    "d"=>{"a"=>{"b"=>{"c"=>{"d"=>{"e"=>8}},"a"=>6, "b"=>7}}},
  #    "a"=>1}

Explanation

I think the best way to show what's happening is to add some puts statements to the code and run it with an example.

INDENT_SIZE = 6

def putsi(str)
  puts "#{' ' * @indent}#{str}"
end

def indent
  @indent = (@indent ||= 0) + INDENT_SIZE
end

def undent
  @indent -= INDENT_SIZE
end

def splat_hash(h)
  puts
  indent
  putsi "enter splat_hash with h=#{h}"
  h.select { |k,_| k.size > 1 }.
    tap { |g| putsi "  select > 1 = #{g}" }.
    group_by { |k,_| k[0] }.
    tap { |g| putsi "  group_by = #{g}" }.
    map { |k0,a| putsi "    calling splat_hash";
          [k0, splat_hash(a.map { |k,v| [k[1..-1],v] }.to_h)] }.
    tap { |a| putsi "  map = #{a}" }.        
    to_h.
    tap { |g| putsi "  to_h = #{g}" }.
    merge(h.select{ |k,_| k.size == 1 }).
    tap { |g| putsi "  returning g = #{g}" }.
    tap { undent }        
end

h = {"c4"=>1, "c8"=>2, "ec"=>3, "faa"=>4, "e4"=>5,  "fab"=>6, "1"=>7 }

splat_hash h
  enter splat_hash with h={"c4"=>1, "c8"=>2, "ec"=>3, "faa"=>4, "e4"=>5,
                           "fab"=>6, "1"=>7}
    select > 1 = {"c4"=>1, "c8"=>2, "ec"=>3, "faa"=>4, "e4"=>5, "fab"=>6}
    group_by = {"c"=>[["c4", 1], ["c8", 2]], "e"=>[["ec", 3], ["e4", 5]],
                "f"=>[["faa", 4], ["fab", 6]]}
      calling splat_hash

        enter splat_hash with h={"4"=>1, "8"=>2}
          select > 1 = {}
          group_by = {}
          map = []
          to_h = {}
          returning g = {"4"=>1, "8"=>2}
      calling splat_hash

        enter splat_hash with h={"c"=>3, "4"=>5}
          select > 1 = {}
          group_by = {}
          map = []
          to_h = {}
          returning g = {"c"=>3, "4"=>5}
      calling splat_hash

        enter splat_hash with h={"aa"=>4, "ab"=>6}
          select > 1 = {"aa"=>4, "ab"=>6}
          group_by = {"a"=>[["aa", 4], ["ab", 6]]}
            calling splat_hash

              enter splat_hash with h={"a"=>4, "b"=>6}
                select > 1 = {}
                group_by = {}
                map = []
                to_h = {}
                returning g = {"a"=>4, "b"=>6}

          map = [["a", {"a"=>4, "b"=>6}]]
          to_h = {"a"=>{"a"=>4, "b"=>6}}
          returning g = {"a"=>{"a"=>4, "b"=>6}}

    map = [["c", {"4"=>1, "8"=>2}], ["e", {"c"=>3, "4"=>5}],
           ["f", {"a"=>{"a"=>4, "b"=>6}}]]
    to_h = {"c"=>{"4"=>1, "8"=>2}, "e"=>{"c"=>3, "4"=>5}, "f"=>{"a"=>{"a"=>4, "b"=>6}}}
    returning g = {"c"=>{"4"=>1, "8"=>2}, "e"=>{"c"=>3, "4"=>5},
                   "f"=>{"a"=>{"a"=>4, "b"=>6}}, "1"=>7}
#=> {"c"=>{"4"=>1, "8"=>2},
#    "e"=>{"c"=>3, "4"=>5},
#    "f"=>{"a"=>{"a"=>4, "b"=>6}}, "1"=>7} 
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download