http://www.perlmonks.org?node_id=1019960

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

I want a slice from a hash, in one step?

examples: GIVE ME A HASH (from another hash) that only includes keys beginning with 'cat'. Or where the value is > 10. I usually resort to this sort of ugliness:

my %h2;
for ( keys %h )
{
next unless /^cat/;
$h2{$_} = $h{$_};
}


Seems like there oughta be other easier way to slice this thing up in one line with some sorta

@h2{@something} = @something++

TY Monks
  • Comment on Is there a simple syntax to logically slice a hash?

Replies are listed 'Best First'.
Re: Is there a simple syntax to logically slice a hash?
by BrowserUk (Patriarch) on Feb 21, 2013 at 15:49 UTC

    I tend to do:

    my %h2; /^cat/ and $h2{ $_ } = $h1{ $_ } for keys %h1;

    Which might be seen as "cheating" as a one-liner; but it is often difficult to put the initialisation of a hash inline.

    It is also quite more efficient than map and/or grep for larger hashes, by avoiding the creation of intermediate lists.


    With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Is there a simple syntax to logically slice a hash?
by space_monk (Chaplain) on Feb 21, 2013 at 15:36 UTC
    map perhaps??
    You can map one hash to another, but it is probably just as untidy as your example :-)
    Something like these for the two examples you gave (I haven't got perl on this machine so please send me a private message and I'll fix it if wrong)
    %hash2 = map { $_ => $hash{$_} } grep (/^cat/} keys %hash; # return empty list if ternary test fails.... %hash2 = map { $hash{$_} > 10 ? $_ => $hash{$_} : () } keys %hash;
    A Monk aims to give answers to those who have none, and to learn from those who know more.
Re: Is there a simple syntax to logically slice a hash?
by Athanasius (Archbishop) on Feb 21, 2013 at 15:38 UTC

    You can take a functional approach and chain grep and map on a single line:

    #! perl use strict; use warnings; use Data::Dump; my %h1 = (catastrophe => 1, cataclysm => 2, dogma => 3); my %h2 = map { $_ => $h1{$_} } grep { /^cat/ } keys %h1; dd %h2;

    Output:

    1:33 >perl 542_SoPW.pl ("catastrophe", 1, "cataclysm", 2) 1:37 >

    Hope that helps,

    Athanasius <°(((><contra mundum Iustus alius egestas vitae, eros Piratica,

Re: Is there a simple syntax to logically slice a hash?
by tobyink (Canon) on Feb 21, 2013 at 15:42 UTC

    Something like this?

    use strict; use warnings; use Data::Dumper; my %orig = ( cat => 22, dog => 23, category => 66, catalyst => 77, cataclysm => 88, dogma => 89, dogstar => 92, ); my %cats_only = map { $_ => $orig{$_} } grep /^cat/, keys %orig; print Dumper \%cats_only;
    package Cow { use Moo; has name => (is => 'lazy', default => sub { 'Mooington' }) } say Cow->new->name
Re: Is there a simple syntax to logically slice a hash?
by LanX (Saint) on Feb 22, 2013 at 01:50 UTC
    define your own hgrep!
    $a=key $b=value
    DB<142> sub hgrep (&%) { my $f=shift; my @res; local ($a,$b); while ( ($a,$b)= splice(@_,0,2) ) { push @res, $a => $b if $f->() } return @res } DB<143> %h = ( cat=>0, cut=>1, caty=>2 ) => ("cat", 0, "cut", 1, "caty", 2) DB<144> hgrep { $a =~ /^cat/ } %h => ("cat", 0, "caty", 2) DB<145> hgrep { $b>1 } %h => ("caty", 2)

    Cheers Rolf

    UPDATE: see Hash::MostUtils for a similar approach.

Re: Is there a simple syntax to logically slice a hash?
by TomDLux (Vicar) on Feb 21, 2013 at 16:29 UTC

    Using the perl debugger, for speed and convenience ...

    # Lets create a hash that has some similar keys and some unsimilar key +s %h1 = ( cat => 1, dog=> 2, catalog => 3, doggerel => 4 } x \%h1 0 HASH(0x7f9cb41aaad0) 'cat' => 1 'catalog' => 3 'dog' => 2 'doggerel' => 4 # See ALL the keys ... x keys %h1 0 'cat' 1 'doggerel' 2 'catalog' 3 'dog' # grep allows us to filter that list x grep { /cat/ } keys %h1 0 'cat' 1 'catalog' # You access a single hash element with $h{$key} # You can access multiple elements, called a slice, # using an array or list of keys. In this case you have to put # a '@' sigil in front, rather than a '$', to indicate #you're expecting multiple values x @h1{grep { /cat/ } keys %h1} 0 1 1 3 # So 'cat' -like keys have the values, '1' and '3'. # If you wanted to assign those to the keys in # another hash, I would save the set of keys in a temp: @a = grep { /cat/ } keys %h1 @b{@a} = @h1{@a} DB<14> x \%b 0 HASH(0x7f9cb41bd0e8) 'cat' => 1 'catalog' => 3

    The last bit is saying, create a new hash called 'b' ( we know it's a hash because of how it's elements are being referenced with curly braces ), and create elements for each value in @a, assigning the elements extraced from %h1. You COULD repeat the 'grep' part, once in the LHS and once on the RHS of the assignment, to really get a one-line assignment, but that's messy. While you don't know the order of the elements, it would be the same on each side, but if the set is huge, you don't want to go through all that .. and it's a maintainence problem, and hard to verify the two are identical; so I would split it into two parts.

    As Occam said: Entia non sunt multiplicanda praeter necessitatem.

      Ouch!. That creates 5 unneccesary temporary lists and an unneccesary temporary array:

      #T1 L2 L1 @a = grep { /cat/ } keys %h1 # L5 L4 L3 @b{@a} = @h1{@a}

      With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
      Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
      "Science is about questioning the status quo. Questioning authority".
      In the absence of evidence, opinion is indistinguishable from prejudice.