문자열 처리 - STRING·UNSTRING·INSPECT
이 장에서 배우는 것
지금까지 만든 프로그램은 숫자와 고정 길이 코드를 주로 다뤘다. 실무 입력에는 글자가 섞인 한 줄짜리 텍스트도 많다. 창구에서 올라온 메모, 쉼표로 나뉜 전송 데이터, 소문자와 대문자가 섞인 이름이 그런 예다. 이 장은 글자 데이터를 쪼개고, 고치고, 다시 붙이는 도구를 다룬다. 마지막에는 거래 한 줄을 읽어 보고서용 한 줄로 바꾸는 프로그램을 만든다.
- STRING 으로 여러 조각을 하나의 항목에 이어 붙이고, 끝 위치를 포인터로 알아낸다.
- UNSTRING 으로 구분자 기준으로 한 줄을 여러 항목에 나누고, 몇 개가 채워졌는지 센다.
- INSPECT 로 특정 글자를 세고(TALLYING) 바꾼다(REPLACING).
- 참조 수정(reference modification)으로 항목의 일부분만 읽고 쓴다.
- FUNCTION TRIM 과 FUNCTION UPPER-CASE 로 공백을 정리하고 대문자로 통일한다.
문제 상황
조합의 지점 창구에서 하루 거래 내역을 텍스트 한 줄씩 올려 보낸다고 하자. 한 줄은 이름, 계좌번호, 거래 종류, 금액을 쉼표로 이어 놓은 모양이다.
kim minsu,110-2345-01,DEP,00150000
park jiho ,110-7788-02,WDR,00045000
choi,330-4500-01
이 줄에는 다루기 까다로운 점이 여럿 있다.
- 이름이 소문자이고, 어떤 줄은 앞뒤에 공백이 붙어 있다.
- 금액은 앞에 0이 채워진 여덟 자리 글자다.
- 계좌번호 맨 앞 세 자리는 지점 코드다. 보고서에는 계좌번호를 가려서 찍어야 한다.
- 세 번째 줄처럼 항목이 모자란 줄이 섞여 들어오기도 한다.
목표는 이런 줄을 KIM MINSU [********-01] DEPOSIT 150000 같은 보고서용 한 줄로 바꾸는 것이다. 모자란 줄은 걸러서 건너뛴다. 한 줄 안의 글자를 나누고, 세고, 바꾸고, 붙이는 일을 모두 해야 한다.
STRING 으로 이어 붙이기
STRING 은 보내는 조각들을 받는 항목 하나에 차례로 이어 붙인다. 각 조각 뒤에는 DELIMITED BY 로 어디까지 보낼지 적는다.
STRING 조각1 DELIMITED BY SIZE
조각2 DELIMITED BY SPACE
INTO 받는항목
WITH POINTER 위치
ON OVERFLOW 문장
END-STRING
- DELIMITED BY SIZE는 조각 전체를 보낸다.
- DELIMITED BY SPACE는 첫 공백 앞까지만 보낸다. 이름 속 공백에서 잘리므로 "KIM MINSU" 같은 값에는 쓰면 안 된다.
- WITH POINTER의 변수는 다음에 쓸 칸의 번호다. 1 로 시작하고, 조각을 하나 쓸 때마다 늘어난다. 모든 조각을 붙인 뒤 이 값에서 1 을 빼면 실제로 쓴 길이다.
- ON OVERFLOW는 받는 항목이 모자라 다 붙이지 못했을 때 실행할 문장이다.
STRING 은 받는 항목을 지우지 않는다. 쓴 칸만 덮어쓰고 나머지는 이전 내용 그대로 둔다. 그래서 STRING 앞에서 받는 항목을 공백으로 채우고 포인터를 1 로 두는 것이 습관이 되어야 한다. 아래 그림은 완성 코드의 STRING 이 레이블을 채우는 모습이다.
그림의 ·는 공백 한 칸이다. 칸 위아래 숫자는 그 조각을 쓰기 직전의 포인터 값이다.
글자 수는 바이트로 센다
GnuCOBOL 3.2 에서 PIC X(10) 은 열 글자가 아니라 10 바이트다. 한글은 UTF-8 에서 한 글자가 3 바이트라서, 한글 이름을 다루려면 칸을 넉넉히 잡고 잘리지 않는지 확인해야 한다. 이 장의 예제는 영문 이름으로 구성했다.
UNSTRING 으로 나누기
UNSTRING 은 STRING 의 반대 방향이다. 한 항목을 구분자 기준으로 잘라 여러 항목에 나누어 담는다.
UNSTRING 원본 DELIMITED BY ","
INTO 항목1 COUNT IN 길이1
항목2
항목3
TALLYING IN 개수
END-UNSTRING
- COUNT IN은 그 항목에 옮겨진 글자 수를 길이 변수에 담는다.
- TALLYING IN은 값이 들어간 받는 항목의 수를 더한다. 입력 줄에 항목이 몇 개 있었는지 알 수 있다.
- 구분자가 연달아 나올 때 하나로 취급하려면 DELIMITED BY ALL "," 로 쓴다.
UNSTRING 이 건드리는 것은 값을 받은 항목뿐이다. 줄이 짧아서 끝까지 가지 못하면 남은 항목은 이전 값 그대로다. TALLYING 변수도 0 으로 되돌려 주지 않고 더하기만 한다. 두 가지 모두 UNSTRING 앞에서 직접 초기화해야 한다.
INSPECT 로 세고 바꾸기
INSPECT 는 항목의 글자를 훑으면서 세거나(TALLYING) 바꾼다(REPLACING). 항목 자체를 늘리거나 줄이지는 않는다.
| 형태 | 뜻 | 예 |
|---|---|---|
| TALLYING ... FOR ALL "-" | 모든 하이픈의 개수를 센다 | 계좌번호의 구분 기호 수 |
| TALLYING ... FOR CHARACTERS BEFORE INITIAL "-" | 첫 하이픈 앞의 글자 수를 센다 | 지점 코드의 길이 |
| REPLACING LEADING "0" BY SPACE | 맨 앞에 연속된 0 만 공백으로 바꾼다 | 금액 앞의 채움 0 제거 |
| REPLACING ALL "-" BY "." | 모든 하이픈을 마침표로 바꾼다 | 표기 통일 |
REPLACING 에서 바꾸기 전과 후의 글자 수는 같아야 한다. 글자를 지워서 줄이는 용도로는 쓸 수 없다. 앞의 0 을 지울 때도 지우는 대신 공백으로 바꾸고, 뒤에 FUNCTION TRIM 으로 공백을 정리한다. TALLYING 의 카운터는 INSPECT 도 초기화해 주지 않으므로 앞에서 0 으로 만들어 둔다.
참조 수정과 내장 함수
참조 수정
항목 이름 뒤에 (시작:길이)를 붙이면 그 항목의 일부분만 가리킨다. 시작은 1 부터 센다. 길이를 생략하면 시작 위치부터 끝까지다.
WS-ACCT(1:3) *> 계좌번호 맨 앞 3글자
WS-ACCT(5:) *> 5번째부터 끝까지
MOVE ALL "*" TO WS-MASKED(1:8)
마지막 줄처럼 쓰기 대상에도 쓸 수 있다. 시작과 길이는 변수여도 된다. 범위를 벗어났는지는 기본 설정에서 검사하지 않는다. 컴파일할 때 cobc 에 -debug 를 더하면 실행 중 검사하고 멈춘다.
FUNCTION TRIM 과 UPPER-CASE
내장 함수(intrinsic function)는 FUNCTION 이라는 예약어로 호출하는, 값을 돌려주는 기능이다.
| 함수 | 하는 일 | 비고 |
|---|---|---|
| FUNCTION TRIM(항목) | 앞뒤 공백을 뗀 글자를 돌려준다 | LEADING 이나 TRAILING 을 붙이면 한쪽만 뗀다 |
| FUNCTION UPPER-CASE(항목) | 영문 소문자를 대문자로 바꿔 돌려준다 | 원래 항목은 바뀌지 않는다 |
| FUNCTION LENGTH(값) | 글자 수(바이트)를 돌려준다 | TRIM 결과에 쓰면 공백을 뗀 길이다 |
함수는 MOVE 의 보내는 쪽, STRING 의 조각, COMPUTE 의 식에 쓸 수 있고 서로 겹쳐서 쓸 수도 있다. 완성 코드의 FUNCTION UPPER-CASE(FUNCTION TRIM(...)) 는 공백을 먼저 떼고 그 결과를 대문자로 바꾼다.
완성 코드
입력은 프로그램 안에서 네 줄을 직접 채워 넣는다. 파일 읽기는 앞 장들의 주제이므로 여기서는 문자열 처리에만 집중한다. 줄마다 UNSTRING 으로 나누고, INSPECT 로 세고 바꾸고, STRING 으로 레이블을 만든다.
main.cob
IDENTIFICATION DIVISION.
PROGRAM-ID. MAIN.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-RAW-TABLE.
05 WS-RAW-LINE PIC X(40) OCCURS 4 TIMES.
01 WS-IDX PIC 9 VALUE 0.
01 WS-FIELD-CNT PIC 9 VALUE 0.
01 WS-OK-CNT PIC 9 VALUE 0.
01 WS-BAD-CNT PIC 9 VALUE 0.
01 WS-NAME-RAW PIC X(20).
01 WS-ACCT PIC X(12).
01 WS-TYPE PIC X(3).
01 WS-AMT PIC X(20).
01 WS-NAME-LEN PIC 99 VALUE 0.
01 WS-TRIM-LEN PIC 99 VALUE 0.
01 WS-HYPHENS PIC 9 VALUE 0.
01 WS-BR-LEN PIC 99 VALUE 0.
01 WS-NAME-CLEAN PIC X(20).
01 WS-MASKED PIC X(12).
01 WS-KIND PIC X(8).
01 WS-LABEL PIC X(60).
01 WS-PTR PIC 99 VALUE 1.
01 WS-USED PIC 99 VALUE 0.
PROCEDURE DIVISION.
MAIN-PARA.
PERFORM LOAD-DATA
PERFORM VARYING WS-IDX FROM 1 BY 1 UNTIL WS-IDX > 4
PERFORM PARSE-LINE
END-PERFORM
DISPLAY "PROCESSED " WS-OK-CNT " SKIPPED " WS-BAD-CNT
STOP RUN.
LOAD-DATA.
MOVE "kim minsu,110-2345-01,DEP,00150000"
TO WS-RAW-LINE(1)
MOVE " park jiho ,110-7788-02,WDR,00045000"
TO WS-RAW-LINE(2)
MOVE "lee seoyeon,220-1002-03,DEP,03200000"
TO WS-RAW-LINE(3)
MOVE "choi,330-4500-01"
TO WS-RAW-LINE(4).
PARSE-LINE.
MOVE SPACES TO WS-NAME-RAW WS-ACCT WS-TYPE WS-AMT
MOVE 0 TO WS-FIELD-CNT WS-NAME-LEN
UNSTRING WS-RAW-LINE(WS-IDX) DELIMITED BY ","
INTO WS-NAME-RAW COUNT IN WS-NAME-LEN
WS-ACCT
WS-TYPE
WS-AMT
TALLYING IN WS-FIELD-CNT
END-UNSTRING
IF WS-FIELD-CNT < 4
ADD 1 TO WS-BAD-CNT
DISPLAY "LINE " WS-IDX " FIELDS " WS-FIELD-CNT
" SKIPPED"
ELSE
PERFORM BUILD-LABEL
END-IF.
BUILD-LABEL.
MOVE 0 TO WS-HYPHENS WS-BR-LEN
INSPECT WS-ACCT TALLYING WS-HYPHENS FOR ALL "-"
INSPECT WS-ACCT TALLYING WS-BR-LEN
FOR CHARACTERS BEFORE INITIAL "-"
INSPECT WS-AMT REPLACING LEADING "0" BY SPACE
COMPUTE WS-TRIM-LEN =
FUNCTION LENGTH(FUNCTION TRIM(WS-NAME-RAW))
MOVE FUNCTION UPPER-CASE(FUNCTION TRIM(WS-NAME-RAW))
TO WS-NAME-CLEAN
MOVE WS-ACCT TO WS-MASKED
MOVE ALL "*" TO WS-MASKED(1:8)
EVALUATE WS-TYPE
WHEN "DEP"
MOVE "DEPOSIT" TO WS-KIND
WHEN "WDR"
MOVE "WITHDRAW" TO WS-KIND
WHEN OTHER
MOVE "UNKNOWN" TO WS-KIND
END-EVALUATE
MOVE SPACES TO WS-LABEL
MOVE 1 TO WS-PTR
STRING FUNCTION TRIM(WS-NAME-CLEAN) DELIMITED BY SIZE
" [" DELIMITED BY SIZE
WS-MASKED DELIMITED BY SPACE
"] " DELIMITED BY SIZE
FUNCTION TRIM(WS-KIND) DELIMITED BY SIZE
" " DELIMITED BY SIZE
FUNCTION TRIM(WS-AMT) DELIMITED BY SIZE
INTO WS-LABEL
WITH POINTER WS-PTR
ON OVERFLOW DISPLAY "LABEL OVERFLOW"
END-STRING
COMPUTE WS-USED = WS-PTR - 1
ADD 1 TO WS-OK-CNT
DISPLAY "LINE " WS-IDX " FIELDS " WS-FIELD-CNT
" RAW-LEN " WS-NAME-LEN " TRIM-LEN " WS-TRIM-LEN
" HYPHENS " WS-HYPHENS
DISPLAY " " WS-LABEL(1:WS-USED)
DISPLAY " BRANCH " WS-ACCT(1:WS-BR-LEN).
줄별 해설
자료 정의
WS-RAW-TABLE 은 40바이트짜리 줄 네 개를 담는 테이블이다. WS-NAME-RAW, WS-ACCT, WS-TYPE, WS-AMT 는 UNSTRING 이 나누어 담을 항목이다. WS-NAME-LEN 은 COUNT IN 이, WS-FIELD-CNT 는 TALLYING IN 이 채운다. WS-HYPHENS 와 WS-BR-LEN 은 INSPECT 의 카운터다. WS-PTR 과 WS-USED 는 STRING 의 포인터와 사용 길이다.
MAIN-PARA 와 LOAD-DATA
메인 문단은 데이터를 채운 뒤 1 부터 4 까지 PARSE-LINE 을 부르고 건수를 찍는다. LOAD-DATA 는 입력 네 줄을 MOVE 로 테이블에 넣는다. 두 번째 줄에는 이름 앞뒤에 공백이 붙어 있고, 네 번째 줄은 항목이 둘뿐이다.
PARSE-LINE
먼저 받는 항목을 공백으로, 카운터를 0 으로 되돌린다. UNSTRING 은 이전 줄의 값을 지워 주지 않기 때문이다. UNSTRING 은 쉼표 기준으로 나눈다. 첫 항목의 글자 수는 WS-NAME-LEN 에 들어가고, 값을 받은 항목의 수는 WS-FIELD-CNT 에 쌓인다. 4 보다 작으면 잘못된 줄로 보고 건너뛴다.
BUILD-LABEL: 세고 바꾸기
INSPECT 첫 문장은 하이픈 개수를, 둘째 문장은 첫 하이픈 앞의 글자 수를 센다. 계좌번호 "110-2345-01" 에서 각각 2 와 3 이 나온다. 셋째 INSPECT 는 금액의 앞쪽 0 을 공백으로 바꾼다. "00150000" 이 " 150000" 이 된다.
BUILD-LABEL: 이름과 가림
COMPUTE 는 공백을 뗀 이름의 길이를 구한다. 이어서 TRIM 으로 공백을 떼고 UPPER-CASE 로 바꾼 이름을 WS-NAME-CLEAN 에 담는다. 계좌번호는 WS-MASKED 에 복사한 뒤 참조 수정 (1:8) 에 "*" 를 채워서 앞 여덟 자리를 가린다. 원본 WS-ACCT 는 그대로 남는다. EVALUATE 는 거래 코드를 읽기 쉬운 단어로 바꾼다.
BUILD-LABEL: STRING 과 출력
STRING 앞에서 WS-LABEL 을 공백으로, 포인터를 1 로 되돌린다. 이름, " [", 가린 계좌번호, "] ", 거래 종류, 공백, 금액 순서로 붙인다. 가린 계좌번호에만 DELIMITED BY SPACE 를 쓴 것은 그 값에 공백이 없어서 뒤쪽 빈칸만 잘라 내기 위해서다. 나머지는 SIZE 로 전체를 보낸다. 끝난 뒤 포인터에서 1 을 뺀 값이 사용한 길이이고, DISPLAY 에서 WS-LABEL(1:WS-USED) 로 그 길이만큼만 출력한다. 마지막 줄은 WS-BR-LEN 을 길이로 써서 지점 코드만 꺼낸다.
실행 결과
$ cobc -x -Wall main.cob
$ ./main
LINE 1 FIELDS 4 RAW-LEN 09 TRIM-LEN 09 HYPHENS 2
KIM MINSU [********-01] DEPOSIT 150000
BRANCH 110
LINE 2 FIELDS 4 RAW-LEN 12 TRIM-LEN 09 HYPHENS 2
PARK JIHO [********-02] WITHDRAW 45000
BRANCH 110
LINE 3 FIELDS 4 RAW-LEN 11 TRIM-LEN 11 HYPHENS 2
LEE SEOYEON [********-03] DEPOSIT 3200000
BRANCH 220
LINE 4 FIELDS 2 SKIPPED
PROCESSED 3 SKIPPED 1
두 번째 줄에서 RAW-LEN 은 12, TRIM-LEN 은 9 다. 앞뒤 공백 세 칸이 TRIM 으로 사라진 것이다. 금액은 앞의 0 이 지워져 "45000" 이 되었다.
실무에서 자주 틀리는 것
이름 속 공백에서 잘린다
틀린 코드:
STRING WS-NAME-CLEAN DELIMITED BY SPACE
" [" DELIMITED BY SIZE
INTO WS-LABEL
END-STRING
"KIM MINSU" 가 첫 공백 앞에서 끊겨 "KIM [" 만 붙는다. 이름 항목은 20칸 중 뒤쪽이 공백이라, TRIM 으로 뒤쪽 공백을 떼고 SIZE 로 보낸다.
STRING FUNCTION TRIM(WS-NAME-CLEAN) DELIMITED BY SIZE
" [" DELIMITED BY SIZE
INTO WS-LABEL
END-STRING
STRING 앞에서 대상과 포인터를 초기화하지 않는다
틀린 코드:
STRING "LEE SEOYEON" DELIMITED BY SIZE INTO WS-LABEL
STRING "PARK" DELIMITED BY SIZE INTO WS-LABEL
두 번째 STRING 은 앞 네 칸만 덮어쓴다. WS-LABEL 은 "PARKSEOYEON" 이 된다. 포인터를 쓴다면 이전 값에서 이어 붙게 된다. 매번 지우고 1 로 되돌린다.
MOVE SPACES TO WS-LABEL
MOVE 1 TO WS-PTR
STRING "PARK" DELIMITED BY SIZE
INTO WS-LABEL WITH POINTER WS-PTR
END-STRING
UNSTRING 과 INSPECT 의 카운터를 0 으로 두지 않는다
틀린 코드:
UNSTRING WS-RAW-LINE(WS-IDX) DELIMITED BY ","
INTO WS-NAME-RAW WS-ACCT WS-TYPE WS-AMT
TALLYING IN WS-FIELD-CNT
END-UNSTRING
WS-FIELD-CNT 는 이전 줄의 4 에서 계속 더해진다. 항목이 모자란 줄도 "4 이상" 으로 보여서 걸러지지 않는다. 받는 항목도 이전 줄 값이 남아 있다. 실행 전에 둘 다 초기화한다. INSPECT 의 카운터도 마찬가지다.
MOVE SPACES TO WS-NAME-RAW WS-ACCT WS-TYPE WS-AMT
MOVE 0 TO WS-FIELD-CNT
참조 수정의 길이가 0 이 된다
틀린 코드:
INSPECT WS-ACCT TALLYING WS-BR-LEN
FOR CHARACTERS BEFORE INITIAL "-"
DISPLAY WS-ACCT(1:WS-BR-LEN)
계좌번호가 "-" 로 시작하면 WS-BR-LEN 은 0 이다. 길이 0 의 참조 수정은 잘못된 범위지만 기본 설정에서는 검사하지 않아서, 엉뚱한 글자가 나오거나 조용히 지나간다. 길이를 확인한 뒤 쓰고, 개발 중에는 -debug 로 컴파일한다.
IF WS-BR-LEN > 0
DISPLAY WS-ACCT(1:WS-BR-LEN)
END-IF
한눈에 보기
| 문장·기능 | 하는 일 | 사용 전에 할 일 |
|---|---|---|
| STRING ... DELIMITED BY SIZE | 조각을 이어 붙인다 | 대상을 공백으로, 포인터를 1 로 |
| UNSTRING ... INTO | 구분자 기준으로 나눈다 | 받는 항목을 지우고 TALLYING 변수를 0 으로 |
| INSPECT TALLYING | 글자를 센다 | 카운터를 0 으로 |
| INSPECT REPLACING | 같은 길이의 글자로 바꾼다 | 바꾸기 전후 길이를 맞춘다 |
| 항목(시작:길이) | 일부분을 읽고 쓴다 | 범위와 길이 0 을 확인한다 |
| FUNCTION TRIM, UPPER-CASE | 공백 정리, 대문자 변환 | 없음 |
연습 문제
- WS-BR 은 PIC X(3) 이고 "110", WS-NM 은 PIC X(10) 이고 "KIM" 이 들어 있다. WS-OUT 에 "110/KIM" 이 만들어지도록 STRING 문을 쓰라. WS-NM 뒤의 빈칸은 들어가면 안 된다.
- WS-NAME 은 PIC X(20) 이고 "LEE SEO YEON" 이 들어 있다. INSPECT WS-NAME TALLYING WS-CNT FOR ALL SPACE 를 하면 2 가 아니라 10 이 나온다. 이유를 설명하고, 2 가 나오도록 고치라.
- WS-DATE 는 PIC X(10) 이고 "2026-10-02" 이다. 연, 월, 일을 WS-Y(X(4)), WS-M(X(2)), WS-D(X(2)) 로 나누는 UNSTRING 문을 쓰라.
- WS-MASKED 에 "110-2345-01" 이 들어 있다. 가운데 네 자리만 "XXXX" 로 바꾸는 문장을 한 줄로 쓰고, 결과를 적으라.
정답과 해설
-
MOVE SPACES TO WS-OUT STRING WS-BR DELIMITED BY SIZE "/" DELIMITED BY SIZE WS-NM DELIMITED BY SPACE INTO WS-OUT END-STRINGWS-NM 은 이름 속 공백이 없으니 DELIMITED BY SPACE 로 뒤쪽 빈칸을 잘라낼 수 있다. 이름에 공백이 있다면 TRIM 과 SIZE 를 쓴다. 결과는 "110/KIM" 이다.
-
WS-NAME 은 20칸이어서 뒤쪽 8칸의 빈칸까지 모두 공백으로 세어진다. 단어 사이 공백 2개와 뒤쪽 8개를 합친 10 이다. 실제 글자 길이를 먼저 구해 그 범위만 검사하면 된다.
MOVE 0 TO WS-CNT COMPUTE WS-LEN = FUNCTION LENGTH(FUNCTION TRIM(WS-NAME TRAILING)) INSPECT WS-NAME(1:WS-LEN) TALLYING WS-CNT FOR ALL SPACEWS-LEN 은 12 이고 WS-CNT 는 2 다. 단어 수는 2 + 1 = 3 이다.
-
UNSTRING WS-DATE DELIMITED BY "-" INTO WS-Y WS-M WS-D END-UNSTRING결과는 WS-Y "2026", WS-M "10", WS-D "02" 다. 줄 끝에서 남는 글자가 없으니 마지막 항목까지 정확히 채워진다.
-
MOVE "XXXX" TO WS-MASKED(5:4)다섯 번째 글자부터 네 칸이 바뀌어 "110-XXXX-01" 이 된다. 참조 수정의 시작은 1 부터 센다.