bash에서 두 개의 유효 숫자가 있는 부동 소수점 숫자를 인쇄하고 싶습니다(awk, bc, dc, perl 등과 같은 일반적인 도구를 사용할 수도 있음).
예:
- 76543은 76000으로 인쇄되어야 합니다.
- 0.0076543은 0.0076으로 인쇄되어야 합니다.
두 경우 모두 유효한 숫자는 7과 6입니다. 다음과 같은 유사한 질문에 대한 답변을 읽었습니다.
쉘에서 부동 소수점 숫자를 반올림하는 방법은 무엇입니까?
그러나 대답은 유효 숫자보다는 소수 자릿수(예: 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=C
printf 명령을 설정하는 것입니다. 그러나 이는 소수점을 고정 소수점으로 설정합니다. 이는 쉼표(또는 기타)가 일반적인 문자인 로케일에서 문제가 됩니다.
해결책은 이를 실행하는 셸 스크립트 내부에 로케일 소수 구분 기호가 무엇인지 알아내는 것입니다. 이것은 매우 간단합니다.
$ 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
이는 모든 쉘이나 로케일에서 자동으로 작동합니다.
몇 가지 기본 사항.
%.*e
printf의 서식을 사용하거나 서식을 지정하려는 숫자를 자르는 서식을 사용하는 것은 직관적이어야 합니다. 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이 제거되지 않는다는 것입니다. 연습으로 남겨두겠습니다.