Site logo
Tác giả
  • avatar Nguyễn Đức Xinh
    Name
    Nguyễn Đức Xinh
    Twitter
Ngày xuất bản
Ngày xuất bản

WORKING-STORAGE nâng cao trong COBOL – Hiểu rõ OCCURS và REDEFINES qua ví dụ thực tế

Vì sao phải học WORKING-STORAGE nâng cao (OCCURS & REDEFINES)?

Ở các bài trước, bạn đã:

  • Bài 002: hiểu level number, PIC, group item, numeric/string
  • Bài 006: quen với PERFORM VARYING – vòng lặp có biến đếm
  • Bài 007: làm việc với FILE SECTION và record 01 để đọc file tuần tự

Trong chương trình batch thực tế như RZZBSQLN1.PCO, WORKING-STORAGE không chỉ có vài biến lẻ 9(4) hay X(20) mà thường chứa:

  • Table (mảng 1 chiều) để lưu nhiều record tạm thời – dùng OCCURS
  • Struct "2 mặt" cho cùng một vùng nhớ – dùng REDEFINES để chia nhỏ date/time, phân tích record phức tạp, đổi format dữ liệu

Bài này tập trung vào hai công cụ rất mạnh (và cũng dễ gây bug nếu không hiểu kỹ):

  • OCCURS – khai báo table/mảng trong COBOL
  • REDEFINES – cho phép nhiều layout khác nhau cùng "chồng" lên một vùng nhớ

Chúng ta sẽ:

  • Xây dựng mental model tương đương với array/struct trong Java/Python
  • Phân tích các ví dụ thực tế lấy cảm hứng từ RZZBSQLN1 như WK_SQL_TBL, WK_PARM_TBL, SYS_DATE/SYS_DATE-R
  • Làm bài tập thiết kế table và layout date/time

Ôn lại: WORKING-STORAGE và level number

WORKING-STORAGE dùng để khai báo biến dùng suốt chương trình:

       DATA DIVISION.
       WORKING-STORAGE SECTION.

       01 CUSTOMER.
          05 CUST-ID      PIC 9(5).
          05 CUST-NAME    PIC X(30).

       77 WS-COUNTER      PIC 9(5) VALUE 0.
  • 01 CUSTOMERrecord (group item) – giống một struct/class instance
  • 05 CUST-ID, 05 CUST-NAME là field con bên trong
  • 77 WS-COUNTER là biến đơn lẻ (không thuộc group)

Khi thêm OCCURSREDEFINES, bạn đang chuyển từ biến đơn sang cấu trúc dữ liệu phức tạp.


OCCURS – Khai báo table (mảng) trong COBOL

1. OCCURS là gì?

OCCURS n TIMES cho COBOL biết rằng một field hoặc group sẽ lặp lại n lần, tương đương với:

  • Mảng 1 chiều (array) trong Java/C
  • List có kích thước cố định trong các ngôn ngữ modern

Ví dụ đơn giản:

       01 SCORE-TABLE.
          05 SCORE-ITEM OCCURS 5 TIMES.
             10 SCORE-VALUE PIC 9(3).

Ý nghĩa:

  • SCORE-TABLE là group item
  • SCORE-ITEM lặp lại 5 lần
  • Mỗi SCORE-ITEM có 1 field SCORE-VALUE (3 chữ số)

Tổng dung lượng:

  • SCORE-VALUE = 3 bytes
  • SCORE-ITEM OCCURS 5 TIMES → 3 * 5 = 15 bytes liên tiếp trong bộ nhớ

2. Truy cập phần tử với index

Truy cập phần tử thứ i dùng cú pháp field-name (index):

MOVE 100 TO SCORE-VALUE (1).
MOVE  80 TO SCORE-VALUE (2).
MOVE  75 TO SCORE-VALUE (3).

Index trong COBOL bắt đầu từ 1, không phải 0.

Bạn có thể kết hợp với PERFORM VARYING:

       01  IDX   PIC 9(2).

       PROCEDURE DIVISION.
           PERFORM VARYING IDX FROM 1 BY 1
                   UNTIL IDX > 5
               DISPLAY "SCORE(" IDX ") = " SCORE-VALUE (IDX)
           END-PERFORM.

Mapping Java:

int[] score = new int[5];
score[0] = 100;
score[1] = 80;
...
for (int i = 0; i < 5; i++) {
    System.out.println("SCORE(" + (i+1) + ") = " + score[i]);
}

Nhớ: SCORE-VALUE(1) ~ score[0] nếu bạn muốn mapping index theo tư duy 0-based.


Group table vs elementary table

OCCURS có thể dùng ở level group hoặc level field:

1. Elementary table – OCCURS trên field

01 SCORE-TABLE.
   05 SCORE-VALUE   PIC 9(3) OCCURS 5 TIMES.
  • SCORE-VALUE(1)..SCORE-VALUE(5) là các phần tử numeric
  • Thích hợp khi mỗi phần tử chỉ có một field

2. Group table – OCCURS trên group

01 STUDENT-TABLE.
   05 STUDENT-ITEM OCCURS 100 TIMES.
      10 STU-ID      PIC 9(5).
      10 STU-NAME    PIC X(30).
      10 STU-AGE     PIC 9(3).
  • STUDENT-ITEM(1) là một record gồm 3 field: ID, NAME, AGE

  • Truy cập field con:

    MOVE 00001   TO STU-ID   (1).
    MOVE "Nguyen Van A" TO STU-NAME (1).
    MOVE 20      TO STU-AGE  (1).
    
  • Phù hợp khi mỗi phần tử có nhiều field – giống mảng struct/class.

So sánh nhanh

Kiểu OCCURS Ví dụ Use case
Elementary table SCORE-VALUE PIC 9(3) OCCURS 5 Danh sách điểm, số lượng đơn giản
Group table STUDENT-ITEM OCCURS 100 TIMES Danh sách record phức tạp (ID, NAME)

Pattern OCCURS trong RZZBSQLN1: WK_SQL_TBL và WK_PARM_TBL

Trong RZZBSQLN1.PCO, có các khai báo kiểu:

01  WK_SQL_TBL.
    03 WK_SQL_ITEM OCCURS 50 TIMES.
       05 WK_SQL     PIC X(82).

01  WK_PARM_TBL.
    03 WK_PARM_ITEM OCCURS 4 TIMES.
       05 WK_PARM    PIC X(64).

Ý nghĩa:

  • WK_SQL_TBL là table chứa tối đa 50 dòng SQL
  • WK_SQL(n) là SQL thứ n đọc được từ file input
  • WK_PARM_TBL là table chứa tối đa 4 parameter (ví dụ schema, DB name,...)

Truy cập trong code (giả lập):

COMPUTE WK_SQL_IDX = WK_SQL_IDX + 1.
IF WK_SQL_IDX <= 50
    MOVE INP1_SQL TO WK_SQL (WK_SQL_IDX)
END-IF.

Mapping Java:

String[] wkSql = new String[50];
int wkSqlIdx = 0;

wkSqlIdx = wkSqlIdx + 1;
if (wkSqlIdx <= 50) {
    wkSql[wkSqlIdx - 1] = inp1Sql; // nếu Java index từ 0
}

Khi học tới bài 010–011 về embedded SQL, bạn sẽ thấy table này được duyệt bằng PERFORM VARYING để build và execute từng SQL.


REDEFINES – Một vùng nhớ, nhiều layout

1. REDEFINES là gì?

REDEFINES cho phép hai (hoặc nhiều) biến chia sẻ cùng một vùng nhớ, nhưng được mô tả với layout khác nhau.

Ví dụ đơn giản với date:

01 SYS-DATE         PIC X(8).
01 SYS-DATE-R REDEFINES SYS-DATE.
   05 SYS-YYYY      PIC X(4).
   05 SYS-MM        PIC X(2).
   05 SYS-DD        PIC X(2).

Ý nghĩa:

  • SYS-DATE là chuỗi 8 ký tự, ví dụ "20260304"
  • SYS-DATE-R không chiếm thêm bộ nhớ, chỉ "nhìn" vùng nhớ đó dưới dạng 3 field con: YYYY, MM, DD

Cách dùng:

ACCEPT SYS-DATE FROM DATE.
DISPLAY SYS-DATE.
DISPLAY "YEAR=" SYS-YYYY " MONTH=" SYS-MM " DAY=" SYS-DD.

Mapping mental model:

  • Giống việc dùng substring trong Java, nhưng được compiler hỗ trợ layout ngay trong khai báo dữ liệu
String sysDate = getToday(); // "20260304"
String yyyy = sysDate.substring(0, 4);
String mm   = sysDate.substring(4, 6);
String dd   = sysDate.substring(6, 8);

2. Yêu cầu quan trọng khi dùng REDEFINES

  • Tổng độ dài (bytes) của các layout REDEFINES phải bằng nhau hoặc bạn hiểu rõ rủi ro khi không bằng
  • Các biến REDEFINES phải cùng level (thường là level 01 hoặc 05)

Ví dụ an toàn:

01  AMOUNT-FIELD.
    05 AMOUNT-RAW    PIC X(10).

01  AMOUNT-NUM REDEFINES AMOUNT-FIELD.
    05 AMOUNT-SIGN   PIC X(1).
    05 AMOUNT-VALUE  PIC 9(9).

Both = 10 bytes → an toàn.


Pattern REDEFINES với date/time trong batch program

Lấy cảm hứng từ các chương trình như RZZBSQLN1, pattern phổ biến:

01  SYS-DATE2        PIC 9(8).
01  SYS-DATE2-R REDEFINES SYS-DATE2.
    05 SYS-DYYYY2    PIC 9(4).
    05 SYS-DMM2      PIC 9(2).
    05 SYS-DDD2      PIC 9(2).

Sau đó code có thể làm:

ACCEPT SYS-DATE2 FROM DATE.
IF SYS-DYYYY2 < 2000
    ADD 19000000 TO SYS-DATE2
ELSE
    ADD 20000000 TO SYS-DATE2
END-IF.

Tư duy backend dev:

  • Input từ hệ thống có thể trả về date dạng YYMMDD → cần chuyển thành YYYYMMDD
  • REDEFINES giúp bạn thao tác trực tiếp trên phần YEAR mà không cần substring thủ công

Mapping Java pseudo-code:

int sysDate2 = acceptDate(); // 8-digit integer
int yyyy = sysDate2 / 10000; // phần năm
if (yyyy < 2000) {
    sysDate2 = sysDate2 + 19000000;
} else {
    sysDate2 = sysDate2 + 20000000;
}

Trong COBOL, REDEFINES cho phép:

  • Vừa làm việc với cả chuỗi 8-digit (SYS-DATE2)
  • Vừa thao tác trên từng phần con (SYS-DYYYY2, SYS-DMM2, SYS-DDD2)

So sánh OCCURS và REDEFINES

Khía cạnh OCCURS REDEFINES
Mục đích chính Tạo table/mảng nhiều phần tử Tạo nhiều "view" khác nhau cho cùng vùng nhớ
Kiểu cấu trúc Lặp lại cùng layout nhiều lần (1 chiều) Nhiều layout song song, cùng độ dài
Mapping hiện đại Array/List Struct với union/variant, hoặc parse bằng substring
Use case điển hình Danh sách record tạm, buffer SQL, table Date/time, record đa format, union type
Rủi ro Tràn index, overflow table Layout lệch độ dài → truncate/garbage khó debug

Khi đọc WORKING-STORAGE phức tạp:

  1. Tìm các block dùng OCCURS → đây là table
  2. Tìm các block dùng REDEFINES → đây là layout song song, cần hiểu mối quan hệ

Ví dụ tổng hợp: table log và REDEFINES status code

Mục tiêu

  • Lưu tối đa 10 dòng log message trong WORKING-STORAGE
  • Mỗi dòng gồm: timestamp (8), type (1), message (31)
  • Có thêm một layout REDEFINES để nhìn type theo dạng condition

Khai báo dữ liệu

       WORKING-STORAGE SECTION.

       01  LOG-TABLE.
           05 LOG-ITEM OCCURS 10 TIMES.
              10 LOG-TIME     PIC X(8).
              10 LOG-TYPE     PIC X(1).
              10 LOG-MSG      PIC X(31).

       01  LOG-IDX           PIC 9(2) VALUE 0.

       01  STATUS-CODE       PIC X(2).
       01  STATUS-CODE-R REDEFINES STATUS-CODE.
           05 STATUS-MAIN    PIC X(1).
           05 STATUS-SUB     PIC X(1).

       88  STATUS-OK         VALUE "00".
       88  STATUS-WARN       VALUE "10" THRU "19".
       88  STATUS-ERROR      VALUE "90" THRU "99".
  • LOG-TABLE là group table 10 phần tử
  • STATUS-CODE-R chia status 2-char thành 2 phần: main/sub
  • Level 88 (sẽ học sâu hơn ở bài khác) giúp đặt condition name cho các giá trị cụ thể

PROCEDURE DIVISION (rút gọn)

       PROCEDURE DIVISION.

       MAIN-RTN.
           MOVE "00" TO STATUS-CODE
           PERFORM ADD-LOG-LINE
           MOVE "15" TO STATUS-CODE
           PERFORM ADD-LOG-LINE
           MOVE "91" TO STATUS-CODE
           PERFORM ADD-LOG-LINE

           PERFORM PRINT-LOG
           STOP RUN.

       ADD-LOG-LINE.
           IF LOG-IDX < 10
               ADD 1 TO LOG-IDX
               ACCEPT LOG-TIME (LOG-IDX) FROM TIME
               MOVE STATUS-MAIN TO LOG-TYPE (LOG-IDX)
               MOVE "Status = " TO LOG-MSG (LOG-IDX)
           END-IF
           EXIT.

       PRINT-LOG.
           PERFORM VARYING LOG-IDX FROM 1 BY 1
                   UNTIL LOG-IDX > 10
               IF LOG-TIME (LOG-IDX) NOT = SPACES
                   DISPLAY LOG-TIME (LOG-IDX)
                   DISPLAY " TYPE=" LOG-TYPE (LOG-IDX)
                   DISPLAY " MSG =" LOG-MSG (LOG-IDX)
               END-IF
           END-PERFORM
           EXIT.

Ý tưởng:

  • STATUS-MAIN (ký tự đầu) có thể đại diện cho loại status: OK/WARN/ERROR
  • Mỗi lần log, ta chỉ lưu STATUS-MAIN vào LOG-TYPE

Bài tập thực hành

Bài 1 – Thiết kế table sản phẩm với OCCURS

Yêu cầu:

  • Khai báo PRODUCT-TBL với tối đa 100 sản phẩm
  • Mỗi sản phẩm có:
    • PROD-IDX(10)
    • PROD-NAMEX(30)
    • PROD-PRICE9(9)
  • Dùng OCCURS (group table)

Sau đó, viết skeleton PROCEDURE DIVISION:

  • Biến IDX (9(3)) dùng làm index
  • PERFORM VARYING IDX FROM 1 BY 1 UNTIL IDX > PROD-COUNT để display danh sách sản phẩm

Bài 2 – REDEFINES cho amount + sign

Thiết kế biến lưu số tiền với format sau trong file:

  • 1 ký tự sign (+ hoặc -)
  • 9 chữ số amount (không có dấu chấm thập phân)

Yêu cầu khai báo:

  • Biến raw AMOUNT-RAW 10 ký tự
  • Biến AMOUNT-SPLIT REDEFINES AMOUNT-RAW gồm:
    • AMT-SIGNX(1)
    • AMT-VALUE9(9)

Viết đoạn code nhỏ:

  • Gán thử "+000001000""-000000500" vào AMOUNT-RAW
  • Hiển thị sign và numeric value tách riêng

Bài 3 – Áp dụng OCCURS như WK_SQL_TBL

Giả sử bạn có file chứa tối đa 20 câu lệnh SQL, mỗi dòng tối đa 100 ký tự.

Yêu cầu:

  • Khai báo SQL-TBL với OCCURS 20 TIMES, mỗi phần tử có field SQL-TEXT PIC X(100)
  • Khai báo biến SQL-IDX PIC 9(2) làm index
  • Viết skeleton logic:
    • Đọc từng dòng từ file input vào WS-INPUT-LINE
    • Tăng SQL-IDX, MOVE WS-INPUT-LINE TO SQL-TEXT (SQL-IDX) nếu chưa vượt quá 20
    • Sau khi đọc xong, dùng PERFORM VARYING để DISPLAY SQL-TEXT (i) từ 1 đến SQL-IDX

Bài 4 – Thiết kế REDEFINES cho date/time

Thiết kế cặp biến để xử lý datetime dạng YYYYMMDDHHMISS (14 ký tự):

  • Biến raw: DT-RAW PIC X(14)
  • Biến DT-PARTS REDEFINES DT-RAW gồm:
    • DT-YYYY PIC X(4)
    • DT-MM PIC X(2)
    • DT-DD PIC X(2)
    • DT-HH PIC X(2)
    • DT-MI PIC X(2)
    • DT-SS PIC X(2)

Viết đoạn code nhỏ gán giá trị ví dụ "20260304153045" rồi DISPLAY từng phần.


Kết luận

Trong bài 008, bạn đã đi sâu vào WORKING-STORAGE nâng cao với hai công cụ cực kỳ quan trọng:

  • OCCURS – để khai báo table/mảng, lưu nhiều record tạm thời (ví dụ WK_SQL_TBL, WK_PARM_TBL)
  • REDEFINES – để tạo nhiều view/layout cho cùng vùng nhớ (đặc biệt hữu ích với date/time, amount, record đa format)

Hiểu vững hai khái niệm này giúp bạn:

  • Đọc được các WORKING-STORAGE block phức tạp trong chương trình COBOL enterprise
  • Mapping được cấu trúc COBOL sang array/struct/class trong Java/C#/PL-SQL
  • Thiết kế data structure hợp lý cho batch job mới

Ở các bài tiếp theo, chúng ta sẽ:

  • Khai thác ACCEPT FROM DATE/TIME và pattern xử lý ngày giờ (bài 009)
  • Bắt đầu bước vào thế giới embedded SQL trong COBOL (bài 010–011), nơi các table như WK_SQL_TBL thật sự phát huy tác dụng khi build và thực thi SQL động.

Khi quay lại đọc RZZBSQLN1.PCO, hãy thử:

  • Xác định tất cả block dùng OCCURS → đó là các table quan trọng
  • Xác định tất cả block dùng REDEFINES → đó là các layout đặc biệt bạn cần hiểu kỹ

Làm được điều này, bạn đã tiến thêm một bước lớn trên hành trình đọc hiểu và maintain hệ thống COBOL legacy.