วิธีการจัดรูปแบบเลขทศนิยมด้วยเลข 2 ตัวที่สำคัญในการทุบตี?


17

ฉันต้องการพิมพ์เลขทศนิยมด้วยเลขนัยสำคัญสองตัวใน bash (อาจใช้เครื่องมือทั่วไปเช่น awk, bc, dc, perl และอื่น ๆ )

ตัวอย่าง:

  • ควรพิมพ์ 76543 เป็น 76000
  • 0.0076543 ควรพิมพ์เป็น 0.0076

ในทั้งสองกรณีตัวเลขที่สำคัญคือ 7 และ 6 ฉันได้อ่านคำตอบสำหรับปัญหาที่คล้ายกันเช่น:

วิธีการปัดเลขทศนิยมในเปลือก?

Bash จำกัดความแม่นยำของตัวแปร floating point

แต่คำตอบจะเน้นที่การ จำกัด จำนวนตำแหน่งทศนิยม (เช่นbcคำสั่งด้วยscale=2หรือprintfคำสั่งด้วย%.2f) แทนตัวเลขที่มีนัยสำคัญ

มีวิธีที่ง่ายในการจัดรูปแบบตัวเลขด้วยตัวเลข 2 ตัวที่สำคัญหรือฉันต้องเขียนฟังก์ชั่นของตัวเองหรือไม่?

คำตอบ:


13

คำตอบสำหรับคำถามที่เชื่อมโยงแรกนี้มีจุดที่เกือบจะไม่ทันจบ:

ดูเพิ่มเติม%gสำหรับการปัดเศษจำนวนหลักที่ระบุ

ดังนั้นคุณสามารถเขียน

printf "%.2g" "$n"

(แต่ดูที่ส่วนด้านล่างเกี่ยวกับตัวคั่นทศนิยมและตำแหน่งที่ตั้งและโปรดทราบว่าการไม่ใช้ Bash printfไม่ต้องการการสนับสนุน%fและ%g)

ตัวอย่าง:

$ printf "%.2g\n" 76543 0.0076543
7.7e+04
0.0077

แน่นอนตอนนี้คุณมีตัวแทน mantissa-exponent แทนที่จะเป็นทศนิยมแท้ดังนั้นคุณจะต้องการแปลงกลับ:

$ 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

หมายเหตุเกี่ยวกับตัวคั่นทศนิยมและสถานที่

การทำงานทั้งหมดข้างต้นถือว่าสมมติว่าตัวอักษร radix (หรือที่รู้จักกันว่าตัวแยกทศนิยม) คือ.ในภาษาอังกฤษส่วนใหญ่ โลแคลอื่น ๆ ใช้,แทนและเชลล์บางตัวมีบิวด์อินprintfที่เคารพโลแคล ในเชลล์เหล่านี้คุณอาจต้องตั้งค่าLC_NUMERIC=Cให้บังคับใช้.เป็นอักขระ radix หรือเขียน/usr/bin/printfเพื่อป้องกันการใช้เวอร์ชันในตัว สิ่งหลังนี้มีความซับซ้อนเนื่องจากข้อเท็จจริงที่ว่า (อย่างน้อยบางเวอร์ชัน) ดูเหมือนจะวิเคราะห์อาร์กิวเมนต์ที่ใช้เสมอ.แต่พิมพ์โดยใช้การตั้งค่าตำแหน่งที่ตั้งปัจจุบัน


@ Stéphane Chazelas ทำไมคุณเปลี่ยนเปลือกหอย POSIX ที่ผ่านการทดสอบอย่างรอบคอบของฉันกลับไปที่ Bash หลังจากฉันลบ bashism? ความคิดเห็นของคุณกล่าวถึง%f/ %gแต่นั่นคือprintfเหตุผลและไม่จำเป็นต้องมี POSIX printfเพื่อให้มีเชลล์ POSIX ฉันคิดว่าคุณควรแสดงความคิดเห็นแทนการแก้ไขที่นั่น
Toby Speight

printf %gไม่สามารถใช้ในสคริปต์ POSIX มันเป็นความจริงมันลงไปที่printfยูทิลิตี้ แต่ยูทิลิตี้นั้นสร้างขึ้นในเชลล์ส่วนใหญ่ OP ติดแท็กเป็นทุบตีดังนั้นการใช้ bash shebang เป็นวิธีที่ง่ายวิธีหนึ่งในการรับ printf ที่รองรับ% g มิฉะนั้นคุณจะต้องเพิ่มสมมติของคุณ printf (หรือ printf builtin ของคุณshถ้าprintfมี builtin ที่นั่น) สนับสนุนที่ไม่ได้มาตรฐาน (แต่ค่อนข้างบ่อย) %g...
Stéphane Chazelas

dashมี builtin printf(ซึ่งรองรับ%g) ในระบบของ GNU mkshน่าจะเป็นเปลือกเพียงวันนี้ที่จะไม่ได้มี printfbuiltin
Stéphane Chazelas

ขอขอบคุณสำหรับการปรับปรุงของคุณ - ฉันเพิ่งแก้ไขให้ลบ shebang (เนื่องจากมีการติดแท็กคำถามbash) และลดบางสิ่งเพื่อบันทึก - มันดูถูกต้องแล้วหรือยัง?
Toby Speight

1
น่าเศร้าที่นี่ไม่ได้พิมพ์จำนวนหลักที่ถูกต้องหากตัวเลขต่อท้ายเป็นศูนย์ ตัวอย่างเช่นprintf "%.3g\n" 0.400ให้ 0.4 ไม่ใช่ 0.400
phiresky

4

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

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" ในตอนเริ่มต้นจากนั้นใช้การดำเนินการซับสตริงแบบง่าย ๆ ในส่วนที่เหลือ ข้อแม้หนึ่งเกี่ยวกับข้างต้นคือการที่ 0 นำหลายรายการไม่ถูกลบออก ฉันจะปล่อยให้มันเป็นแบบฝึกหัด


1
มากกว่าแบบฝึกหัด: มันไม่ได้ปัดเลขจำนวนเต็มเป็นศูนย์และไม่ได้คำนวณหาจุดทศนิยมแบบฝัง แต่ใช่มันสามารถทำได้โดยใช้วิธีการนี้ (แม้ว่าการบรรลุซึ่งอาจเกินทักษะของ OP)
Thomas Dickey
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.