1

I'm a Perl developer trying to learn Ruby... So, I'll demonstrate in Perl what I'm trying to accomplish in Ruby and then summarize at the end...

#!/usr/bin/perl -w

use strict;
use Data::Dumper;

# Given the following data structure (an array of hashes)
my $j  = [ 
            {   
                id   => 1,
                key1 => 'name1',
                key2 => 'data',
                key3 => 'data',
                key4 => 'foo',
            },  
            {   
                id   => 2,
                key1 => 'name1',
                key2 => 'data',
                key3 => 'data',
                key4 => 'bar',
            },  
            {   
                id   => 3,
                key1 => 'name2',
                key2 => 'data',
                key3 => 'data',
                key4 => 'baz',
            },  
        ];  

print ~~@$j,"\n";
print Dumper($j)."\n";

my $myHash; # make it a reference to a hoa.

for my $array ( @{$j} )
{
   # the key to my new key-name is always known
    push(@{$myHash->{$array->{key1}}},$array->{key4});
}

print Dumper($myHash)."\n";

And the output:

Initial array:

$VAR1 = [
          {
            'key2' => 'data',
            'key4' => 'foo',
            'key1' => 'name1',
            'id' => 1,
            'key3' => 'data'
          },
          {
            'key2' => 'data',
            'key4' => 'bar',
            'key1' => 'name1',
            'id' => 2,
            'key3' => 'data'
          },
          {
            'key2' => 'data',
            'key4' => 'baz',
            'key1' => 'name2',
            'id' => 3,
            'key3' => 'data'
          }
        ];

What I'm trying to get:

$VAR1 = {
          'name2' => [
                       'baz'
                     ],
          'name1' => [
                       'foo',
                       'bar'
                     ]
        };

...and I'm trying to do it in as succinct code as possible, which has proven to be a pain given my lack of Ruby goodness. I've tried multiple attempts at this including several tries at map similar to the same code structure I used in Perl to no avail.

That said, I did find the following snippet, just now, which almost works but I'm sure I'm doing something wrong...

h = Hash[j.collect {|array| [array.key1,array.key4] }]

This gets me the right hash key but it doesn't push the key4 value into an array of the hash. Still looking but some nudge in the right direction would be appreciated. If I find the answer before getting an answer here, I'll answer the question for the edification of future readers.

EDIT! I need to clarify something that I just figured out and is probably the wrench in my clock. The data that I'm getting is not a pure array. It's actually an object from DataMapper.

Jim
  • 1,499
  • 1
  • 24
  • 43
  • 1
    It may go against Perl-ish tradition, but you probably want to prefer clarity to brevity - just because you *can* make it a one-liner, doesn't mean you *should*. What you *do* want is to be able to come back to it in 6 months, and not think "What the hell was whoever-wrote-this thinking?" – nickgrim Mar 06 '13 at 22:46
  • 1
    In Ruby, you can have clarity *and* brevity, with properly factored code. – Mark Thomas Mar 06 '13 at 23:07

4 Answers4

4

You're performing a group by operation. Enumerable#group_by produces a hash whose keys are the values from the block and whose values are arrays of all initial elements that has that key.

You can then use Enumerable#each_with_object to create a new hash with those keys, but the values reduced to just the key4 element you desired.

a = [
  { id: 1, key1: 'name1', key2: 'data', key3: 'data', key4: 'foo', },
  { id: 2, key1: 'name1', key2: 'data', key3: 'data', key4: 'bar', },
  { id: 3, key1: 'name2', key2: 'data', key3: 'data', key4: 'baz',  },
]

res = a.group_by { |e| e[:key1] }.
        each_with_object({}) { |(k,v),m| m[k] = v.map{|e| e[:key4]}  }

puts res.inspect
# => {"name1"=>["foo", "bar"], "name2"=>["baz"]}
dbenhur
  • 20,008
  • 4
  • 48
  • 45
2

When you started with "given the following data structure (array of hashes)" you were already thinking in Perl. In idiomatic Ruby, it would be an array of objects. In fact, this is typical of the output of a database call, etc. A sample class:

class Item
  attr_accessor :id, :name, :data, :foo
end

If you had an array of such items, your answer might look like this:

items.group_by(&:name)

which is of course much cleaner and more expressive.

Note that the above doesn't give you the exact data structure you are asking for. Slinging data structures around is an anti-pattern. The output you're asking for is certainly an intermediate structure that will be used for something else entirely (printing output in a view; serializing for storage, etc). Therefore, I would argue that it isn't necessary. You can do what you need with a grouped list of objects.

Mark Thomas
  • 37,131
  • 11
  • 74
  • 101
  • Ah, this is very interesting and I am definitely thinking Perl vs Ruby so this seems to be a good nudge in the right direction... – Jim Mar 06 '13 at 22:52
  • 1
    @Jim I came from a Perl background myself. In Perl, data structures are easier than OO. In Ruby, it's the opposite. – Mark Thomas Mar 06 '13 at 23:10
  • 1
    Even modeling the elements as objects, your answer doesn't get to the OP's desired result which was a mapping from :name => [set of :data]. `items.group_by(&:name).each_with_object({}) {|(k,v),m| m[k] = v.map(&:data) }` get's all the way. – dbenhur Mar 07 '13 at 00:02
  • My point was that you don't necessarily have to think in data structures, either as input *or* as output. I wasn't giving him his desired result, because I was asking him to rethink his desired result. :) – Mark Thomas Mar 07 '13 at 04:00
  • If I had code like most of these answers, I'd refactor. Too much complexity for the type of work being done. – Mark Thomas Mar 07 '13 at 04:08
0

Here is a oneliner. Assume 'ar' is bound to your array. Assign r={} and then:

ar.map { |a| { a[:key1] => a[:key4] }}.each { |m| m.each { |key,val| r[key] = if not r[key] then [val] else r[key].push(val) end }

The result is in r.

GoZoner
  • 67,920
  • 20
  • 95
  • 145
0

Another way:

Hash[j.chunk {|e| e['key1'] }.map {|k, ary| [k, ary.map {|x| x['key4'] }] }]
=> {"name1"=>["foo", "bar"], "name2"=>["baz"]}

but I think I like dbenhur's answer the best so far.

rainkinz
  • 10,082
  • 5
  • 45
  • 73