在 Perl 中为每个 ID 添加数组中的所有值

Add all values in array for each ID in Perl

我有这个table:

NAME  |12/31/2016|VALUE
AAA   |1/31/2017 |10
AAA   |2/1/2017  |20
AAA   |2/2/2017  |30
AAA   |2/3/2017  |40
AAA   |2/4/2017  |50
NAME  |2/9/2017  |VALUE
BBB   |2/10/2017 |20
BBB   |2/11/2017 |30
BBB   |2/12/2017 |40
BBB   |2/13/2017 |50
BBB   |2/14/2017 |60

这就是我想要的输出:

NAME  |DATE       |VALUE
AAA   |12/31/2016 |150
AAA   |1/31/2017  |140
AAA   |2/1/2017   |120
NAME  |DATE       |VALUE
BBB   |2/9/2017   |200
BBB   |2/10/2017  |180
BBB   |2/11/2017  |150

我想要做的是,对于每个有效符号,(AAABBB) 我想要三行。

对于每一列的第一行,我想要添加所有值,

例如,AAA 的第 1 行值:

10+20+30+40+50 = 150

然后对于第 2 行,我只想将第二个值添加到最后一个值。

例如第 2 行的值 AAA

20+30+40+50 = 140

等等 BBB.

我想向下移动日期以便 12/31/2016 匹配 AAA,然后获取每行的前三个日期。

我目前有这个代码。但这没什么用。它只是给了我一堆数字。

use strict;
use warnings;

use Scalar::Util qw(looks_like_number);
use Data::Dumper;

sub uniq {
    my %seen;
    grep !$seen{$_}++, @_;
}

my %cashflow;
my %fields = (
    ID    => 0,
    DATES => 1,
    VALUE => 2,
);

my @total;
my @IDs;
my @uniqueIDs;
my @dates;
my @add;
my $i = 0;
my @values;

my $counter = 3;

open( FILE, "try.CSV" );

while ( my $line = <FILE> ) {
    chomp( $line );
    my @lineVals = split( /\|/, $line );

    if ( $lineVals[ $fields{ID} ] !~ /^SYMBOL$/i ) {
        push @IDs, $lineVals[ $fields{ID} ];
    }
    @uniqueIDs = uniq( @IDs );

    #push all CASH FLOW AMOUNTS to @cashflow
    if ( looks_like_number( $lineVals[ $fields{VALUE} ] ) ) {
        $lineVals[ $fields{VALUE} ] =~ s/\r//;
        push @total, $lineVals[ $fields{VALUE} ];
    }

    if ( $lineVals[ $fields{DATES} ] =~ /(\d{1,2})\/(\d{1,2})\/(\d{4})/ ) {
        $lineVals[ $fields{DATES} ] = sprintf( '%04d%02d%02d', , ,  );
    }

    $cashflow{ uc $lineVals[ $fields{ID} ] }{DATES} = $lineVals[ $fields{DATES} ];
    $cashflow{ uc $lineVals[ $fields{ID} ] }{VALUE} = $lineVals[ $fields{VALUE} ];

    foreach my $ID ( @uniqueIDs ) {

        foreach my $symb ( keys %cashflow ) {

            if ( $ID = $symb ) {

                if ( looks_like_number( $lineVals[ $fields{VALUE} ] ) ) {

                    $lineVals[ $fields{VALUE} ] =~ s/\r//;
                    push @total, $lineVals[ $fields{VALUE} ];

                    my $i     = 0;
                    my $grand = 0;

                    foreach my $val ( @total ) {

                        while ( $i < $counter ) {

                            $grand += $val;
                            print "$grand \n";
                            $i++;
                        }

                        shift @total;
                    }
                }
            }
        }
    }
}

close FILE;

我真的被这个困住了。我不知道该怎么办。

可能的解决方案:

#!perl
use strict;
use warnings;

sub trim {
    my ($str) = @_;
    s!\A\s+!!, s!\s+\z!! for $str;
    $str
}

my $file = 'try.CSV';    
open my $fh, '<', $file or die "[=10=]: $file: $!\n";

my ($group_name, @dates, @values);
my $sum = 0;

my $print_group = sub {
    return if !defined $group_name;
    my $format = "    %-6s|%-11s|%s\n";
    printf $format, 'NAME', 'DATE', 'VALUE';
    for my $date (@dates) {
        printf $format, $group_name, $date, $sum;
        $sum -= shift @values if @values;
    }
};

while (my $line = readline $fh) {
    my ($name, $date, $value) = map trim($_), split /\|/, $line;
    if ($name eq 'NAME') {
        $print_group->();
        $group_name = undef;
        @dates = $date;
        @values = ();
        $sum = 0;
        next;
    }
    $group_name ||= $name;
    push @dates, $date if @dates < 3;
    push @values, $value if @values < 2;
    $sum += $value;
}
$print_group->();

让我们回顾一下。

sub trim {
    my ($str) = @_;
    s!\A\s+!!, s!\s+\z!! for $str;
    $str
}

用于从字符串中删除 leading/trailing 空格的辅助函数。我们在这里使用 ! 作为 s 分隔符,因为 / 破坏了 SO 的语法突出显示。耸耸肩。

my $file = 'try.CSV';    
open my $fh, '<', $file or die "[=12=]: $file: $!\n";

打开我们的输入文件。注意:我们使用词法变量 ($fh) 而不是裸字文件句柄,并且我们使用 3-argument open。强烈建议这样做。我们还检查 open 的 return 值并在失败时生成一个很好的错误消息,包括无法打开的文件名 ($file) 和失败的原因 ($!).

my ($group_name, @dates, @values);
my $sum = 0;

我们设置了一些我们希望在循环迭代中保留的状态变量。 $group_name 是我们当前正在处理的组的名称,@dates 是我们目前看到的保存日期,@values 是我们目前看到的保存值。 $sum 是当前组中所有值的 运行 总和,它从 0.

开始
my $print_group = sub {
    return if !defined $group_name;
    my $format = "    %-6s|%-11s|%s\n";
    printf $format, 'NAME', 'DATE', 'VALUE';
    for my $date (@dates) {
        printf $format, $group_name, $date, $sum;
        $sum -= shift @values if @values;
    }
};

用于打印单个组输出的辅助函数。如果 $group_name 没有设置,我们还没有处理当前组的任何输入,所以我们什么也不做,return。否则我们打印 NAME | DATE | VALUE header,然后是 @dates 中每个元素的一行数据。对于每个 $date,我们输出当前组名(例如 AAA)、$date 和值的总和(所有这些都使用 printf 很好地格式化)。最初 $sum 是所有组值的总和,但在第一次迭代后我们开始从 @values 中减去值:如果输入中的值列表是 x1x2, x3, x4, ..., 然后 $sum 最初是 x1 + x2 + x3 + x4 + ...,这就是输出第一行中打印的内容。之后我们减去 x1,所以下一行得到 x1 + x2 + x3 + x4 + ... - x1,即 x2 + x3 + x4 + ...。之后我们减去x2,所以第三行数据得到x3 + x4 + ....

while (my $line = readline $fh) {
    my ($name, $date, $value) = map trim($_), split /\|/, $line;

我们的主循环。我们读取一行输入,将其拆分为 |,每个字段 trim。

    if ($name eq 'NAME') {
        $print_group->();
        $group_name = undef;
        @dates = $date;
        @values = ();
        $sum = 0;
        next;
    }

如果$name'NAME',这是一个新组的开始。打印当前组的输出(如果没有当前组,$print_group->() 什么也不做),然后将我们的状态变量重置为初始值,除了 @dates,它被 $date 来自 header 行的值。然后开始循环的下一次迭代,因为我们已经完成了这一行。

    $group_name ||= $name;
    push @dates, $date if @dates < 3;
    push @values, $value if @values < 2;
    $sum += $value;

如果我们到了这里,这行不是新组的开始。如果尚未设置,我们设置 $group_name。我们将 $date 添加到我们保存的日期列表中(但我们只需要 3 个日期,所以如果我们已经有 3 个则什么都不做)。我们将 $value 添加到保存值列表中(但我们只需要其中的 2 个)。最后,我们将 $value 添加到组中的总数 $sum

}
$print_group->();

在循环结束时我们也刚刚处理完一组,所以我们也需要在这里调用$print_group

这将按照您的要求进行。它将整个数据文件读入一个数组数组,并在打印之前操作该数组。这些块从末尾向后处理,以便在删除尾随行时其他块保持原位

此程序需要输入文件的路径作为命令行参数,并将结果写入 STDOUT

use strict;
use warnings 'all';

my @data = map [ /[^|\s]+/g ], <>;

# Make a list of the indices of all the header rows
my @headers = grep { $data[$_][0] eq 'NAME' } 0 .. $#data;

# Make a list of the indices of the first
# and last lines of all the data blocks
my @blocks = map {
    [
        $headers[$_] + 1,
        $_ == $#headers ? $#data : $headers[$_+1] - 1
    ]
} 0 .. $#headers;

# Shift the second column down
# Replace the col2 header with 'DATE'
#
$data[$_][1] = $data[$_-1][1] for reverse 1 .. $#data;
$data[$_][1] = 'DATE' for @headers;


# Edit each block of data
#
for my $block ( reverse @blocks ) {

    my ( $beg, $end ) = @$block;

    # Calculate the block total
    my $total = 0;
    for ( $beg ... $end ) {
        $total += $data[$_][2];
    }

    # Calculate the first three data values
    for my $i ( $beg .. $beg + 2 ) {
        my $next = $total - $data[$i][2];
        $data[$i][2] = $total;
        $total = $next;
    }

    # Remove everything except those three lines
    splice @data, $beg+3, $end-$beg-2;
}

print join('|', @$_), "\n" for @data;

产出

NAME|DATE|VALUE
AAA|12/31/2016|150
AAA|1/31/2017|140
AAA|2/1/2017|120
NAME|DATE|VALUE
BBB|2/9/2017|200
BBB|2/10/2017|180
BBB|2/11/2017|150