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.- Таблицы вместе с циклом перебора — основа для расчётов вроде суммы, среднего, поиска максимума в наборе однотипных данных внутри одной записи.