bashで正確に2つの有効数字で浮動小数点数をフォーマットする方法は?


17

浮動小数点数をbashで正確に2桁の有効数字で出力したい(おそらくawk、bc、dc、perlなどの一般的なツールを使用して)。

例:

  • 76543は76000として印刷する必要があります
  • 0.0076543は0.0076として印刷する必要があります

どちらの場合も、有効数字は7と6です。次のような同様の問題に対するいくつかの回答を読みました。

シェルで浮動小数点数を丸める方法は?

浮動小数点変数のbash制限精度

しかし、答えは、有効桁数の代わりに小数点以下の桁数を制限することに焦点を当てています(例:でのbcコマンドscale=2またはでのprintfコマンド%.2f)。

数字を正確に2桁の有効数字でフォーマットする簡単な方法はありますか、それとも独自の関数を作成する必要がありますか?

回答:


13

最初のリンクされた質問へのこの答えには、最後にほとんど使い捨ての行があります:

%g指定された有効桁数への丸めについても参照してください。

だからあなたは簡単に書くことができます

printf "%.2g" "$n"

(ただし、小数点とロケールに関する以下のセクションを参照してください。また、非Bash printf%fandをサポートする必要がないことに注意してください%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あるため、ここで大丈夫です。 / 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、組み込みバージョンの使用を防ぐために書き込む必要があります。この後者は(少なくとも一部のバージョンでは)を使用して常に引数を解析するように見え.ますが、現在のロケール設定を使用して印刷するため、複雑です。


@StéphaneChazelas、バシズムを取り除いた後、なぜ慎重にテストしたPOSIXシェルシバンをBashに戻したのですか?あなたのコメントは%f/ %gに言及していますが、それはprintf議論でありprintf、POSIXシェルを持つためにPOSIXを必要としません。そこで編集するのではなく、コメントすべきだったと思います。
トビースパイト

printf %gPOSIXスクリプトでは使用できません。それはprintfユーティリティ次第ですが、そのユーティリティはほとんどのシェルに組み込まれています。OPはbashとしてタグ付けされているため、bash shebangを使用すると、%gをサポートするprintfを簡単に取得できます。そうしないと、あなたは追加する必要があるだろうと仮定すると、あなたのprintf(またはのprintfのビルトインをお使いのsh場合printfが組み込みである)非標準(が、かなり共通)をサポートしてい%g...
ステファンChazelas

dashには組み込みのprintf(がサポートされています%g)があります。GNUシステムで mkshは、おそらく組み込みのない最近の唯一のシェルprintfです。
ステファンシャゼラス

改善してくれてありがとう-シェバンを削除して(質問にタグが付けられているのでbash)編集して、この一部をノートに委ねました。
トビースパイト

4

TL; DR

sigfセクションの関数をコピーして使用するだけA reasonably good "significant numbers" function:です。(この回答のすべてのコードとして)dashで動作するように書かれています。

それは与えるprintfの近似をNの整数部分$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

1つの一般的な(および誤った解決策)は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は、数字のカウント方法です。1つはフルカウントを使用し、もう1つは1未満のカウントを必要とします。

$ 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  

不要な多くのゼロが追加されます。

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

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」を削除して保存し、残りの部分では単純な部分文字列操作を使用します。上記に関する注意点の1つは、複数の先行0が削除されないことです。演習として残しておきます。


1
演習以上:整数をゼロで埋めたり、小数点を埋め込んだりしません。しかし、はい、それはこのアプローチを使用して実行可能です(ただし、その達成はOPのスキルを超える場合があります)。
トーマスディッキー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.