Как я могу разбить массив 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.