使用 awk 数组时,对所述问题进行大数求和并打印带有所有小数点的结果

使用 awk 数组时,对所述问题进行大数求和并打印带有所有小数点的结果

我有一个下面的输入文件,我需要根据第三列中的日期将其拆分为多个文件。基本上所有相同日期的交易都应该分成特定日期的文件。分割后我需要创建标题和预告片。预告片应在第四列中包含记录数和金额总和(该日期的金额总和)。在这种情况下,正如我上面所说,我有非常大的数字,如何在下面的代码中集成 bc 。

输入文件

H|~^20200425|~^abcd|~^sum
R|~^abc|~^2019-03-06T12:33:52.27|~^123562388.23456|~^2018-04-12T12:33:52.27|~^hhh
R|~^abc|~^2019-03-05T12:33:52.27|~^105603.042|~^2018-10-23T12:33:52.27|~^aus
R|~^abc|~^2019-03-05T12:33:52.27|~^2054.026|~^2018-10-24T12:33:52.27|~^usa
R|~^abc|~^2019-03-06T12:33:52.27|~^10.00|~^2018-09-11T12:33:52.27|~^virginia
R|~^abc|~^2019-03-05T12:33:52.27|~^30.00|~^2018-08-05T12:33:52.27|~^ddd
R|~^abc|~^2019-03-06T12:33:52.27|~^15.03|~^2018-10-23T12:33:52.27|~^jjj
R|~^abc|~^2019-03-06T12:33:52.27|~^10.04|~^2018-04-08T12:33:52.27|~^jj
R|~^abc|~^2019-03-05T12:33:52.27|~^20.00|~^2018-07-23T12:33:52.27|~^audg
T|~^20200425|~^8|~^xxx|~^123670130.37256

输出文件20190305.txt

H|~^20200425|~^abcd|~^sum
R|~^abc|~^2019-03-05T12:33:52.27|~^105603.042|~^2018-10-23T12:33:52.27|~^aus
R|~^abc|~^2019-03-05T12:33:52.27|~^2054.026|~^2018-10-24T12:33:52.27|~^usa
R|~^abc|~^2019-03-05T12:33:52.27|~^30.00|~^2018-08-05T12:33:52.27|~^ddd
R|~^abc|~^2019-03-05T12:33:52.27|~^20.00|~^2018-07-23T12:33:52.27|~^audg
T|~^20200425|~^4|~^xxx|~^107707.068

输出文件20190306.txt

H|~^20200425|~^abcd|~^sum
R|~^abc|~^2019-03-06T12:33:52.27|~^123562388.23456|~^2018-04-12T12:33:52.27|~^hhh
R|~^abc|~^2019-03-06T12:33:52.27|~^10.00|~^2018-09-11T12:33:52.27|~^virginia
R|~^abc|~^2019-03-06T12:33:52.27|~^15.03|~^2018-10-23T12:33:52.27|~^jjj
R|~^abc|~^2019-03-06T12:33:52.27|~^10.04|~^2018-04-08T12:33:52.27|~^jj
T|~^20200425|~^4|~^xxx|~^123562423.30456

我正在使用的代码(PS:由我们的一位社区成员建议)这是一个awk解决方案:

awk -F'\\|~\\^' '{ 
            if($1=="H"){ 
                head=$0
            }
            else if($1=="T"){
                foot=$1"|~^"$2
                foot4=$4
            }
            else{
                date=$3;
                sub("T.*","", date);
                data[date][NR]=$0;
                sum[date]+=$4; 
                num[date]++
            }
           }
           END{
            for(date in data){
                file=date".txt";
                gsub("-","",file); 
                print head > file; 
                for(line in data[date]){
                    print data[date][line] > file
                } 
                printf "%s|~^%s|~^%s|~^%s\n", foot, num[date], 
                                              foot4, sum[date] > file
            }
           }' file 

代码运行得非常好。但在这一步中

sum[date]+=$4;

它无法对大数求和。因为我在最后一步使用%s,所以预告片总和以指数值打印。

printf "%s|~^%s|~^%s|~^%s\n", foot, num[date], 
                                                  foot4, sum[date] > file

在这里,我只想对大数应用求和并打印精确的总和。 (我在这里尝试了 bc(bash 计算器),但被卡住了,因为这个总和是基于数组的,而且它是根据特定日期添加的)。请帮我解决这个问题

另外,我尝试"%.15g"了拖车步骤

printf "%s|~^%s|~^%s|~^%.15g\n", foot, num[date], 
                                                  foot4, sum[date] > file

在此,如果结果有 15 位(包括小数点),我就能得到准确的总和。如果总和结果超过 15 位,则此方法不起作用。请帮忙

答案1

如果不考虑你的大数字问题,我会编写awk如下程序:

BEGIN {
        FS = "\\|~\\^"
        OFS= "|~^"
}

$1 == "H" {
        header = $0
}

$1 == "R" {
        name = $3
        sub("T.*", "", name)

        sum[name] += $4
        cnt[name] += 1

        if (cnt[name] == 1)
                print header >name ".txt"

        print >name ".txt"
}

$1 == "T" {
        for (name in sum)
                print $1, $2, cnt[name], $4, sum[name] >name ".txt"
}

为了方便起见,我将输出字段分隔符 , 设置OFS|~^。这使我不必担心将其插入到我输出的字段之间。输入的字段分隔符FS, 设置为与该字符串匹配的正则表达式。

然后我有三个主要代码块:

  1. 一个用于解析该H行。假设只有其中一个并且它在一开始就发生。这只是将标题行存储在变量中header

  2. 一个用于解析R行。每条记录都包含应在第三个字段中用作输出文件名的日期。其解析方式与您的解析方式相同。该日期的总和会被累加,并且计数器也会递增。

    如果计数器为 1,即如果这是我们第一次看到该特定日期,则我们将标头写入与该日期关联的输出文件。然后我们将当前记录写入文件。

  3. 最后一个块解析该T行。假设只有其中之一并且它出现在最后。这只是将每个单独日期的累积总和和计数以及原始T行中的一些数据输出到与该日期关联的文件。

支持任意大数(你说别处如果您的数字需要超过 100 位来存储,因此会溢出awk) 中的整数,我们将任意精度计算器用作bc“协进程”(一种计算服务)。该行sum[name] += $4被替换为

if (sum[name] == "") sum[name] = 0
printf "%s + %s\n", sum[name], $4 |& "bc"
"bc" |& getline sum[name]

这需要 GNU awk(以一种或另一种方式可用于大多数 Unix 系统)。

其作用是,如果当前日期还没有总和,则首先将当前日期的总和初始化为零。我们这样做是因为我们需要提供初始总和0bc

bc然后,我们打印应使用 GNUawk特定管道进行计算的表达式,|&以写入协进程。该bc实用程序将与我们的脚本并行启动并运行awk,进行计算,然后将另一个管道的getline输出直接读取到.bc|&sum[name]

据我了解,GNUawk不会bc为每个求和生成一个单独的进程,而是会维护一个bc作为协进程运行的进程。因此,这比在本地进行计算要慢,但比为每个求和awk生成单独的计算要快得多。bc

对于给定的数据,将创建以下两个文件:

$ cat 2019-03-05.txt
H|~^20200425|~^abcd|~^sum
R|~^abc|~^2019-03-05T12:33:52.27|~^105603.042|~^2018-10-23T12:33:52.27|~^aus
R|~^abc|~^2019-03-05T12:33:52.27|~^2054.026|~^2018-10-24T12:33:52.27|~^usa
R|~^abc|~^2019-03-05T12:33:52.27|~^30.00|~^2018-08-05T12:33:52.27|~^ddd
R|~^abc|~^2019-03-05T12:33:52.27|~^20.00|~^2018-07-23T12:33:52.27|~^audg
T|~^20200425|~^4|~^xxx|~^107707.068
$ cat 2019-03-06.txt
H|~^20200425|~^abcd|~^sum
R|~^abc|~^2019-03-06T12:33:52.27|~^123562388.23456|~^2018-04-12T12:33:52.27|~^hhh
R|~^abc|~^2019-03-06T12:33:52.27|~^10.00|~^2018-09-11T12:33:52.27|~^virginia
R|~^abc|~^2019-03-06T12:33:52.27|~^15.03|~^2018-10-23T12:33:52.27|~^jjj
R|~^abc|~^2019-03-06T12:33:52.27|~^10.04|~^2018-04-08T12:33:52.27|~^jj
T|~^20200425|~^4|~^xxx|~^123562423.30456

答案2

我已经写了一个awk 代码来解决这个问题它的运行速度比您在此处呈现的代码更快。

您过去已经问过对许多数字求和并得到不精确答案的问题。这个问题与另一个问题非常相似为什么这两个 sum 命令之间存在差异?

该问题的文件大小为 20 MB,有 700 多行。
您已声明您的文件的顺序为文件大小约为 500 至 600 mb。这会将行数增加到 1000 万行的范围。

问题是要添加的数字:

  • 可能相差很大:范围从 3 位数字12.8到 28 位数字1245637.34526234567299999999

  • 将 28 位数字相加 1000 万次,需要 28 + 7 = 35 位数字。这是假设数字不全是小数或整数。如果发生这种情况,我们讨论的是 70 位数字(35 个整数 + 35 个小数)。

  • 浮点数的表示始终是精确数字的近似值,这是浮点数的一个基本问题。如果必须有精确的总和,则必须将它们全部作为整数相加。

作为问题的解决方案,可能是使用具有更长位数的 GNU awk。 awk 中的默认浮点数使用 53 位尾数,仅适用于 15 位数字。

如果您使用使用 MPFR(可靠多精度浮点)和 GMP(GNU 多精度算术库)编译的 GNU AWK,则其 --version 文本的结果应包含该信息 (execute awk --version)。在这种情况下,您可以使用更多位。为了能够保留 40 位浮点数(上面计算的 35 位数字 + 一些安全边际),您将需要:

b = ceil(d log2(10)) + 1

b = ceil( 40 * 3.321928 ) + 1 = 133 + 1 = 134 binary digits (bits)

因此,awk 调用应该是:

 awk -M -v PREC=134 

警告:使用更多数字会使程序变慢。

并且仍然使用相同的 awk 程序

awk -M -v PREC=134 '

     BEGIN  { FS="\\|~\\^"; OFS="|~^" }
     $1=="H"{ header=$0; hdr=$2 }
     $1=="R"{
              t=gensub(/-/, "","g",$3)
              file=gensub(/T.*/,"",1,t);
              sum[file]+=$4
              if(count[file]==0){ print header >file }
              count[file]++
              print $0 >>file
            }
     END    {
              for( i in sum ){
                  printf "T %s %10d xxx %45.25f",hdr,count[i],"xxx",sum[i] >> i;
                  close(i)
                  }
            }
' "inputfile"

仅供参考:您一遍又一遍地问几乎相同的问题:

相关内容