TL; DR
Chỉ cần sao chép và sử dụng chức năng sigf
trong phần A reasonably good "significant numbers" function:
. Nó được viết (như tất cả các mã trong câu trả lời này) để làm việc với dấu gạch ngang .
Nó sẽ đưa ra printf
xấp xỉ cho phần nguyên của N với các $sig
chữ số.
Về dấu phân cách thập phân.
Vấn đề đầu tiên cần giải quyết với printf là hiệu ứng và việc sử dụng "dấu thập phân", ở Mỹ là một điểm và trong DE là dấu phẩy (ví dụ). Đó là một vấn đề bởi vì những gì hoạt động cho một số miền (hoặc vỏ) sẽ thất bại với một số miền khác. Thí dụ:
$ 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
Một giải pháp phổ biến (và không chính xác) là đặt LC_ALL=C
cho lệnh printf. Nhưng điều đó đặt dấu thập phân thành dấu thập phân cố định. Đối với các địa điểm nơi dấu phẩy (hoặc khác) là ký tự được sử dụng phổ biến là một vấn đề.
Giải pháp là tìm ra bên trong tập lệnh cho shell chạy nó là dấu phân cách thập phân cục bộ. Điều đó khá đơn giản:
$ printf '%1.1f' 0
0,0 # for a comma locale (or shell).
Loại bỏ số không:
$ dec="$(IFS=0; printf '%s' $(printf '%.1f'))"; echo "$dec"
, # for a comma locale (or shell).
Giá trị đó được sử dụng để thay đổi tệp với danh sách kiểm tra:
sed -i 's/[,.]/'"$dec"'/g' infile
Điều đó làm cho các lần chạy trên bất kỳ shell hoặc miền địa phương tự động hợp lệ.
Một số điều cơ bản.
Nó nên trực quan để cắt số được định dạng với định dạng %.*e
hoặc thậm chí %.*g
của printf. Sự khác biệt chính giữa việc sử dụng %.*e
hoặc %.*g
là cách họ đếm các chữ số. Một người sử dụng số đếm đầy đủ, người kia cần số lượng ít hơn 1:
$ printf '%.*e %.*g' $((4-1)) 1,23456e0 4 1,23456e0
1,235e+00 1,235
Điều đó làm việc tốt cho 4 chữ số có nghĩa.
Sau khi số chữ số đã được cắt từ số, chúng ta cần một bước bổ sung để định dạng các số có số mũ khác 0 (như ở trên).
$ N=$(printf '%.*e' $((4-1)) 1,23456e3); echo "$N"
1,235e+03
$ printf '%4.0f' "$N"
1235
Điều này hoạt động chính xác. Tổng số phần nguyên (ở bên trái dấu thập phân) chỉ là giá trị của số mũ ($ exp). Số lượng thập phân cần thiết là số chữ số có nghĩa ($ sig) ít hơn số lượng chữ số đã được sử dụng ở phần bên trái của dấu tách thập phân:
a=$((exp<0?0:exp)) ### count of integer characters.
b=$((exp<sig?sig-exp:0)) ### count of decimal characters.
printf '%*.*f' "$a" "$b" "$N"
Vì phần không thể thiếu cho f
định dạng không có giới hạn, nên trên thực tế không cần phải khai báo rõ ràng và mã này (đơn giản hơn) hoạt động:
a=$((exp<sig?sig-exp:0)) ### count of decimal characters.
printf '%0.*f' "$a" "$N"
Thử thách đầu tiên.
Một chức năng đầu tiên có thể làm điều này theo cách tự động hơ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.
}
Lần thử đầu tiên này hoạt động với nhiều số nhưng sẽ thất bại với các số có số chữ số khả dụng nhỏ hơn số lượng đáng kể được yêu cầu và số mũ nhỏ hơn -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ó sẽ thêm nhiều số không cần thiết.
Thử nghiệm thứ hai.
Để giải quyết điều đó, chúng ta cần làm sạch N của số mũ và bất kỳ số 0 nào. Sau đó, chúng ta có thể có được độ dài hiệu quả của các chữ số có sẵn và làm việc với điều đó:
# 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.
}
Tuy nhiên, đó là sử dụng toán học dấu phẩy động và "không có gì đơn giản trong dấu phẩy động": Tại sao số của tôi không cộng lại?
Nhưng không có gì trong "điểm nổi" là đơn giản.
printf "%.2g " 76500,00001 76500
7,7e+04 7,6e+04
Tuy nhiên:
printf "%.2g " 75500,00001 75500
7,6e+04 7,6e+04
Tại sao?:
printf "%.32g\n" 76500,00001e30 76500e30
7,6500000010000000001207515928855e+34
7,6499999999999999997831226199114e+34
Và, cũng, lệnh printf
là một nội dung của nhiều đạn pháo.
Những gì printf
in có thể thay đổi với vỏ:
$ 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
Một chức năng "số lượng đáng kể" hợp lý tốt:
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"
}
Và kết quả là:
$ 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
, nhưng đó làprintf
đối số và người ta không cần POSIXprintf
để có vỏ POSIX. Tôi nghĩ bạn nên bình luận thay vì chỉnh sửa ở đó.