OCCURS: таблицы (массивы) в COBOL

Разбираемся, как хранить в COBOL не одно значение, а сразу набор однотипных — например, суммы платежей за 12 месяцев — и как перебирать их в цикле.

Таблица (table) — так в COBOL называют то, что в большинстве других языков называют массивом: набор ячеек одного типа, к каждой из которых можно обратиться по номеру.

Зачем это нужно

Вспомни прошлый урок: там мы обрабатывали клиентов банка по одному — прочитали запись, вывели, перешли к следующей. Но что если внутри одной записи нужно хранить сразу несколько однотипных значений? Например, у счёта есть история платежей за последние 12 месяцев, и всё это — часть одной записи клиента. Заводить 12 отдельных полей MONTH-1, MONTH-2, ..., MONTH-12 — можно, но мучительно: любую операцию над ними придётся повторять 12 раз вручную, а если понадобится 24 месяца — переписывать всю программу.

Именно для этого в COBOL есть предложение OCCURS — оно превращает обычное поле в таблицу из N одинаковых ячеек, к которым можно обращаться по индексу и перебирать в цикле.

Синтаксис OCCURS

Таблица объявляется как обычное поле в WORKING-STORAGE SECTION, только с добавлением OCCURS N TIMES у элемента, который повторяется.

       01  MONTHLY-PAYMENTS.
           05  PAYMENT-AMOUNT  PIC 9(5)V99 OCCURS 12 TIMES.

Читается это так: «поле PAYMENT-AMOUNT с форматом числа 9(5)V99 (пять цифр, точка, ещё две цифры) повторяется 12 раз». В памяти это выглядит как 12 одинаковых ячеек подряд, и любую из них можно достать по номеру от 1 до 12 — нумерация в COBOL традиционно начинается с единицы, а не с нуля, как в большинстве современных языков.

Обратиться к конкретной ячейке можно через скобки с индексом:

       MOVE 1500.00 TO PAYMENT-AMOUNT (1).
       DISPLAY PAYMENT-AMOUNT (1).

Здесь PAYMENT-AMOUNT (1) — это первая ячейка таблицы, то есть платёж за первый месяц. Индекс может быть как литералом (числом прямо в коде), так и переменной — и именно переменная-индекс делает таблицы по-настоящему полезными, потому что позволяет перебирать все ячейки в цикле, а не писать 12 раз одну и ту же строку.

Перебор таблицы: PERFORM VARYING

Чтобы пройтись по всем ячейкам таблицы, в COBOL используют цикл PERFORM VARYING — это ближайший аналог классического for из C-подобных языков: «начни с такого-то значения, увеличивай на столько-то, пока не дойдёшь до предела».

       WORKING-STORAGE SECTION.
       01  MONTHLY-PAYMENTS.
           05  PAYMENT-AMOUNT  PIC 9(5)V99 OCCURS 12 TIMES.
       01  WS-MONTH-INDEX      PIC 9(2).
       01  WS-TOTAL            PIC 9(7)V99 VALUE 0.

       PROCEDURE DIVISION.
       MAIN-LOGIC.
           PERFORM VARYING WS-MONTH-INDEX FROM 1 BY 1
                   UNTIL WS-MONTH-INDEX > 12
               DISPLAY "Месяц " WS-MONTH-INDEX ": "
                       PAYMENT-AMOUNT (WS-MONTH-INDEX)
               ADD PAYMENT-AMOUNT (WS-MONTH-INDEX) TO WS-TOTAL
           END-PERFORM.

           DISPLAY "Итого за год: " WS-TOTAL.
           STOP RUN.

Разберём построчно. WS-MONTH-INDEX — обычное числовое поле, которое будет играть роль счётчика (индекса). WS-TOTAL — поле для накопления суммы, стартующее с нуля через VALUE 0.

PERFORM VARYING WS-MONTH-INDEX FROM 1 BY 1 UNTIL WS-MONTH-INDEX > 12 — это и есть сердце цикла. Читается по частям: «выполняй, меняя WS-MONTH-INDEX, начиная с 1 (FROM 1), прибавляя по 1 на каждом шаге (BY 1), пока условие WS-MONTH-INDEX > 12 не станет истинным (UNTIL)». То есть цикл честно проходит значения 1, 2, 3, ..., 12, а когда индекс становится 13 — останавливается, так и не выполнив тело цикла с этим значением.

Внутри цикла — два действия. DISPLAY печатает номер месяца и значение соответствующей ячейки таблицы PAYMENT-AMOUNT (WS-MONTH-INDEX) — обрати внимание, индексом служит сама переменная цикла, поэтому на первом шаге это PAYMENT-AMOUNT (1), на втором — PAYMENT-AMOUNT (2), и так далее автоматически. ADD PAYMENT-AMOUNT (WS-MONTH-INDEX) TO WS-TOTAL прибавляет значение текущей ячейки к общей сумме — так за 12 шагов цикла в WS-TOTAL накопится сумма платежей за весь год.

END-PERFORM закрывает тело цикла — как END-IF закрывает условие. После цикла выводим накопленный итог.

Практический мини-пример: средний платёж

Раз таблицу легко перебрать и просуммировать, посчитать среднее — тоже пара строк. Добавим это к предыдущему примеру.

       01  WS-AVERAGE          PIC 9(5)V99.

       *> ... после цикла суммирования из примера выше:
       DIVIDE WS-TOTAL BY 12 GIVING WS-AVERAGE.
       DISPLAY "Средний платёж в месяц: " WS-AVERAGE.

DIVIDE WS-TOTAL BY 12 GIVING WS-AVERAGE — делим накопленную годовую сумму на количество месяцев и кладём результат в WS-AVERAGE. Строка, начинающаяся с *>, — это комментарий (свободный формат COBOL, комментарии игнорируются компилятором и нужны только для человека).

Вывод (если платежи за первые три месяца — 1500.00, 980.50 и 2200.00, а остальные девять месяцев по 1000.00):

Месяц 01: 001500.00
Месяц 02: 000980.50
Месяц 03: 002200.00
Месяц 04: 001000.00
...
Месяц 12: 001000.00
Итого за год: 0013680.50
Средний платёж в месяц: 01140.04

Частые ошибки

  • Выход за границы таблицы. Если OCCURS 12 TIMES, а индекс случайно дошёл до 13 или до 0, программа обратится к памяти за пределами таблицы — это классическая и опасная ошибка, приводящая к порче соседних данных или падению программы.
  • Индексация с нуля по привычке из других языков. В COBOL по умолчанию первая ячейка — это индекс 1, а не 0. Начать цикл с FROM 0 — верный способ либо получить ошибку выхода за границы, либо пропустить последнюю ячейку.
  • Забыть обнулить накопитель (WS-TOTAL) перед циклом суммирования. Если поле не проинициализировано через VALUE 0 и программа запускается повторно без явного сброса, сумма накопится «поверх» старого значения.
  • Перепутать условие UNTIL с условием продолжения. UNTIL WS-MONTH-INDEX > 12 — это условие ОСТАНОВКИ, а не условие продолжения. Написать по ошибке UNTIL WS-MONTH-INDEX = 12 — значит пропустить обработку двенадцатого месяца, потому что цикл остановится, едва увидев именно это значение, но уже после того, как не выполнит с ним тело (порядок проверки — до выполнения тела).

Итоги

  • OCCURS N TIMES превращает поле в таблицу из N одинаковых ячеек, к которым обращаются по индексу в круглых скобках: ПОЛЕ (индекс).
  • Нумерация ячеек в COBOL начинается с 1, а не с 0.
  • PERFORM VARYING ... FROM ... BY ... UNTIL ... — стандартный способ перебрать таблицу целиком, аналог цикла for.
  • Таблицы вместе с циклом перебора — основа для расчётов вроде суммы, среднего, поиска максимума в наборе однотипных данных внутри одной записи.
Проверьте себя
1. С какого индекса нумеруются ячейки таблицы, объявленной через OCCURS, в стандартном COBOL?
AС 0
BС 1
CС -1
DИндексация не нужна, доступ только последовательный
2. В конструкции PERFORM VARYING WS-IDX FROM 1 BY 1 UNTIL WS-IDX > 12 условие UNTIL описывает...
AУсловие, при котором цикл продолжает выполняться
BУсловие остановки цикла
CНачальное значение счётчика
DШаг изменения счётчика