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

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

예 :

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

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

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

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

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

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



답변

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

%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내장 버전을 사용하지 못하도록 쓰기 를 설정해야 할 수 있습니다 . 후자는 (적어도 일부 버전) 항상을 사용하여 인수를 구문 분석 .하지만 현재 로케일 설정을 사용하여 인쇄 한다는 사실로 인해 복잡합니다 .


답변

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


답변

숫자가 이미 문자열, 즉 “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이 제거되지 않는다는 것입니다. 나는 그것을 운동으로 남겨 둘 것이다.


답변