TL; DR
เพียงคัดลอกและใช้ฟังก์ชั่นในส่วนsigf
A reasonably good "significant numbers" function:
มันเขียน (รหัสทั้งหมดในคำตอบนี้) เพื่อการทำงานที่มีประ
มันจะให้การ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
วิธีแก้ไขปัญหาหนึ่งที่พบบ่อย (และไม่ถูกต้อง) คือการตั้งค่า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
ซึ่งทำให้การรันบนเชลล์หรือโลแคลใด ๆ ถูกต้องโดยอัตโนมัติ
พื้นฐานบางอย่าง
ควรตัดตัวเลขที่จะจัดรูปแบบด้วยรูปแบบ%.*e
หรือแม้กระทั่ง%.*g
ของ printf ความแตกต่างที่สำคัญระหว่างการใช้งาน%.*e
หรือ%.*g
เป็นวิธีการนับตัวเลข หนึ่งใช้การนับแบบเต็มส่วนอีกต้องการการนับน้อยกว่า 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
มันจะเพิ่มศูนย์จำนวนมากที่ไม่จำเป็น
การทดลองที่สอง
ในการแก้ปัญหานั้นเราต้องทำความสะอาด 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
ก็คือ builtin ของกระสุนจำนวนมาก
สิ่งที่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
%f
/%g
แต่นั่นคือprintf
เหตุผลและไม่จำเป็นต้องมี POSIXprintf
เพื่อให้มีเชลล์ POSIX ฉันคิดว่าคุณควรแสดงความคิดเห็นแทนการแก้ไขที่นั่น