Как я могу разбить массив Perl на куски одинакового размера?
У меня есть массив фиксированного размера, размер массива которого всегда равен 3.
my @array = ('foo', 'bar', 'qux', 'foo1', 'bar', 'qux2', 3, 4, 5);
Как я могу сгруппировать член массива таким образом, чтобы мы могли получить
массив группы массивов на 3:
$VAR = [ ['foo','bar','qux'],
['foo1','bar','qux2'],
[3, 4, 5] ];
Ответы
Ответ 1
my @VAR;
push @VAR, [ splice @array, 0, 3 ] while @array;
или вы можете использовать natatime
из List::MoreUtils
use List::MoreUtils qw(natatime);
my @VAR;
{
my $iter = natatime 3, @array;
while( my @tmp = $iter->() ){
push @VAR, \@tmp;
}
}
Ответ 2
Или это:
my $VAR;
while( my @list = splice( @array, 0, 3 ) ) {
push @$VAR, \@list;
}
Ответ 3
Другой ответ (вариация на Tore's, используя сращивание, но избегая цикла while в пользу большей карты Perl-y)
my $result = [ map { [splice(@array, 0, 3)] } (1 .. (scalar(@array) + 2) % 3) ];
Ответ 4
Мне очень нравится List:: MoreUtils и часто его использую. Тем не менее, мне никогда не нравилась функция natatime
. Он не выводит вывод, который может использоваться с циклом for или map
или grep
.
Мне нравится привязывать операции map/grep/apply в моем коде. Как только вы поймете, как работают эти функции, они могут быть очень выразительными и очень мощными.
Но легко заставить функцию работать как natatime, которая возвращает список массивов refs.
sub group_by ([email protected]) {
my $n = shift;
my @array = @_;
croak "group_by count argument must be a non-zero positive integer"
unless $n > 0 and int($n) == $n;
my @groups;
push @groups, [ splice @array, 0, $n ] while @array;
return @groups;
}
Теперь вы можете делать такие вещи:
my @grouped = map [ reverse @$_ ],
group_by 3, @array;
** Обновить рекомендации Криса Лутца **
Крис, я вижу, что заслуживаю внимания в предлагаемом дополнении кода ref к интерфейсу. Таким образом построено поведение, подобное карте.
# equivalent to my map/group_by above
group_by { [ reverse @_ ] } 3, @array;
Это красиво и лаконично. Но чтобы сохранить симпатичную семантику кода ref {}
, мы поставили аргумент count 3
в труднодоступное место.
Думаю, мне нравятся вещи лучше, как я писал изначально.
Скопированная карта не намного сложнее, чем то, что мы получаем с расширенным API.
С исходным подходом можно использовать grep или другую аналогичную функцию без необходимости ее переопределения.
Например, если код ref добавлен в API, вам нужно сделать следующее:
my @result = group_by { $_[0] =~ /foo/ ? [@_] : () } 3, @array;
чтобы получить эквивалент:
my @result = grep $_->[0] =~ /foo/,
group_by 3, @array;
Поскольку я предложил это ради легкой цепочки, мне нравится оригинал лучше.
Конечно, было бы легко разрешить любую форму:
sub _copy_to_ref { [ @_ ] }
sub group_by ([email protected]) {
my $code = \&_copy_to_ref;
my $n = shift;
if( reftype $n eq 'CODE' ) {
$code = $n;
$n = shift;
}
my @array = @_;
croak "group_by count argument must be a non-zero positive integer"
unless $n > 0 and int($n) == $n;
my @groups;
push @groups, $code->(splice @array, 0, $n) while @array;
return @groups;
}
Теперь любая форма должна работать (непроверенная). Я не уверен, нравится ли мне оригинальный API, или этот со встроенными возможностями карты лучше.
Мысли кого-нибудь?
** Обновлено снова **
Крис прав, чтобы указать, что дополнительная версия кода ref заставит пользователей делать:
group_by sub { foo }, 3, @array;
Что не так приятно и нарушает ожидания. Поскольку нет способа иметь гибкий прототип (который я знаю), который помещает kibosh в расширенный API, и я буду придерживаться оригинала.
С другой стороны, я начал с анонимного sub в альтернативном API, но я изменил его на именованный подраздел, потому что я был тонко обеспокоен тем, как выглядел код. Нет реальной веской причины, просто интуитивная реакция. Я не знаю, имеет ли это значение в любом случае.
Ответ 5
В качестве учебного опыта я решил сделать это в Perl6
Первый, пожалуй, самый простой способ, который я пытался использовать map
.
my @output := @array.map: -> $a, $b?, $c? { [ $a, $b // Nil, $c // Nil ] };
.say for @output;
foo bar qux
foo1 bar qux2
3 4 5
Это не казалось очень масштабируемым. Что, если я захочу взять элементы из списка 10 за раз, что будет очень неприятно писать.... Хм, я просто упомянул "take" , и есть ключевое слово с именем take
позволяет попробовать это в подпрограмме, чтобы сделать его более полезным.
sub at-a-time ( @array, $n = 1 ){
my $pos = 0;
# gather is used with take
gather loop {
my $next = ($pos + $n);
# try to get just enough for this iteration
my $gimme = @array.gimme($next);
# stop the loop if there is no more elements left
last unless $pos < $gimme;
take item @array[ $pos ..^ $next ];
$pos = $next;
}
}
Для kicks позволяет попробовать его против бесконечного списка последовательности фибоначчи
my @fib = 1, 1, *+* ... *;
my @output := at-a-time( @fib, 3 );
.say for @output[^5]; # just print out the first 5
1 1 2
3 5 8
13 21 34
55 89 144
233 377 610
Обратите внимание, что я использовал :=
вместо =
, то есть оператор привязки (я мог бы также использовать ::=
для привязки только для чтения). Необходимо было запретить Perl6 пытаться найти все элементы списка (может быть, не нужно, когда Perl 6 завершен). Поскольку он пытался получить все элементы из бесконечного списка, он никогда не останавливался бы до тех пор, пока компьютер не исчерпал память.
Что вызывает еще один вопрос: почему не было дополнительного раздутия при предоставлении бесконечного списка?
Это то, о чем gather
, оно эффективно определяет lazy list, охватывая цикл цикла, который вызывается каждый раз, когда списку требуется больше элементов. take
используется для размещения большего количества элементов в ленивый список, который gather
создает.
item
используется здесь, чтобы предотвратить сглаживание во внешний список, что может не понадобиться, когда Perl 6 наконец-то.
Подождите минуту, я просто вспомнил .rotor
.
my @output := @fib.rotor(3);
@output[^5].map: *.say;
1 1 2
3 5 8
13 21 34
55 89 144
233 377 610
.rotor
на самом деле намного сильнее, чем я показал.
В настоящее время (из рабочей ветки в git), если вы хотите вернуть частичное совпадение в конце, вам нужно будет добавить :partial
к аргументам .rotor
.
Ответ 6
Попробуйте следующее:
$VAR = [map $_ % 3 == 0 ? ([ $array[$_], $array[$_ + 1], $array[$_ + 2] ])
: (),
0..$#array];
Ответ 7
perl -e '
use List::NSect qw{spart};
use Data::Dumper qw{Dumper};
my @array = ("foo", "bar", "qux", "foo1", "bar", "qux2", 3, 4, 5);
my $var = spart(3, @array);
print Dumper $var;
'
$VAR1 = [
[
'foo',
'bar',
'qux'
],
[
'foo1',
'bar',
'qux2'
],
[
3,
4,
5
]
];
Ответ 8
Другое универсальное решение, неразрушающее исходный массив:
use Data::Dumper;
sub partition {
my ($arr, $N) = @_;
my @res;
my $i = 0;
while ($i + $N-1 <= $#$arr) {
push @res, [@$arr[$i .. $i+$N-1]];
$i += $N;
}
if ($i <= $#$arr) {
push @res, [@$arr[$i .. $#$arr]];
}
return \@res;
}
print Dumper partition(
['foo', 'bar', 'qux', 'foo1', 'bar', 'qux2', 3, 4, 5],
3
);
Выход:
$VAR1 = [
[
'foo',
'bar',
'qux'
],
[
'foo1',
'bar',
'qux2'
],
[
3,
4,
5
]
];
Ответ 9
Ниже более общее решение проблемы:
my @array = ('foo', 'bar', 1, 2);
my $n = 3;
my @VAR = map { [] } 1..$n;
my @idx = sort map { $_ % $n } 0..$#array;
for my $i ( 0..$#array ){
push @VAR[ $idx[ $i ] ], @array[ $i ];
}
Это также работает, когда количество элементов в массиве не является фактором 3.
В приведенном выше примере другие решения, например, splice
создаст два массива длины 2 и один из длины 0.