Bash에서 부동 소수점 숫자를 유효 숫자 2자리로 형식화하는 방법은 무엇입니까?

Bash에서 부동 소수점 숫자를 유효 숫자 2자리로 형식화하는 방법은 무엇입니까?

bash에서 두 개의 유효 숫자가 있는 부동 소수점 숫자를 인쇄하고 싶습니다(awk, bc, dc, perl 등과 같은 일반적인 도구를 사용할 수도 있음).

예:

  • 76543은 76000으로 인쇄되어야 합니다.
  • 0.0076543은 0.0076으로 인쇄되어야 합니다.

두 경우 모두 유효한 숫자는 7과 6입니다. 다음과 같은 유사한 질문에 대한 답변을 읽었습니다.

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

Bash는 부동 소수점 변수의 정밀도를 제한합니다.

그러나 대답은 유효 숫자보다는 소수 자릿수(예: bc명령 scale=2또는 printf명령 )를 제한하는 데 중점을 둡니다.%.2f

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

답변1

이 답변첫 번째 관련 질문 끝에는 거의 폐기된 줄이 있습니다.

%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.

답변2

긴 이야기 짧게

sigf섹션의 기능을 복사하여 사용하세요 A reasonably good "significant numbers" function:. (이 답변의 모든 코드와 마찬가지로) 다음을 사용하도록 작성되었습니다.스프린트.

printf대략적인 정보를 제공합니다.N의 정수 부분숫자 로 $sig.

소수 구분 기호에 대해.

printf가 해결해야 할 첫 번째 문제는 "소수점"의 역할과 사용입니다. 예를 들어 US에서는 점이고 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).

0을 제거합니다.

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

이 값은 테스트 목록이 포함된 파일을 변경하는 데 사용됩니다.

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

이는 모든 쉘이나 로케일에서 자동으로 작동합니다.


몇 가지 기본 사항.

%.*eprintf의 서식을 사용하거나 서식을 지정하려는 숫자를 자르는 서식을 사용하는 것은 직관적이어야 합니다. or 사용 %.*g의 주요 차이점 은 숫자를 계산하는 방법입니다. 하나는 전체 카운트를 사용하고 다른 하나는 카운트를 1씩 줄여야 합니다.%.*e%.*g

$ 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

답변3

이미 문자열(예: "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이 제거되지 않는다는 것입니다. 연습으로 남겨두겠습니다.

관련 정보