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

Here's something that might be fun for people with too much spare time to play around with :) Consider an array and an integer mask.

my @arr = qw (and you will be pleased with the results); my $mask = 0x2b; # 0 0 1 0 1 0 1 1 my @result = wave_hands( $mask, @arr ) # returns qw(will pleased the results)

That is, take some of the elements of the array according to whether the corresponding bit is set in $mask. If possible, without destroying @arr. Assume that no bit-value in $mask will point beyond the end of @arr.

I've been wondering what's the fastest way to achieve the result. I don't particularly mind if the mask gets applied "backwards", which would result in qw(and you be with) because at the very worst, I'd only need to reverse the original array to get the correct result.

The values of @arr could be unique, if using a hash would help, but in the general case, duplicates could occur. I can think of at least three techniques:

... Or there some really simple trick that does this without a whole lot of make-work code?

• another intruder with the mooring in the heart of the Perl

Replies are listed 'Best First'.
Re: Taking arbitrary elements from an array
by Corion (Patriarch) on Nov 28, 2005 at 14:18 UTC

    If Perl had a =>> operator, the code would look like this:

    perl -le "$mask = 0x2b; print for map {$mask =>> 1?$_:()} @ARGV" and y +ou will be pleased with the results

    ... but as Perl lacks shift-and-assign operators that return the shifted-off bits, my code is uglier:

    perl -le "$mask = 0x2b; print for map {my $sel = $mask & 1; $mask = $m +ask >> 1; $sel?$_:()} @ARGV" and you will be pleased with the results and you be with

    Update: I found >>= and <<=, but they don't seem to act like I'd want, namely returning the shifted-off bits. In fact, my experiments don't make it clear to me what the operators do, as they leave $mask unchanged it seems:

    perl -le "$mask = 0x2b; print for map {$mask >>= 1?$_:();} @ARGV" and +you will be pleased with the results

    prints

    43 43 43 43 43 43 43 43

    ... which I don't really understand.

    Update 2: D'oh. Rereading the documentation, I now understand that $mask >>= 1 is equivalent to $mask = $mask >> 1, which returns the (new) value of $mask instead of returning the shifted-off bits. So it's not as usable as I'd need, for this case of golfing.

      Your second example also has a glitch due to precedence.  The expression:
      $mask >>= 1? $_: ();
      was intended, I believe, to be:
      ($mask >>= 1)? $_: ();
      but is actually equivalent to:
      $mask >>= (1? $_: ());
      This evaluates to true each time (1?) thus using each word ($_) as the value by which to shift $mask, and each of the strings evaluates to numerical zero.  That's why $mask never changes, and gives you a 43 (0x2b) resulting from the map applied to each value from @ARGV;

      @ARGV=split//,"/:L"; map{print substr crypt($_,ord pop),2,3}qw"PerlyouC READPIPE provides"
Re: Taking arbitrary elements from an array
by Fletch (Bishop) on Nov 28, 2005 at 14:28 UTC

    Yeah, I'd go with the unpack and a slice.

    my @arr = qw( and you will be pleased with the results ); my $mask = 0x2b; sub mask_array (\@$) { my( $arr, $mask ) = @_; my @want = split '', unpack( "b*", $mask ); return @{ $arr }[ grep { $want[ $_ ] } 0..$#{ $arr } ]; } my @result = mask_array @arr => $mask;

    Update: Hypothetically you could use return @{ $arr }[ grep { vec( $mask, $_, 1 ) } 0..$#{ $arr } ] instead of making @want, but I'd bet that's a little slower than the above version.

Re: Taking arbitrary elements from an array
by demerphq (Chancellor) on Nov 28, 2005 at 14:47 UTC

    Heres my go, although wave_hands() seems like a funny name for the routine... ;-)

    sub wave_hands { my $mask=0+shift; my $i=1; my @r; for ( my $p=$#_ ; $p>=0 && $i<=$mask ; $p-- ) { unshift @r,$_[$p] if $mask & $i; $i<<=1; } return @r; } my @arr = qw (and you will be pleased with the results); my $mask = 0x2b; # 0 0 1 0 1 0 1 1 my @result = wave_hands( $mask, @arr ); print "@result"; # "will pleased the results"

    If speed is a concern you shouldn't pass this stuff on the stack, use refs instead...

    ---
    $world=~s/war/peace/g

Re: Taking arbitrary elements from an array
by holli (Abbot) on Nov 28, 2005 at 15:47 UTC
    similar to Fletch's version:
    # 1 2 3 4 5 + 6 # 012345678901234567890123456789012345678901234567890123456 +7890123456789 my @result = @arr[grep{defined}map{$i++;$_?$i-1:undef}split"",sprintf( +"%08b",0x2b)];
    69


    holli, /regexed monk/
Re: Taking arbitrary elements from an array
by sauoq (Abbot) on Nov 28, 2005 at 16:03 UTC
    • Use vec (erm, somehow)

    That probably wouldn't make sense unless your mask was an actual bit mask rather than an "integer mask." In other words, if you started with $mask = "\x2b"; rather than $mask = 0x2b; then using vec would be appropriate.

    If you took that approach, you could do it like so:

    my @arr = qw( and you will be pleased with the results ); my $mask = "\x2b"; my @new = do { my $i = 0; map { vec($mask, $#arr - $i++, 1) ? $_ : () +} @arr };

    -sauoq
    "My two cents aren't worth a dime.";
    
Re: Taking arbitrary elements from an array
by ozboomer (Friar) on Feb 14, 2006 at 03:39 UTC
    I'm going ga-ga here, trying to get these suggestions to work with Active Perl (Build 813) under WinXP...

    Example 1:

    my @arr = qw( and you will be pleased with the results ); # 0 0 1 0 1 0 1 1 my $mask = 0x2b; sub mask_array (\@$) { my( $arr, $mask ) = @_; my @want = split '', unpack( "b*", $mask ); return @{ $arr }[ grep { $want[ $_ ] } 0..$#{ $arr } ]; } my @result = mask_array @arr => $mask; foreach (@result) { printf("%s\n", $_); }
    This is the same as is posted earlier in the discussion. This version is broken as the output is:
    will
    pleased
    with

    Example 2:

    my @arr = qw( and you will be pleased with the results ); # 0 0 1 0 1 0 1 1 my $mask = "\x2b"; my @new = do { my $i = 0; map { vec($mask, $#arr - $i++, 1) ? $_ : () +} @arr }; foreach (@new) { printf("%s\n", $_); }
    This code is the same as entered above in the discussion; its output is correct:
    will
    pleased
    the
    results

    Example 3:

    @arr = qw( and you will be pleased with the results XX YY AA BB ); # \x2b 0 0 1 0 1 0 1 1 0 0 1 0 # \x2b20 0 0 1 0 0 0 1 0 1 0 1 1 $mask = "\x2b20"; @new = do { my $i = 0; map { vec($mask, $#arr - $i++, 1) ? $_ : () } @ +arr }; foreach (@new) { printf("%s\n", $_); }
    The main code of this version is the same as Example 2 but I'm trying to work out how the bit map is supposed to work. This version breaks when there is more than 8 elements from which to select; its output is:
    will
    the
    XX
    AA
    BB

    I'm not flash on this bit manipulation and working with byte boundaries, etc... but these approaches look like they'd be useful with my current job: to be able to specify an arbitrary list of items to select from an arbitrarily long array.

    Any further suggestions/explanations?

    Thanks.

Re: Taking arbitrary elements from an array
by ozboomer (Friar) on Feb 15, 2006 at 09:25 UTC
    Something for other novices who may be watching this thread :)

    Having played about with this for a while now, I've found what works... and a little bit as to why...

    The primary thing seems to be that the number of elements in the array must match the number of bits supplied in the integer map; that is, if you have 13 options in an array, you must have a 13-bit value integer (or express it in 16-bits with extra '0's in the MSB). To make this simpler to see in the code, I think it helps to specify the mask value as a binary value.

    With this in mind, I've explored demerphq's suggestion and have found it to be the one that reliably works (with Active State perl under WinXP anyway). Here's an example:

    my @arr = qw (and you will be pleased with the results now we have it +working ); # 1 0 1 0 1 0 1 1 0 1 0 0 +1 my $mask = 0b1010101101001; # For clarity # $mask = 0b0001010101101001; # Force to byte boundary # $mask = 0x1569; # ...if you prefer to think in nybbl +es my @result = wave_hands( $mask, @arr ); my $n = @arr; printf("The array contains $n elements\n"); print "@result"; # "and will pleased the results we working" sub wave_hands { my $mask=0+shift; my $i=1; my @r; for ( my $p=$#_ ; $p>=0 && $i<=$mask ; $p-- ) { unshift @r,$_[$p] if $mask & $i; $i<<=1; } return @r; } # end wave_hands