Dc (컴퓨터 프로그램)

dc(desk calculator)는 임의 정밀도 연산을 지원하는 크로스 플랫폼 역폴란드 표기법 계산기이다. 벨 연구소의 로린다 체리와 로버트 모리스가 작성하였다. dc는 가장 오래된 유닉스 유틸리티 중 하나로, 심지어 C 프로그래밍 언어의 발명보다도 앞서 등장했다. 그 시대의 다른 유틸리티들처럼 강력한 기능 세트를 갖추고 있지만 구문은 매우 간결하다.
전통적으로 중위 표기법을 사용하는 bc 계산기 프로그램이 dc를 기반으로 구현되었으나, 현대적인 GNU dc 구현체는 bc를 기반으로 한다.

 역사 
dc는 현존하는 가장 오래된 유닉스 언어 프로그램이다. dc가 개발된 벨 연구소에 PDP-11이 도입되었을 때, B로 작성된 dc는 어셈블러보다도 먼저 새 컴퓨터에서 실행된 첫 번째 프로그램이었다. 켄 톰프슨은 dc가 해당 기계에서 작성된 최초의 프로그램이었을 것이라는 의견을 피력한 바 있다.

 기본 연산 
dc에서 4와 5를 곱하는 방법은 다음과 같다 (대부분의 공백 문자는 선택 사항임):

$ cat << EOF > cal.txt
4 5 *
p
EOF

$ dc cal.txt
20
$

다음 명령어로도 결과를 얻을 수 있다:

$ echo "4 5 * p" | dc

또는

$ dc -
4 5*pq
20

$ dc
4 5 *
p
20
q

$ dc -e '4 5 * p'

이 과정은 "4와 5를 스택에 푸시한 다음, 곱셈 연산자로 스택에서 두 요소를 팝하여 곱하고 그 결과를 다시 스택에 푸시하라"는 뜻이다. 그 후 p 명령어를 사용하여 스택의 최상단 요소를 검사(화면에 출력)한다. q 명령어는 실행 중인 dc 인스턴스를 종료한다. 일부 연산자는 띄어쓰기가 필요 없지만 숫자는 서로 띄어 써야 한다는 점에 유의해야 한다.

산술 정밀도는 k 명령어로 변경할 수 있으며, 이는 산술 연산에 사용될 소수점 아래 자릿수를 설정한다. 기본 정밀도는 0이므로, 다음 명령어 시퀀스는 결과로 0을 출력한다:

2 3 / p

k를 통해 정밀도를 조정하면 임의의 소수점 자릿수를 생성할 수 있다. 이 명령어 시퀀스는 .66666을 출력한다.

5 k
2 3 / p

를 계산하려면 다음과 같이 한다 (v는 스택 최상단의 제곱근을 계산하고, _는 음수를 입력하는 데 사용됨):

12 _3 4 ^ + 11 / v 22 -
p

스택의 최상단 두 요소를 교체하려면 r 명령어를 사용한다. 최상단 요소를 복제하려면 d 명령어를 사용한다.

 입력/출력 
표준 입력에서 한 줄을 읽으려면 ? 명령어를 사용한다. 이는 해당 줄을 마치 dc 명령어인 것처럼 평가하므로 구문이 정확해야 하며, ! 명령어를 통해 임의의 명령을 실행할 수 있어 보안 문제가 발생할 수 있다.

앞서 언급했듯이 p는 스택 최상단을 출력하고 그 뒤에 줄바꿈을 붙인다. n은 스택 최상단을 팝하고 줄바꿈 없이 출력한다. f는 전체 스택을 한 줄에 하나씩 출력한다.

dc는 임의의 입력 및 출력 진법을 지원한다. i 명령어는 스택 최상단을 팝하여 입력 진법으로 사용한다. 16진수 숫자는 dc 명령어와의 충돌을 피하기 위해 대문자여야 하며 A-F로 제한된다. o 명령어는 출력 진법에 대해 동일한 작업을 수행하지만, 입력 진법이 이후의 모든 수치 파싱에 영향을 미치므로 보통 출력 진법을 먼저 설정하는 것이 권장된다. 따라서 10o는 출력 진법을 현재의 입력 진법으로 설정하며, 일반적으로 10진수로 설정되지는 않는다. 그럼에도 불구하고 Ao는 입력 진법에 상관없이 출력 진법을 10진수로 재설정한다. 값을 읽기 위해 K, I, O 명령어는 현재의 정밀도, 입력 진법, 출력 진법을 스택 최상단에 푸시한다.

예를 들어, 16진수를 2진수로 변환하려면 다음과 같이 한다:

$ echo 16i2o DEADBEEFp | dc
11011110101011011011111011101111

 언어 기능 

 레지스터 
이러한 기본 산술 및 스택 연산 외에도 dc는 매크로, 조건문 및 나중에 검색하기 위한 결과 저장을 지원한다.

매크로와 조건문의 기초가 되는 메커니즘은 레지스터이다. dc에서 레지스터는 단일 문자 이름을 가진 저장 위치로, 값을 저장하거나 불러올 수 있다. sc는 스택 최상단을 팝하여 레지스터 c에 저장하고, lc는 레지스터 c의 값을 스택으로 푸시한다. 예를 들면 다음과 같다:

3 sc 4 lc * p

레지스터는 보조 스택으로도 취급될 수 있으므로, S와 L 명령어를 사용하여 주 스택과 레지스터 간에 값을 푸시하고 팝할 수 있다.

 문자열 
문자열 값은 [와 ] 문자로 둘러싸이며 스택에 푸시되거나 레지스터에 저장될 수 있다. a 명령어는 수치 값의 하위 바이트를 ASCII 문자로 변환하거나, 스택 최상단이 문자열인 경우 이를 문자열의 첫 번째 문자로 대체한다. 문자열을 작성하거나 문자열 조작을 수행하는 방법은 x 명령어로 실행하거나 P 명령어로 출력하는 것 외에는 없다.

# 문자는 줄 끝까지 주석 처리를 시작한다.

 매크로 
레지스터와 스택 항목에 숫자뿐만 아니라 문자열도 허용함으로써 매크로가 구현된다. 문자열은 출력될 수도 있지만, 실행(즉, dc 명령어의 시퀀스로 처리)될 수도 있다. 예를 들어, 1을 더한 다음 2를 곱하는 매크로를 레지스터 m에 저장할 수 있다:

[1 + 2 *] sm

그 다음 (스택 최상단을 실행하는 x 명령어를 사용하여) 다음과 같이 사용할 수 있다:

3 lm x p

 조건문 
마지막으로, 이 매크로 메커니즘을 사용하여 조건문을 제공할 수 있다. =r 명령어는 스택에서 두 값을 팝하고, 두 값이 같을 때만 레지스터 r에 저장된 매크로를 실행한다. 따라서 다음은 스택 최상단 두 값이 같을 때만 equal 문자열을 출력한다:

[[equal]p] sr 5 5 =r

다른 조건문으로는 >, !>, <, !<, !=가 있으며, 각각 스택 최상단 두 값이 더 크거나, 작거나 같을 때("크지 않을 때"), 작거나, 크거나 같을 때("작지 않을 때"), 그리고 같지 않을 때 지정된 매크로를 실행한다. 부등호 비교에서 피연산자의 순서는 산술 연산 순서와 반대라는 점에 유의해야 한다. 는 로 평가되지만, 는 이므로  레지스터의 내용을 실행한다.

 루프 
루프는 (조건부로) 자기 자신을 다시 호출하는 매크로를 정의함으로써 가능하다. 스택 최상단 값의 간단한 계승(factorial)은 다음과 같이 구현할 수 있다:

# F(x): return x!
# if x-1 > 1
#     return x * F(x-1)
# otherwise
#     return x
[d1-d1<F*]dsFxp

1Q 명령어는 매크로에서 빠져나가 조기 반환을 가능하게 한다. q는 매크로 두 단계(그리고 호출 스택에 두 단계 미만이 남아있다면 dc 자체)를 종료한다. z는 z 연산 전의 현재 스택 깊이를 푸시한다.

 예제 

 전체 스택 합산 

이는 레지스터 a에 저장된 매크로를 통해 구현되며, 스택에 값이 하나만 남을 때까지 매번 덧셈을 수행하며 자기 자신을 조건부로 호출한다. z 연산자는 스택에 있는 항목의 수를 스택에 푸시하는 데 사용된다. 비교 연산자 >는 비교를 수행하면서 스택에서 두 값을 팝한다.

dc -e "1 2 4 8 16 100 0d[+z1<a]dsaxp"

결과는 131이다.

 파일의 각 줄에 있는 모든 dc 표현식 합산 

숫자 하나만 있어도 유효한 dc 표현식이므로, 이를 사용하여 각 줄에 숫자 하나가 포함된 파일의 합계를 구할 수 있다.

이 또한 레지스터 a에 저장된 매크로로 구현되며, 스택에 값이 하나만 남을 때까지 매번 덧셈을 수행하며 자기 자신을 조건부로 호출한다.

dc -e "0d[?+z1<a]dsaxp" < file

? 연산자는 입력 스트림에서 다른 명령어를 읽는다. 입력 줄에 10진수가 포함되어 있으면 그 값이 스택에 추가된다. 입력 파일이 끝에 도달하면 명령어는 null이 되고 스택에 값이 추가되지 않는다.

{ echo "5"; echo "7"; } | dc -e "0d[?+z1<a]dsaxp"

결과는 12이다.

입력 줄은 복잡한 dc 명령어일 수도 있다.

{ echo "3 5 *"; echo "4 3 *"; echo "5dd++"; } | dc -e "0d[?+z1<a]dsaxp"

결과는 42이다.

dc는 임의 정밀도를 지원하므로, AWK에서의 유사하고 간결한 해결책과 달리 입력 스트림에 얼마나 많은 줄이 포함되어 있든 수치 오버플로나 정밀도 손실에 대해 걱정할 필요가 없다.

이 해결책의 단점은 입력 스트림에서 빈 줄(엄밀히 말하면 스택에 최소 하나 이상의 수치를 추가하지 않는 모든 입력 줄)을 만나면 루프가 중단된다는 점이다. 또한 음수를 처리할 때 dc의 비표준 음수 기호 때문에 입력 스트림에서 음수 부호를 나타내는 선행 '-'를 '_'로 바꿔야 한다. dc의 ? 연산자는 빈 줄을 읽는 것과 파일 끝을 읽는 것을 구별하는 명확한 방법을 제공하지 않는다.

 단위 변환 

dc로 작성된 비교적 간단한 프로그램의 예로, 다음 명령어(1줄)는:

dc -e '[[Enter a number (metres), or 0 to exit]PAP]sh[q]sz[lhx?d0=zAk.0254/.5+0kC~1/rn[ feet ]Pn[ inches]PAPdx]dx'

거리를 미터에서 피트와 인치로 변환한다. 대부분의 코드는 입력을 요청하고 적절한 형식으로 결과를 출력하며 다른 숫자를 변환하기 위해 루프를 도는 것과 관련이 있다.

 최대 공약수 

예를 들어, 최대 공약수(GCD)를 구하기 위한 유클리드 호제법의 구현은 다음과 같다:

dc -e '??[dSarLa%d0<a]dsax+p'                  # 가장 짧은 버전
dc -e '[a=]P?[b=]P?[dSarLa%d0<a]dsax+[GCD:]Pp' # 읽기 쉬운 버전

 계승 

입력 값의 계승 계산, 

dc -e '?[q]sQ[d1=Qd1-lFx*]dsFxp'

 dc에서의 콰인 

dc 프로그래밍 언어에는 자신의 소스 코드를 출력으로 생성하는 프로그램인 콰인도 존재한다.

dc -e '[91Pn[dx]93Pn]dx'

dc -e '[91PP93P[dx]P]dx'

 모든 소수 출력 

dc -e '2p3p[dl!d2+s!%0=@l!l^!<#]s#[s/0ds^]s@[p]s&[ddvs^3s!l#x0<&2+l.x]ds.x'
이 프로그램은 Michel Charpentier가 작성하였다.
소수 수열을 출력한다.
기호를 14개 더 적게 사용하는 더 짧은 구현도 가능하다.
dc -e '2p3p[pq]s$[l!2+ds!l^<$dl!%0<#]s#[+dvs^1s!l#x2l.x]ds.x'

 정수 소인수분해 

dc -e '[n=]P?[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2'
이 프로그램 역시 Michel Charpentier가 작성하였다.

더 짧은 버전은 다음과 같다.
dc -e "[n=]P?[lfp/dlf%0=Fdvsr]sF[dsf]sJdvsr2sf[dlf%0=Flfdd2%+1+sflr<Jd1<M]dsMx"

그리고 더 빠른 해결책은 다음과 같다 (200비트 숫자  (입력 2 200^1-)로 시도해 보라).
dc -e "[n=]P?[lfp/dlf% 0=Fdvsr]sFdvsr2sfd2%0=F3sfd3%0=F5sf[dlf%0=Flfd4+sflr>M]sN[dlf%0=Flfd2+sflr>N]dsMx[p]sMd1<M"

후자는 상수에 대한 접근을 레지스터 접근으로 대체하면 속도를 더욱 높일 수 있다.

dc -e "[n=]P?[lfp/dlf%l0=Fdvsr]sF2s2dvsr2sf4s4d2%0=F3sfd3%0=F5sf[dlf%l0=Flfdl4+sflr>M]sN[dlf%l0=Flfdl2+sflr>N]dsMx[p]sMd1<M"

 파이(π) 계산 

dc 프로그래밍 언어로 구현된 추드노프스키 알고리즘이다. 프로그램은 실행될수록 점점 더 정확한 근사치를 출력한다. 하지만 파이는 초월수이므로 프로그램은 중단되거나 실행 중인 기계의 자원이 고갈될 때까지 계속된다.

dc -e '_640320[0ksslk3^16lkd12+sk*-lm*lhd1+sh3^/smlxlj*sxll545140134+dsllm*lxlnk/ls+dls!=P]sP3^sj7sn[6sk1ddshsxsm13591409dsllPx10005v426880*ls/K3-k1/pcln14+snlMx]dsMx'

각 반복마다 크기가 두 배로 늘어나는 동일한 공식의 빠른 분할 정복 구현이다. 유한한 수의 합을 정확한 유리수로 평가하고 각 반복마다 한 번의 큰 나눗셈과 제곱근 연산만 수행한다. 빠르지만 분수의 크기가 커짐에 따라 여전히 빠르게 느려진다.

dc -e '1Sk1SR13591409dSBSP426880dSQ4/3^9*SC[0r-]s-[lkE*1-k10005vlQ*lP/nAan0k]dSox[Lkd1+Skdd1+Sk3^lC*SQ2*1-d3*d*4-*dSR545140134LB+dSB*lk2%0=-SP]dszx[LRLRdLP*LPLQdLQ*SQ*+SP*SR]sc[d1-d0<yd0<yd0=z0=zlcx]sy0[lcxlox1+lyxllx]dslx'

 디피-헬먼 키 교환 

펄 스크립트에 포함된 dc 사용의 더 복잡한 예로 디피-헬먼 키 교환을 수행한다. 이것은 ITAR 논쟁 당시 사이퍼펑크들 사이에서 서명 블록으로 인기가 있었는데, 유닉스 계열 운영체제 어디에나 있는 펄과 dc만으로 짧은 스크립트를 실행할 수 있었기 때문이다:

#!/usr/bin/env perl -- -export-a-crypto-system-sig Diffie-Hellman-2-lines
($g, $e, $m) = @ARGV, $m || die "$0 gen exp mod\n";
print `echo "16dio1[d2%Sa2/d0<X+d*La1=z\U$m%0]SX$e"[$g*]\EszlXx+p | dc`

주석이 달린 버전은 이해하기가 조금 더 쉬우며 루프, 조건문, 그리고 매크로에서 반환하기 위한 q 명령어의 사용법을 보여준다. GNU 버전의 dc에서는 | 명령어를 사용하여 X 함수를 직접 작성할 필요 없이 임의 정밀도 모듈러 거듭제곱을 수행할 수 있다.

#!/usr/bin/env perl

my ($g, $e, $m) = map { "\U$_" } @ARGV;
die "$0 gen exp mod\n" unless $m;

print `echo $g $e $m | dc -e '
# Hex 입력 및 출력
16dio
# stdin의 한 줄에서 m, e, g를 읽음
?SmSeSg

# 함수 z: g * 스택 최상단 반환
[lg*]sz

# 함수 Q: 스택 최상단을 제거하고 1 반환
[sb1q]sQ

# 함수 X(e): g^e % m을 재귀적으로 계산
# Sm^Lm%와 같지만, 임의로 큰 지수를 처리함.
# 진입 시 스택: e
# 종료 시 스택: g^e % m
# e가 매우 클 수 있으므로, g^e % m == 을 사용하는 속성을 이용함
#     if( e == 0 )
#         return 1
#     x = (g^(e/2)) ^ 2
#     if( e % 2 == 1 )
#         x *= g
#     return x %
[
    d 0=Q   # e==0이면 1 반환 (그렇지 않으면 스택: e)
    d 2% Sa # e%2를 a에 저장 (스택: e)
    2/      # e/2 계산
    lXx     # X(e/2) 호출
    d*      # X(e/2)^2 계산
    La1=z   # e%2==1이면 g를 곱함
    lm %    # (g^e) % m 계산
] SX

le          # 레지스터에서 e 로드
lXx         # g^e % m 계산
p           # 결과 출력
'`;

 환경 변수 
환경 변수 DC_LINE_LENGTH가 존재하고 1보다 크고 보다 작은 정수를 포함하는 경우, 숫자 자릿수의 출력(출력 진법에 따름)은 이 값으로 제한되며, 그 뒤에 역슬래시와 줄바꿈이 삽입된다. 기본 줄 길이는 70이다. 특별한 값인 0은 줄바꿈을 비활성화한다.

 같이 보기 
 bc (프로그래밍 언어)
 역폴란드 표기법
 스택 머신

 각주 

 외부 링크 
 데비안 GNU/리눅스 저장소의 패키지 dc
 dc를 포함하는 최신 패키지 GNU bc-1.08.1.


 dc를 포함하는 bc의 네이티브 윈도우 포트

분류:스택 지향 프로그래밍 언어
분류:수치 프로그래밍 언어
분류:유닉스 소프트웨어
분류:자유 수학 소프트웨어
분류:크로스 플랫폼 소프트웨어