Kayan nokta sayısı bash'da tam 2 anlamlı basamakla nasıl biçimlendirilir?


17

Kayan nokta sayısını bash'da tam iki önemli basamakla yazdırmak istiyorum (belki awk, bc, dc, perl vb. Gibi ortak bir araç kullanarak).

Örnekler:

  • 76543, 76000 olarak yazdırılmalıdır
  • 0.0076543, 0.0076 olarak yazdırılmalıdır

Her iki durumda da önemli basamaklar 7 ve 6'dır. Benzer problemler için bazı cevapları okudum:

Kabukta kayan nokta sayıları nasıl yuvarlanır?

Kayan nokta değişkenlerinin bas sınırlayıcı hassasiyeti

ama cevaplar ondalık basamak sayısını (örneğin. sınırlandırılması odaklanmak bcile komutu scale=2veya printfkomut ile%.2f anlamlı basamaklar yerine ).

Sayıyı tam olarak 2 önemli basamakla biçimlendirmenin kolay bir yolu var mı yoksa kendi işlevimi yazmak zorunda mıyım?

Yanıtlar:


13

Bağlantılı ilk soruya verilen bu cevap , sonunda neredeyse atılan çizgiye sahiptir:

Ayrıca %g, belirtilen sayıda anlamlı basamağa yuvarlama için de bakın .

Böylece basitçe yazabilirsiniz

printf "%.2g" "$n"

(ancak ondalık ayırıcı ve yerel ayarla ilgili aşağıdaki bölüme bakın ve Bash olmayan öğelerin printfdesteklenmesi %fve%g ).

Örnekler:

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

Elbette, artık saf ondalık yerine mantis-üstel temsiliniz var, bu yüzden geri dönüş yapmak isteyeceksiniz:

$ printf "%0.f\n" 7.7e+06
7700000

$ printf "%0.7f\n" 7.7e-06
0.0000077

Tüm bunları bir araya getirmek ve bir işleve sarmak:

# 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
}

(Not - bu işlev taşınabilir (POSIX) kabukta yazılmıştır, ancak printfkayan nokta dönüşümlerini işlediğini varsayar .printf , bu yüzden burada iyisinizdir ve GNU uygulaması da çalışır, bu nedenle çoğu GNU / Linux sistemleri Dash'i güvenle kullanabilir).

Test senaryoları

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

Test sonuçları

.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

Ondalık ayırıcı ve yerel ayar hakkında bir not

Yukarıdaki tüm çalışmalar, çoğu İngilizce yerelinde olduğu gibi, radix karakterinin (ondalık ayırıcı olarak da bilinir) olduğunu varsayar .. Bunun ,yerine diğer yerel ayarlar kullanır ve bazı kabuklarda printfyerel ayara uyan yerleşik bir yapı bulunur . Bu kabuklarda, sayı tabanı karakteri olarak LC_NUMERIC=Ckullanmaya zorlamanız .veya /usr/bin/printfyerleşik sürümün kullanılmasını önlemek için yazmanız gerekebilir . Bu sonuncusu, (en azından bazı sürümler) her zaman argümanları kullanarak argümanları ayrıştırıyor ., ancak geçerli yerel ayarları kullanarak yazdırıyor gibi görünüyor .


@ Stéphane Chazelas, bashizmi çıkardıktan sonra neden dikkatle test edilmiş POSIX mermi gövdesini Bash'a geri değiştirdin? Yorumunuz %f/ ifadesinden bahsediyor %g, ancak bu bir printfargüman ve printfbir POSIX kabuğuna sahip olmak için POSIX gerekmez . Bence orada düzenlemek yerine yorum yapmış olmalısın.
Toby Speight

printf %gPOSIX betiğinde kullanılamaz. printfYardımcı programa bağlı olduğu doğru , ancak bu yardımcı program çoğu kabukta yerleşiktir. OP bash olarak etiketlendi, bu yüzden bir bash shebang kullanmak% g destekleyen bir printf almanın kolay bir yoludur. Aksi takdirde, bir eklemeniz gerekir varsayarak printf (veya printf yerleşiği senin sheğer printforada yerleşiğidir) standart dışı (ama oldukça yaygın) destekler %g...
Stéphane Chazelas

dash'in bir yerleşkesi vardır printf(bunu destekler %g). GNU sistemlerinde, mkshbu günlerde yerleşik olmayacak tek kabuk muhtemelen printf.
Stéphane Chazelas

İyileştirmeleriniz için teşekkürler - Sadece soruyu kaldırmak için düzenledim (soru etiketlendiğinden bash) ve bunlardan bazılarını notlara aktardım - şimdi doğru görünüyor mu?
Toby Speight

1
Ne yazık ki, sondaki rakamlar sıfırsa, doğru basamak sayısını yazdırmaz. Örneğin printf "%.3g\n" 0.400, 0.400 yerine 0.4 verir
phiresky

4

TL; DR

Sadece sigfbölümündeki işlevi kopyalayıp kullanın A reasonably good "significant numbers" function:. Kısa çizgi ile çalışmak için (bu cevaptaki tüm kodlar gibi) yazılmıştır .

Bu verecek printfyaklaşma N tamsayı kısmı ile $sigbasamağı.

Ondalık ayırıcı hakkında.

Printf ile çözülmesi gereken ilk sorun, ABD'de bir nokta olan ve DE'de virgül olan "ondalık işareti" nin etkisi ve kullanımıdır (örneğin). Bu bir sorundur, çünkü bazı yerel ayar (veya kabuk) için işe yarayan diğer yerel ayarlarla başarısız olur. Misal:

$ 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

Yaygın (ve yanlış bir çözüm) LC_ALL=Cprintf komutu için ayarlanmıştır . Ancak bu, ondalık işaretini sabit bir ondalık noktaya ayarlar. Virgülün (veya başka bir karakterin) ortak kullanılan ve sorun olan karakter olduğu yerler için.

Çözüm yerel kod ondalık ayırıcı nedir çalıştıran kabuk için komut dosyası içinde bulmaktır. Bu oldukça basit:

$ printf '%1.1f' 0
0,0                            # for a comma locale (or shell).

Sıfırları kaldırma:

$ dec="$(IFS=0; printf '%s' $(printf '%.1f'))"; echo "$dec"
,                              # for a comma locale (or shell).

Bu değer, dosyayı test listesiyle değiştirmek için kullanılır:

sed -i 's/[,.]/'"$dec"'/g' infile

Bu, herhangi bir kabuk veya yerel ayardaki işlemleri otomatik olarak geçerli kılar.


Bazı temel bilgiler.

Biçimlendirilecek sayıyı print %.*eveya formatf ile kesmek sezgisel olmalıdır %.*g. %.*eVeya arasındaki temel fark %.*gbasamakları sayma şeklidir. Biri tam sayıyı kullanır, diğeri daha az saymaya ihtiyaç duyar 1:

$ printf '%.*e  %.*g' $((4-1)) 1,23456e0 4 1,23456e0
1,235e+00  1,235

Bu 4 önemli basamak için iyi çalıştı.

Rakam sayısı rakamdan kesildikten sonra, sayıları 0'dan farklı üslerle (yukarıda olduğu gibi) biçimlendirmek için ek bir adıma ihtiyacımız var.

$ N=$(printf '%.*e' $((4-1)) 1,23456e3); echo "$N"
1,235e+03
$ printf '%4.0f' "$N"
1235

Bu doğru çalışıyor. Tamsayı bölümünün sayısı (ondalık işaretinin solunda) yalnızca üs değerinin ($ exp) değeridir. Gereken ondalık sayısı, ondalık ayırıcının sol tarafında zaten kullanılan basamak miktarından daha az önemli basamak sayısıdır ($ 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"

Biçimin ayrılmaz bir parçası fsınırsız olduğu için, aslında açıkça bildirmeye gerek yoktur ve bu (daha basit) kod çalışır:

a=$((exp<sig?sig-exp:0))                ### count of decimal characters.
printf '%0.*f' "$a" "$N"

İlk deneme.

Bunu daha otomatik bir şekilde yapabilen ilk işlev:

# 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.
}

Bu ilk deneme birçok sayı ile çalışır, ancak kullanılabilir basamak miktarının istenen önemli sayıdan daha az olduğu ve üs -4'ten az olduğu sayılarda başarısız olur:

   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  

Gerekli olmayan birçok sıfır ekleyecektir.

İkinci deneme.

Bunu çözmek için üssün N'sini ve sondaki sıfırları temizlememiz gerekir. Sonra etkili basamak uzunluğunu elde edebilir ve bununla çalışabiliriz:

# 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.
}

Ancak, bu kayan nokta matematiğini kullanıyor ve "kayan noktada hiçbir şey basit değil ": Sayılarım neden toplanmıyor?

Ancak "kayan nokta" daki hiçbir şey basit değildir.

printf "%.2g  " 76500,00001 76500
7,7e+04  7,6e+04

Ancak:

 printf "%.2g  " 75500,00001 75500
 7,6e+04  7,6e+04

Neden?:

printf "%.32g\n" 76500,00001e30 76500e30
7,6500000010000000001207515928855e+34
7,6499999999999999997831226199114e+34

Ve ayrıca, komut printfbirçok merminin yerleşikidir. Kabuk ile
hangi printfbaskılar değişebilir:

$ 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

Oldukça iyi bir "anlamlı sayılar" işlevi:

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"
}

Ve sonuçlar:

$ 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

Zaten bir dize, yani "3456" veya "0.003756" olarak numaraya sahipseniz, bunu yalnızca dize manipülasyonu kullanarak yapabilirsiniz. Aşağıdakiler kafamın üst kısmında değil, iyice test edilmedi ve sed kullanıyor, ancak düşünün:

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"
}

Temelde başlangıçta herhangi bir "-0.000" öğeyi çıkarır ve kaydederseniz, geri kalanında basit bir alt dize işlemi kullanın. Yukarıdakiler hakkında bir uyarı, birden fazla önde gelen 0'ın çıkarılmamasıdır. Bunu bir egzersiz olarak bırakacağım.


1
Bir alıştırmadan daha fazlası: tamsayıyı sıfırlarla doldurmaz veya gömülü ondalık noktasını hesaba katmaz. Ancak evet, bu yaklaşımı kullanmak mümkündür (bunu başarmak OP'nin becerilerinin ötesinde olabilir).
Thomas Dickey
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.