How to traverse this hash within one line?

2019-07-07 08:55发布

Each key in a hash has a value that's also a hash.

    {
      100 => {
        1 => 'ruby',
        2 => 'enumerables'
      },
      50 => {
        3 => 'can',
        4 => 'cause'
      },
      15 => {
        5 => 'occassional',
        6 => 'insanity'
      }
    }

For each hash object, I want to discard the top-level key, and replace it with the key and value of the nested hash objects.

{
  1 => 'ruby',
  2 => 'enumerables',
  3 => 'can',
  4 => 'cause',
  5 => 'occasional',
  6 => 'insanity'
}

I have it working, but my method uses a merge!, and requires creating another hash to store the values. I'm curious to see if it can be done in one line. I tried to use reduce(), but could not make it work.

6条回答
神经病院院长
2楼-- · 2019-07-07 08:59

While it isn't as brief as some of the other answers, I think each_with_object deserves a representation.

output = input.each_with_object Hash.new do |(_,subhash), hash|
  hash.merge! subhash
end
查看更多
倾城 Initia
3楼-- · 2019-07-07 09:00
Hash[original_hash.values.flat_map(&:to_a)]
查看更多
聊天终结者
4楼-- · 2019-07-07 09:04

This works:

hash.values.inject(&:merge)

Edit: Another option, using reduce (which is the same as inject), and noting tokland's comment that to_proc is automatically called when you use a symbol:

hash.values.reduce(:merge)

Then it becomes not only concise but very readable.

查看更多
forever°为你锁心
5楼-- · 2019-07-07 09:05

I like the answer by @MarkThomas best, but for speed and memory efficiency I suggest:

flatter = {}.tap{ |h| original.values.each{ |h2| h.merge!(h2) } }

Benchmarking 200,000 iterations of the current answers shows this to be the fastest:

                          user     system      total        real
Phrogz                0.710000   0.020000   0.730000 (  0.728706)
Joshua Creek          0.830000   0.010000   0.840000 (  0.830700)
Mark Thomas symbol    1.460000   0.020000   1.480000 (  1.486463)
Mark Thomas to_proc   1.540000   0.030000   1.570000 (  1.565354)
Tim Peters            1.650000   0.030000   1.680000 (  1.678283)

Since the comment by @tokland—original.values.reduce(:update)—modifies the original hash we cannot compare it directly to the other methods. However, if we modify all tests to put a duplicate of the first hash back into the original each iteration, @tokland's answer becomes the fastest, though still not quite as fast as mine:

                          user     system      total        real
tokland's destroyer   0.760000   0.010000   0.770000 (  0.772774)
Phrogz                1.020000   0.020000   1.040000 (  1.034755)
Joshua Creek          1.060000   0.000000   1.060000 (  1.063874)
Mark Thomas symbol    1.780000   0.040000   1.820000 (  1.816909)
Mark Thomas to_proc   1.790000   0.030000   1.820000 (  1.819014)
Tim Peters            1.800000   0.040000   1.840000 (  1.827984)

If you need absolute speed and it's OK to modify the original values, use @tokland's answer. If you do so and want to preserve the original unmerged hashes unscathed, then you can:

first_k,orig_v = original.each{ |k,v| break [k,v.dup] }
merged = original.values.reduce(:update)
original[first_k] = orig_v

Note that your question title says traverse; if you don't really want to merge the values—if you might want to visit a duplicate key twice instead of last-in-wins—then simply do:

original.values.each{ |h| h.each{ |k,v|
  # hey, we're traversing inside!
} }
查看更多
迷人小祖宗
6楼-- · 2019-07-07 09:12

Just took a stab at it, first try was brute force and better methods (in both senses of the word...) are out there.

h.map {|k,v| v}.inject({}) {|i,h| i.merge(h)}
查看更多
别忘想泡老子
7楼-- · 2019-07-07 09:15

Since you don't value about the top level keys, use #values to get an array of the values (in this case, also hashes). Then you can use #inject to build up a new hash, merging them as you go.

yourhash.values.inject{|hash, thing| hash.merge(thing)}

There are probably other ways to do it.

查看更多
登录 后发表回答