1. OCCURS — declarando tabelas
A cláusula OCCURS define quantas vezes um campo se repete. Em vez de declarar TAXA-1, TAXA-2, TAXA-3... você declara uma única entrada com OCCURS:
WORKING-STORAGE SECTION. * Tabela simples: 12 acumuladores mensais 01 WS-ACUMULADORES. 05 WS-TOTAL-MES PIC 9(13)V99 OCCURS 12 TIMES. * Tabela de registros: 50 agências com campos 01 WS-TAB-AGENCIA. 05 WS-AGENCIA OCCURS 50 TIMES. 10 AG-CODIGO PIC 9(04). 10 AG-NOME PIC X(30). 10 AG-SALDO PIC S9(13)V99. 10 AG-ATIVA PIC X(01). 88 AGENCIA-ATIVA VALUE 'S'. * Tabela de 5 taxas mensais 01 WS-TAB-TAXA. 05 WS-TAXA PIC V9(06) OCCURS 5 TIMES.
💗 Para iniciantes — onde OCCURS pode e não pode aparecer
OCCURSvai no nível 02 a 49 — nunca no nível 01, 66, 77 ou 88- Todos os subordinados de um item com
OCCURSse repetem junto com ele - O número de ocorrências é fixo em tempo de compilação (exceto com
DEPENDING ON, que vemos adiante)
2. Subscripts — acessando elementos
Para acessar um elemento específico da tabela você usa um subscript — um número inteiro entre parênteses que indica qual posição quer acessar. O subscript começa em 1, não em 0:
WORKING-STORAGE SECTION. 01 WS-IDX PIC 9(02) VALUE 1. PROCEDURE DIVISION. * Subscript literal: acessa o 3º elemento diretamente MOVE 0 TO WS-TOTAL-MES(3) * Subscript variável: acessa o elemento indicado por WS-IDX ADD WS-VALOR TO WS-TOTAL-MES(WS-IDX) * Referência a subcampo de uma tabela de registros MOVE 'São Paulo' TO AG-NOME(1) MOVE 0001 TO AG-CODIGO(1) MOVE 'S' TO AG-ATIVA(1) * Percorrendo com PERFORM VARYING PERFORM VARYING WS-IDX FROM 1 BY 1 UNTIL WS-IDX > 12 MOVE 0 TO WS-TOTAL-MES(WS-IDX) END-PERFORM
⚠️ Subscript fora dos limites — S0C5 (protection exception)
Se WS-IDX for 0, negativo ou maior que o número de ocorrências, o programa vai abonar com S0C5 ou S0C7. O COBOL não verifica automaticamente os limites da tabela em tempo de execução (exceto com a opção de compilação SSRANGE). Sempre valide o subscript antes de usá-lo.
3. INDEXED BY — índices nativos
Além dos subscripts (campos numéricos comuns), o COBOL tem o conceito de index — um registrador interno otimizado para percorrer tabelas. O index é declarado com INDEXED BY e manipulado com SET:
WORKING-STORAGE SECTION. 01 WS-TAB-AGENCIA. 05 WS-AGENCIA OCCURS 50 TIMES INDEXED BY IDX-AG. 10 AG-CODIGO PIC 9(04). 10 AG-NOME PIC X(30). PROCEDURE DIVISION. * SET inicializa ou move um valor para o index SET IDX-AG TO 1 * SET UP / DOWN incrementa ou decrementa SET IDX-AG UP BY 1 SET IDX-AG DOWN BY 1 * Acessando com o index (parênteses igual ao subscript) MOVE 'Centro' TO AG-NOME(IDX-AG) * Comparação com index usa SET também IF IDX-AG > 50 CONTINUE END-IF * Para copiar valor do index para campo numérico SET WS-IDX TO IDX-AG *WS-IDX é PIC 9(02)
🦕 Subscript vs Index — qual usar?
Subscript (campo PIC 9) é mais simples de entender e manipular — você pode usar ADD, SUBTRACT, MOVE normalmente. Index (INDEXED BY) é internamente um deslocamento em bytes, não um contador, então é mais eficiente em CPU mas exige SET para mover valores. Use index quando precisar de SEARCH ou SEARCH ALL — esses verbos exigem que a tabela tenha INDEXED BY. Para iterações simples com PERFORM VARYING, subscript é suficiente.
4. Populando tabelas
Existem três formas principais de carregar dados em uma tabela:
4.1 Valores iniciais com VALUE e REDEFINES
WORKING-STORAGE SECTION. * 1) Declara os dados como uma string com VALUE 01 WS-MESES-DADO. 05 VALUE 'JAN'. 05 VALUE 'FEV'. 05 VALUE 'MAR'. 05 VALUE 'ABR'. 05 VALUE 'MAI'. 05 VALUE 'JUN'. 05 VALUE 'JUL'. 05 VALUE 'AGO'. 05 VALUE 'SET'. 05 VALUE 'OUT'. 05 VALUE 'NOV'. 05 VALUE 'DEZ'. * 2) REDEFINES enxerga o mesmo espaço de memória como tabela 01 WS-TAB-MESES REDEFINES WS-MESES-DADO. 05 WS-MES PIC X(03) OCCURS 12 TIMES INDEXED BY IDX-MES.
✅ Por que REDEFINES?
Em COBOL você não pode ter OCCURS e VALUE no mesmo item (exceto nível 88). O padrão é declarar os valores em uma área com VALUE e sobrepô-la com REDEFINES já com OCCURS. Assim você tem uma tabela pré-carregada sem precisar de código na PROCEDURE DIVISION.
4.2 Carregando de um arquivo
WORKING-STORAGE SECTION. 01 WS-QTD-AGENCIAS PIC 9(02) VALUE ZEROS. PROCEDURE DIVISION. 1200-CARGA-TABELA. MOVE 0 TO WS-QTD-AGENCIAS OPEN INPUT ARQ-AGENCIAS PERFORM UNTIL FIM-ARQUIVO-AG READ ARQ-AGENCIAS AT END SET FIM-ARQUIVO-AG TO TRUE NOT AT END ADD 1 TO WS-QTD-AGENCIAS MOVE REG-AGENCIA TO WS-AGENCIA(WS-QTD-AGENCIAS) END-READ END-PERFORM CLOSE ARQ-AGENCIAS.
5. Tabelas bidimensionais
COBOL suporta até três dimensões com OCCURS aninhado. O exemplo clássico é uma matriz de valores por mês e por filial:
WORKING-STORAGE SECTION. * 10 filiais, 12 meses cada 01 WS-MATRIZ-SALDO. 05 WS-FILIAL OCCURS 10 TIMES INDEXED BY IDX-FIL. 10 WS-SALDO-MES PIC S9(13)V99 OCCURS 12 TIMES INDEXED BY IDX-MES. PROCEDURE DIVISION. * Acessa filial 3, mês 7 ADD WS-VALOR TO WS-SALDO-MES(3, 7) * Percorre toda a matriz PERFORM VARYING WS-FIL-IDX FROM 1 BY 1 UNTIL WS-FIL-IDX > 10 PERFORM VARYING WS-MES-IDX FROM 1 BY 1 UNTIL WS-MES-IDX > 12 MOVE 0 TO WS-SALDO-MES(WS-FIL-IDX, WS-MES-IDX) END-PERFORM END-PERFORM
6. OCCURS DEPENDING ON
Quando o número de ocorrências só é conhecido em tempo de execução, use OCCURS … DEPENDING ON. O campo referenciado em DEPENDING ON controla quantos elementos são considerados válidos:
WORKING-STORAGE SECTION. 01 WS-QTD-ITENS PIC 9(03) VALUE ZEROS. 01 WS-TAB-ITENS. 05 WS-ITEM OCCURS 1 TO 100 TIMES DEPENDING ON WS-QTD-ITENS INDEXED BY IDX-ITEM. 10 IT-CODIGO PIC X(05). 10 IT-VALOR PIC 9(09)V99. PROCEDURE DIVISION. * Lendo da entrada e populando a tabela MOVE 0 TO WS-QTD-ITENS PERFORM UNTIL FIM-ENTRADA READ ARQ-ITENS AT END SET FIM-ENTRADA TO TRUE NOT AT END ADD 1 TO WS-QTD-ITENS MOVE IT-CODIGO-ENT TO IT-CODIGO(WS-QTD-ITENS) MOVE IT-VALOR-ENT TO IT-VALOR(WS-QTD-ITENS) END-READ END-PERFORM
🟣 Para quem já programa — ODO e MOVE de grupo
Com OCCURS DEPENDING ON (ODO), cuidado ao fazer MOVE de grupo: o tamanho movido considera o valor atual de WS-QTD-ITENS no momento do MOVE. Se WS-QTD-ITENS = 5, apenas 5 elementos são copiados. Isso pode truncar dados se o destino espera 100 elementos e a origem tem apenas 5. Em operações de MOVE entre estruturas ODO, sempre alinhe os tamanhos primeiro.
7. SEARCH — busca linear
O verbo SEARCH percorre uma tabela da posição atual do index até encontrar uma condição (WHEN) ou chegar ao fim (AT END). A tabela precisa ter INDEXED BY:
WORKING-STORAGE SECTION. 01 WS-TAB-AGENCIA. 05 WS-AGENCIA OCCURS 50 TIMES INDEXED BY IDX-AG. 10 AG-CODIGO PIC 9(04). 10 AG-NOME PIC X(30). 01 WS-CHAVE-BUSCA PIC 9(04). 01 WS-ACHOU PIC X(01). 88 ACHOU VALUE 'S'. PROCEDURE DIVISION. 2200-BUSCA-AGENCIA. * Obrigatório: inicializa o index antes do SEARCH SET IDX-AG TO 1 MOVE 'N' TO WS-ACHOU SEARCH WS-AGENCIA AT END MOVE 'AGENCIA NAO ENCONTRADA' TO WS-MSG PERFORM 9100-TRATA-NAO-ENCONTRADO WHEN AG-CODIGO(IDX-AG) = WS-CHAVE-BUSCA SET ACHOU TO TRUE MOVE AG-NOME(IDX-AG) TO WS-NOME-SAIDA END-SEARCH * SEARCH com múltiplas condições WHEN SET IDX-AG TO 1 SEARCH WS-AGENCIA AT END CONTINUE WHEN AG-CODIGO(IDX-AG) = WS-CHAVE-BUSCA AND AGENCIA-ATIVA(IDX-AG) PERFORM 2300-PROCESSA-AGENCIA WHEN AG-CODIGO(IDX-AG) > WS-CHAVE-BUSCA CONTINUE *interrompe a busca cedo se passou da chave END-SEARCH
💗 Como o SEARCH funciona por dentro
O SEARCH começa no elemento apontado pelo index (por isso o SET IDX TO 1 é obrigatório antes) e avança automaticamente até que: (1) uma cláusula WHEN seja verdadeira — executa o bloco e para; ou (2) o index ultrapasse o limite da tabela — executa o AT END e para. O index fica apontando para o elemento que satisfez a condição, então você ainda pode acessá-lo depois do END-SEARCH.
8. SEARCH ALL — busca binária
O SEARCH ALL faz uma busca binária — muito mais rápida que o SEARCH linear para tabelas grandes. Para funcionar, a tabela precisa estar ordenada e ter ASCENDING KEY ou DESCENDING KEY declarada:
WORKING-STORAGE SECTION. * A tabela precisa de ASCENDING KEY ou DESCENDING KEY 01 WS-TAB-PRODUTO. 05 WS-PRODUTO OCCURS 200 TIMES ASCENDING KEY IS PROD-CODIGO INDEXED BY IDX-PROD. 10 PROD-CODIGO PIC X(08). 10 PROD-DESCR PIC X(40). 10 PROD-PRECO PIC 9(07)V99. 01 WS-CHAVE-PROD PIC X(08). PROCEDURE DIVISION. 2400-BUSCA-PRODUTO. * SEARCH ALL não precisa de SET antes — posiciona sozinho SEARCH ALL WS-PRODUTO AT END MOVE 'PRODUTO INEXISTENTE' TO WS-MSG WHEN PROD-CODIGO(IDX-PROD) = WS-CHAVE-PROD MOVE PROD-PRECO(IDX-PROD) TO WS-PRECO-SAIDA MOVE PROD-DESCR(IDX-PROD) TO WS-DESCR-SAIDA END-SEARCH
| SEARCH | SEARCH ALL | |
|---|---|---|
| Algoritmo | Linear (sequencial) | Binário (divide e conquista) |
| Requer ordenação | Não | Sim (ASCENDING/DESCENDING KEY) |
| SET antes | Sim (SET IDX TO 1) | Não (posiciona automaticamente) |
| Múltiplos WHEN | Sim | Não (apenas um WHEN, só com =) |
| Performance | O(n) — lento para tabelas grandes | O(log n) — muito rápido |
| Uso típico | Tabelas pequenas ou sem ordem | Tabelas grandes e ordenadas |
⚠️ SEARCH ALL só aceita igualdade no WHEN
A cláusula WHEN do SEARCH ALL só pode testar igualdade (=) com a chave declarada em ASCENDING KEY. Você não pode usar >, <, AND com múltiplas condições ou comparar outro campo que não seja a chave. Se precisar de uma condição mais complexa, use SEARCH linear.
9. Exemplo completo
Um programa que carrega uma tabela de taxas de juros por prazo, depois aplica a taxa correta a cada operação de crédito lida de um arquivo:
IDENTIFICATION DIVISION. PROGRAM-ID. APLITAXA. ENVIRONMENT DIVISION. INPUT-OUTPUT SECTION. FILE-CONTROL. SELECT ARQ-TAXAS ASSIGN TO DDTAXAS ORGANIZATION IS SEQUENTIAL FILE STATUS IS WS-FS-TAX. SELECT ARQ-CREDITO ASSIGN TO DDCRED ORGANIZATION IS SEQUENTIAL FILE STATUS IS WS-FS-CRED. SELECT ARQ-SAIDA ASSIGN TO DDSAIDA ORGANIZATION IS SEQUENTIAL FILE STATUS IS WS-FS-SAI. DATA DIVISION. FILE SECTION. FD ARQ-TAXAS. 01 REG-TAXA. 05 TX-PRAZO PIC 9(03). *prazo em meses 05 TX-TAXA PIC V9(06). *taxa mensal decimal FD ARQ-CREDITO. 01 REG-CREDITO. 05 CR-CPF PIC X(11). 05 CR-VALOR PIC 9(11)V99. 05 CR-PRAZO PIC 9(03). FD ARQ-SAIDA. 01 REG-SAIDA. 05 SA-CPF PIC X(11). 05 SA-VALOR PIC 9(11)V99. 05 SA-PRAZO PIC 9(03). 05 SA-TAXA PIC V9(06). 05 SA-STATUS PIC X(01). WORKING-STORAGE SECTION. 01 WS-FS-TAX PIC X(02). 01 WS-FS-CRED PIC X(02). 01 WS-FS-SAI PIC X(02). 01 WS-FIM-TAXA PIC X(01). 88 FIM-TAXAS VALUE 'S'. 01 WS-FIM-CRED PIC X(01). 88 FIM-CREDITOS VALUE 'S'. 01 WS-QTD-TAXAS PIC 9(03) VALUE ZEROS. * Tabela de taxas ordenada por prazo — permite SEARCH ALL 01 WS-TAB-TAXA. 05 WS-TAXA OCCURS 1 TO 100 TIMES DEPENDING ON WS-QTD-TAXAS ASCENDING KEY IS TAB-PRAZO INDEXED BY IDX-TX. 10 TAB-PRAZO PIC 9(03). 10 TAB-TAXA PIC V9(06). PROCEDURE DIVISION. 0000-INICIO. PERFORM 1000-INICIALIZA PERFORM 1100-CARGA-TAXAS PERFORM 2000-PROCESSA PERFORM 3000-FINALIZA STOP RUN. 1000-INICIALIZA. MOVE 'N' TO WS-FIM-TAXA MOVE 'N' TO WS-FIM-CRED OPEN INPUT ARQ-TAXAS ARQ-CREDITO OUTPUT ARQ-SAIDA. 1100-CARGA-TAXAS. PERFORM UNTIL FIM-TAXAS READ ARQ-TAXAS AT END SET FIM-TAXAS TO TRUE NOT AT END ADD 1 TO WS-QTD-TAXAS MOVE TX-PRAZO TO TAB-PRAZO(WS-QTD-TAXAS) MOVE TX-TAXA TO TAB-TAXA(WS-QTD-TAXAS) END-READ END-PERFORM. 2000-PROCESSA. READ ARQ-CREDITO AT END SET FIM-CREDITOS TO TRUE END-READ PERFORM UNTIL FIM-CREDITOS PERFORM 2100-APLICA-TAXA READ ARQ-CREDITO AT END SET FIM-CREDITOS TO TRUE END-READ END-PERFORM. 2100-APLICA-TAXA. MOVE CR-CPF TO SA-CPF MOVE CR-VALOR TO SA-VALOR MOVE CR-PRAZO TO SA-PRAZO SEARCH ALL WS-TAXA AT END MOVE 'E' TO SA-STATUS *prazo sem taxa cadastrada MOVE 0 TO SA-TAXA WHEN TAB-PRAZO(IDX-TX) = CR-PRAZO MOVE TAB-TAXA(IDX-TX) TO SA-TAXA MOVE 'S' TO SA-STATUS END-SEARCH WRITE REG-SAIDA FROM SA-CPF. 3000-FINALIZA. CLOSE ARQ-TAXAS ARQ-CREDITO ARQ-SAIDA.
10. Erros comuns
1. Esquecer o SET antes do SEARCH
* ERRADO: index ficou na posição da última busca SEARCH WS-AGENCIA AT END CONTINUE WHEN AG-CODIGO(IDX-AG) = WS-CHAVE PERFORM 2300-PROCESSA END-SEARCH * CORRETO: sempre reposiciona antes SET IDX-AG TO 1 SEARCH WS-AGENCIA AT END CONTINUE WHEN AG-CODIGO(IDX-AG) = WS-CHAVE PERFORM 2300-PROCESSA END-SEARCH
2. Subscript com valor zero ou negativo — S0C5
* ERRADO: WS-MES pode ser 0 se veio do arquivo com espaços ADD WS-VALOR TO WS-TOTAL-MES(WS-MES) * CORRETO: valida antes de acessar IF WS-MES >= 1 AND WS-MES <= 12 ADD WS-VALOR TO WS-TOTAL-MES(WS-MES) ELSE PERFORM 9000-MES-INVALIDO END-IF
3. SEARCH ALL com tabela não ordenada
* Se a tabela carregada do arquivo não veio ordenada por prazo, * o SEARCH ALL pode retornar "não encontrado" para registros * que existem — a busca binária pressupõe ordenação * Solução 1: garantir que o arquivo de entrada já vem ordenado * (SORT no JCL antes de chamar o programa) * Solução 2: usar SEARCH linear para tabelas não ordenadas SET IDX-TX TO 1 SEARCH WS-TAXA AT END CONTINUE WHEN TAB-PRAZO(IDX-TX) = CR-PRAZO MOVE TAB-TAXA(IDX-TX) TO SA-TAXA END-SEARCH
4. INITIALIZE zera subscript/index
* CUIDADO: INITIALIZE em WS-CONTROLES zera WS-IDX-AGENCIA 01 WS-CONTROLES. 05 WS-IDX-AGENCIA PIC 9(02). 05 WS-TOTAL-GERAL PIC 9(10). * Depois do INITIALIZE, WS-IDX-AGENCIA = 0 * Tentar acessar AGENCIA(WS-IDX-AGENCIA) → abend S0C5 * Correto: reatribua o subscript após INITIALIZE INITIALIZE WS-CONTROLES MOVE 1 TO WS-IDX-AGENCIA
🟣 Para quem já programa — SORT interno de tabelas
O COBOL não tem um verbo nativo para ordenar tabelas em memória. As abordagens comuns em produção são: (1) garantir que o arquivo de entrada já chegue ordenado via SORT STEP no JCL; (2) usar o verbo SORT do COBOL com arquivo de trabalho temporário; (3) em alguns compiladores como Enterprise COBOL, chamar uma sub-rotina de sort via CALL 'SORT'. O SEARCH ALL pressupõe que você garantiu a ordenação — não ordena sozinho.