数字比较在 awk 中产生不正确的结果

Numeric comparisons yielding incorrect result in awk

我最近在网站上发现了一个脚本:

bash, find nearest next value, forward and backward

相对较旧,需要 50 个代表才能评论,我没有。我试图让它工作,并且不太了解 awk 语法,但我正在尝试。在我使用的测试文件中:

 -3.793  0.9804E+00  0.3000E+02
 -3.560  0.1924E-01  0.3000E+02
 -3.327  0.3051E-04  0.3000E+02
 -3.093  0.3567E-08  0.3000E+02
 -2.860  0.3765E-06  0.3000E+02
 -2.627  0.1119E-02  0.3000E+02
 -2.394  0.2520E+00  0.3006E+02

这是脚本:

{
if ($fld > tgt) {
    del = $fld - tgt
    if ( (del < minGtDel) || (++gtHit == 1) ) {
        minGtDel = del
        minGtVal = $fld
    }
}
else if ($fld < tgt) {
    del = tgt - $fld
    if ( (del < minLtDel) || (++ltHit == 1) ) {
        minLtDel = del
        minLtVal = $fld
    }
}
else {
    minEqVal = $fld
}
}
END {
print (minGtVal == "" ? "NaN" : minGtVal)
print (minLtVal == "" ? "NaN" : minLtVal)
}

其中,当运行如此时:

$ awk -v fld=1 -v tgt=-3 -f awk DOSCAR

产生:

 -2.860
 NaN

尽管有下限,但我不太确定如何解决它。原来的post里面没有负数,所以没有这个问题。任何帮助表示赞赏。

您的输入文件中有一个空行,它触发了一个典型的 awk 问题。

核心问题是 awk 的比较运算符的奇怪行为,它不需要您指定是要进行数字比较还是字符串比较。 (这正是自动比较运算符不是一个好主意的原因。

简而言之,awk 中共有三种标量类型:数字、字符串和"numeric strings"。程序中的文字要么是数字要么是字符串,算术运算符的结果始终是数字,而字符串连接的结果始终是字符串。但是您正在比较的值 - $fldtgt - 都可能是 "numeric strings",因为它们来自用户输入。

A "numeric string" 是一个来自用户输入的字符串,恰好 "look like" 一个数字。总的来说,"looks like a number" 的定义并不奇怪,除了一个细节:空字符串不算。

如果比较两个数字,比较的是数字。如果比较两个字符串,比较是字典顺序的。但是,如果您正在比较的事物中的一个(或两个)可能是 "numeric string",那么比较的类型取决于它是否实际上是 "numeric string"。如果它是 "numeric string",它会变成一个数字;否则,另一个值会变成字符串。

因此,如果 $fld 是一个空字符串,那么将它与 tgt 进行比较将是字符串比较而不是数字比较。而空字符串是字符串比较的最小字符串,所以它会更小。但是,当您随后计算 $fld - tgt 时,$fld 将被强制转换为数字,在这种情况下,空字符串将变为 0.

所以有两种可能。最简单的就是强行把$fld改成数字;这至少是一致的:

{
    val = $fld + 0
    if (val > tgt) {
        del = val - tgt
        if ( (del < minGtDel) || (++gtHit == 1) ) {
            minGtDel = del
            minGtVal = val
        }
    }
    else if (val < tgt) {
        del = tgt - val
        if ( (del < minLtDel) || (++ltHit == 1) ) {
            minLtDel = del
            minLtVal = val
        }
    }
    else {
        minEqVal = val
    }  
}
END {
    print (minGtVal == "" ? "NaN" : minGtVal)
    print (minLtVal == "" ? "NaN" : minLtVal)
}

另一种方法是删除指定字段不能为数字的行。一个简单且通常可靠的数值测试是将值与强制转换为数字的自身进行比较:

(val = $fld + 0) == $fld {
    if (val > tgt) {
        del = val - tgt
        if ( (del < minGtDel) || (++gtHit == 1) ) {
            minGtDel = del
            minGtVal = val
        }
    }
    else if (val < tgt) {
        del = tgt - val
        if ( (del < minLtDel) || (++ltHit == 1) ) {
            minLtDel = del
            minLtVal = val
        }
    }
    else {
        minEqVal = val
    }  
}
END {
    print (minGtVal == "" ? "NaN" : minGtVal)
    print (minLtVal == "" ? "NaN" : minLtVal)
}