如何在bash中使用正好2个有效数字格式化浮点数?


17

我想在bash中用正好两个有效数字打印浮点数(也许使用awk,bc,dc,perl等通用工具)。

例子:

  • 76543应打印为76000
  • 0.0076543应打印为0.0076

在这两种情况下,有效数字分别为7和6。对于类似的问题,我已经阅读了一些答案,例如:

如何在shell中四舍五入浮点数?

浮点变量的Bash限制精度

但是答案集中在限制小数位数(例如bcscale=2或的printf命令%.2f)上,而不是有效位数上。

有没有一种简单的方法可以用2个有效数字来格式化数字,或者我必须编写自己的函数?

Answers:


13

对第一个链接的问题的答案结尾处几乎是乱扔一线:

另请参见%g有关四舍五入到指定数量的有效数字的信息。

所以你可以简单地写

printf "%.2g" "$n"

(但请参见以下有关十进制分隔符和语言环境的部分,并请注意,非Bash printf不需要支持%fand %g)。

例子:

$ printf "%.2g\n" 76543 0.0076543
7.7e+04
0.0077

当然,您现在有了尾数指数表示形式,而不是纯十进制形式,因此您需要转换回:

$ printf "%0.f\n" 7.7e+06
7700000

$ printf "%0.7f\n" 7.7e-06
0.0000077

将所有这些放在一起,并将其包装在一个函数中:

# Function round(precision, number)
round() {
    n=$(printf "%.${1}g" "$2")
    if [ "$n" != "${n#*e}" ]
    then
        f="${n##*e-}"
        test "$n" = "$f" && f= || f=$(( ${f#0}+$1-1 ))
        printf "%0.${f}f" "$n"
    else
        printf "%s" "$n"
    fi
}

(注意-此函数是用可移植(POSIX)Shell编写的,但是假定该printf函数可以处理浮点转换。Bash具有内置的printf函数,因此您可以在这里使用,GNU实现也可以使用,因此大多数GNU / Linux系统可以安全地使用Dash)。

测试用例

radix=$(printf %.1f 0)
for i in $(seq 12 | sed -e 's/.*/dc -e "12k 1.234 10 & 6 -^*p"/e' -e "y/_._/$radix/")
do
    echo $i "->" $(round 2 $i)
done

检测结果

.000012340000 -> 0.000012
.000123400000 -> 0.00012
.001234000000 -> 0.0012
.012340000000 -> 0.012
.123400000000 -> 0.12
1.234 -> 1.2
12.340 -> 12
123.400 -> 120
1234.000 -> 1200
12340.000 -> 12000
123400.000 -> 120000
1234000.000 -> 1200000

关于小数点分隔符和语言环境的注释

上面的所有工作都假设基数字符(也称为十进制分隔符)为.,在大多数英语语言环境中都是如此。,取而代之的是其他语言环境,并且某些外壳程序具有printf尊重语言环境的内置功能。在这些shell中,您可能需要设置LC_NUMERIC=C为强制使用.as作为基数字符,或者编写/usr/bin/printf以防止使用内置版本。后一种情况因以下事实而变得复杂:(至少某些版本)似乎总是使用来解析参数.,而使用当前的语言环境设置进行打印。


@StéphaneChazelas,为什么在我删除了Bashism之后,您为什么将经过仔细测试的POSIX shell shebang改回Bash?您的注释中提到了%f/ %g,但这是自printf变量,并且不需要POSIX printf即可拥有POSIX shell。我认为您应该发表评论而不是在此处进行编辑。
Toby Speight,

printf %g不能在POSIX脚本中使用。的确,这printf取决于实用程序,但是该实用程序内置在大多数Shell中。OP标记为bash,因此使用bash shebang是获得支持%g的printf的一种简便方法。否则,你需要添加一个假设你的printf(或的内置的printf你sh如果printf是内置有)支持非标准(但很常见)%g...
斯特凡Chazelas

dash的具有内置功能printf(支持%g)。在GNU系统上, mksh可能是当今仅有的没有buildin的外壳printf
斯特凡Chazelas

感谢您的改进-我已经进行了编辑,只删除了shebang(因为标记了问题bash)并将其中的一部分放到便笺中-现在看起来正确吗?
Toby Speight,

1
遗憾的是,如果尾随数字为零,这将无法打印正确的数字。例如printf "%.3g\n" 0.400给出0.4而不是0.400
phiresky

4

TL; DR

只需复制并使用sigf该部分中的功能A reasonably good "significant numbers" function:。它被编写(作为此答案中的所有代码)以与dash一起使用

它将用数字给出Nprintf整数部分的近似值$sig

关于小数点分隔符。

用printf解决的第一个问题是“小数点”的作用和使用,“小数点”在美国是一个点,而在DE中是一个逗号(例如)。这是一个问题,因为对某些语言环境(或外壳程序)有效的方法将在其他语言环境下失败。例:

$ dash -c 'printf "%2.3f\n" 12.3045'
12.305
$  ksh -c 'printf "%2.3f\n" 12.3045'
ksh: printf: 12.3045: arithmetic syntax error
ksh: printf: 12.3045: arithmetic syntax error
ksh: printf: warning: invalid argument of type f
12,000
$ ksh -c 'printf "%2.2f\n" 12,3045'
12,304

一种常见(且不正确的解决方案)是LC_ALL=C为printf命令设置的。但这会将小数点设置为固定的小数点。对于逗号(或其他)是问题的常用字符的语言环境。

解决方案是在运行它的外壳程序的脚本中找出什么是区域设置十进制分隔符。那很简单:

$ printf '%1.1f' 0
0,0                            # for a comma locale (or shell).

删除零:

$ dec="$(IFS=0; printf '%s' $(printf '%.1f'))"; echo "$dec"
,                              # for a comma locale (or shell).

该值用于更改带有测试列表的文件:

sed -i 's/[,.]/'"$dec"'/g' infile

这使得在任何外壳程序或语言环境上的运行都自动有效。


一些基本知识。

剪切要使用format %.*e甚至%.*gprintf 格式化的数字应该很直观。使用%.*e或之间的主要区别%.*g是它们对数字的计数方式。一种使用完整计数,另一种则需要较少计数:

$ printf '%.*e  %.*g' $((4-1)) 1,23456e0 4 1,23456e0
1,235e+00  1,235

对于4个有效数字,效果很好。

从数字中减去数字位数后,我们需要执行其他步骤来格式化指数不同于0的数字(如上所述)。

$ N=$(printf '%.*e' $((4-1)) 1,23456e3); echo "$N"
1,235e+03
$ printf '%4.0f' "$N"
1235

这可以正常工作。整数部分的计数(在小数点左边)只是指数($ exp)的值。所需的小数位数是有效位数($ sig)减去小数点分隔符左侧已经使用的位数:

a=$((exp<0?0:exp))                      ### count of integer characters.
b=$((exp<sig?sig-exp:0))                ### count of decimal characters.
printf '%*.*f' "$a" "$b" "$N"

由于f格式的组成部分没有限制,因此实际上无需显式声明它,并且此(简单)代码有效:

a=$((exp<sig?sig-exp:0))                ### count of decimal characters.
printf '%0.*f' "$a" "$N"

初审。

可以以更自动化的方式执行此操作的第一个功能:

# Function significant (number, precision)
sig1(){
    sig=$(($2>0?$2:1))                      ### significant digits (>0)
    N=$(printf "%0.*e" "$(($sig-1))" "$1")  ### N in sci (cut to $sig digits).
    exp=$(echo "${N##*[eE+]}+1"|bc)         ### get the exponent.
    a="$((exp<sig?sig-exp:0))"              ### calc number of decimals.
    printf "%0.*f" "$a" "$N"                ### re-format number.
}

第一次尝试可用于许多数字,但对于那些可用位数少于请求的有效位数且指数小于-4的数字将失败:

   Number       sig                       Result        Correct?
   123456789 --> 4<                       123500000 >--| yes
       23455 --> 4<                           23460 >--| yes
       23465 --> 4<                           23460 >--| yes
      1,2e-5 --> 6<                    0,0000120000 >--| no
     1,2e-15 -->15< 0,00000000000000120000000000000 >--| no
          12 --> 6<                         12,0000 >--| no  

它将添加许多不需要的零。

第二次审判。

为了解决这个问题,我们需要清除N的指数和任何尾随零。然后,我们可以获得可用数字的有效长度并进行处理:

# Function significant (number, precision)
sig2(){ local sig N exp n len a
    sig=$(($2>0?$2:1))                      ### significant digits (>0)
    N=$(printf "%+0.*e" "$(($sig-1))" "$1") ### N in sci (cut to $sig digits).
    exp=$(echo "${N##*[eE+]}+1"|bc)         ### get the exponent.
    n=${N%%[Ee]*}                           ### remove sign (first character).
    n=${n%"${n##*[!0]}"}                    ### remove all trailing zeros
    len=$(( ${#n}-2 ))                      ### len of N (less sign and dec).
    len=$((len<sig?len:sig))                ### select the minimum.
    a="$((exp<len?len-exp:0))"              ### use $len to count decimals.
    printf "%0.*f" "$a" "$N"                ### re-format the number.
}

但是,那是使用浮点数学,并且“浮点没有什么简单的”:为什么我的数字不累加?

但是在“浮点数”中没有什么是简单的。

printf "%.2g  " 76500,00001 76500
7,7e+04  7,6e+04

然而:

 printf "%.2g  " 75500,00001 75500
 7,6e+04  7,6e+04

为什么?:

printf "%.32g\n" 76500,00001e30 76500e30
7,6500000010000000001207515928855e+34
7,6499999999999999997831226199114e+34

而且,该命令printf是许多shell的内置函数。外壳可能会更改
哪些printf打印内容:

$ dash -c 'printf "%.*f" 4 123456e+25'
1234560000000000020450486779904.0000
$  ksh -c 'printf "%.*f" 4 123456e+25'
1234559999999999999886313162278,3840

$  dash ./script.sh
   123456789 --> 4<                       123500000 >--| yes
       23455 --> 4<                           23460 >--| yes
       23465 --> 4<                           23460 >--| yes
      1.2e-5 --> 6<                        0.000012 >--| yes
     1.2e-15 -->15<              0.0000000000000012 >--| yes
          12 --> 6<                              12 >--| yes
  123456e+25 --> 4< 1234999999999999958410892148736 >--| no

合理的“有效数字”功能:

dec=$(IFS=0; printf '%s' $(printf '%.1f'))   ### What is the decimal separator?.
sed -i 's/[,.]/'"$dec"'/g' infile

zeros(){ # create an string of $1 zeros (for $1 positive or zero).
         printf '%.*d' $(( $1>0?$1:0 )) 0
       }

# Function significant (number, precision)
sigf(){ local sig sci exp N sgn len z1 z2 b c
    sig=$(($2>0?$2:1))                      ### significant digits (>0)
    N=$(printf '%+e\n' $1)                  ### use scientific format.
    exp=$(echo "${N##*[eE+]}+1"|bc)         ### find ceiling{log(N)}.
    N=${N%%[eE]*}                           ### cut after `e` or `E`.
    sgn=${N%%"${N#-}"}                      ### keep the sign (if any).
    N=${N#[+-]}                             ### remove the sign
    N=${N%[!0-9]*}${N#??}                   ### remove the $dec
    N=${N#"${N%%[!0]*}"}                    ### remove all leading zeros
    N=${N%"${N##*[!0]}"}                    ### remove all trailing zeros
    len=$((${#N}<sig?${#N}:sig))            ### count of selected characters.
    N=$(printf '%0.*s' "$len" "$N")         ### use the first $len characters.

    result="$N"

    # add the decimal separator or lead zeros or trail zeros.
    if   [ "$exp" -gt 0 ] && [ "$exp" -lt "$len" ]; then
            b=$(printf '%0.*s' "$exp" "$result")
            c=${result#"$b"}
            result="$b$dec$c"
    elif [ "$exp" -le 0 ]; then
            # fill front with leading zeros ($exp length).
            z1="$(zeros "$((-exp))")"
            result="0$dec$z1$result"
    elif [ "$exp" -ge "$len" ]; then
            # fill back with trailing zeros.
            z2=$(zeros "$((exp-len))")
            result="$result$z2"
    fi
    # place the sign back.
    printf '%s' "$sgn$result"
}

结果是:

$ dash ./script.sh
       123456789 --> 4<                       123400000 >--| yes
           23455 --> 4<                           23450 >--| yes
           23465 --> 4<                           23460 >--| yes
          1.2e-5 --> 6<                        0.000012 >--| yes
         1.2e-15 -->15<              0.0000000000000012 >--| yes
              12 --> 6<                              12 >--| yes
      123456e+25 --> 4< 1234000000000000000000000000000 >--| yes
      123456e-25 --> 4<       0.00000000000000000001234 >--| yes
 -12345.61234e-3 --> 4<                          -12.34 >--| yes
 -1.234561234e-3 --> 4<                       -0.001234 >--| yes
           76543 --> 2<                           76000 >--| yes
          -76543 --> 2<                          -76000 >--| yes
          123456 --> 4<                          123400 >--| yes
           12345 --> 4<                           12340 >--| yes
            1234 --> 4<                            1234 >--| yes
           123.4 --> 4<                           123.4 >--| yes
       12.345678 --> 4<                           12.34 >--| yes
      1.23456789 --> 4<                           1.234 >--| yes
    0.1234555646 --> 4<                          0.1234 >--| yes
       0.0076543 --> 2<                          0.0076 >--| yes
   .000000123400 --> 2<                      0.00000012 >--| yes
   .000001234000 --> 2<                       0.0000012 >--| yes
   .000012340000 --> 2<                        0.000012 >--| yes
   .000123400000 --> 2<                         0.00012 >--| yes
   .001234000000 --> 2<                          0.0012 >--| yes
   .012340000000 --> 2<                           0.012 >--| yes
   .123400000000 --> 2<                            0.12 >--| yes
           1.234 --> 2<                             1.2 >--| yes
          12.340 --> 2<                              12 >--| yes
         123.400 --> 2<                             120 >--| yes
        1234.000 --> 2<                            1200 >--| yes
       12340.000 --> 2<                           12000 >--| yes
      123400.000 --> 2<                          120000 >--| yes

0

如果您已经将数字作为字符串使用,即“ 3456”或“ 0.003756”,则可能仅使用字符串处理即可。以下内容令人难以置信,没有经过全面测试,并使用sed,但请考虑:

f() {
    local A="$1"
    local B="$(echo "$A" | sed -E "s/^-?0?\.?0*//")"
    local C="$(eval echo "${A%$B}")"
    if ((${#B} > 2)); then
        D="${B:0:2}"
    else
        D="$B"
    fi
    echo "$C$D"
}

基本上,您首先剥离并保存任何“ -0.000”内容,然后其余部分使用简单的子字符串操作。关于上述内容的一个警告是未删除多个前导0。我将其保留为练习。


1
不仅仅是练习:它不会用零填充整数,也不会解释嵌入的小数点。但是是的,使用这种方法是可行的(尽管实现这一点可能超出了OP的技能)。
Thomas Dickey
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.