bashで2つの有効数字を持つ浮動小数点数を印刷したいです(awk、bc、dc、perlなどの一般的なツールを使用することもできます)。
例:
- 76543は76000として印刷する必要があります。
- 0.0076543は0.0076として印刷する必要があります。
どちらの場合も、有効な数字は7と6です。次のような同様の質問に対する回答を読んだ。
ただし、答えは有効数字ではなく、小数点以下の桁数(bc
コマンドscale=2
やprintf
コマンドなど)を制限することに焦点を当てています。%.2f
数値の形式を正確に2桁の有効数字として指定する簡単な方法はありますか?それとも関数を直接書く必要がありますか?
答え1
この回答最初の関連質問の最後には、ほぼ破棄された行があります。
%g
指定された有効数字に丸めを参照してください。
だから簡単に書けばいいです
printf "%.2g" "$n"
(ただし、小数点区切り文字とロケールについては、以下のセクションを参照し、Bash以外の場合とprintf
サポートする必要はありません。)%f
%g
例:
$ printf "%.2g\n" 76543 0.0076543
7.7e+04
0.0077
もちろん、純粋な10進数の代わりに歌手指数表現があるので、再変換する必要があります。
$ 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)シェルで書かれていますが、浮動printf
小数点変換を処理していると仮定します。Bashには浮動小数点変換を処理する組み込み関数があるため、printf
ここでは問題ありません。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
これらのシェルでは、デフォルトの文字としてLC_NUMERIC=C
強制的に有効にするか、組み込みバージョンを無効にする必要があるかもしれません。後者は(少なくとも一部のバージョンでは)解析引数が常に使用されているように見えますが、印刷は現在のロケールを使用して実行されるという事実によって複雑です。.
/usr/bin/printf
.
答え2
長い話を短く
sigf
セクションの機能をコピーして使用してくださいA reasonably good "significant numbers" function:
。 (この回答のすべてのコードと同様に)以下を使用するように書かれています。スプリント。
printf
おおよその情報を提供します。Nの整数部分数字で$sig
。
小数点区切り記号について。
printf が解決すべき最初の問題は、「小数点」の役割と使用です。たとえば、US ではドット、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).
0を削除します。
$ dec="$(IFS=0; printf '%s' $(printf '%.1f'))"; echo "$dec"
, # for a comma locale (or shell).
この値は、テストリストを含むファイルを変更するために使用されます。
sed -i 's/[,.]/'"$dec"'/g' infile
これはすべてのシェルまたはロケールで自動的に機能します。
いくつかの基本。
%.*e
printf の書式を使用するか、書式設定する数字を切り捨てる書式を使用することは直感的でなければなりません。 or使用%.*g
の主な違いは、数値を計算する方法です。 1つはカウント全体を使用し、もう1つはカウントを1ずつ減らす必要があります。%.*e
%.*g
$ printf '%.*e %.*g' $((4-1)) 1,23456e0 4 1,23456e0
1,235e+00 1,235
これは4つの有効数字に適しています。
数値から数字を削除した後にゼロ以外の指数で数値を書式設定するには、追加の手順が必要です(上記を参照)。
$ 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
不要なゼロがたくさん追加されます。
2番目のケース。
この問題を解決するには、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
多くのシェルに組み込まれたコマンドです。シェルの印刷内容が変わる場合があります
。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」エントリを削除して保存し、残りの部分に単純な部分文字列操作を使用します。上記の1つの注意点は、複数の先行ゼロが削除されないことです。練習として残しておきます。