rhesa has asked for the wisdom of the Perl Monks concerning the following question:

How do I extract part of a hash -- as a hash? I (obviously) know about slices, but that only gives you certain values, and I would like to have some key/value pairs1.

Say I have a hash like this:

%hash = ( name => 'Rhesa', fruit => 'Mango', dog => 'Spot', cat => 'Stofje', );
I'd like to grab just the pets out of this hash, so that I end up with:
%pets = ( dog => 'Spot', cat => 'Stofje', );
How would you do this?

1 It occurred to me that the syntax %hash{@keys} would have been ideal for this: it mirrors normal slicing, but instead of a list, it indicates we get a hash back. I realise it's a bit late in the life of perl5 to make this into a serious proposal, but it might have been nice.

The most obvious answer would be:

%pets = ( dog => $hash{dog}, cat => $hash{cat}, );
That's just wrong. Unfortunately, I see it quite often in my code base, most notably when creating Class::DBI objects2.

Of course, there's the two-stage approach using slices:

my @animals = qw( dog cat ); my %pets; @pets{ @animals } = @hash{ @animals };
But I don't like that it requires a temporary variable.

The next best thing I could think of is:

%pets = map { $_ => $hash{$_} } qw( dog cat );
But that shows too much machinery for my taste. It's like having to start your car with a crank instead of a key or button.

I'm gravitating towards writing a function for it:

sub hash_slice { my ($hr, @k) = @_; return map { $_ => $hr->{$_} } @k; } # or using a slice (might be more efficient, but doesn't read as nicel +y) sub hash_slice { my ($hr, @k) = @_; my %ret; @ret{@k} = @$hr{@k}; return %ret; } # use like this: %pets = hash_slice( \%hash, qw( dog cat ) );
Is that the best we can do?

2

I'm importing data from a csv file into a database. The csv file is highly denormalized, while the database is structured nicely. My csv parser returns a hash for each line of data, so I need to store parts of this hash into different tables.

The file has these columns (and more):

@headers = qw( firstname lastname address_line1 address_line2 city zip state country order_number title bookcode isbn quantity );
I need to store this data using Class::DBI classes like Customer, Address, Title, Edition, Order, Orderline etc. So some of these calls look like:
# %orderline comes from the csv my $customer = Customer->find_or_create({ firstname => $orderline{firstname}, lastname => $orderline{lastname}, });
And so on and so forth. It's a pain to read, and maintain.

Replies are listed 'Best First'.
Re: How to extract part of a hash
by FunkyMonk (Bishop) on May 20, 2007 at 11:35 UTC
    I much prefer readability over code density (although I'm happy to use slices, I often find I have to stop and think about them when I come back to them). So
    %pets = map { $_ => $hash{$_} } qw( dog cat );
    is how I'd do it

    Edit: Got rid of "concise". It did not convey my meaning very well :(

Re: How to extract part of a hash
by shmem (Chancellor) on May 20, 2007 at 16:01 UTC
    well, there's the "for() with one variable" trick -
    @pets{@$_} = @hash{@$_} for [qw(dog cat)];

    That's the shortest I can contrieve. The temporary variable still exists, but IMHO it's okay to use $_ for that.

    --shmem

    _($_=" "x(1<<5)."?\n".q·/)Oo.  G°\        /
                                  /\_¯/(q    /
    ----------------------------  \__(m.====·.(_("always off the crowd"))."·
    ");sub _{s./.($e="'Itrs `mnsgdq Gdbj O`qkdq")=~y/"-y/#-z/;$e.e && print}
Re: How to extract part of a hash
by naikonta (Curate) on May 20, 2007 at 14:14 UTC
    First of all, since you actually present some analysis on some alternative ways, I think the proper title would be What's the best way to extract part of hash?. With or without title changing, the essential of your question, at least what read to me, is still to find the best way to do a thing. And, like many questions on the best way to do things in Perl, it really depends on what restrictions to be applied. But, frankly, I still don't understand what your goal is.

    For me, slicing is the most straight way to extract partial elements from hash and array. But we do need temporary variable to store the target hash if what we need is partial hash, along with keys and values. Using map allows us to extract partial elements without temporary variables. Consider this,

    my %order_line = read_from_csv(); my @fields = qw(firstname lastname); my %identity; @identity{@fields} = @order_line{@fields}; my $customer = Customer->find_or_create(\%identity);
    compared to,
    my %order_line = read_from_csv(); my @fields = qw(firstname lastname); my $customer = Customer->find_or_create({ map {$_ => $order_line{$_}} @fields });
    Benchmarking also shows that slicing is slightly faster than using map.
    #!/usr/bin/perl use strict; use warnings; use Benchmark 'cmpthese'; my $count = shift || -1; my %source_hash = ( firstname => 'Perl', lastname => 'Monks', address_line1 => 'http://www.perlmonks.org', address_line2 => 'http://www.perlmonks.com', city => 'cyber', zip => '209.197.123.153', state => 'stable', country => 'Internet', order_number => 'N/A', title => 'not applicable', ); my @partial = qw(firstname lastname); cmpthese $count, { with_slice => sub { my %target_hash; @target_hash{@partial} = @source_hash{@partial}; }, with_map => sub { my %target_hash = map {$_ => $source_hash{$_}} @partial; }, }; # Result: # Rate with_map with_slice # with_map 110277/s -- -28% # with_slice 153325/s 39% --

    Open source softwares? Share and enjoy. Make profit from them if you can. Yet, share and enjoy!

Re: How to extract part of a hash
by blazar (Canon) on May 20, 2007 at 14:50 UTC
    # use like this: %pets = hash_slice( \%hash, qw( dog cat ) );

    For once, this may be a situation in which using a prototype would not be that bad:

    sub hash_slice (\%@) { my $h=shift; map { $_ => $h->{$_} } @_; }

    (Or the other way with slices. I don't really care.)

Re: How to extract part of a hash
by graff (Chancellor) on May 20, 2007 at 17:38 UTC
    # or using a slice (might be more efficient, but doesn't read as nicel +y) sub hash_slice { my ($hr, @k) = @_; my %ret; @ret{@k} = @$hr{@k}; return %ret; }

    Personally, I think this one reads very nicely, and this is most likely the approach I would go with. Nice side-effect: in case I ever need to step through with "perl -d" (e.g. to check for mangled data), this versions provides a handy place for a breakpoint where I can dump %ret before it gets returned.