创建任意分组的子列表

Creating sublists of arbitrary grouping

我正在尝试根据每个字符串的第一部分(即第一个制表符之前的部分,如果有制表符或整个字符串,如果没有制表符)对字符串列表中的几个项目进行分组).

这个有效:

use Test;

my @lines    = "A\tFoo"
             , "A\tBar"
             , "B"
             , "B"
             , "A\tBaz"
             , "B"
             ;

my @expected = ["A\tFoo", "A\tBar"]
             , ["B", "B"]
             , ["A\tBaz"]
             , ["B"]
             ;

my @result = group-lines(@lines);

is @result, @expected, "Grouped correctly";

sub group-lines (@records) {
    my @groups;
    my @current-records;

    my $last-type;
    for @records -> $record {

        my $type = $record.split("\t")[0];

        once { $last-type = $type }

        if $type ne $last-type {
            @groups.push: [@current-records];
            @current-records = ();
        }
        @current-records.push: $record;

        LAST { @groups.push: [@current-records] }
    }

    return @groups;
}

但它似乎很冗长。在 Perl 6 中没有更短的方法来做到这一点吗?请注意,我只想对原始列表中连续成员的项目进行分组。

(更新)组内的顺序很重要。


更新

这是一个更注重数字的例子。它根据第一个数字对后续数字的整除性对数字进行分组。

#!/bin/env perl6
use Test;

my @numbers = 2, 4, 6, 3, 6, 9, 12, 14;

my @expected = [2, 4, 6], [3, 6, 9, 12], [14];

my @result = group-nums(@numbers);

is @result, @expected, "Grouped correctly";

sub group-nums (@numbers) {
    my @groups;
    my @current-group;

    my $denominator = @numbers[0];

    for @numbers -> $num {

        if $num % $denominator {
            @groups.push: [@current-group];
            @current-group = ();
        }
        @current-group.push: $num;

    }
    @groups.push: [@current-group];

    return @groups;
}

您可以使用 categorize(或 categorize-list,如果您希望元素出现在多个类别中,则可以使用 classify 变体)。由于您的分组是动态的,具体取决于之前出现的键,请使用 state 变量来记住之前出现的内容。第二个例子很简单,因为虽然顺序很重要,但不会阻止将元素重新添加到旧组:

my @numbers = <2 4 6 3 6 9 12 14>;
@numbers.classify: {
  state $denom = $_; if $_ !%% $denom { $denom = $_ }; $denom;
};
# result: {2 => [2 4 6], 3 => [3 6 9 12], 14 => [14]}

您的第一个示例需要将每个分组与之前的分组区分开来,因此快速而肮脏的方法是为每个组编制索引,这样您就可以有两个 A 组:

my %result = @lines.classify: {
  state $index = 0; # first group is group 0
  state $prefix = .split("\t")[0]; # The first prefix is based on the first string
  if !.starts-with($prefix) {
    $prefix = .split("\t")[0]; # This is a new prefix. Remember it.
    ++$index; # start a new group
  };
  ($index<> => $prefix<>); # Classify this element with a decontainerized pair. See note.
};
# result: {0      A => [A Foo A   Bar], 1 B => [B B], 2   A => [A Baz], 3 B => [B]}
say %result.values; # output: ([B] [B B] [A   Baz] [A Foo A   Bar])

你需要把这些整理好吗?由于这两种方法都是使用哈希来存储数据,所以结果是无序的。

注意:我使用 <> 运算符显式地去容器化 Pair 中用作分类值的值。由于这个值是一个散列键,没有去容器化,对象 ID(技术上是 .WHICH 值)用于散列,你会发现给定 $one = 1(a => 1).WHICH !eqv (a => $one).WHICH。因此,您删除了容器,因此该对被视为一对普通值,它们将具有相同的哈希键。

注2:分类键可以是列表,会形成嵌套的数据结构。您无需对密钥进行去容器化处理,也无需担心忘记顺序。唯一的烦恼是在输出中多了一层嵌套。要获得嵌套结果,您的分类键将为 ($index, $prefix).

这是一些受功能启发的解决方案,尽管可能有点令人费解:

use Test;

my @lines    = "A\tFoo"
             , "A\tBar"
             , "B"
             , "B"
             , "A\tBaz"
             , "B"
             ;

my @expected = ["A\tFoo", "A\tBar"]
             , ["B", "B"]
             , ["A\tBaz"]
             , ["B"]
             ;

my @eq = @lines.map(*.split("\t")[0]).rotor(2 => -1).map({ [eq] .list});
my @result = [@lines[0],],;
for @lines[1..*] Z @eq -> ($line, $eq) {
    @result.push([]) unless $eq;
    @result[*-1].push: $line;
}

plan 1;
is-deeply @result, @expected;

想法是 @eq 包含每个位置(除了第一个)一个 True 如果前一个元素与当前元素具有相同的前缀。

但我们不假装 Lisp 是独一真神,carcdr 是她的先知,我们可以内联该决定,只需使用数组索引访问我们需要时的前一个元素:

my @result;
for @lines.kv ->  $idx, $elem {
    @result.push([]) if $idx == 0 || $elem.split("\t")[0] ne @lines[$idx-1].split("\t")[0];
    @result[*-1].push: $elem;
}

plan 1;
is-deeply @result, @expected;