Is there a compact Perl operation to slice alternate elements from an array?
If I have an array myarray
in Python, I can use the slice notation
myarray[0::2]
to select only the even-indexed elements. For example:
>>> ar = [ "zero", "one", "two", "three", "four", "five开发者_运维知识库", "six" ]
>>> ar [ 0 : : 2 ]
['zero', 'two', 'four', 'six']
Is there a similar facility in Perl?
Thanks.
A Perl array slice is the @
in front of the array name, then the list of indices you want:
@array[@indices];
There's not a built-in syntax to select multiples, but it's not so hard. Use grep
to produce a list of indices that you want:
my @array = qw( zero one two three four five six );
my @evens = @array[ grep { ! ($_ % 2) } 0 .. $#array ];
If you are using PDL, there are lots of nice slicing options.
There's array slices:
my @slice = @array[1,42,23,0];
There's a way to to generate lists between $x and $y:
my @list = $x .. $y
There's a way to build new lists from lists:
my @new = map { $_ * 2 } @list;
And there's a way to get the length of an array:
my $len = $#array;
Put together:
my @even_indexed_elements = @array[map { $_ * 2 } 0 .. int($#array / 2)];
Granted, not quite as nice as the python equivalent, but it does the same job, and you can of course put that in a subroutine if you're using it a lot and want to save yourself from some writing.
Also there's quite possibly something that'd allow writing this in a more natural way in List::AllUtils
.
I've written the module List::Gen on CPAN that provides an alternative way to do this:
use List::Gen qw/by/;
my @array = qw/zero one two three four five six/;
my @slice = map {$$_[0]} by 2 => @array;
by
partitions @array
into groups of two elements and returns an array of array references. map
then gets this list, so each $_
in the map will be an array reference. $$_[0]
(which could also be written $_->[0]
) then grabs the first element of each group that by
created.
Or, using the mapn
function which by
uses internally:
use List::Gen qw/mapn/;
my @slice = mapn {$_[0]} 2 => @array;
Or, if your source list is huge and you may only need certain elements, you can use List::Gen
's lazy lists:
use List::Gen qw/by gen/;
my $slicer = gen {$$_[0]} by 2 => @array;
$slicer
is now a lazy list (an array ref) that will generate it's slices on demand without processing anything that you didn't ask for. $slicer
also has a bunch of accessor methods if you don't want to use it as an array ref.
I'll do this in a two-step process: first generate the desired indices, and then use a slice operation to extract them:
@indices = map { $_ * 2 } (0 .. int($#array / 2));
my @extracted = @array[@indices];
Step-by-step, thats:
- generate a list of integers from 0 to the last element of the array divided by two
- multiply each integer by two: now we have even numbers from zero to the index of the last element
- extract those elements from the original array
Perl 6 will improve things dramatically, but (so far?) Perl 5 has pretty limited slicing capability: you have to explicitly specify the indexes you want, and it can't be open-ended.
So you'd have to do:
@ar = ( "zero", "one", "two", "three", "four", "five", "six" );
print @ar[ grep $_ % 2 == 0, 0..$#ar ]
One way to make this prettier is to wrap it in something like autobox
.
For example using autobox::Core
:
use autobox::Core;
my @ar = qw/zero one two three four five six/;
# you could do this
@ar->slice_while( sub{ not $_ % 2 } );
# and this
@ar->slice_by(2);
# or even this
@ar->evens;
This is how you can define these autobox methods:
sub autobox::Core::ARRAY::slice_while {
my ($self, $code) = @_;
my @array;
for (my $i = 0; $i <= $#{ $self }; $i++) {
local $_ = $i;
push @array, $self->[ $i ] if $code->();
}
return wantarray ? @array : \@array;
}
sub autobox::Core::ARRAY::slice_by {
my ($self, $by) = @_;
my @array = @$self[ map { $_ * $by } 0 .. int( $#{$self} / $by )];
return wantarray ? @array : \@array;
}
sub autobox::Core::ARRAY::evens {
my $self = shift;
my @array = $self->slice_by(2);
return wantarray ? @array : \@array;
}
/I3az/
If you don't care about the order, and if the odd-numbered elements of the list are unique, you can concisely convert the array to a hash and take the values
:
@even_elements = values %{{@array}};
@odd_elements = keys %{{@array}};
(No, this is not a serious answer)
Another way would be by using grep
:
my @array = qw( zero one two three four five six );
print map { "$_ " } @array[grep { !($_ & 1) } 0 .. $#array]; #even
Output:zero two four six
print map { "$_ " } @array[grep { ($_ & 1) } 0 .. $#array]; #odd
Output:one three five
If you don't mind using an obscure feature of $| you can do this:
{
local $|; # don't mess with global $|
@ar = ( "zero", "one", "two", "three", "four", "five", "six" );
$| = 0;
@even = grep --$|, @ar;
$| = 1;
@odd = grep --$|, @ar;
print "even: @even\\n";
# even: zero two four six
print "odd: @odd\\n";
# odd: one three five
}
or, as a 1 liner:
{ local $|=0; @even = grep --$|, @ar; }
Basically, --$| flip flops between a 0 and 1 value (despite the -- which normally decrements a numeric value), so grep sees a "true" value every other time, thus causing it to return every other item starting with the initial value of $|. Note that you must start with 0 or 1, not some arbitrary index.
Here is the simplest code without creating any index arrays:
sub even { my $f=0; return grep {++$f%2} @_; }
sub odd { my $f=1; return grep {++$f%2} @_; }
精彩评论