我想在 bash 中打印具有两位有效数字的浮点数(也许使用 awk、bc、dc、perl 等常用工具)。
例子:
- 76543 应打印为 76000
- 0.0076543 应打印为 0.0076
在这两种情况下,有效数字都是 7 和 6。我读过一些类似问题的答案,例如:
但答案集中在限制小数位数(例如bc
command withscale=2
或printf
command with %.2f
)而不是有效数字。
有没有一种简单的方法可以将数字格式化为恰好 2 位有效数字,或者我是否必须编写自己的函数?
答案1
这个答案第一个相关问题的末尾有几乎被丢弃的一行:
另请参阅
%g
舍入到指定有效数字位数。
所以你可以简单地写
printf "%.2g" "$n"
(但请参阅下面有关小数点分隔符和区域设置的部分,并注意非 Bashprintf
不需要支持%f
和%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
关于小数点分隔符和区域设置的注释
上述所有工作均假设基数字符(也称为小数点分隔符)是.
,与大多数英语语言环境中一样。其他语言环境使用相反的方式,并且某些 shell 具有尊重语言环境的,
内置功能。printf
在这些 shell 中,您可能需要设置LC_NUMERIC=C
为强制使用.
作为基数字符,或者写入/usr/bin/printf
以阻止使用内置版本。后者由于以下事实而变得复杂:(至少在某些版本中)似乎总是使用 解析参数.
,但使用当前区域设置进行打印。
答案2
长话短说
sigf
只需复制并使用部分中的函数即可A reasonably good "significant numbers" function:
。它的编写(如本答案中的所有代码)是为了使用短跑。
它将给出printf
近似值N 的整数部分与$sig
数字。
关于小数点分隔符。
printf首先要解决的问题是“小数点”的作用和使用,在US中是点,在DE中是逗号(例如)。这是一个问题,因为对某些语言环境(或 shell)有效的方法在其他语言环境中会失败。例子:
$ 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 命令。但这将小数点设置为固定的小数点。对于逗号(或其他)是常用字符的区域设置,这是一个问题。
解决方案是在运行它的 shell 的脚本内部找出区域设置小数分隔符是什么。这很简单:
$ 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
这使得在任何 shell 或语言环境上的运行都自动有效。
一些基础知识。
%.*e
使用 printf 的格式或甚至格式化来截断要格式化的数字应该是直观的。使用或%.*g
之间的主要区别在于它们如何计数数字。一个使用完整计数,另一个需要计数减 1:%.*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
答案3
如果您已经将数字作为字符串,即“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 不会被删除。我将把它作为练习。