TL; DR
Cukup salin dan gunakan fungsi sigf
di bagian ini A reasonably good "significant numbers" function:
. Ada tertulis (karena semua kode dalam jawaban ini) bekerja dengan tanda hubung .
Ini akan memberikan printf
perkiraan ke bagian integer N dengan $sig
digit.
Tentang pemisah desimal.
Masalah pertama yang harus diselesaikan dengan printf adalah efek dan penggunaan "tanda desimal", yang di AS adalah sebuah titik, dan di DE adalah koma (misalnya). Ini adalah masalah karena apa yang berfungsi untuk beberapa lokal (atau shell) akan gagal dengan beberapa lokal lainnya. Contoh:
$ 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
Salah satu solusi umum (dan salah) adalah untuk mengatur LC_ALL=C
perintah printf. Tapi itu menetapkan tanda desimal ke titik desimal tetap. Untuk lokal di mana koma (atau lainnya) adalah karakter yang umum digunakan yang menjadi masalah.
Solusinya adalah mencari tahu di dalam skrip untuk shell menjalankannya apa pemisah desimal lokal. Itu cukup sederhana:
$ printf '%1.1f' 0
0,0 # for a comma locale (or shell).
Menghapus nol:
$ dec="$(IFS=0; printf '%s' $(printf '%.1f'))"; echo "$dec"
, # for a comma locale (or shell).
Nilai itu digunakan untuk mengubah file dengan daftar tes:
sed -i 's/[,.]/'"$dec"'/g' infile
Itu membuat proses pada shell atau lokal apa pun secara otomatis valid.
Beberapa dasar.
Ini harus intuitif untuk memotong nomor yang akan diformat dengan format %.*e
atau bahkan %.*g
printf. Perbedaan utama antara menggunakan %.*e
atau %.*g
bagaimana mereka menghitung angka. Satu menggunakan penghitungan penuh, yang lain membutuhkan penghitungan kurang 1:
$ printf '%.*e %.*g' $((4-1)) 1,23456e0 4 1,23456e0
1,235e+00 1,235
Itu bekerja dengan baik untuk 4 digit signifikan.
Setelah jumlah digit telah dipotong dari angka, kita perlu langkah tambahan untuk memformat angka dengan eksponen yang berbeda dari 0 (seperti di atas).
$ N=$(printf '%.*e' $((4-1)) 1,23456e3); echo "$N"
1,235e+03
$ printf '%4.0f' "$N"
1235
Ini berfungsi dengan benar. Hitungan bilangan bulat (di sebelah kiri tanda desimal) hanya nilai eksponen ($ exp). Hitungan desimal yang dibutuhkan adalah jumlah digit signifikan ($ sig) dikurangi jumlah digit yang sudah digunakan di bagian kiri pemisah desimal:
a=$((exp<0?0:exp)) ### count of integer characters.
b=$((exp<sig?sig-exp:0)) ### count of decimal characters.
printf '%*.*f' "$a" "$b" "$N"
Karena bagian integral dari f
format tidak memiliki batas, sebenarnya tidak perlu mendeklarasikannya secara eksplisit dan kode ini (lebih sederhana) berfungsi:
a=$((exp<sig?sig-exp:0)) ### count of decimal characters.
printf '%0.*f' "$a" "$N"
Percobaan pertama.
Fungsi pertama yang dapat melakukan ini dengan cara yang lebih otomatis:
# 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.
}
Upaya pertama ini bekerja dengan banyak angka tetapi akan gagal dengan angka yang jumlah digit yang tersedia kurang dari jumlah signifikan yang diminta dan eksponen kurang dari -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
Ini akan menambahkan banyak nol yang tidak diperlukan.
Uji coba kedua.
Untuk mengatasinya kita perlu membersihkan N dari eksponen dan angka nol di belakangnya. Kemudian kita bisa mendapatkan panjang digit efektif yang tersedia dan bekerja dengan itu:
# 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.
}
Namun, itu menggunakan matematika titik mengambang, dan "tidak ada yang sederhana di titik mengambang": Mengapa angka saya tidak bertambah?
Tapi tidak ada dalam "floating point" yang sederhana.
printf "%.2g " 76500,00001 76500
7,7e+04 7,6e+04
Namun:
printf "%.2g " 75500,00001 75500
7,6e+04 7,6e+04
Mengapa?:
printf "%.32g\n" 76500,00001e30 76500e30
7,6500000010000000001207515928855e+34
7,6499999999999999997831226199114e+34
Dan, juga, perintahnya printf
adalah builtin dari banyak shell.
Apa yang printf
dicetak dapat berubah dengan shell:
$ 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
Fungsi "angka signifikan" yang cukup baik:
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"
}
Dan hasilnya adalah:
$ 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
%f
/%g
, tapi itulahprintf
argumennya, dan orang tidak perlu POSIXprintf
untuk memiliki shell POSIX. Saya pikir Anda seharusnya berkomentar daripada mengedit di sana.