첫째 날: 진법 이야기

저자

@keedi - Seoul.pm 리더, Perl덕후, 거침없이 배우는 펄의 공동 역자, keedi.k at gmail.com

시작하며

진법은 기수법(記數法, numeral system)이라고 하며 수를 시각적으로 나타내는 방법으로, 기수법을 통해서 나타나는 각각의 숫자는 다른 수들과 구별되는 표기 방식을 가집니다. 현대의 기수법이라는 용어는 일반적으로 숫자의 위치와 계수를 이용하여 수를 나타내는 위치값 기수법(positional system)을 의미합니다. 우리가 평소에 가장 흔하게 사용하는 수 체계는 10진법입니다. 시계를 볼 때는 60진법 또는 12진법, 24진법을 사용하기도 하며, 컴퓨터를 즐겨 사용하는 프로그래머들은 2진법과 8진법, 16진법 역시도 사용하곤 합니다.

10진법을 16진법으로 변환하기 그림 1. 10진법을 16진법으로 변환하기 (원본 / 출처)

이정도면 충분한 것 같지만 사실 상황에 따라 더 다양한 진법이 필요한 경우도 있습니다. 예를 들면 ISBN이 그러한데 ISBN은 0에서 9까지의 십진 숫자 이외에도 X를 추가로 더 사용하기 때문에 11진법이라고 볼 수 있죠. 이론적으로 어떤 숫자라도 N진법으로 얼마든지 표현할 수 있습니다. 따라서 필요에 따라 원하는 진법으로 수를 표현하거나 흔히 사용하는 진법의 숫자로 변환하는 방법을 알아두면 꽤 유용하죠.

준비물

필요한 모듈은 다음과 같습니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan Math::Fleximal

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan Math::Fleximal

2진법 그리고 8진법, 16진법

펄은 내장 함수로 oct()라는 편리한 진법 변환 함수를 지원합니다. 대부분의 프로그래머는 2진법과 8진법, 16진법 정도면 충분하기 때문에 oct()의 사용법 정도만 알아두어도 편리하게 사용할 수 있습니다. oct() 함수는 2진법 또는 8진법, 16진법의 숫자를 10진법의 숫자로 변환합니다.

use utf8;
use strict;
use warnings;
use feature qw( say );

say oct("0b1111");  # ( 1 * 2**3 ) + ( 1 * 2**2 ) + ( 1 * 2**1 ) + ( 1 * 2**0 )
say oct("0755");    # ( 7 * 8**2 ) + ( 5 * 8**1 ) + ( 5 * 8**0 )
say oct("0xFF");    # ( 15 * 16**1) + ( 15 * 16**0 )

oct() 함수는 0b로 시작하는 경우 2진수로, 0으로 시작하는 경우 8진수로, 0x로 시작하는 경우 16진수로 간주합니다. 실행 결과는 다음과 같습니다.

$ perl oct.pl
15
493
255

역으로 10진수를 2진수나, 8진수 16진수로 바꾸려면 sprintf() 내장 함수를 사용합니다. 적절한 형식 문자열을 사용해서 진법 변환을 수행할 수 있는데 %b는 2진수로, %o는 8진수로, %x는 16진수로 변환합니다. 특히 16진수를 대문자로 표시하고 싶다면 %X를 사용하면 됩니다.

use utf8;
use strict;
use warnings;
use feature qw( say );

say sprintf( "0b%b", 15 );
say sprintf( "0%o",  493 );
say sprintf( "0x%X", 255 );

실행 결과는 다음과 같습니다.

$ perl sprintf.pl
0b1111
0755
0xFF

Math::Fleximal

흔히 사용하는 2진법과 8진법, 16진법을 펄 환경에서 사용하는 것은 무척 쉽습니다. 하지만 36진법이 필요하다면 어떨까요? 조금 머리가 아파오죠? :-) CPAN의 Math::Fleximal 모듈은 N진법이 필요한 여러분을 위해 준비되어 있는 편리한 모듈입니다. 이해하기 쉽게 Math::Fleximal 모듈을 사용해 16진법을 표현해보죠. 우선 특정 진법을 사용하겠다면 해당 진법에서 표현할 숫자 범위를 결정해야 합니다. 16진법이라면 0에서 9와 더불어 A에서 F까지의 범위를 사용해 0에서 15까지의 수를 한 자리에 표현합니다.

use utf8;
use strict;
use warnings;
use feature qw( say );

use Math::Fleximal;

my $num1 = Math::Fleximal->new( "FF", [ 0..9, "A".."F" ] );
say $num1->to_str;  # FF
say $num1->base_10; # 255

new() 메소드를 이용해서 특정 진법의 숫자 객체를 생성합니다. 첫 번째 인자는 표현하려는 숫자 문자열이며, 두 번째 인자는 해당 진법에서 사용할 숫자 범위인 배열 참조입니다. 이렇게 생성한 객체를 표현하려면 to_str() 메소드를 사용합니다. 우리가 늘 쓰는 진법이 10진법인 만큼 10진법으로의 변환은 워낙 자주 사용하기 때문에 아예 base_10()이라는 이름으로 메소드가 미리 만들어져 있습니다. 따라서 언제든지 10진수로 표현하려면 해당 메소드를 사용하면 됩니다.

반대로 10진수를 16진수로 바꾸는 방법 역시 대동소이합니다.

my $num2 = Math::Fleximal->new( 255, [ 0..9 ] );
my $num3 = $num2->change_flex( [ 0..9, "A".."F" ] );
say $num3->to_str; # FF

가끔은, 진법 객체 생성 시점에는 어떤 숫자를 저장할지 판단하기 어려울 수 있습니다. 이럴 때는 진법 객체를 먼저 생성한 다음 이후 원하는 숫자를 설정하면 됩니다.

my $num4 = Math::Fleximal->new( "0", [ 0..9, "A".."F" ] );

#
# blah blah...
#

$num4->set_value("F0C9");
my $num5 = $num4->change_flex( [ 0, 1] );
say $num4->to_str;
say $num5->to_str;  # 1111 0000 1100 1001

36진법?

간단한 사용법을 확인했으니 앞서 언급한 36진법을 사용해볼까요? 36진법을 사용하려면 우선 한 자리를 표현하기 위해 필요한 문자 셋트를 정의해야 합니다. 마침 숫자가 10개이며 알파벳이 26개이니 0에서 9와 더불어 A에서 Z까지의 범위를 사용해 0에서 35까지의 수를 한 자리에 표현하면 되겠군요.

use utf8;
use strict;
use warnings;
use feature qw( say );

use Math::Fleximal;

my @digits = ( 0 .. 9, 'A' .. 'Z' );

my $num = Math::Fleximal->new( "0", \@digits );

Math::Fleximal은 간단한 연산도 이미 지원하기 때문에 사칙연산의 경우 매번 N진법의 숫자를 10진법으로 변환한 다음 사칙연산을 수행하고 다시 해당 N진법으로 교체할 필요가 없습니다. 사칙연산은 각각 add(), substr(), mul(), div() 메소드를 이용하면 간단히 처리할 수 있습니다. 컴퓨터를 사용해서 000부터 ZZZ까지 3자리 범위의 숫자 목록 중 1000개를 순차적으로 뽑아보죠. 3자리라고 우습게 볼 수 없는 것이 36진법이기 때문에 범위의 숫자 개수는 36 ** 3 - 1로 무려 46655개입니다. :)

my $num = Math::Fleximal->new( "0", \@digits );
my $count = 0;
while ( $count < 1000 ) {
    printf "%s(36) = %s(10)\n", $num->to_str, $num->base_10;

    ++$count;
    $num = $num->add( $num->one );
}

사칙연산 중 사용한 one() 메소드는 해당 진법 객체로 1을 표현할 때 사용하는 것입니다. 유사하게 zero()를 사용해 0을 표현할 수도 있습니다. 아니, 1이 1이고 0이 0이지라고 생각할 수도 있지만, 사실 Math::Fleximal을 사용해서 객체 생성을 할때 0을 X로 표현할 수도 있고 1을 #으로도 표현하는 등, N진법의 한 자리를 표현하기 위해 사용할 문자는 여러분이 정하는 것이기 때문이죠.

실행 결과는 다음과 같습니다.

$ perl 36.pl
0(36) = 0(10)
1(36) = 1(10)
2(36) = 2(10)
...
X(36) = 33(10)
Y(36) = 34(10)
Z(36) = 35(10)
10(36) = 36(10)
11(36) = 37(10)
12(36) = 38(10)
...
RP(36) = 997(10)
RQ(36) = 998(10)
RR(36) = 999(10)
$

정리하며

아니, 대체 36진법을 어디다 쓰냐구요? :-) 제 경우 후원하는 단체의 내부 물류를 전산화하는 과정 중 물품에 각각의 식별 코드를 달아야 했는데 바코드 리더를 사용했기 때문에 시스템적으로는 많은 물류를 표현하는데 큰 문제는 없었습니다. 하지만 시간이 지나면서 바코드 리더기 없이도 직원들이 물류에 붙인 태그를 읽거나 메모지에 기록을 해야할 상황이 생기기 시작했고, 이 때 바코드로 표현한 10진 숫자의 자릿수가 꽤 길었기 때문에 바코드 리더기와 컴퓨터 없이는 의사소통이 힘든 경우가 있었죠. 이 때 바로 36진법이 크나큰 역할을 합니다. 10진법이 한자리 숫자로 10개를 표현하는데 비해 36진법은 한자리 숫자로 36개의 숫자를 표현하기 때문에 단 4자리의 36진법 숫자로 1679615개의 10진법 숫자를 표현할 수 있었죠. 진법이 더 커진다면 2~3자리로도 가능하겠지만, 현실적으로 숫자 10개와 알파벳 26개를 조합했을때 가능한 수치가 36진법이기 때문에 선택한 것이죠. 물론 알파벳을 대소문자로 구분할 경우 62진법도 가능하지만, 훈련되지 않은 일반인이 알파벳 대소문자를 구분하고 이를 읽거나 적는데 드는 비용은 크다고 판단했으며, 그래서 적절한 선에서 조율한 것이 36진법이었죠. 어찌됐든 펄(Perl)과 CPAN을 이용하면 정말 손쉽게 여러분이 원하는 진법을 표현하고 계산할 수 있다는 점 잊지마세요!

Enjoy Your Perl! ;-)

EOT

blog comments powered by Disqus