0

I have a simple collection:

{
    :metadata=>{:latitude=>25.86222, :longitude=>-80.27901}, 
    :components=>{:primary_number=>"4390", :street_predirection=>"E"},    
    :analysis=>{:footnotes=>"N#", :dpv_footnotes=>"AABB"}
} 

I want this:

{:latitude=>25.86222, :longitude=>-80.27901,  :primary_number=>"4390", :street_predirection=>"E",:footnotes=>"N#", :dpv_footnotes=>"AABB" } 

In other words, I don't care about the first-level keys. I just care about everything else and I want to iterate through everything else as such:

my_hash.each {|k,v| ... }

I looked through the Hash class and felt that values_at will help simplify this task. So I tried:

Record.first.mail_validation
# => {:metadata=>{:county_name=>"Miami-Dade", :latitude=>25.86222, :longitude=>-80.27901, :time_zone=>"Eastern", :rdi=>"Residential"}, :components=>{:primary_number=>"4390", :street_predirection=>"E", :street_name=>"2nd", :street_suffix=>"Ave", :secondary_number=>nil, :secondary_designator=>nil, :city_name=>"Hialeah", :state_abbreviation=>"FL", :zipcode=>"33013", :plus4_code=>"2249"}, :analysis=>{:footnotes=>"N#", :dpv_footnotes=>"AABB", :dpv_match_code=>"Y"}} 
Record.first.mail_validation.keys
# => [:metadata, :components, :analysis] 

This returns no results, since the argument to values_at is a string and I pass an array:

Record.first.mail_validation.values_at(Record.first.mail_validation.keys)
# => [nil]

This doesn't work, because although I pass string as arguments to values_at, the keys in the hash are symbols, not keys:

Record.first.mail_validation.values_at(Record.first.mail_validation.keys.join(", "))
# => [nil]

Any suggestions on this?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Daniel Viglione
  • 8,014
  • 9
  • 67
  • 101
  • `values_at` takes an array of symbols, as far as I'm aware, something like `{a: 1, b: 2}.values_at(:a, :b)` which returns `[1, 2]`. – max pleaner Jul 12 '16 at 16:02
  • @maxpleaner No it doesn't. I have irb open right now and it is not taking it: Record.first.mail_validation.values_at([:metadata, :components, :anaylsis]) => [nil] . But this does work: Record.first.mail_validation.values_at(:metadata, :components, :anaylsis). I just don't know how to achieve the latter since join(", ") will produce a string. – Daniel Viglione Jul 12 '16 at 16:04
  • I do not want to hardcode the keys. I want to get them from Record.first.mail_validation.keys. – Daniel Viglione Jul 12 '16 at 16:05
  • Oh yes of course you're right. You should use the splat operator, I'll post an answer to show – max pleaner Jul 12 '16 at 16:09
  • 2
    What’s wrong with `Record.first.mail_validation.values`? – Aleksei Matiushkin Jul 12 '16 at 16:31
  • 1
    In future consider waiting longer before selecting an answer. There's no rush, and you don't want to discourage other answers or short-circuit those still working on answers. Many here wait at least a couple of hours. – Cary Swoveland Jul 12 '16 at 17:40
  • @mudasobwa values would be a better fit for this situation, you are right. When I wrote the question about values_at, it is because I was skimming through the documentation and found values_at and hence asked my question based on what I found in the documents. – Daniel Viglione Jul 15 '16 at 15:23
  • Your question is an "[XY Problem](https://meta.stackexchange.com/q/66377/153968)". You're asking how to use the solution you decided would work, but it's not the right way to do it. In addition, you needed to read the documentation for `values_at` more closely, because it doesn't take a string, it takes individual key values, which could be a string, an integer, or any object that matches the key. Because of the confusion in the question this should have been closed as an unclear question. – the Tin Man Jun 13 '17 at 21:34

4 Answers4

3
hash.each_with_object({}) { |(_, v), memo| memo.merge! v }
#⇒ {
#        :dpv_footnotes => "AABB",
#            :footnotes => "N#",
#             :latitude => 25.86222,
#            :longitude => -80.27901,
#       :primary_number => "4390",
#  :street_predirection => "E"
# }

or:

hash.values.reduce &:merge
Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
  • 2
    `.values.reduce(&:merge)` is the real answer here – user229044 Jul 12 '16 at 17:06
  • Good answer, good suggestion (@meagar) and great formatting. – Cary Swoveland Jul 12 '16 at 17:32
  • 1
    @meagar Feel free to post your own answer. The real answer here is given above. `each_with_object` is more than twice faster, and this is a common approach to use it always, just in case. And no, this is not a premature optimization, it’s a matter of good habit. – Aleksei Matiushkin Jul 12 '16 at 17:47
  • 2
    No, it is not a "good habit" to employe more complex solutions where they are not needed. There are **three** values in the top-level array, measuring the difference in performance is practically impossible. Use the one-liner that is 27 characters long, rather than the 8 line solution that is 270 characters long. – user229044 Jul 12 '16 at 17:52
2
h = {
    :metadata=>{:latitude=>25.86222, :longitude=>-80.27901}, 
    :components=>{:primary_number=>"4390", :street_predirection=>"E"},    
    :analysis=>{:footnotes=>"N#", :dpv_footnotes=>"AABB"}
} 

h.values.flat_map(&:to_a).to_h
  #=> {:latitude=>25.86222, :longitude=>-80.27901, :primary_number=>"4390",
  #    :street_predirection=>"E", :footnotes=>"N#", :dpv_footnotes=>"AABB"}

The steps:

a = h.values
  #=> [{:latitude=>25.86222, :longitude=>-80.27901},
  #    {:primary_number=>"4390", :street_predirection=>"E"},
  #    {:footnotes=>"N#", :dpv_footnotes=>"AABB"}] 
b = a.flat_map(&:to_a)
  #=> [[:latitude, 25.86222], [:longitude, -80.27901], [:primary_number, "4390"],
  #    [:street_predirection, "E"], [:footnotes, "N#"], [:dpv_footnotes, "AABB"]] 
b.to_h
  #=> {:latitude=>25.86222, :longitude=>-80.27901, :primary_number=>"4390",
  #    :street_predirection=>"E", :footnotes=>"N#", :dpv_footnotes=>"AABB"}
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • No reason to break the values into arrays only to reconstitute them into hashes. Just merge the values. – user229044 Jul 12 '16 at 16:46
  • @meagar, I agree, but that would have been too close to mudsie's answer (which I would have given had it been available, so I gave this one just for variety). – Cary Swoveland Jul 12 '16 at 17:35
0

Say you have a hash like:

hash = { a: 1, b: 2 }

You can use hash.keys to get

[:a, :b]

How do you pass these keys into values_at, which uses sequential parameters and not an array?

Use the "splat" operator, which will take the array's elements and use them for the sequential parameters. See "Using splats to build up and tear apart arrays in Ruby" for more information.

For example:

hash.values_at(*hash.keys)

returns:

[1, 2]

In your case, it would be almost the exact same as your original code. Instead of:

Record.first.mail_validation.values_at(Record.first.mail_validation.keys)

You'd write:

Record.first.mail_validation.values_at(*Record.first.mail_validation.keys)
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
max pleaner
  • 26,189
  • 9
  • 66
  • 118
  • 1
    Cool, and then this will be how I merge the array of hashes into one hash: http://stackoverflow.com/questions/10943909/array-of-hashes-to-hash – Daniel Viglione Jul 12 '16 at 16:16
  • 3
    This is really silly. Given a hash `x`, running `x.values_at(*x.keys)` is completely redundant. Just use `x.values` to get the *values* in a hash. – user229044 Jul 12 '16 at 16:50
  • 1
    I agree it does seem silly to do this in practice, but I posted this answer just to show how it can be done. The question is specifically "how to use rubys values_at" – max pleaner Jul 15 '16 at 16:35
  • While the OP asked about an "[XY Problem](https://meta.stackexchange.com/q/66377/153968)", this is a good answer to the question asked and doesn't deserve being down-voted. – the Tin Man Jun 13 '17 at 21:35
-1

This works for your example:

hash2 = {}
hash.map{ |k,v| hash2.merge!(v) }.first

returns:

{:latitude=>25.86222, :longitude=>-80.27901, :primary_number=>"4390",
 :street_predirection=>"E", :footnotes=>"N#", :dpv_footnotes=>"AABB"}
Sagar Pandya
  • 9,323
  • 2
  • 24
  • 35
  • 2
    This is a pretty incorrect use of `map` and *nothing* like idiomatic Ruby code. It might produce the right result, but nobody should write code this way. – user229044 Jul 12 '16 at 16:49
  • @meagar would `hash.map.with_object({}) {|(k,v),o| o.merge!(v)}` be better? – Sagar Pandya Nov 24 '16 at 18:12
  • 3
    No, you're not *mapping* one thing to another thing, `map` is not only not necessary, it is *wrong*. It implies a *mapping* operation where there isn't one, making your code unnecessarily confusing to anybody trying to read it. This is a **reduction**, the correct method is `reduce`, not `map`. – user229044 Nov 24 '16 at 18:43