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

时间:2017-09-24 08:54:22

标签: arrays perl hash sum shift

我有这张桌子:

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行我想从第二个值添加到最后一个。

例如AAA

的第2行值
20+30+40+50 = 140

等等BBB

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

我目前有这个代码。但这并没有多大作用。它只给了我一堆数字。

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', $3, $2, $1 );
    }

    $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;

我真的坚持这个。我不知道如何处理这个问题。

2 个答案:

答案 0 :(得分:4)

可能的解决方案:

#!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 "$0: $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
}

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

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

打开我们的输入文件。注意:我们使用词法变量($fh)而不是裸字文件句柄,我们使用3参数打开。强烈建议这样做。我们还检查open的返回值并在出现故障时生成一条很好的错误消息,包括无法打开的文件的名称($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,我们尚未处理当前组的任何输入,因此我们不执行任何操作并返回。否则,我们打印NAME | DATE | VALUE标题,然后是@dates中每个元素的一行数据。对于每个$date,我们输出当前的组名称(例如AAA),$date和值的总和(所有这些都使用printf进行了很好的格式化)。最初$sum是所有组值的总和,但在第一次迭代后,我们开始从@values中减去值:如果输入中的值列表为x1,{{1} },x2x3,...,然后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; ,然后修剪每个字段。

|

如果 if ($name eq 'NAME') { $print_group->(); $group_name = undef; @dates = $date; @values = (); $sum = 0; next; } $name,则这是新群组的开头。如果有的话打印当前组的输出(如果没有当前组,则'NAME'什么都不做),然后将我们的状态变量重置回初始值,$print_group->()除外,@dates填充$date标题行中的1}}值。然后开始循环的下一次迭代,因为我们完成了这一行。

    $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

答案 1 :(得分:2)

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

此程序需要输入文件的路径作为命令行上的参数,并将结果写入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