bash에서 부동 소수점 숫자를 정확히 2 자리의 유효 숫자로 포맷하는 방법은 무엇입니까?


17

bash에서 정확히 두 개의 유효 자릿수로 부동 소수점 숫자를 인쇄하고 싶습니다 (awk, bc, dc, perl 등과 같은 공통 도구를 사용할 수 있음).

예 :

  • 76543은 76000으로 인쇄해야합니다
  • 0.0076543은 0.0076으로 인쇄해야합니다

두 경우 모두 유효 숫자는 7과 6입니다. 비슷한 문제에 대한 답변을 읽었습니다.

쉘에서 부동 소수점 숫자를 반올림하는 방법은 무엇입니까?

부동 소수점 변수의 배시 제한 정밀도

그러나 정답 은 유효 숫자 대신 소수점 이하 자릿수 (예 : bccommand with scale=2또는 printfcommand with %.2f) 를 제한하는 데 중점을 둡니다 .

정확히 2 자리의 유효 숫자로 숫자를 형식화하는 쉬운 방법이 있습니까? 아니면 내 함수를 작성해야합니까?

답변:


13

이 답변 첫번째 링크 된 질문은 마지막에 거의-버리는 라인을 가지고 :

%g지정된 유효 자릿수로 반올림하는 방법 도 참조하십시오 .

간단히 쓸 수 있습니다

printf "%.2g" "$n"

(그러나 소수점 구분 기호 및 로캘에 대해서는 아래 섹션을 참조하고 비 Bash printf%f및을 지원할 필요가 없습니다 %g.)

예 :

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

물론 이제 순수 소수점이 아닌 가수 지수 표현을 가지므로 다시 변환하고 싶을 것입니다.

$ 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되어 있으므로 여기에 적합하며 GNU 구현도 작동하므로 대부분의 GNU / 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

소수 구분 기호 및 로캘에 대한 참고 사항

위의 모든 작업 은 대부분의 영어 로케일에서와 같이 기수 문자 (소수 구분 기호라고도 함)가이라고 가정 .합니다. 다른 로케일이 ,대신 사용 되며 일부 쉘에는 printf로케일을 존중 하는 내장 기능이 있습니다. 이 셸에서는 기수 문자를 LC_NUMERIC=C사용하도록 강제 설정 .하거나 /usr/bin/printf내장 버전을 사용하지 못하도록 쓰기 를 설정해야 할 수 있습니다 . 후자는 (적어도 일부 버전) 항상을 사용하여 인수를 구문 분석 .하지만 현재 로케일 설정을 사용하여 인쇄 한다는 사실로 인해 복잡합니다 .


@ Stéphane Chazelas, bashism을 제거한 후 왜 신중하게 테스트 된 POSIX 쉘 shebang을 Bash로 다시 변경 했습니까? 귀하의 의견은 %f/을 언급 %g하지만 이것이 printf인수이며 printfPOSIX 쉘을 사용하기 위해 POSIX 가 필요하지 않습니다 . 편집하지 말고 주석을 달았어야한다고 생각합니다.
Toby Speight

printf %gPOSIX 스크립트에서 사용할 수 없습니다. 그것은 printf유틸리티에 달려 있지만, 그 유틸리티는 대부분의 쉘에 내장되어 있습니다. OP는 bash로 태그되었으므로 bash shebang을 사용하는 것이 % g를 지원하는 printf를 얻는 쉬운 방법 중 하나입니다. 그렇지 않으면, 당신은 추가해야 할 것입니다 가정 당신의 printf (또는의 printf와의 내장을 귀하의 sh경우 printf이 내장입니다) 비표준 (하지만 매우 일반적)을 지원 %g...
스테판 Chazelas가

dash의 내장 기능이 있습니다 printf(지원 %g). GNU 시스템에서, mksh아마도 내장이없는 유일한 쉘일 것 printf입니다.
Stéphane Chazelas

개선 사항에 감사드립니다-질문이 태그 지정되어 있기 때문에 shebang을 제거 bash하고이 내용을 메모에 다시 전달하도록 편집했습니다. 지금 올바르게 표시됩니까?
Toby Speight

1
슬프게도 후행 숫자가 0이면 올바른 자릿수를 인쇄하지 않습니다. 예를 들어 printf "%.3g\n" 0.4000.4하지 0.400 제공
phiresky

4

TL; DR

sigf섹션 의 기능 을 복사하여 사용 하십시오 A reasonably good "significant numbers" function:. dash 와 함께 작동하도록 (이 답변의 모든 코드로) 작성되었습니다 .

그것은 줄 것이다 printf받는 근사 N의 정수 부분$sig자리.

소수점 구분 기호

printf로 해결해야 할 첫 번째 문제는 "소수점"(decimal mark)의 효과와 사용인데, 미국에서는 포인트이고 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=Cprintf 명령 을 설정 하는 것입니다. 그러나 이것은 소수점을 고정 소수점으로 설정합니다. 쉼표 (또는 기타)가 일반적으로 사용되는 문자 인 로케일의 경우 문제가됩니다.

해결책은 스크립트에서 로케일 소수 구분 기호를 실행하는 쉘의 스크립트를 찾는 것입니다. 아주 간단합니다 :

$ 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또는 %.*gprintf 형식으로 숫자를 자르는 것이 직관적이어야합니다 . 사용의 주요 차이점 %.*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  

필요하지 않은 많은 0을 추가합니다.

두 번째 재판.

이를 해결하려면 지수의 N과 후행 0을 모두 청소해야합니다. 그런 다음 유효한 유효 길이의 길이를 가져 와서 사용할 수 있습니다.

# 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은 많은 쉘이 내장되어 있습니다.
무엇 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
연습 이상의 것 : 정수를 0으로 채우거나 소수점을 포함하지도 않습니다. 그러나 그렇습니다.이 방법을 사용하면 가능합니다 (OP의 기술을 능가하는 것은 아니지만).
Thomas Dickey
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.